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

8.4 KiB

Multi-account coding-agent orchestration

Goal: an Eliza orchestrator agent that runs multiple Claude Code + Codex subscriptions (and rotates OpenCode across pooled Cerebras accounts), picks the least-used account for each new sub-agent, tracks per-account session + weekly usage, manages those sub-agents in a shared task room, and decides when to interrupt a running sub-agent vs. let it keep working.

What already existed (do not rebuild)

Layer Location Status
Account contracts (LinkedAccountConfig, LinkedAccountUsage, 12 provider ids) packages/contracts/src/service-routing.ts
Credential storage (<stateDir>/auth/{providerId}/{accountId}.json) packages/agent/src/auth/account-storage.ts
OAuth flows (Anthropic + Codex) + coding-plan keys + API keys packages/agent/src/auth/oauth-flow.ts, credentials.ts
AccountPool — priority / round-robin / least-used / quota-aware, affinity, health packages/app-core/src/services/account-pool.ts
Usage probes (pollAnthropicUsage, pollCodexUsage) + JSONL day counters packages/app-core/src/services/account-usage.ts
Accounts REST API (/api/accounts/* incl. OAuth SSE) packages/agent/src/api/accounts-routes.ts
Settings UI (AccountList / AccountCard / AddAccountDialog / RotationStrategyPicker) + useAccounts packages/ui/src/components/accounts/*
Multi-account API-key routing for the main agent credential-resolver.tsresolveProviderCredentialMulti
Orchestrator tasks, sessions, task-rooms, event bridge, usage rollup, REST + SSE plugins/plugin-agent-orchestrator/*
shouldRespond (RESPOND/IGNORE/STOP) + per-room TurnControllerRegistry abort packages/core/src/*

The keystone gap

The AccountPool was wired into API-key model routing but not into the coding-agent spawn path. Coding agents authenticate by subscription, not API key: applySubscriptionCredentialsLocal never injects Claude/Codex tokens into process.env, and AcpService.buildEnv even strips an OAuth ANTHROPIC_API_KEY so Claude Code falls back to the single machine login (~/.claude). So every spawned sub-agent used one account, with no rotation and no per-account attribution.

The orchestrator plugin depends only on @elizaos/core, so it cannot import the pool. account-pool.ts already solves this with globalThis-symbol bridges (Anthropic + subscription-selector). We add a third: a coding-agent selector bridge.

Hitlist

P0 — Keystone: account selection on spawn (round-robin / least-used)

  • coding-account-bridge.ts (app-core): install the shared CODING_AGENT_SELECTOR_BRIDGE_SYMBOL bridge — select(agentType), markRateLimited, markNeedsReauth, recordUsage, describe().
  • Per-agent credential injection: claude → CLAUDE_CODE_OAUTH_TOKEN; codex → per-account CODEX_HOME/auth.json + minimal config.toml; opencode → CEREBRAS_API_KEY (pooled Cerebras); direct API providers → their env key.
  • Wire into AcpService.spawnSession: select before transport branch, merge envPatch into customCredentials, stamp session.metadata.account*, surface on SpawnResult.metadata.
  • buildEnv: when CLAUDE_CODE_OAUTH_TOKEN is injected for claude, drop ANTHROPIC_API_KEY so the selected subscription wins.
  • OrchestratorTaskSession carries accountProviderId / accountId / accountLabel; populate from result.metadata.
  • Exclude-on-failure retry: on auth/rate-limit error, mark account + re-select excluding it.

P1 — Usage attribution + stats

  • recordUsage also calls the bridge → pool recordCall + account-usage JSONL keyed by the serving account.
  • /api/orchestrator/accounts route: connected accounts + live usage + which sub-agents are on which account.
  • Settings already shows per-account usage; orchestrator dashboard widget adds the accounts + usage summary.

P2 — Room system + interruption decider

  • Per-participant interruption decider: a running sub-agent keeps working; a new room message is classified (deliver / queue / interrupt / ignore) before it is injected. The "busy" check covers tool_running (the dominant mid-turn state), not just busy; queued messages flush when the session returns to ready. An Eliza participant's shouldRespond verdict threads through unchanged when a caller supplies it; coding sub-agents use the structural decider. multiParty (more than one live sub-agent in the room) is computed in the forward handler so ambient-chatter suppression is live.
  • Task-room participant view: the "Coding accounts" sidebar widget shows the live sub-agent → account assignment map + per-account usage (the flat global map). The dedicated per-room participant roster (orchestrator + owning user + each sub-agent grouped by room, with activeAgentCount / multiParty) now ships as GET /api/orchestrator/rooms (OrchestratorTaskService.getRoomRoster), the room-scoped counterpart to /accounts. Rooms with no sub-agent sessions are omitted.

P3 — Tests + mocks + live E2E

  • Mock account fixtures (multi-account, multi-provider) + an in-memory pool for unit tests.
  • Unit tests: selection strategy, per-agent env injection + API-key drop (Claude & Codex), intra-provider retry, usage attribution, interruption decider, route registration.
  • Live E2E (gated ORCHESTRATOR_LIVE_MULTI_ACCOUNT=1): validates the selector bridge + usage probe against ≥2 real linked accounts (distinct-account rotation via the exclude set; live session/weekly usage).
  • Composed offline E2E (bun run --cwd plugins/plugin-agent-orchestrator test:e2e:multi-account): real app-core AccountPool (real-format mock accounts in a temp state dir) → installed selector bridge → orchestrator AcpService.spawnSession / sendToSession → a REAL spawned subprocess (a fake "acpx" standing in for the subscription CLI). Asserts 11/11: two Claude spawns pick distinct accounts (least-used round-robin), each subprocess receives its account's CLAUDE_CODE_OAUTH_TOKEN, a follow-up prompt for the first Claude session reuses that session's selected account token, the parent ANTHROPIC_API_KEY/OPENAI_API_KEY are dropped, Codex gets a per-account CODEX_HOME/auth.json with the right token+account_id. Point ELIZA_ACP_CLI at the real binary + connect real accounts for true live-key validation. (This harness caught a real cli-transport bug: runAcpx rebuilt the env without agentType, re-adding the parent keys — now fixed.)

P4 — Connect-accounts window

  • The connect-accounts window (Settings → Accounts: AddAccountDialog OAuth / API-key / coding-plan-key flows, AccountList, RotationStrategyPicker) pre-exists and is the surface for linking multiple accounts of each type.

Known constraints / follow-ups

  • OpenCode pool-rotates across Cerebras accounts only. OpenCode resolves a pooled key for exactly one backend — Cerebras (CEREBRAS_API_KEY, see buildOpencodeSpawnConfig) — so opencode is a multi-account selector type mapped to cerebras-api: it least-used-rotates across linked Cerebras accounts (the bridge injects the selected CEREBRAS_API_KEY, which OpenCode's config reads) and no-ops when none are linked (Eliza Cloud / single-key setups are unchanged). OpenCode's other backends (Eliza Cloud, local, user-configured opencode.json) are not pooled. Precedence: a CEREBRAS_API_KEY runtime setting still wins over a pooled injection — pooling is authoritative only when no single key is configured.
  • z.ai / Kimi / GLM have no first-party coding CLI. Their linked accounts serve the main runtime's API-key routing (resolveProviderCredentialMulti for zai-api / moonshot-api) and OpenCode's provider config — there is no zai/kimi/glm spawnable agent type, so they are not advertised as coding-agent selector candidates.

Quality bar

  • No regression when zero accounts are linked (bridge returns null → today's behavior).
  • Selected account is observable (session metadata + structured log + dashboard), never assumed.
  • Subscription tokens only ever flow to the first-party coding subprocess (TOS), never into runtime process.env.
  • Per-agent credential precedence is enforced: a selected Claude subscription drops ANTHROPIC_API_KEY; a selected Codex subscription (per-account CODEX_HOME) drops a forwarded OPENAI_API_KEY — so the chosen account always authenticates.