Multi-agent governance
When agents spawn sub-agents, hand off to peers, or invoke other agents as tools, the same policy engine that gates tool_call events also gates the lifecycle events. Two structured event types cover the multi-agent lifecycle:
| Event | Fires when… |
|---|---|
agent.spawn | An agent creates a sub-agent with requested capabilities |
agent.delegate | An agent hands a task to another agent (peer or child) |
Both go through agent_lifecycle.rego, and the resulting child sessions inherit delegation.rego constraints on what they're allowed to do.
The two-tier mental model
| Concern | Gated by |
|---|---|
| Can this agent create / delegate to a child? | agent_lifecycle.rego (lifecycle event itself) |
| What is the resulting child allowed to do? | delegation.rego (per-tool-call rules referencing delegation_depth) |
OSS defaults: spawn capped at depth 2, delegate capped at depth 1, depth-1 children capped at $50 refunds, depth-2+ children blocked from refunds entirely.
Pattern 1 — governed_handoff (framework-agnostic)
The lowest-level helper. Use this from any custom multi-agent code or framework not covered by an adapter:
from kitelogik.adapters._base import governed_handoff
from kitelogik.governed import GovernanceError
try:
await governed_handoff(
gate=gate,
context=parent_context,
target="refund_agent", # delegation_target on the event
action="agent.delegate", # OPA action — defaults to this
requested_capabilities=["approve_refund"],
)
except GovernanceError as exc:
print(f"Delegation denied: {exc.decision.reason}")
return
# allow → run the delegation
await refund_agent.run(task)Constructs an agent.delegate event, runs it through the gate, raises on deny. Returns None on allow — the caller decides what to do next.
Pattern 2 — OpenAI Agents SDK register_handoff
For the OpenAI Agents SDK, use the adapter's higher-level helpers — they wrap agents.handoff() and agent.as_tool() with delegation governance:
from agents import Agent
from kitelogik.adapters.openai_agents import OpenAIAgentsAdapter
adapter = OpenAIAgentsAdapter(gate=gate, context=parent_context)
triage_agent = Agent(name="triage_agent", ...)
refund_agent = Agent(name="refund_agent", ...)
# Wrap the handoff so every transfer hits the policy gate
to_refund = adapter.register_handoff(
target_agent=refund_agent,
action="agent.delegate",
)
triage_agent.handoffs = [to_refund]If the policy denies, the SDK's handoff transfer is rejected. Optional on_handoff= callback fires only on allow. See the Agents SDK adapter page for typed-input caveats and register_agent_as_tool().
Pattern 3 — manually fire agent.spawn
If your runtime is responsible for the actual sub-agent creation, fire the spawn event yourself before instantiating the child:
from kitelogik.tether.models import GovernanceEvent
from kitelogik.governed import GovernanceError
spawn_event = GovernanceEvent(
event_type="agent.spawn",
session_id=parent_context.session_id,
action="agent.spawn",
context=parent_context,
requested_capabilities=["read_customer", "send_notification"],
)
decision = await gate.evaluate(spawn_event)
if not decision.allow:
raise GovernanceError(f"Spawn denied: {decision.reason}", decision=decision)
# Allowed — create the child session with the requested scopes
child_context = parent_context.model_copy(update={
"session_id": "sess_002_child",
"session_scopes": ["read_customer", "send_notification"],
"delegation_depth": parent_context.delegation_depth + 1,
"parent_token_id": parent_context.token_id,
"parent_session_id": parent_context.session_id,
})The OSS agent_lifecycle.rego rules check that every requested capability is in the parent's session_scopes (subset rule), so child contexts always have a smaller surface than their parent.
What agent_lifecycle.rego enforces by default
From agent_lifecycle:
agent.spawnallowed up todelegation_depth ≤ 2, requested capabilities must be subset ofsession_scopesagent.delegateallowed up todelegation_depth ≤ 1, requested scopes must be subset ofsession_scopes- Both reject malformed events (missing or non-numeric
delegation_depth) - Both reject capability/scope expansion attempts
What delegation.rego enforces by default
From delegation:
- Hard cap: no chain deeper than 2 (any tool call beyond depth 2 →
SECURITY_CRITICALdeny) - Depth-1 refund cap: $50 (and amounts must be numeric and non-negative)
- Depth-2+ refund: blocked entirely
Both modules' denials become SECURITY_CRITICAL in main.rego — no HITL escalation, no scope override.
Audit trail
Every governed event — including agent.spawn and agent.delegate — is recorded with parent_session_id, delegation_depth, requested_capabilities, and the rule_matched that produced the decision. So you can answer "which agent created this child, with what scope, on what policy version?" from a JSONL grep or a SQL query.
See the audit trail page for the decision shape.
Related
agent_lifecycle.rego— the lifecycle gatedelegation.rego— the per-tool-call action gate- Governance events — the full event shapes
- OpenAI Agents SDK adapter —
register_handoff+register_agent_as_tool