Pipelines Docs is in beta — content is actively being added.
AgentsInteraction Patterns

Multi-agent systems

Declare an internally multi-agent system's topology from the CLI, SDK, or dashboard, attribute each tool call to the acting sub-agent at run time, and grade the system's collaboration.

If the system under test is internally multi-agent, Pipelines can attribute each tool call to the acting sub-agent, render delegation structure, and order handoff events on a timeline. The trace tab then renders the Multi-agent structure card, including sub-agent roster, delegation edges, and handoff timeline above the run timeline. The judge evaluates delegation structure in addition to final answer quality.

No proxy protocol changes are required. A single-agent integration remains valid. Multi-agent capture adds two steps:

  1. Declare topology, optional but recommended. This defines which sub-agents exist and how they connect, so declared but idle agents still render and unlabeled calls can be attributed where possible.
  2. Stamp acting sub-agent at run time, so every proxy call carries actor_id and handoffs are recorded.

Single-agent systems do not require this configuration. If no topology is declared and no actor_id is stamped, runs remain in single-agent shape with a flat timeline and no Multi-agent structure card.

Step 1: Declare your topology

Declared topology is a possibility graph. It defines which sub-agents exist, which tools each owns, who can delegate to whom through talks_to, and which sub-agent is entry. This declaration informs graph rendering but does not gate execution. Missing or malformed declarations degrade to runtime inference.

It lives in agent.config as a sub_agents plus topology object:

{
  "sub_agents": [
    { "actor_id": "triage",  "tools": ["lookup_order"], "talks_to": ["refunds", "billing"] },
    { "actor_id": "refunds", "tools": ["issue_refund"] },
    { "actor_id": "billing", "tools": ["charge"] }
  ],
  "topology": { "entry": "triage" }
}

You can produce this declaration through three paths, depending on workflow.

Option A — CLI

Point the CLI to a zero-argument factory that builds the root agent and output the topology section. Extraction reads constructed object structure only; no run and no LLM call are required.

pipelines odyssey dump-agent \
  --framework openai \
  --factory app.agents:build_root_agent \
  --section topology

--framework accepts OpenAI, LangChain, Anthropic, and Strands. --section topology prints sub_agents and topology only and can be pasted into the dashboard Multi-agent Import JSON dialog. --section all prints tools_schema plus topology for agent registration payloads. --section tools prints the tool catalog only.

pipelines odyssey dump-agent --framework openai \
  --factory app.agents:build_root_agent --section topology | pbcopy

Run the command from project root so the --factory module imports correctly. The factory must accept zero arguments. Provide defaults for parameters such as model so static extraction can construct the object graph.

Option B — SDK

Call adapter extract_topology for topology-only output or build_agent_manifest for tools plus topology output on the root agent:

from pipelines.odyssey.adapters.openai_agents import (
    build_agent_manifest,
    extract_topology,
)

root = build_root_agent()

# Just the declared topology slice:
print(extract_topology(root))
# {'sub_agents': [...], 'topology': {'entry': 'triage'}}

# Full registration payload (tools_schema + sub_agents + topology):
print(build_agent_manifest(root))

Traversal is breadth-first over the framework handoff graph. A supervisor with empty tools still inherits the union of sub-agent tools in manifest output. A single-agent root with no handoffs returns an empty declaration.

Option C — Dashboard

On the agent's page, open the Topology editor and add each sub-agent (its actor_id, owned tools, and talks_to targets) and the entry sub-agent — or paste the CLI/SDK JSON into the Import JSON dialog.

Option D — Raw API

If you're scripting registration without the SDK, set the topology directly with PUT /api/agents/{id}. The endpoint merges the submitted config on top of the stored one (shallow, per top-level key), so sending only sub_agents + topology preserves everything else — endpoint_url, auth, tools_schema. That's why a later endpoint repoint (a dev-tunnel swap, say) doesn't wipe your topology, and vice versa.

curl -X PUT "$PIPELINES_BASE_URL/api/agents/42" \
  -H "Authorization: Bearer $PIPELINES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "config": {
      "sub_agents": [
        { "actor_id": "triage",  "tools": ["lookup_order"], "talks_to": ["refunds", "billing"] },
        { "actor_id": "refunds", "tools": ["issue_refund"] },
        { "actor_id": "billing", "tools": ["charge"] }
      ],
      "topology": { "entry": "triage" }
    }
  }'

Unlike credential creation, this route accepts the pk_live_ org API key (same as POST /agents and /tool-endpoints). To set the topology at create time instead, include the same sub_agents / topology keys in the config of your POST /api/agents body.

The merge clears a key only when you send it explicitly as null. To drop a topology entirely, PUT {"config": {"sub_agents": null, "topology": null}}.

Step 2: Stamp the acting sub-agent at run time

Topology declaration describes potential structure. To capture actual behavior, each proxy tool call should carry actor_id of the acting sub-agent. The recommended method is structural binding through a per-sub-agent handle.

envelope.for_actor with an actor id string returns a handle scoped to one sub-agent. Build each sub-agent tool from its own handle by passing actor to adapter tool factories. Every call emitted by those tools is stamped with that label in the same frame as the proxy call. This avoids runtime caller inference from framework internals and keeps attribution deterministic across async boundaries.

from agents import Agent, Runner
from pipelines.odyssey import register_dispatch_route, async_proxy_call
from pipelines.odyssey.adapters.openai_agents import (
    pipelines_function_tool,
    pipelines_run_hooks,
)


def build_agents(envelope):
    refund = envelope.for_actor("refund_agent")
    billing = envelope.for_actor("billing_agent")

    @pipelines_function_tool(actor=refund, name_override="issue_refund")
    async def issue_refund(order_id: str) -> dict:
        return await async_proxy_call("issue_refund", {"order_id": order_id})

    # The SAME shared tool, built once per owner → each copy stamps its own label.
    @pipelines_function_tool(actor=refund, name_override="search_policy")
    async def refund_search(query: str) -> dict:
        return await async_proxy_call("search_policy", {"query": query})

    @pipelines_function_tool(actor=billing, name_override="search_policy")
    async def billing_search(query: str) -> dict:
        return await async_proxy_call("search_policy", {"query": query})

    refund_agent = Agent(name="refund_agent", tools=[issue_refund, refund_search])
    billing_agent = Agent(name="billing_agent", tools=[billing_search])
    return Agent(name="supervisor", handoffs=[refund_agent, billing_agent])


@register_dispatch_route(app, agent_token_env="AGENT_TOKEN")
async def run(envelope):
    result = await Runner.run(
        build_agents(envelope),
        envelope.user_instruction,
        hooks=pipelines_run_hooks(),   # records handoff edges
    )
    return result.final_output

The same actor= parameter is on every adapter's tool/loop factory:

FrameworkBind a sub-agent's tools
OpenAI Agentspipelines_function_tool(actor=h, ...) / proxy_tool(name, actor=h)
LangChain / LangGraphproxy_tool(name, actor=h, ...) / pipelines_proxy(actor=h)
Anthropicrun_anthropic_loop(actor=h, ...)
Strandsproxy_tool(name, actor=h)

Shared tools require explicit per-owner binding. Ownership alone cannot disambiguate which sub-agent invoked a shared tool. Build one tool instance per owner handle so per-call labels remain unambiguous. Reuse tool logic, not handle objects. Use verify_actor_tools in factory code to catch copied handles before execution.

for_actor validates labels against the declared run catalog, based on registered sub_agents sent on each dispatch. Off-catalog labels emit warnings but calls still execute. The UI marks those nodes as undeclared.

Add the handoff timeline

pipelines_run_hooks emits a handoff edge on each control transfer in OpenAI integrations, enabling ordered handoff timelines. Frameworks without native handoff events can record edges explicitly through post_handoff calls. Attribution remains independent of handoff emission because actor stamping occurs on tool calls.

Fallback: runtime discovery

If actor is not provided, OpenAI and LangChain adapters fall back to framework context inference, using live ToolContext.agent or active langgraph node. This is zero-configuration but best-effort. Prefer explicit for_actor binding and use discovery only when per-sub-agent handle wiring is impractical.

Keep declared and runtime labels aligned

Declared actor_id values and runtime-stamped labels must match exactly. Otherwise, one logical sub-agent can appear twice, once declared and once undeclared. With for_actor, runtime label is exactly the passed literal string. Use declared actor_id values directly to avoid drift.

When topology is extracted through CLI, discovery fallback labels derive from agent-name slug defaults. If these do not match desired actor_id labels, pass a name_to_actor resolver to both extract_topology and adapter configuration.

When using for_actor with pipelines odyssey dump-agent, remember that CLI extraction builds from a zero-argument factory, while for_actor requires run-scoped envelope. Use an optional envelope parameter in factory design so envelope=None yields structural graph for extraction and envelope-provided calls wire actor handles for runtime dispatch.

Actor-ID label rules

An actor_id is a slash-delimited call path. The leaf segment is acting sub-agent and the prefix encodes ancestry, for example supervisor/refund_worker:

RuleValue
Charset per segment[a-zA-Z0-9_.:-]
Max segment length64
Max depth (segments)8
Max total length256

Malformed labels fail fast in local code because SDK validates before emission. This avoids opaque proxy 400 failures mid-run. Out-of-catalog labels are accepted and surfaced as undeclared in UI.

Step 3: Read the agent graph

Open the run in the trace tab. When the run is multi-agent the Agent Trace tab renders a Multi-agent structure card with nodes for sub-agents, delegation edges, ordered handoff edges, and a handoff timeline. Every node and edge is tagged with its provenance so you can tell intent from behavior:

ProvenanceMeaning
bothdeclared and observed acting at run time
observedobserved at run time but not declared, shown as undeclared
derivednot explicitly labeled, but call credited because tool has exactly one declared owner
declareddeclared but never acted, shown as declared-only or idle

The card also surfaces the honesty metrics:

  • Undeclared sub-agents, observed at run time but missing from topology (a label mismatch, or a dynamically-constructed sub-agent).
  • Declared-only sub-agents, declared but idle in this run.
  • Unattributed tool calls, calls without actor_id where tool has no single declared owner and therefore no safe derivation. High counts indicate incomplete attribution. Wrap more tools in Step 2 or declare tool ownership (Step 1).

When graph data is present, the judge receives a multi-agent evidence block and scores delegation and handoff structure in addition to output quality. Single-agent runs omit this block.

LangGraph is supported through extract_topology and pipelines odyssey dump-agent --framework langchain entry points by targeting compiled StateGraph or Pregel factories. Other frameworks, including Anthropic, Strands, and custom HTTP stacks, can still attribute calls by setting the X-Pipelines-Actor-Id header directly and posting handoff trace events.

Troubleshooting

SymptomCause and fix
Flat timeline, no graphNo actor_id stamping and no topology declaration. Implement Step 2 and optionally Step 1.
A sub-agent appears twiceDeclared and runtime labels differ. Use identical actor_id values in declaration and for_actor wiring, or align name_to_actor resolver when using discovery.
Many unattributed callsTools are not bound with for_actor and not wrapped with adapter tool wrappers, or shared tools rely on derivation. Build shared tools per owner with explicit actor binding.
A declared agent shows declared-onlyAgent did not act in this run, or name slug defaults differ from declared actor_id.
ActorLabelError at run timeLabel violates actor_id rules such as charset or depth. Correct label or name_to_actor resolver.

Next