Skip to content

Implement Multi-Agent Delegation

Delegation transfers authority from one agent session to another. The Coordinator records a directed edge in the delegation graph. The STS validates the edge on every token exchange. Revoking an edge cascades automatically to all downstream agents that received authority through it.

  • The Caracal SDK installed in the orchestrating service (TypeScript, Python, or Go).
  • The Coordinator URL reachable at CARACAL_COORDINATOR_URL.
  • An active policy that allows delegation exchanges.

Every execution begins with spawn(). The SDK opens a session on the Coordinator and binds it to the current async context:

TypeScript:

import { Caracal, AgentKind } from '@caracalai/sdk';
const caracal = Caracal.fromEnv();
await caracal.spawn(async () => {
const ctx = caracal.current()!;
console.log('root session:', ctx.agentSessionId);
// depth = 0
}, { kind: AgentKind.Instance, ttlSeconds: 3600 });

Python:

from caracalai_sdk import Caracal
from caracalai_sdk.advanced import AgentKind
caracal = Caracal.from_env()
async with caracal.spawn(kind=AgentKind.INSTANCE, ttl_seconds=3600) as ctx:
print('root session:', ctx.agent_session_id)

Go:

err = client.Spawn(ctx, func(ctx context.Context) error {
cc, _ := sdk.Current(ctx)
fmt.Printf("root session: %s\n", cc.AgentSessionID)
return nil
}, sdk.SpawnOptions{Kind: sdk.KindInstance, TTLSeconds: 3600})

Call spawn() again inside the first callback. The Coordinator records the parent-child relationship:

TypeScript:

await caracal.spawn(async () => {
const parentCtx = caracal.current()!;
// Second spawn inside the first — becomes a child
await caracal.spawn(async () => {
const childCtx = caracal.current()!;
console.log('child session:', childCtx.agentSessionId);
// depth = 1, parent = parentCtx.agentSessionId
}, { kind: AgentKind.Ephemeral });
});

Session limits enforced by the Coordinator:

LimitValue
Max depth10
Max children per session10
Max active sessions per zone per application50
Max active sessions per application across zones200

delegate() creates a directed authority edge from the current session to the target session. The target can then exchange that edge for a mandate scoped to the delegated permissions.

TypeScript:

await caracal.spawn(async () => {
const sourceCtx = caracal.current()!;
// targetAgentSessionId comes from the spawned child or another agent
await caracal.delegate(
{
to: targetAgentSessionId,
toApplicationId: 'payments-worker',
scopes: ['payment:submit', 'payment:read'],
ttlSeconds: 300,
},
async () => {
const ctx = caracal.current()!;
console.log('delegation edge:', ctx.delegationEdgeId);
// The delegated context is now active
}
);
});

Python:

from caracalai_sdk import DelegationConstraints
async with caracal.spawn() as source:
async with caracal.delegate(
to=target_agent_session_id,
to_application_id='payments-worker',
scopes=['payment:submit', 'payment:read'],
constraints=DelegationConstraints(
resources=['resource://payments'],
max_depth=2,
),
ttl_seconds=300,
) as delegated:
print('edge:', delegated.delegation_edge_id)

Go:

err = client.Spawn(ctx, func(ctx context.Context) error {
return client.Delegate(ctx, sdk.DelegateOptions{
To: targetAgentSessionID,
ToApplicationID: "payments-worker",
Scopes: []string{"payment:submit", "payment:read"},
Constraints: &sdk.DelegationConstraints{
Resources: []string{"resource://payments"},
MaxDepth: 2,
},
TTLSeconds: 300,
}, func(ctx context.Context) error {
cc, _ := sdk.Current(ctx)
fmt.Printf("edge: %s\n", cc.DelegationEdgeID)
return nil
})
})

DelegationConstraints / constraints_json narrows what the target can do:

ConstraintEffect
resourcesRestricts the edge to specific resource identifiers
actionsRestricts the allowed action strings
max_depthCaps how many more hops authority can flow through from the target
expires_atHard expiry for the edge independent of the session TTL

The STS validates all constraints on each exchange. A constraint violation fails the exchange before OPA is evaluated.

Inspect active sessions and their delegation edges:

Terminal window
# List all active agent sessions in the zone
caracal agent list --json
# Show the child sessions of a specific session
caracal agent tree sess-abc123
# Show inbound edges (authority flowing IN to the session)
caracal delegation inbound sess-abc123
# Show outbound edges (authority flowing OUT from the session)
caracal delegation outbound sess-abc123
# Walk the full delegation chain from a specific edge
caracal delegation traverse edge-def456

Revoking an edge cascades forward through the entire subgraph. All sessions that received authority through the revoked edge are terminated:

Terminal window
caracal delegation revoke edge-def456

The Coordinator runs the cascade revocation CTE and publishes a caracal.sessions.revoke event for every affected session. The Gateway blocks requests from affected sessions within seconds.

Terminate a specific agent session and all its descendants:

Terminal window
caracal agent terminate sess-abc123

Suspension affects the entire subtree. Active mandates for suspended sessions are still valid until their 15-minute TTL expires, but new exchanges are blocked:

Terminal window
caracal agent suspend sess-abc123 # Block new exchanges for entire subtree
caracal agent resume sess-abc123 # Re-enable the subtree

Verify a mandate includes the expected chain

Section titled “Verify a mandate includes the expected chain”

On the receiving side, check that the delegation chain contains the expected orchestrator application:

TypeScript:

import { verify } from '@caracalai/identity';
const claims = await verify(token, {
issuer: process.env.CARACAL_STS_URL!,
audience: 'resource://payments',
requireChainContains: ['orchestrator-app'],
});

Go:

claims, err := identity.Verify(token, identity.Config{
Issuer: "http://sts:8080",
Audience: "resource://payments",
RequireChainContains: []string{"orchestrator-app"},
})