Skip to content

Atomic Behaviours

What you'll learn: The 12 behaviour types that form Kyvvu's canonical vocabulary, how kind + verb classifies them, and where properties fit in.


The vocabulary

Kyvvu's vocabulary contains 12 atomic behaviour types across two namespaces. The four task.* types mark the boundaries of a task; the eight step.* types describe what the agent does inside one. The split names a real asymmetry rather than a routing difference: a task.* boundary marker carries no verb and neither input nor output, while a step.* behaviour carries the full {properties, input, output} triplet — which is what lets an absent output mean intended rather than no result yet. Together with an HTTP-style verb (GET, POST, PATCH, DELETE, or none), they form the vocabulary the engine operates on.

The kind field identifies the action kind; Python callers use the ActionKind enum for its values.

Each task-boundary behaviour follows the step lifecycle: it is emitted through the normal step path, evaluated at step_execution, and matched by kind. The kyvvu Python client exports the four task-boundary types as TASK_BOUNDARY_KINDS. Test for a boundary with that frozenset, not with a startswith("task.") prefix check — the prefix is correct today, which is exactly why it spreads; the frozenset is the one place that changes when the vocabulary does.

Action kind Valid verbs What it represents
task.start -- A task begins.
task.end -- A task completes normally.
task.error -- A task terminates with an error.
task.idle -- The task is paused, for example while awaiting input. It stays open.
step.resource GET/POST/PATCH/DELETE Read or mutate an external resource (DB, file, API).
step.message GET/POST Receive (GET) or send (POST) a message.
step.self GET/POST/PATCH/DELETE Read/write the agent's own internal state (memory, scratchpad, plan).
step.model -- Send a prompt to an LLM and receive a completion; no verb.
step.credential GET/POST/PATCH/DELETE Read, issue, rotate, or revoke a secret, token, or credential.
step.exec -- Execute code (run a script, call a function, shell out).
step.gate -- Cross a gate — human approval, policy check, guardrail.
step.unknown -- Uncategorisable behaviour (template fallback).

Python client and Claude task.start events declare aag_version: "0.7.0", matching the pinned AAG vocabulary version. Legacy starts without the field remain valid. Other action kinds cannot carry aag_version.

These combinations are enforced by the Behavior model validator, which checks (kind, verb) pairs. Any pair outside the valid set raises on construction. Behavior also forbids input keys it does not define, so an unexpected field raises there too rather than being silently dropped.

Valid kind and verb pairs

A behaviour is a kind plus, for some types, a verb. Exactly 22 pairs are valid; any other pair is refused when the behaviour is built (in Python, as a ValueError; over REST, as HTTP 422; in a kyvvu try steps file, as an error naming the step). The table is generated from VALID_COMBINATIONS in kyvvu_engine/schemas.py, so it is the whole list.

kind Valid verb values
task.start, task.end, task.error, task.idle none: leave verb out (or null)
step.resource GET, POST, PATCH, DELETE
step.self GET, POST, PATCH, DELETE
step.message GET (receive), POST (send)
step.model none: leave verb out (or null)
step.credential GET (read), POST (issue), PATCH (rotate), DELETE (revoke)
step.exec, step.gate, step.unknown none: leave verb out (or null)

Verbs are upper case and exact: post is not POST. step.model, step.exec, step.gate and step.unknown take no verb, and neither do the four task.* types. Giving one a verb is an error, and so is leaving the verb out of a type that needs one (a step.resource without a verb). Model calls send context and receive a result, so policies with a verb filter alone no longer match them; filter on step.model explicitly. Historical model rows with POST remain in stored logs. The error says what the type takes:

Invalid behavior combination: kind='step.model', verb='GET'. step.model takes no verb (omit it).
Invalid behavior combination: kind='step.exec', verb='POST'. step.exec takes no verb (omit it).

Policy enforcement points

Policies are evaluated at two enforcement points:

  1. Agent registration — at registration, and again whenever the agent's agent_registration policy set changes while it runs. Policies with enforcement_point: agent_registration run against the agent's metadata (purpose, tools, classification).

  2. Every step — before each step executes. Policies with enforcement_point: step_execution run against the intended Behavior and the task's history.

At step_execution, all 12 behaviour types follow the policy-evaluation path. A rule without an action-kind filter sees task-boundary behaviours as well as action behaviours. working_hours_only, for instance, checks the timestamp of every behaviour it sees, and task.idle keeps that check active during a long pause.

Properties

Everything beyond the (kind, verb) pair lives in properties — a nested dict that policies can inspect. Properties distinguish a step.resource reading customer-data from one reading product-data. The type is the same; the property is different.

Standard property groups:

Group Purpose Example keys
target The thing being acted on domain, table, object_id, system, trust
auth Authentication access level access_level (read/write/admin), principal
data Payload classification classification (pii, public), fields
model_params Sampling details (for step.model) temperature
message Message details (for step.message) channel, sender, recipient
usage Usage figures a step declares, read by usage_budget total_tokens, cost_usd

Some AAG properties are bare leaves, not groups: properties.provider and properties.model (strings) on a step.model, and properties.command (string) on a step.exec. The earlier nested spellings (model.provider, model.name, exec.command) are gone.

For step.gate, use the bare properties.kind value human or guardrail. A completed gate records output.decision as "pass" or "deny". By default, only a completed "pass" satisfies step_requires_gate.

Custom property groups are permitted. The engine passes them through unchanged, and rule functions read them via dot-path accessors (e.g. target.table, data.classification).

Worked example

A step.resource GET reading customer data:

from kyvvu_engine import Behavior, ActionKind, Verb

Behavior(
    agent_id="agent-123",
    task_id="task-abc",
    kind=ActionKind.step_resource,
    verb=Verb.GET,
    step_name="read_customer_record",
    input={"customer_id": "CUST-9981"},
    properties={
        "target": {
            "system": "salesforce",
            "table": "customer-data",
            "host": "internal.crm.acme.com",
        },
        "auth": {"access_level": "read", "principal": "agent-123"},
        "data": {"classification": "pii", "fields": ["name", "email", "phone"]},
    },
)

A policy using the domain_allowlist rule, with step.resource in its target_kinds, would check target.host. The history_contains rule could check whether this behaviour has already occurred in the task.


Mapping from frameworks

Templates bridge framework-specific events to the behavior vocabulary above. Each template is a YAML file with ordered rules that map event types (e.g. LangChain's on_llm_start, or a decorator's kind) to (kind, verb) pairs with property extraction. See Templates for how this works, and the Template Authoring Guide for how to write your own.


Next steps