项目文件夹

文件
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

228 行
9.3 KiB
Markdown

# 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:
```ts
{
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 1–5
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.
## Related files
- [src/services/sub-agent-router.ts](../src/services/sub-agent-router.ts)
- [src/providers/active-sub-agents.ts](../src/providers/active-sub-agents.ts)
- [src/services/acp-service.ts](../src/services/acp-service.ts)
- [src/actions/create-task.ts](../src/actions/create-task.ts)
- [src/actions/send-to-agent.ts](../src/actions/send-to-agent.ts)