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:
-
Agent registration — at registration, and again whenever the agent's
agent_registrationpolicy set changes while it runs. Policies withenforcement_point: agent_registrationrun against the agent's metadata (purpose, tools, classification). -
Every step — before each step executes. Policies with
enforcement_point: step_executionrun 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¶
- Policies and Rules — how policies use behaviour types and properties to make decisions
- Templates — how the Python client maps framework events to this vocabulary
- Built-in Rules Reference — all 25 rules and which behaviour types they operate on