Role delegation mid-execution GitHub issue

vibecode
{"vibecode": {"example": "role_delegation_mid_execution",
    "shows": "drinian_hash_at_a_moment_when_user_role_is_delegating_to_agent_role_inside_a_role_delegate_to_block_and_the_agent_returned_function_is_running_and_has_called_into_stdlib",
    "shape": "frame_scoped_delegations_field_on_the_delegate_to_frame_role_transitions_user_to_agent_to_stdlib_across_call_stack",
    "key_point": "delegations_live_on_the_stack_frame_that_established_them_not_on_the_roles_registry"}}

Drinian mid-execution at a moment when user is delegating to agent. The program entered a %role.delegate_to($agent.object.role) do ... end block, called $agent.yield(...), the engine received the agent-authored function back, and is now invoking that function — which has itself made a string-method call on the connection-string passed in as a kwarg.

The Caspian source:

caspian
$db = '[some database connection]'
$agent = %puck['https://agents.example.com/claude'].new()

$result = %role.delegate_to($agent.object.role) do
    $agent.yield(prompt: 'find recent users', db: $db)
end

We're paused inside the agent's returned function, partway through a $db.split(';') call (the agent parsing the connection string). The Drinian hash:

json
{
    "roles": {
        "user": {},
        "stdlib": {},
        "agent": {}
    },

    "call_stack": [
        {
            "comment": "Frame 0: the bootstrap frame — the program's outermost scope. $db was assigned on line 1 (a plain string), $agent was constructed on line 2. We're past line 4 — execution is inside the delegate_to block at this point, so this frame's src points at the line that initiated the block. The bootstrap frame has no `call` field; it wasn't invoked the way other frames were.",
            "action": "top_level",
            "role": "user",
            "src": ["a", 4],
            "locals": {
                "db": {"value": "[some database connection]", "src": ["a", 1]},
                "agent": {"ref": "obj_43"}
            }
        },
        {
            "comment": "Frame 1: the delegate_to block. The delegations field records that this frame grants the agent's role (obtained via $agent.object.role) the user role's permissions for the block's lifetime. The call field carries the call object — owned by user (the caller's role), accessible as %call from inside this frame.",
            "action": "delegate_to",
            "role": "user",
            "delegations": {"agent": {}},
            "src": ["a", 4],
            "locals": {},
            "call": {
                "role": "user",
                "dispatcher": "%role",
                "method": "delegate_to",
                "src": ["a", 4]
            }
        },
        {
            "comment": "Frame 2: the $agent.yield(...) call. The engine has already round-tripped to the agent over ACP, received the agent-authored function, and is now invoking it. Action is method_call from user's perspective; the actual function-invocation frame is Frame 3. The call's role is user — the caller — even though the dispatcher is the agent object.",
            "action": "method_call",
            "receiver_type": "agent",
            "method": "yield",
            "role": "user",
            "src": ["a", 5],
            "locals": {},
            "call": {
                "role": "user",
                "dispatcher": {"ref": "obj_43"},
                "method": "yield",
                "src": ["a", 5]
            }
        },
        {
            "comment": "Frame 3: the agent-authored function the engine is invoking. Cross-role into agent. Per the agent-yield protocol the function's first param is the agent object itself, followed by the kwargs the caller supplied (in the order they were given: prompt, then db). The agent's role has user's permissions because Frame 1's delegation is on the stack above this frame. The call's role is STILL user — the caller is user, even though the called code runs as agent.",
            "action": "function_invocation",
            "role": "agent",
            "src": null,
            "locals": {
                "agent": {"ref": "obj_43"},
                "prompt": {"value": "find recent users", "src": ["a", 5]},
                "db": {"value": "[some database connection]", "src": ["a", 1]}
            },
            "call": {
                "role": "user",
                "src": ["a", 5],
                "note": "Function-invocation entered via Frame 2's yield call. The call object stays owned by user (the original caller); the function code, running as agent, reads it via cross-role access."
            }
        },
        {
            "comment": "Frame 4: the agent's function called $db.split(';') to parse the connection string. Cross-role into stdlib, which owns string methods. Even though Frame 1 delegated user's permissions to agent, this frame is running AS stdlib — the engine walks the stack looking for delegations targeting stdlib, finds none (Frame 1 targets agent), and stdlib uses its own permissions. The call's role is agent because the agent's function (Frame 3) is the caller here.",
            "action": "method_call",
            "receiver_type": "string",
            "method": "split",
            "role": "stdlib",
            "src": null,
            "locals": {
                "separator": {"value": ";"}
            },
            "call": {
                "role": "agent",
                "dispatcher": {"value": "[some database connection]"},
                "method": "split",
                "src": null
            }
        }
    ]
}

What to notice GitHub issue

If the same program had nested delegate_to blocks, additional delegations-bearing frames would stack up in the same way; permission resolution walks the stack and applies each in order; unwinding undoes each in reverse.

See also GitHub issue


© 2026 Puck.uno