Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

9.3 KiB
Raw Permalink Blame History

Sub-agent routing

Canonical orchestration path for ACP sub-agents. ACP-spawned sessions route through AcpService and SubAgentRouter.

Goals

  1. Origin tracking. When the main agent spawns a sub-agent in response to a user message in room R, the sub-agent's terminal output (task_complete, error, blocked) lands back in room R, addressed appropriately.
  2. Main-agent-in-the-loop. When a sub-agent reports done, the main agent — not a separate coordinator — decides whether to:
    • reply to the user (sub-agent finished, here's the result),
    • reply to the sub-agent via SEND_TO_AGENT (proof not satisfying; keep going),
    • or both in one turn.
  3. Cache friendliness. Sub-agent updates should not invalidate the stable provider/system prefix on every event.

Components

AcpService

Spawn surface. TASKS op=create records origin context in session.metadata at spawn time:

{
  messageId: message.id,    // parent message UUID
  roomId:    taskRoomId,    // task-owner room, defaults to message.roomId
  originRoomId: message.roomId,
  taskRoomId,
  worktreeRoomId,
  swarmRooms: [
    { roomId: taskRoomId, roles: ["task"] },
    { roomId: worktreeRoomId, roles: ["worktree"] },
  ],
  worldId:   message.worldId,
  userId:    message.entityId,
  label,
  source:    content.source,
}

SubAgentRouter (services/sub-agent-router.ts)

Subscribes to AcpService.onSessionEvent. On task_complete, error, or blocked (boundary events only — not streaming chunks), it:

  1. Reads session.metadata for origin and swarm-room keys.
  2. Constructs a synthetic Memory with:
    • entityId = a deterministic per-session sub-agent UUID derived locally via SHA1 of <runtime.agentId>:acpx:sub-agent:<sessionId> (no runtime dependency on @elizaos/core's createUniqueUuid so the router stays type-only on core),
    • agentId = runtime.agentId,
    • roomId = the selected swarm target room,
    • content.source = "sub_agent",
    • content.inReplyTo = origin messageId,
    • content.metadata.subAgent* carries the structured event (subAgentSessionId, subAgentLabel, subAgentEvent, subAgentStatus, subAgentAgentType, subAgentRoundTrip, subAgentRoundTripCap, subAgentCapExceeded, originUserId, originMessageId, originSource).
    • content.metadata.subAgentRoutingKind, subAgentTargetRoomId, subAgentTargetRoomRole, subAgentTargetRoomRoles, taskRoomId, worktreeRoomId, and subAgentSwarmRooms tell the main agent why this memory landed in this room and which other swarm room(s) exist for the task.
  3. Delivers via runtime.messageService.handleMessage(runtime, memory, callback), which also persists the memory. If messageService is unavailable, it falls back to runtime.createMemory(..., "messages") plus MESSAGE_RECEIVED.

For platform-originated tasks, the router builds a short-lived callback from runtime.sendMessageToTarget so the planner's answer can return to the same selected swarm room.

Why only boundary events

Streaming events (agent_message_chunk, tool_running, ready) would re-fire the planner constantly and burn the prompt cache. Live status is exposed via the provider instead. The router is the channel for events that warrant an action decision.

Dedup / idempotency

Events are deduped in-memory by <sessionId>|<event>|<status>|<short hash of payload>. Same sub-agent re-emitting the same task_complete payload posts once. A different response payload posts again — that's "the sub-agent did more work and reported a new state".

Swarm target rooms are also normalized before posting. taskRoomId is first, worktreeRoomId is second, duplicate room IDs collapse into one target, and the collapsed target keeps both roles (["task", "worktree"]). That means a task room that is also the worktree coordination room gets one useful message rather than two identical messages with ambiguous purpose.

Routing kinds

Most terminal events use subAgentRoutingKind: "TASK_STATUS" and fan out to the normalized task/worktree swarm rooms. Two explicit coordination events are targeted:

  • QUESTION_FOR_TASK_CREATOR routes only to the task room and carries subAgentTargetRoomRole: "task".
  • AGENT_COORDINATION routes to the worktree room when present, otherwise the task room, and carries the selected target role.

blocked events default to QUESTION_FOR_TASK_CREATOR because the sub-agent is waiting on human or parent-agent input.

Disable switch

ACPX_SUB_AGENT_ROUTER_DISABLED=1 keeps the service registered but unbound (useful for tests, headless backfills, or staging where you want spawning without runtime injection).

Round-trip cap

To prevent ping-pong loops where the main agent and a sub-agent endlessly ask each other to keep going, the router tracks per-session inject count. When the count exceeds ACPX_SUB_AGENT_ROUND_TRIP_CAP (default 32) the router force-stops the session and emits a single round_trip_cap_exceeded memory carrying subAgentRoundTrip, subAgentRoundTripCap, and subAgentCapExceeded: true. Subsequent events from the same capped session are suppressed.

Set ACPX_SUB_AGENT_ROUND_TRIP_CAP=N in the runtime config to override. The default of 32 is generous; a typical sub-agent task hits 15 round-trips before terminal completion.

activeSubAgentsProvider (providers/active-sub-agents.ts)

Cache-friendly view of live sub-agent sessions. Filters to:

  • sessions whose metadata.roomId is set (i.e. routed by createTaskAction),
  • sessions not in a terminal status (stopped, completed, error, errored, cancelled).

The text is structural only — id, label, agentType, bucketed status, last two workdir segments. No timestamps, no message excerpts. Sorted by sessionId so the rendered text is byte-stable across turns when the active set is unchanged.

Status bucketing: ready, running, busy, tool_running, and authenticating all collapse to the literal string "active" in the provider text. blocked is preserved as a distinct value (the planner needs to know a session is waiting for input). Terminal statuses (stopped, completed, error, errored, cancelled) cause the session to be filtered out entirely. This keeps the cached provider segment byte-identical across transient status flips like ready → tool_running → ready, which would otherwise invalidate the prefix cache on every tool call.

This is the live status channel. The synthetic Memory posted by the router is the per-event channel.

Action set

The main agent's planner sees:

  • REPLY (from the bootstrap action set) — replies to the user in current room.
  • SEND_TO_AGENT { sessionId, text } — pushes a follow-up to a live sub-agent. Use when the sub-agent's proof is unsatisfying or it asked a clarifying question.
  • STOP_AGENT { sessionId } — terminates. Use when the sub-agent's output is clearly final and you don't want it idling.
  • ACPX_CREATE_TASK — spawn additional sub-agents.

Multi-action plans (e.g. [REPLY, SEND_TO_AGENT]) are supported by the planner and execute sequentially in one turn.

Cache discipline

Anthropic prompt caching breaks at segment boundaries (see plugin-anthropic/models/text.ts). The plugin marks providers and action examples as stable: true. Sub-agent flow is designed around this:

  • Stable prefix (cached): system prompt, character bio, action examples, active-sub-agents provider text (structural only, sorted, deterministic).
  • Volatile suffix (re-tokenized): the sub-agent's synthetic message text (the per-event narration).

Each new sub-agent event invalidates only the message tail. The provider text changes only when a session enters or leaves the active set, not on every chunk.

A per-session router invocation is one cache-miss tail; everything before the most recent turn stays warm.

Loop safety

  • The router emits inbound memories with entityId set to the sub-agent's pseudo-UUID (not runtime.agentId), so the runtime processes them as messages from another entity, not as the agent's own outputs.
  • The main agent's reply via SEND_TO_AGENT does not directly trigger a new task_complete. The sub-agent has to actually do work first, which bounds re-entry.
  • Dedup prevents accidental double-injection from event re-emission.
  • The round-trip cap (above) is the hard ceiling for ping-pong loops.

ACP Boundary

plugin-agent-orchestrator has a single task-agent transport: AcpService. There is no PTY/coordinator fallback path.

Autonomous follow-up decisions are handled by the main agent's normal action selection over the synthetic Memory emitted by SubAgentRouter.

Testing

  • __tests__/unit/sub-agent-router.test.ts — origin tracking, dedup, streaming-event filtering, disable switch, error narration, fallback emit, unsubscribe.
  • __tests__/unit/active-sub-agents.test.ts — origin filtering, terminal exclusion, deterministic sort, no volatile fields, action-hint text.