Skip to content

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:

EventFires when…
agent.spawnAn agent creates a sub-agent with requested capabilities
agent.delegateAn 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

ConcernGated 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:

python
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:

python
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:

python
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.spawn allowed up to delegation_depth ≤ 2, requested capabilities must be subset of session_scopes
  • agent.delegate allowed up to delegation_depth ≤ 1, requested scopes must be subset of session_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_CRITICAL deny)
  • 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.

Released under the Apache 2.0 License.