Architecture¶
What you'll learn: The package split, why the engine is in-process, and how data flows from your agent to the platform.
Packages¶
Kyvvu is split into packages with different licenses and deployment boundaries:
| Package | License | What it does |
|---|---|---|
kyvvu-engine |
BSL 1.1 | Evaluates policies against Behaviors, tracks per-task history, registers the agent and fetches policies through PlatformClient, exports traces; the server behind kyvvu serve is its serve extra. PolicyEngine itself does no I/O. |
kyvvu-templates |
Apache 2.0 | Maps framework events to Behaviors from a YAML template. |
kyvvu-baseline |
Apache 2.0 | The 15 baseline policies and their loader. |
kyvvu (Python client) |
Apache 2.0 | Python client: @kv.step, task lifecycle, FrameworkAdapter for LangChain and CrewAI. |
kyvvu-cli |
Apache 2.0 | The kyvvu command: try, init, serve, doctor, integrations, plugins. |
| Kyvvu Platform API | Proprietary | Policy storage, incident management, behavioral trace ingestion, audit reporting, dashboard. Runs at platform.kyvvu.com. |
pip install kyvvu installs the Python client and engine; uv tool install kyvvu-cli installs the command and engine server. Framework integrations are separate packages: kyvvu-langchain (LangChain and LangGraph), kyvvu-crewai and kyvvu-claude (Claude Code hooks), each installed alongside kyvvu. The platform API is a hosted service.
Why the engine is in-process¶
The engine runs inside your agent process, not as a remote service. This is a deliberate architectural choice.
Performance. Agent steps are frequent and small. A task can emit dozens of atomic Behaviors. If each one required a network round-trip to a policy service, you'd add milliseconds per step that compound into seconds per task. The engine evaluates policies in sub-millisecond time:
| Scenario | p99 latency |
|---|---|
| 100 policies, 50-step history | 296 us |
| 10 policies, empty history | 34 us |
| 0 policies (baseline) | 3 us |
At four orders of magnitude faster than a typical LLM call, the engine adds no perceptible latency.
Coverage. A gateway proxy only sees LLM calls. It misses tool invocations, resource reads, credential fetches, and decision gates between model calls — precisely where the governable behaviour lives. The in-process engine evaluates every step the agent takes.
Resilience. If the platform API is down, the engine continues evaluating with its cached policies. Without a usable policy set, the default fail mode blocks step.* actions. For additional resilience:
- Disk cache (
KV_POLICY_CACHE_PATH): policies are written to disk after each successful fetch and loaded on cold start if the API is unreachable — surviving process restarts during outages. - Fail mode (
KV_POLICY_FAIL_MODE=closedby default): blocksstep.*actions when the policy state isunavailable.task.*markers and a successfully loaded empty set (none_configured) are allowed. SetKV_POLICY_FAIL_MODE=opento allow actions when policies are unavailable.
See Engine settings for details on these settings.
For the full argument, see The Hot Path Tax.
Policies on paths¶
The engine evaluates policies against the full ordered history of the current task, not just the current step. This is the "policies on paths" model, formalised in the paper Runtime Governance for AI Agents: Policies on Paths.
Example: the same step.exec call is allowed or blocked depending on what happened earlier. If a step.gate (human approval) precedes it in the task history, it passes. If not, it's blocked. The decision is path-dependent.
This is what makes Kyvvu different from point-in-time content filters or LLM guardrails. A content filter checks one input at a time. Kyvvu checks the step in context — what the agent has already done, what resources it accessed, what approvals it received.
Data flow¶
Agent code
|
| @kv.step / LangChain handler
v
Python client (template matching)
|
| Behavior object
v
Engine (PolicyEngine.evaluate)
|
| Reads: cached policies + task history
| Returns: allow / warn / block
v
Agent code (continues or catches KyvvuBlockedError)
|
| During the task and at end_task() (several batches allowed)
v
Engine (KyvvuRunner)
| Async: flush behavioral trace to log endpoint
| Async: fire incident webhook on violations
v
Platform API
| Stores: logs, incidents, events
v
Dashboard
What happens synchronously (hot path)¶
- Template matching (the Python client maps framework event to Behavior)
- Policy evaluation (engine runs rules against history)
- Step recording (engine appends to in-memory history)
All sub-millisecond and in-process; evaluation itself does no I/O.
What happens asynchronously (background)¶
- Policy fetch (TTL-based, default every 300 seconds)
- Log flush (at the latest on
end_task(), possibly in several batches; POST to log endpoint) - Incident webhook (on
warn/block, POST to incident endpoint)
Log flushes and incident webhooks share one bounded queue and one worker thread. end_task() and evaluate() only enqueue and return; the worker sends in FIFO order, so a task's batches arrive in the order they were produced. That order matters because, in 1.0, the engine computes each task's hash chain and the platform verifies it at ingestion (chain v1). A task may arrive in several batches, and the platform stores each batch before it answers 201. The hash input fields are agent_id, task_id, seq, input_data (the stored input), and previous_hash (when present). The chain does not cover kind, verb, timestamp, output, properties, or meta. Network failures in background operations are logged and swallowed.
The bound. After the first policy fetch, evaluate() and end_task() never wait on the network. Their cost is an in-memory enqueue. The "p99 296 µs" figure measures evaluate() itself.
Overflow. The queue holds 1000 reports by default (KV_REPORT_QUEUE_SIZE). When it is full, the newest report is dropped and counted: read kv.report_status().dropped (or runner.report_status()), and a WARNING with the running total is logged. Nothing is dropped silently.
Exit. An atexit hook waits up to 2 seconds (KV_REPORT_EXIT_FLUSH_SECONDS) for queued reports to be delivered, so short-lived scripts keep their trace. Call kv.flush(timeout=...) to wait explicitly. A hard kill (SIGKILL, power loss) can lose reports still in the queue.
The one synchronous wait. The first policy fetch runs on the calling thread, before any step is evaluated. If the platform is unreachable it can take up to KV_HTTP_TIMEOUT_SECONDS (default 10) before the disk cache or inline policy set is used. After that, refreshes run on their own thread.
Stdout and file sinks, and callback exporters, have no network to wait on and run inline, so their output appears when the call returns.
One engine per agent¶
Each KyvvuRunner (and each Kyvvu client instance) owns one PolicyEngine. Engines are per-agent and are not shared across agents. This is by design — policies are targeted at specific agents, and task histories must not leak between agents.
For LangGraph, the KyvvuLangChainHandler treats the entire graph execution as a single task — all nodes (LLM calls, tool calls) are flattened into steps within that task. Each graph invocation is one task, not one task per node.
For truly separate agents (e.g., independent microservices or processes), each agent gets its own Kyvvu instance. There is no cross-agent or cross-task state in the engine: each agent's policies see only its own task history.
Next steps¶
- Atomic Behaviours — the 12 behaviour types the engine operates on
- Policies and Rules — how policies are structured and evaluated
- Tasks and History — task lifecycle and path-dependent evaluation