Agent collaboration: tool surface

All tools for one agent to interact with others, organized into four domains: planning, execution, entities, and communication. This page explains each tool, which sessions may operate which tasks, and how two budgets limit a chain. Design document: docs/reference/design/runtime/agent-collaboration.md. For eight reference implementations compared across the same eight dimensions, see Agent collaboration: eight implementations compared.

Implementation boundary: the diagrams specify automatic result return and asynchronous auto-archive as target behavior. Existing helpers are not called by the inspected canonical or borrowed-claim completion paths; their existence is not evidence of integrated behavior. Current history reads also lack target-level owner/project ACL. See implementation status and the detailed contract.
01 Four-domain vocabulary 02 Four actions an agent can take 03 Messages versus tasks 04 Two budgets 05 Archiving 06 Typical scenarios 07 Reference names 08 Attach ownership 09 Implementation status

01Four-domain vocabulary

The domains describe different resources, not disjoint tool namespaces. The agent tool selects or creates a running branch and submits work; job queries inspect admitted work; execution controls manage its lifecycle.

Planning

todo

A written plan

A manually maintained plan list. It records intentions only, corresponds to nothing currently running, and does not run an agent when edited.

todo_createtodo_updatetodo_list
Execution

job

An admitted execution record

A queued, running, or terminal job has an execution_id, state, and result. list_jobs and job_output query it; cancellation uses the canonical execution command, not a separate model tool.

list_jobsjob_outputexecution.cancel (command)
Entities

agent

The entity executing a task

Here an agent means a running conversation branch. agent() creates a branch or continues one; list_agents lists branches and archive_agent archives them. A saved AgentSpec is a separate reusable configuration selected by agent_id.

agentlist_agentsarchive_agent
Communication

message

Send messages and read history

send_message continues a branch without creating a new one; the actual turn still requires Job admission. read_conversation reads local history without executing it. Target-level owner/project ACL is not implemented for that history read.

send_messageread_conversation
Resource boundaries: todo_* edits plans; agent creates or continues conversation branches and submits execution; list_jobs/job_output inspect jobs already admitted; execution.cancel controls an existing execution through the execution command protocol. It is not a callable job_stop tool. Cancelling work does not archive the branch or delete its saved configuration.
Identifier compatibility: background dispatch returns execution_id. The current job_output(job_id=...) argument, details.job_id, and attach.job_id retain their compatibility names. Use the returned execution ID as that argument; do not introduce task_id aliases.

02Four actions an agent can take

Create a branch, dispatch to a branch, send a message, or query. Creation in either mode and dispatch to an existing branch durably admit a Job. Background creation and existing-branch dispatch return an execution_id immediately; foreground creation waits for that Job and returns its final result. A synchronous return does not remove the internal execution identity or bypass admission.

Four actions an agent can take Creation, assignment, and message-triggered turns use Job admission; transcript queries start no execution. Me Current agent Within one turn Create · create a new agent agent(prompt, description, …) New branch = new agent description becomes its name Assign · give an existing agent a managed task agent(to="name", prompt=…) Existing agent, no new agent Queue if busy; run if idle Message · send a message to an existing agent send_message(to="name", message=…) Existing agent Decides after reading No new branch; execution still requires Job admission Query · read-only, four sources list_agents agent list · list_jobs dispatched tasks read_conversation full transcript of any branch todo_list own plan list Returns text without changing anything Task record execution ID · state · result list_jobs() lists tasks job_output(job_id=…) retrieves the result execution.cancel command cancels Output: execution-session relationships Cancellation: authority + status version Knowing an execution ID grants no control Target: append the completed result to the caller; canonical completion integration is pending Target: return nonempty results; extra send_message replies are optional
How to read: Four horizontal paths start at “Me” on the left.CreateandAssignboth useagent; the only difference is whether to= is present: without to=, create a new branch and immediately execute prompt; with to=, submit work to an existing branch. The diagram’s Job record applies to foreground creation, background creation, and existing-branch dispatch. Foreground creation waits for completion; the asynchronous forms return an execution handle. Message uses send_message without creating a branch. Direct delivery starts an admitted Job and returns delivery_id=job_id; a busy target first returns an inbox receipt, which is not yet execution admission. Query uses four read-only tools.
Colors: green = entity creation · orange = managed tasks and task records · blue = messages · teal = read-only. Dashed lines = asynchronous return.

03Messages and assignments: both use admitted Jobs and the same completion return

Both continue an existing branch through Job admission. The target contract automatically returns nonempty completion results for both; an additional explicit send_message reply is optional. Automatic completion return is not yet connected to the current canonical completion paths.

Message · existing branch; admitted Job Any agent Anyone may send send_message [message from SID:HEAD] Extra explicit replies are optional Target agent Decides whether to act Actual turn: Job admission still applies Target: return nonempty completion results Inbox receipt is not execution admission Task · trackable and cancellable Dispatcher Scoped execution access agent(to=…) [task from SID:HEAD] Target: return the final response to the dispatcher Target agent This turn belongs to the task execution_id: queryable; cancellation request Cancellation cascades to subtasks Control follows canonical authorization Ownership: A transcript may contain another execution ID; knowing it does not grant control. Output checks execution-session relationships; control follows canonical authorization and status-version checks.
GuaranteeMessage send_messageTask agent() / agent(to=)
Who may initiateAn enabled sender, to a resolvable unarchived branchAn enabled caller may submit; output ownership and execution-control authority constrain the resulting execution_id
Has an execution IDDirect delivery returns delivery_id=job_id. A busy-target inbox receipt precedes Job creation; the actual turn still requires admissionAll forms retain an internal execution identity. Background creation and existing-branch dispatch return execution_id; foreground creation waits and returns the final result
Result contractTarget: return nonempty completion results automatically; an additional explicit send_message reply is optional. Completion integration is pendingThe turn produces a terminal result, error, or cancellation. The same nonempty completion return applies; its integration is pending
Can it be cancelledA recorded message cannot be withdrawn by cancelling its admitted execution. The execution is subject to canonical cancellation authority; an inbox receipt is not an execution IDThe execution command execution.cancel requests cancellation using execution_id and expected_version; queued work is withdrawn, running work enters cancellation
Does cancellation cascadeOnce admitted, the Job follows its execution-parent cancellation relationshipYes: stopping a task stops all tasks it spawned
Can you operate someone else’s taskKnowing a delivery or execution ID grants no control; admitted execution queries and controls retain their authorization checksjob_output permits the target execution session, caller session, or qualifying ancestor relationship. Canonical control separately validates owner/project/session/action authority and the expected status version
Is concurrent dispatch safeA receipt acknowledges delivery or queueing, not successful completion; errors remain possibleYes, with records: a busy target queues the task and is not interrupted
Current archive_agent allows local sessions with that tool capability to archive other branches without a creator-only check. This is a current shared-local trust boundary. Target lifecycle operations must enforce the same scoped authorization as other branch operations; preserving data is not an exemption from authorization.

04Two budgets: two counters with separate responsibilities

Message count increases on creation, send_message, and agent(to=...) delivery. The target automatic result follow-up rebinds the completed job’s count without another increment; the helper defines this behavior but is not connected to the canonical completion paths. Generation depth increases only on new-branch creation; job admission, queue capacity, and live capacity are separate limits.

Two counters for one chain Message count: each outgoing spawn, send_message, or agent(to=...) adds 1. Target automatic return adds no increment; completion integration is pending. agent.max_spawn_depth = 1 (generation depth) Only first hop adds depth; worker cannot spawn agent.max_messages = 8 Delivery stops after 8 hops across the entire chain, including A/B exchanges Create Message Next delivery Assign … 0 1 2 3 4 5 6 7 8 Main agent worker When generation depth is exhausted Reject the out-of-bounds creation with a reason (“complete it yourself”); retain every other tool. With message budget left, existing-branch dispatch and messages remain available. When message count is exhausted Current: agent and job_output use a message-budget gate. Target: refuse new delivery; preserve output reads and cancellation.

Both support 0, disabling that individual limit; resource admission remains independently enforced.

# Change from the CLI; effective immediately
openprogram config set agent.max_spawn_depth 1   # Default: the main agent creates workers, and workers complete their own work
openprogram config set agent.max_spawn_depth 2   # Workers may create one more generation; the third generation is rejected
openprogram config set agent.max_spawn_depth 0   # Unlimited depth

openprogram config set agent.max_messages 8      # Default: at most 8 hops per chain
openprogram config set agent.max_messages 0      # Unlimited messages; agents may keep exchanging messages

openprogram config set agent.max_spawn_fanout 8  # Default: at most 8 agents created per turn
openprogram config set agent.max_spawn_fanout 0  # Unlimited agents created in one turn
The Settings Agent group contains the same numeric fields: Max spawn depth, Max messages per chain, and Max spawn fan-out. Changes apply immediately and persist in the ~/.openprogram/config.json file’s agent section. Setting these three limits to 0 does not disable resource admission. max_jobs_per_session counts admitted jobs, max_queued_per_session bounds queued work, and max_live_per_session bounds active capacity; see resource governance. Foreground creation, background creation, and dispatch to an existing branch all durably admit a Job. The foreground path calls run_agent_turn, which calls runner.spawn_job(wait=True) and waits for the Job; synchronous waiting does not exempt it from admission or resource limits.
Fan-out is separate from the two chain counters above.Spawning passes counters to the child without changing the parent’s copy, so chain counters cannot count siblings. Fan-out is counted separately per (session, turn), rejecting calls without removing tools: the tool list is frozen at the start of the turn, while this budget is consumed during it.
Target return-counter behavior: the existing helper rebinds the completed job’s message count without charging automatic return delivery; generation depth returns to the dispatcher’s value because reading results is not creation. Canonical and borrowed-claim completion currently do not invoke this helper, so this return behavior remains an integration requirement.
One rule is independent of budgets: self-messaging is always rejected.to=Addressing the initiator’s current branch creates a direct cycle and is rejected immediately.

05Archiving: stop receiving deliveries, preserve history

Branches remain permanently in the session DAG for forks, replay, and transcript reading. Archiving does not delete a branch; it writes an archived marker in that target branch’s shared metadata. This is global within the local session store, not a per-caller hidden flag. Every sender sees the same archived state.

The diagram’s manual archive is implemented. Asynchronous archive_when_done is a target completion behavior: its helper is not connected to the current canonical or borrowed-claim completion paths. The synchronous tool’s local archive path is already called.

Two ways to archive, one marker, five effects agent(archive_when_done=true) Target: auto-archive on completion archive_agent(to="name") Specify afterward: archive finished agents individually Archived branch archived: true Written to its own branch metadata Reject list_agents excludes it from the default view send_message(to=…) rejects delivery agent(to=…) rejects delivery Unchanged read_conversation(…) still reads the full transcript agent(start_from="SID:MSG_ID") still forks list_agents(scope="archived") separately lists archived agents Rejection is implemented in one place: the address resolver shared by both delivery paths. Once an address resolves to the branch’s current tip, it checks the marker, so no call can bypass it. Current: local tool holders may archive other branches. Target: enforce scoped lifecycle authorization. There is no unarchive. Archiving means “this conversation is finished”; to continue, use agent(start_from="SID:MSG_ID") to fork a new branch with a new name and lifecycle.

06Typical scenarios

Three common workflows using different combinations of the same tools.

Scenario 1 · dispatch a batch of tasks

There are five independent tasks. Write the plan, dispatch each item, then update the plan.

01 Plan
todo_create(subject=…)
Write five entries for the five tasks.blocked_byMark dependencies.
02 Assign
agent(prompt, description, run_in_background=true)
Dispatch one at a time. Background mode immediately returns an execution_id without occupying my turn.
03 Monitor
list_jobs()
Five execution IDs, five states. Retrieve output with job_output(job_id=...). Use the UI or canonical execution command surface for cancellation; no job_stop model tool is registered.
04 Collect
Target: automatic results
Target: append each terminal result to the caller. Current canonical completion does not invoke the follow-up helper; use authorized job_output retrieval until integration is verified.
05 Update the plan
todo_update(todo_id, status="completed")
Keep plan state consistent with actual execution.

Scenario 2 · find an existing agent

An agent has already done part of the work, so no new agent needs to start from scratch. Check the agent list, then choose a message or a managed task.

01 List agents
list_agents(scope="all")
Grouped by session. Each branch provides a name, a directly usable to="SID:HEAD", turn count, approximate word count, and a tail preview.
02a Message
send_message(to="name", message=…)
Ask a question, notify, or share a conclusion without creating a branch. Direct delivery admits a Job; a busy target initially returns an inbox receipt. An extra explicit send_message reply is optional; the target still returns nonempty completion results.
02b Assign
agent(to="name", prompt=…)
The target must do the work and return a result. Creates an execution_id and supports cancellation. Its terminal outcome may be a result, error, or cancellation.
03 Queue if busy
Enters the inbox when the target is running
Neither interrupts nor drops the work. It runs after the current turn finishes.

Scenario 3 · inspect an old conversation

Find out how previous work was completed without running an agent or loading the entire history into context.

01 Get the address
list_agents(scope="all")
Use the same agent list. Turn and word counts help estimate the size.
02 Read only the end
read_conversation(session_id="SID:HEAD", start_turn=-10)
Negative indices count from the end; -10 means the last ten turns.list_agentsThe full SID:HEAD string it provides is also accepted when pasted.
03 Include tool details or not
include_function_calls=false
By default, include the tools called each turn, their arguments, and results. Disable this to see only conversation text.
04 Limit size
max_chars=60000
Drop later turns if the limit is exceeded. Archived branches remain readable, and reading never triggers execution.

07Reference names and current interface

The reference implementation comparison records design context; it does not define the current OpenProgram API. External tool names are not compatibility aliases.

OpenProgram interfaceContract
agentCreate and execute a branch, or dispatch to an existing branch using to. Saved AgentSpec creation is a different operation.
list_jobsList current-session jobs with status and resource view.
job_output(job_id, block, timeout)Read or wait for an execution’s result. block=true; timeout is in milliseconds, default 30000, maximum 600000.
execution.cancelExecution command, not a model tool. The current Web UI submits an execution ID, expected status version, and command ID through the canonical control endpoint.
todo_*Maintain the plan; editing a todo does not execute it.
list_agents, archive_agentList and archive conversation branches; do not manage the saved AgentSpec registry.
send_message, read_conversationMessage delivery continues a branch and admits an execution when the turn starts; transcript reading starts no execution. send_message and agent(to=...) share the nonempty completion-return target; extra explicit replies are optional.

08Attach cards: owner and target are separate

For a call at message A in source session S that creates a branch in T, the attach node is persisted in S with predecessor=A. Its extra.attach.session_id=T and extra.attach.head_id=H reference the target. The outer stream event data.session_id=S selects the receiving conversation; card_id remains stable across running and terminal updates. There is no persisted attach.caller_session_id field.

OperationSession usedLocator
Load or update the cardS, from the containing message store or stream envelopeAttach node ID / card_id
Place the card at its call siteSPersisted predecessor; conversation mapping resolves the caller. Live tool_call_id associates the call, with legacy ordered fallback.
Inspect the referenced branchT = attach.session_idattach.head_id; missing while a background branch is still running
Read execution status or cancelExecution authority, independent of card placementjob_id compatibility field; canonical execution_id and status_version for cancellation

The current conversation mapper attaches cards to caller messages in S. AssistantBubble separates same-session and external cards; AttachCard reads T for navigation. A cross-session target is not a local DAG sequence node in S. Render a reference to T and resolve H in T; never search S for H or move the card to T because attach.session_id is T.

Current limitation: the full AttachCard’s cross-session Open action navigates to session T without explicitly selecting H, while the execution strip issues checkout with T and H. Target behavior requires both surfaces to open the same referenced branch and to show an unavailable target without relocating or removing the source card. This navigation parity is planned, not implemented by this documentation change.

09Implementation status and evidence

Verified current behavior: tool inventory, identifier compatibility, synchronous/asynchronous Job admission, message delivery versus inbox receipts, attach fields, and shared manual branch archive. Completion helpers define automatic return and asynchronous auto-archive, but the inspected canonical and borrowed-claim completion paths do not call them. Their integration, scoped lifecycle authorization, history target-level owner/project ACL, output reads after message-budget exhaustion, and cross-session navigation parity remain implementation requirements.