From 1e477804c638c57118e90f524b96e95a3764d802 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Thu, 15 Oct 2026 17:42:41 +0800 Subject: [PATCH 001/212] fix(checks): preserve Cordis preset and event identifiers --- scripts/rescope-vendor.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/scripts/rescope-vendor.ts b/scripts/rescope-vendor.ts index e6292fb2a5..bbb36682b2 100644 --- a/scripts/rescope-vendor.ts +++ b/scripts/rescope-vendor.ts @@ -98,6 +98,22 @@ const GENERIC_SKIPS: readonly GenericSkip[] = [ { file: 'apps/cli/tests/profiles/web/tests/fixtures/creator-plugin-manager.mjs', upstream: ['cordis'] }, { file: 'apps/web/tests/agent-preset-authoring.e2e.ts', upstream: ['cordis'] }, { file: 'packages/preset/agent-preset-registry/tests/session.spec.ts', upstream: ['cordis'] }, + { file: 'apps/cli/tests/optional-bundle-transitions.e2e.ts', upstream: ['cordis'] }, + { file: 'apps/web/tests/agent-preset-selection.e2e.ts', upstream: ['cordis'] }, + { file: 'apps/web/tests/developer-tools-settings.e2e.ts', upstream: ['cordis'] }, + { file: 'docs/subsystems/schedule.md', upstream: ['cordis'] }, + { file: 'docs/subsystems/schedule.zh.md', upstream: ['cordis'] }, + { file: 'docs/upgrade-guide/v0.2.0-rc.2/schedule-bundle-retired/guide.md', upstream: ['cordis'] }, + { file: 'docs/upgrade-guide/v0.2.0-rc.2/schedule-bundle-retired/guide.zh.md', upstream: ['cordis'] }, + { file: 'docs/upgrade-guide/v0.2.1-alpha.1/subagent-preset-composition/guide.md', upstream: ['cordis'] }, + { file: 'docs/upgrade-guide/v0.2.1-alpha.1/subagent-preset-composition/guide.zh.md', upstream: ['cordis'] }, + { file: 'docs/user/guide/schedule.md', upstream: ['cordis'] }, + { file: 'docs/user/guide/schedule.zh.md', upstream: ['cordis'] }, + { file: 'packages/bundle/web-app/cordis.patch.yml', upstream: ['cordis'] }, + { file: 'packages/client/ui-agent-preset/src/client/CreatePluginMenuItem.tsx', upstream: ['cordis'] }, + { file: 'packages/client/ui-agent-preset/tests/components.client.spec.tsx', upstream: ['cordis'] }, + { file: 'packages/client/ui-agent-preset/tests/create-plugin-menu-item.client.spec.tsx', upstream: ['cordis'] }, + { file: 'packages/client/ui-agent-preset/tests/section-store.client.spec.ts', upstream: ['cordis'] }, // The preset-roster loop names the `cordis` preset id, not a package. { file: 'apps/cli/tests/windows-shell.spec.ts', upstream: ['cordis'] }, // GROUP_ORDER holds `packages//` directory names, not package names. @@ -124,6 +140,9 @@ const GENERIC_SKIPS: readonly GenericSkip[] = [ { file: 'packages/extensions/tool-cordis/src/providers.ts', upstream: ['cordis'] }, { file: 'packages/extensions/ui-cordis/src/client/index.ts', upstream: ['cordis'] }, { file: 'packages/extensions/ui-cordis/src/client/inventory.ts', upstream: ['cordis'] }, + { file: 'packages/extensions/cordis-host-runner/tests/inspect-registry.spec.ts', upstream: ['cordis'] }, + { file: 'snapshots/session/cordis-inspect-liveness/client-fixture.mjs', upstream: ['cordis'] }, + { file: 'snapshots/session/cordis-inspect-timeout/client-fixture.mjs', upstream: ['cordis'] }, // `cordis/tree` is an Inspector observation topic, not a package subpath. { file: 'packages/experimental/inspector/src/shared/bridge/messages/cordis.ts', upstream: ['cordis'] }, { file: 'packages/experimental/inspector/tests/cordis-query.host.spec.ts', upstream: ['cordis'] }, From 11479fca1346d12b4053a4be95c3b0983d1ad855 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Thu, 15 Oct 2026 17:43:51 +0800 Subject: [PATCH 002/212] refactor(team): reuse subagents with id-only addressing --- .agents/notes/archived/manifest.json | 3 + .../2026-09-26-team-direct-inbox.i18n.yaml | 6 + .../2026-09-26-team-direct-inbox.md | 1 + .../2026-09-26-team-direct-inbox.zh.md | 1 + ...7-adjacent-agent-steer-messaging.i18n.yaml | 4 +- ...26-08-27-adjacent-agent-steer-messaging.md | 7 +- ...08-27-adjacent-agent-steer-messaging.zh.md | 7 +- ...13-team-identity-runtime-context.i18n.yaml | 8 +- ...026-10-13-team-identity-runtime-context.md | 6 +- ...-10-13-team-identity-runtime-context.zh.md | 6 +- ...6-10-15-agent-teams-as-subagents.i18n.yaml | 22 + .../2026-10-15-agent-teams-as-subagents.md | 39 + .../2026-10-15-agent-teams-as-subagents.zh.md | 39 + .../feature/2026-08-05-agent-teams.i18n.yaml | 36 +- .../feature/2026-08-05-agent-teams.md | 38 +- .../feature/2026-08-05-agent-teams.zh.md | 38 +- .../2026-09-26-team-direct-inbox.i18n.yaml | 19 - apps/cli/tests/agent-team-headless.e2e.ts | 7 +- .../profiles/headless/team-snapshot.patch.yml | 21 - .../headless/tests/fixtures/team-llm.mjs | 42 +- .../headless/tests/headless.expected.e2e.ts | 44 +- apps/web/tests/agent-team-panel.e2e.ts | 6 +- apps/web/tests/agent-team-panel.overlay.yml | 18 - .../agent-team-panel/resize.expected.md | 4 +- .../agent-team-panel/task.expected.md | 4 +- docs/architecture.i18n.yaml | 4 +- docs/architecture.md | 2 +- docs/architecture.zh.md | 2 +- docs/config-catalog.md | 23 +- docs/config-catalog.zh.md | 23 +- docs/event-producer-consumer.md | 16 +- docs/event-producer-consumer.zh.md | 16 +- docs/module-graph.md | 3 +- docs/module-graph.zh.md | 3 +- docs/persistence-catalog.i18n.yaml | 68 +- docs/persistence-catalog.md | 36 +- docs/persistence-catalog.zh.md | 36 +- docs/persistence-schema.json | 28 +- docs/subsystems/agent-team.i18n.yaml | 24 +- docs/subsystems/agent-team.md | 74 +- docs/subsystems/agent-team.zh.md | 74 +- docs/subsystems/subagent.i18n.yaml | 4 +- docs/subsystems/subagent.md | 8 +- docs/subsystems/subagent.zh.md | 8 +- docs/tool-catalog.i18n.yaml | 26 +- docs/tool-catalog.md | 115 +- docs/tool-catalog.zh.md | 118 +- .../agent-team-subagents/guide.i18n.yaml | 16 + .../agent-team-subagents/guide.md | 26 + .../agent-team-subagents/guide.zh.md | 26 + .../core/tools/tests/gen-tool-catalog.spec.ts | 8 +- .../agent-team-profile/README.i18n.yaml | 36 +- .../experimental/agent-team-profile/README.md | 25 +- .../agent-team-profile/README.zh.md | 25 +- .../agent-team-profile/cordis.patch.yml | 23 +- .../agent-team-profile/tests/profile.spec.ts | 14 +- .../experimental/agent-team/README.i18n.yaml | 123 +- packages/experimental/agent-team/README.md | 121 +- packages/experimental/agent-team/README.zh.md | 121 +- .../experimental/agent-team/locale/en.json | 2 +- .../experimental/agent-team/locale/zh.json | 2 +- packages/experimental/agent-team/package.json | 3 +- .../experimental/agent-team/src/activity.ts | 15 +- .../experimental/agent-team/src/client.ts | 1 - packages/experimental/agent-team/src/error.ts | 12 - packages/experimental/agent-team/src/index.ts | 201 +-- .../experimental/agent-team/src/lifecycle.ts | 107 -- .../experimental/agent-team/src/membership.ts | 99 ++ .../experimental/agent-team/src/persisted.ts | 34 - .../experimental/agent-team/src/projection.ts | 83 +- .../experimental/agent-team/src/roster.ts | 477 -------- .../agent-team/src/session-message.ts | 31 - .../experimental/agent-team/src/task-board.ts | 15 +- .../experimental/agent-team/src/task-view.ts | 15 +- packages/experimental/agent-team/src/types.ts | 70 +- .../agent-team/tests/persistence.spec.ts | 244 +--- .../tests/projection-events.spec.ts | 66 +- .../agent-team/tests/team.spec.ts | 1081 +++-------------- .../experimental/agent-team/tsconfig.json | 1 - .../client-ui-agent-team/README.i18n.yaml | 12 +- .../client-ui-agent-team/README.md | 6 +- .../client-ui-agent-team/README.zh.md | 6 +- .../src/client/TeamAction.tsx | 18 +- .../src/client/locales.ts | 2 + .../client-ui-agent-team/src/client/mount.ts | 6 +- .../src/client/team-root.ts | 29 + .../tests/browser-plugin.client.spec.ts | 31 +- .../tests/team-action.client.spec.tsx | 58 +- .../tool-agent-team/README.i18n.yaml | 99 +- .../experimental/tool-agent-team/README.md | 96 +- .../experimental/tool-agent-team/README.zh.md | 96 +- .../tool-agent-team/locale/en.json | 2 +- .../tool-agent-team/locale/zh.json | 2 +- .../experimental/tool-agent-team/src/index.ts | 203 +--- .../tool-agent-team/tests/tool-team.spec.ts | 491 +++----- .../extensions/tool-cordis/src/api-catalog.ts | 68 +- .../references/packages.md | 2 +- packages/subagent/subagent/README.i18n.yaml | 12 +- packages/subagent/subagent/README.md | 6 +- packages/subagent/subagent/README.zh.md | 6 +- packages/subagent/subagent/src/catalog.ts | 21 +- packages/subagent/subagent/src/index.ts | 7 +- packages/subagent/subagent/src/internal.ts | 28 - packages/subagent/subagent/src/manager.ts | 58 +- .../subagent/subagent/tests/catalog.spec.ts | 14 + .../subagent/tests/sibling-messages.spec.ts | 150 +++ .../tool-subagent-control/README.i18n.yaml | 64 +- .../subagent/tool-subagent-control/README.md | 32 +- .../tool-subagent-control/README.zh.md | 32 +- .../tool-subagent-control/src/index.ts | 2 +- .../tool-subagent-control/src/list-agents.ts | 72 +- .../tests/list-agents.spec.ts | 58 +- .../tests/tool-subagent-control.spec.ts | 2 +- scripts/gen-cordis-catalog.ts | 6 +- scripts/gen-tool-catalog.ts | 5 +- scripts/no-unknown-casts.baseline.json | 5 +- .../sdk/bash-tool/tool-schemas.expected.json | 6 +- .../tool-schemas.1.expected.json | 6 +- .../tool-schemas.1.expected.json | 6 +- .../tool-schemas.1.expected.json | 6 +- .../tool-schemas.expected.json | 6 +- .../tool-schemas.1.expected.json | 6 +- .../tool-schemas.expected.json | 6 +- .../tool-schemas.1.expected.json | 6 +- .../tool-schemas.1.expected.json | 6 +- .../sdk/text-turn/tool-schemas.expected.json | 6 +- .../system-prompt.expected.md | 12 +- .../tool-schemas.expected.json | 6 +- .../system-prompt.expected.md | 12 +- .../tool-schemas.expected.json | 12 +- .../tool-schemas.expected.json | 12 +- .../tool-schemas.expected.json | 6 +- .../both-mode-turn/system-prompt.expected.md | 12 +- .../tool-schemas.expected.json | 6 +- .../tool-schemas.expected.json | 6 +- .../tool-schemas.expected.json | 6 +- .../tool-schemas.expected.json | 6 +- .../cordis.snapshot.yml | 2 +- .../compaction-output-reserve/cordis.yml | 2 +- .../session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 12 +- .../cordis.snapshot.yml | 2 +- .../compaction-summary-headroom/cordis.yml | 2 +- .../session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 6 +- .../tool-schemas.expected.json | 6 +- .../tool-schemas.expected.json | 6 +- .../cua-native/tool-schemas.expected.json | 6 +- .../session/cua-ptc/system-prompt.expected.md | 12 +- .../tool-schemas.expected.json | 6 +- .../tool-schemas.expected.json | 6 +- .../tool-schemas.expected.json | 18 +- .../tool-schemas.expected.json | 6 +- .../tool-schemas.expected.json | 6 +- .../lsp-definition/tool-schemas.expected.json | 6 +- .../system-prompt.expected.md | 12 +- .../mcp-resources/tool-schemas.expected.json | 6 +- .../tool-schemas.expected.json | 6 +- .../tool-schemas.expected.json | 6 +- .../system-prompt.expected.md | 12 +- .../office-skills/tool-schemas.expected.json | 6 +- .../tool-schemas.expected.json | 6 +- .../tool-schemas.expected.json | 12 +- .../plugin-manager/tool-schemas.expected.json | 6 +- .../tool-schemas.expected.json | 6 +- .../tool-schemas.expected.json | 6 +- .../tool-schemas.expected.json | 6 +- .../provider-cwd/tool-schemas.expected.json | 6 +- .../system-prompt.expected.md | 12 +- .../system-prompt.expected.md | 12 +- .../ptc-persistent/system-prompt.expected.md | 12 +- .../system-prompt.expected.md | 12 +- .../ptc-python-turn/system-prompt.expected.md | 17 +- .../ptc-read-image/system-prompt.expected.md | 12 +- .../ptc-turn/system-prompt.expected.md | 12 +- .../tool-schemas.expected.json | 6 +- .../pwsh-tool-turn/tool-schemas.expected.json | 6 +- .../ralph-loop/tool-schemas.1.expected.json | 6 +- .../ralph-loop/tool-schemas.2.expected.json | 6 +- .../ralph-loop/tool-schemas.expected.json | 6 +- .../tool-schemas.expected.json | 6 +- .../tool-schemas.expected.json | 6 +- .../tool-schemas.1.expected.json | 6 +- .../tool-schemas.expected.json | 6 +- .../tool-schemas.expected.json | 6 +- .../tool-schemas.1.expected.json | 4 +- .../tool-schemas.1.expected.json | 6 +- .../tool-schemas.expected.json | 6 +- .../tool-schemas.1.expected.json | 6 +- .../tool-schemas.expected.json | 6 +- .../team-direct-inbox/session.1.v4.jsonl | 8 +- .../team-direct-inbox/session.v4.jsonl | 34 +- .../session/team-targets/cordis.snapshot.yml | 10 - snapshots/session/team-targets/cordis.yml | 10 - .../session/team-targets/session.v4.jsonl | 16 +- .../team-targets/system-prompt.expected.md | 6 +- .../team-targets/tool-schemas.expected.json | 79 +- .../text-turn/tool-schemas.expected.json | 6 +- .../web-fetch/tool-schemas.expected.json | 6 +- .../tool-schemas.expected.json | 6 +- .../tool-schemas.expected.json | 6 +- .../worktree/tool-schemas.expected.json | 6 +- .../tool-schemas.expected.json | 6 +- .../system-prompt.expected.md | 12 +- .../system-prompt.expected.md | 12 +- .../web/ptc-round/system-prompt.expected.md | 12 +- 206 files changed, 2611 insertions(+), 4228 deletions(-) create mode 100644 .agents/notes/archived/simplification/2026-09-26-team-direct-inbox.i18n.yaml rename .agents/notes/{implemented => archived}/simplification/2026-09-26-team-direct-inbox.md (99%) rename .agents/notes/{implemented => archived}/simplification/2026-09-26-team-direct-inbox.zh.md (99%) create mode 100644 .agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.i18n.yaml create mode 100644 .agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md create mode 100644 .agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.zh.md delete mode 100644 .agents/notes/implemented/simplification/2026-09-26-team-direct-inbox.i18n.yaml create mode 100644 docs/upgrade-guide/v0.2.1-alpha.3/agent-team-subagents/guide.i18n.yaml create mode 100644 docs/upgrade-guide/v0.2.1-alpha.3/agent-team-subagents/guide.md create mode 100644 docs/upgrade-guide/v0.2.1-alpha.3/agent-team-subagents/guide.zh.md delete mode 100644 packages/experimental/agent-team/src/lifecycle.ts create mode 100644 packages/experimental/agent-team/src/membership.ts delete mode 100644 packages/experimental/agent-team/src/persisted.ts delete mode 100644 packages/experimental/agent-team/src/roster.ts delete mode 100644 packages/experimental/agent-team/src/session-message.ts create mode 100644 packages/experimental/client-ui-agent-team/src/client/team-root.ts create mode 100644 packages/subagent/subagent/tests/sibling-messages.spec.ts diff --git a/.agents/notes/archived/manifest.json b/.agents/notes/archived/manifest.json index 28ecf269b2..cec3377536 100644 --- a/.agents/notes/archived/manifest.json +++ b/.agents/notes/archived/manifest.json @@ -2656,6 +2656,9 @@ "simplification/2026-09-19-deepseek-messages-only.i18n.yaml": "sha256:7ffd708c5823c39cc9536178292e7e7fa57db5267bbc9acdda0a87e4953d5ba8", "simplification/2026-09-19-deepseek-messages-only.md": "sha256:241861200ce73e02f77d182633d6ea10b1f29c696c81bff002680762db01d9b5", "simplification/2026-09-19-deepseek-messages-only.zh.md": "sha256:311ef6f8f4cf60dae2a43ca94d772028b645631501fb3648aaeb406ba174991e", + "simplification/2026-09-26-team-direct-inbox.i18n.yaml": "sha256:9a6f1137f0c68d976fda4f44c5c301b851a8ee8e46965f3ea808ec5551045fb0", + "simplification/2026-09-26-team-direct-inbox.md": "sha256:88f7d9be3ae0b740ddb5a61a16e3c64044a207584b973acab8c3a40c36a7ff30", + "simplification/2026-09-26-team-direct-inbox.zh.md": "sha256:d6d1fafa4aa7035a11930cca3bf998d676a62b5f9b75176dff69c3c5ff6b92f0", "testing/2026-06-11-property-based-testing.i18n.yaml": "sha256:9d378912101ad30203f1fcbcad23e0f5d6ec6f39e04a83f17f90887b5c5d761e", "testing/2026-06-11-property-based-testing.md": "sha256:cca3b6299784cff94965f21b6d59966109a0bb41fdc2f8cf56653fa6c417a20a", "testing/2026-06-11-property-based-testing.zh.md": "sha256:09f3d1e2defd2792f87fb0e9892fb7e1db006f3c75ea4581f97e9cde4ea98175", diff --git a/.agents/notes/archived/simplification/2026-09-26-team-direct-inbox.i18n.yaml b/.agents/notes/archived/simplification/2026-09-26-team-direct-inbox.i18n.yaml new file mode 100644 index 0000000000..50788468df --- /dev/null +++ b/.agents/notes/archived/simplification/2026-09-26-team-direct-inbox.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/archived/simplification/2026-09-26-team-direct-inbox.md +2026-09-26-team-direct-inbox.md: 9b90d7ae30e0b711c602de9280cc11845d0525f0 +2026-09-26-team-direct-inbox.zh.md: 8838fb4a2824838b76142f30a6ff4d0aedf0276b diff --git a/.agents/notes/implemented/simplification/2026-09-26-team-direct-inbox.md b/.agents/notes/archived/simplification/2026-09-26-team-direct-inbox.md similarity index 99% rename from .agents/notes/implemented/simplification/2026-09-26-team-direct-inbox.md rename to .agents/notes/archived/simplification/2026-09-26-team-direct-inbox.md index 58d18d0ecf..9b90d7ae30 100644 --- a/.agents/notes/implemented/simplification/2026-09-26-team-direct-inbox.md +++ b/.agents/notes/archived/simplification/2026-09-26-team-direct-inbox.md @@ -1,6 +1,7 @@ # Agent Note: Team messages use the target inbox Status: implemented +Archived: 2026-10-15 English | [中文](2026-09-26-team-direct-inbox.zh.md) diff --git a/.agents/notes/implemented/simplification/2026-09-26-team-direct-inbox.zh.md b/.agents/notes/archived/simplification/2026-09-26-team-direct-inbox.zh.md similarity index 99% rename from .agents/notes/implemented/simplification/2026-09-26-team-direct-inbox.zh.md rename to .agents/notes/archived/simplification/2026-09-26-team-direct-inbox.zh.md index 6928631c8f..8838fb4a28 100644 --- a/.agents/notes/implemented/simplification/2026-09-26-team-direct-inbox.zh.md +++ b/.agents/notes/archived/simplification/2026-09-26-team-direct-inbox.zh.md @@ -1,6 +1,7 @@ # Agent Note: Team 消息使用目标 inbox Status: implemented +Archived: 2026-10-15 [English](2026-09-26-team-direct-inbox.md) | 中文 diff --git a/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.i18n.yaml index 260adf6610..352ae4028d 100644 --- a/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.i18n.yaml @@ -9,8 +9,8 @@ en: c988b8a3be4e6f19 zh: b58011fd8d2e878e /agent-note-adjacent-agents-share-one-steer-send-message-operation/decision: - en: 1188c699ce6e6ca2 - zh: 4d5b974cd1216f06 + en: 666b95a49f9ad05c + zh: 3d5ad7c8986428b8 /agent-note-adjacent-agents-share-one-steer-send-message-operation/decision/one-model-tool-and-one-return-instruction: en: 4b0a25389324fa91 zh: c2cdd9f7e40ef43b diff --git a/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.md b/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.md index 4efb9d334e..e026432e29 100644 --- a/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.md +++ b/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.md @@ -17,13 +17,14 @@ The child-only tool and system-prompt section also preceded every inherited fork `SubagentRuntime.sendMessage(sender, targetId, content, { signal })` is the only public model-authored message operation. The continuation manager accepts only the exact live sender and a target on one adjacent edge: - parent to direct continuable child, authorized by the child's durable `SessionHeader.parentSession`; -- resident continuable child to its exact live direct parent, authorized by the child's Activation. +- resident continuable child to its exact live direct parent, authorized by the child's Activation; +- a resident continuable child to a continuable sibling of the same live parent, authorized by that parent’s child authority ([subagent teammates](2026-10-15-agent-teams-as-subagents.md)). -Siblings, self-targets, ancestors beyond one edge, stale Agent objects, unknown targets, and one-shot children are not alternate routes. The operation has no caller-supplied source, delivery mode, offline parent mailbox, or provider dispatch. +Self-targets, ancestors beyond one edge, stale Agent objects, unknown targets, and one-shot children are not alternate routes. The operation has no caller-supplied source, delivery mode, offline parent mailbox, or provider dispatch. Every accepted message uses `Agent.steer()`. A running target receives it at the nearest step boundary; an idle target starts a turn. An absent direct child is cold-resumed through the existing continuation lifecycle before the same Steer delivery. The manager retains waking-send accounting so a continuation-managed target cannot settle between synchronous inbox insertion and driver admission. -Every direction uses one durable source. The service derives `senderSessionId` from the authorized Agent and frames the model-visible content as `Agent sent a message:`, so attribution cannot diverge from authority. +Every direction uses one durable source. The service derives `senderSessionId` from the exact live sender and frames content as `Agent sent a message:`, including sibling delivery under the parent’s authority. ```ts import type { SessionId } from '@deepseek-ai/dsh-session' diff --git a/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md b/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md index b7ed157f54..1acb29d0f7 100644 --- a/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md @@ -17,13 +17,14 @@ child 专属工具与系统提示词 section 还位于每个继承 fork 轮次 `SubagentRuntime.sendMessage(sender, targetId, content, { signal })` 是唯一公开的模型编写消息操作。继续执行管理器只接受确切在线 sender 与一条相邻边上的目标: - parent 到直接可继续 child,由 child 的持久化 `SessionHeader.parentSession` 授权; -- 驻留的可继续 child 到其确切在线直接 parent,由 child 的 Activation 授权。 +- 驻留的可继续 child 到其确切在线直接 parent,由 child 的 Activation 授权; +- 驻留的可继续子级向同一在线父级的可继续同级发消息,由该父级对子级的权限授权([子代理队友](2026-10-15-agent-teams-as-subagents.zh.md))。 -sibling、自身目标、超过一条边的 ancestor、陈旧 Agent 对象、未知目标与一次性 child 都不是替代路由。该操作没有调用方提供的 source、投递模式、离线 parent mailbox 或提供方分发。 +自身目标、跨越一层的祖先、过期 Agent 对象、未知目标和一次性子级不是替代路由。操作不接受调用方提供的 source、投递模式、离线父级邮箱或提供方分发。 每条被接受的消息都使用 `Agent.steer()`。运行中目标在最近 step 边界接收消息;空闲目标启动轮次。缺失的直接 child 会先通过现有继续执行生命周期冷恢复,再接受同一 Steer 投递。管理器保留唤醒发送记账,因此受继续执行管理的目标不会在同步 inbox 插入与 driver 准入之间结算。 -两个方向使用同一种持久来源。服务从已授权 Agent 推导 `senderSessionId`,并把模型可见内容组装为 `Agent sent a message:`,因此来源信息不会偏离权限。 +所有方向使用同一种持久 source。服务从确切在线发送方推导 `senderSessionId`,并以 `Agent sent a message:` 包装内容,使用父级权限的同级投递也一样。 ```ts import type { SessionId } from '@deepseek-ai/dsh-session' diff --git a/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.i18n.yaml index 65205260c3..45bb569e9c 100644 --- a/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.i18n.yaml @@ -9,11 +9,11 @@ en: c9a81cb731ddf7b2 zh: 428c0e6f8937d7c5 /agent-note-team-identity-in-required-runtime-context/decision: - en: 6c73479973c1e378 - zh: 98677c77f77f8d81 + en: cd0ccfd8721918a2 + zh: ff518d014555397e /agent-note-team-identity-in-required-runtime-context/alternatives-considered: en: 774566ffbf9e9db8 zh: ea761d876af462f2 /agent-note-team-identity-in-required-runtime-context/consequences: - en: 18d1aa67936f1227 - zh: 7e06c1f51553ecdd + en: 27c6817a25943991 + zh: a749cb8bc5d50f37 diff --git a/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.md b/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.md index 39794f02d3..c84e17e75e 100644 --- a/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.md +++ b/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.md @@ -10,11 +10,11 @@ A teammate identity embedded in its initial task depends on ordinary history ret ## Decision -`tool-agent-team` contributes `team:identity` through the Agent-scoped `systemPrompt.context()` registry with `required: true`. Its provider reads the existing Team membership and renders the member's role, name, and addressing instructions. Optional-context suppression cannot remove it. Fixed collaboration policy stays in the system prompt; roster and task state remain tool queries. Initial tasks contain only their supplied content. +`tool-agent-team` contributes required `team:identity` through Agent-scoped `systemPrompt.context()`. It reads current Team membership and states the member’s role and, for teammates, the Lead’s id. Optional-context suppression cannot remove it. Fixed collaboration policy stays in the system prompt; roster and task state remain tool queries. Initial tasks contain only supplied content. The loop owns logging, retained-snapshot comparison, and re-admission after compaction. A changed role appends the current full runtime-context snapshot after inherited history; unchanged retained context adds no message. No Team-specific retention state, event type, or log migration is introduced. Existing recorded reminders remain historical messages. -This supersedes only the initial-task identity decision in [Agent Teams](../feature/2026-08-05-agent-teams.md#team-identity); its roster, mailbox, and task decisions remain active. +This supersedes only the initial-task identity decision in [Agent Teams](../feature/2026-08-05-agent-teams.md#team-identity); its task-board and shared-checkout decisions remain active, while teammate creation and messaging belong to [subagents](2026-10-15-agent-teams-as-subagents.md). ## Alternatives considered @@ -28,4 +28,4 @@ This supersedes only the initial-task identity decision in [Agent Teams](../feat Members pay for identity within the full runtime-context snapshot. Other context changes repeat it, while stable retained context adds no new message. Forks preserve inherited history and append current identity; actual provider cache hits remain best-effort. Existing Team authorization remains in tool execution. -Verification covers fresh and fork teammates, an independent fork from a teammate, optional-context suppression, compaction restoration, cold resume, and registration disposal. The recorded `team-targets` scenario pins Lead context; the keyless headless Team workflow pins teammate context through the shipped profile. +Verification covers fresh and fork teammates, an independent fork from a teammate, optional-context suppression, compaction restoration, cold resume, and registration disposal. The recorded `team-targets` scenario pins Lead context; the keyless headless Team workflow pins teammate role through the shipped profile. diff --git a/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.zh.md b/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.zh.md index d0e333a71e..cb8c5a4e0d 100644 --- a/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.zh.md +++ b/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.zh.md @@ -10,11 +10,11 @@ Status: implemented ## Decision -`tool-agent-team` 通过 Agent 作用域的 `systemPrompt.context()` 注册 `team:identity`,并设置 `required: true`。提供方读取现有 Team membership,渲染成员角色、名字与寻址说明。关闭可选 context 不能移除它。固定协作策略保留在 system prompt;roster 和 task 状态仍通过工具查询。初始任务只包含传入的正文。 +`tool-agent-team` 通过 Agent 作用域的 `systemPrompt.context()` 提供必需的 `team:identity`。它读取当前 Team 成员身份,声明角色,并为队友提供 Lead id。关闭可选上下文不能移除它。固定协作策略留在系统提示词,成员列表和任务状态通过工具查询。初始任务只包含传入正文。 Loop 负责日志记录、保留快照比较和压缩后的重新接纳。角色变化会在继承历史之后追加当前完整 runtime-context 快照;保留的 context 未变化时不追加消息。不引入 Team 专属的保留状态、事件类型或日志迁移。已有身份提醒仍作为历史消息保留。 -本决策仅取代 [Agent Teams](../feature/2026-08-05-agent-teams.zh.md#team-identity) 中的初始任务身份决策;其 roster、mailbox 和 task 决策仍然有效。 +本决策仅取代 [Agent Teams](../feature/2026-08-05-agent-teams.zh.md#team-identity) 中的初始任务身份决策;其 task board 与共享检出决策仍然有效,而 teammate 的创建与消息由[subagent](2026-10-15-agent-teams-as-subagents.zh.md)负责。 ## Alternatives considered @@ -28,4 +28,4 @@ Loop 负责日志记录、保留快照比较和压缩后的重新接纳。角色 成员承担完整 runtime-context 快照中的身份 token 成本。其他 context 变化会重复身份,保留的 context 稳定时则不追加消息。Fork 保留继承历史并追加当前身份;提供方实际缓存命中仍为尽力而为。现有 Team 授权仍在工具执行时落实。 -验证覆盖 fresh 和 fork teammate、从 teammate 创建的独立 fork、关闭可选 context、压缩后恢复、冷恢复和注册释放。录制的 `team-targets` 场景固定 Lead context;无密钥 headless Team 工作流通过内置 profile 固定 teammate context。 +验证覆盖 fresh 和 fork teammate、从 teammate 创建的独立 fork、关闭可选 context、压缩后恢复、冷恢复和注册释放。录制的 `team-targets` 场景固定 Lead context;无密钥 headless Team 工作流通过内置 profile 固定 teammate 角色。 diff --git a/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.i18n.yaml new file mode 100644 index 0000000000..0409e65f17 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.i18n.yaml @@ -0,0 +1,22 @@ +# Bilingual-pair consistency record for 2026-10-15-agent-teams-as-subagents.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md +/agent-note-agent-teams-teammates-are-subagents: + en: 067bfa44266e764e + zh: dcdeff6c10e492be +/agent-note-agent-teams-teammates-are-subagents/problem: + en: ca2848e3bbc3a009 + zh: 0551e32ff6d187d0 +/agent-note-agent-teams-teammates-are-subagents/decision: + en: b639631e54322fd9 + zh: b80c1dad17f9afd1 +/agent-note-agent-teams-teammates-are-subagents/alternatives-considered: + en: ef0cf648ff54784f + zh: 2a0b3745a54e7072 +/agent-note-agent-teams-teammates-are-subagents/consequences: + en: 48e0970fdfe79cbc + zh: f36b1825ba65a673 +/agent-note-agent-teams-teammates-are-subagents/testing: + en: 7ea558d6e3fab9a7 + zh: d51053c7d4f0b10f diff --git a/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md b/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md new file mode 100644 index 0000000000..29fb17bd56 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md @@ -0,0 +1,39 @@ +# Agent Note: Agent Teams teammates are subagents + +Status: implemented + +English | [中文](2026-10-15-agent-teams-as-subagents.zh.md) + +## Problem + +Teammates already use the subagent runtime for creation, Session identity, catalog membership, capacity, cold resume, settlement, and interruption. A separate Team roster duplicates identity, provisioning recovery, and teardown. Team-scoped messaging and control tools shadow the standard subagent tools, forcing profiles to disable ordinary delegation and making fork tool schemas diverge from their parent. + +## Decision + +Use one subagent identity: the Session id. The existing `description` tool argument supplies the catalog label and display text; descriptions may repeat and never address an agent. Creation accepts no member name, reserves no names, and adds no catalog payload version. + +An exact live sender may message a direct continuable child. A resident continuable child may additionally message its direct parent or continuable siblings by id. Sibling delivery uses the shared live parent’s child authority and cold resume; the message retains the actual sender’s id. `list_agents` exposes parent and sibling ids. Interruption keeps ancestor authority and id addressing. + +Every root Agent leads the Team identified by its Session id; every live subagent below it shares the task board and waits. The roster projects the root, its direct continuable catalog children, and historical `team/member` records. Team creates no agents and registers no messaging, listing, or interruption tools. The Team profile only adds rows. Parent and fork children receive identical Team policies and tool schemas, with role identity in required runtime context. + +Task ownership uses `ownerId`, and reassignment accepts a Session id. Team persists only task changes. Messages use the target Agent inbox; historical `team/message/*` records remain readable but are never delivered or acknowledged. + +This supersedes the roster, provisioning, recovery, and messaging decisions in [Agent Teams](../feature/2026-08-05-agent-teams.md) and the [Team direct inbox decision](../../archived/simplification/2026-09-26-team-direct-inbox.md). The task-board and shared-checkout decisions remain active. [Adjacent Agent messaging](2026-08-27-adjacent-agent-steer-messaging.md) includes sibling routing, while [required runtime-context identity](2026-10-13-team-identity-runtime-context.md) owns the role statement. The [upgrade guide](../../../../docs/upgrade-guide/v0.2.1-alpha.3/agent-team-subagents/guide.md) lists removed tools, methods, and configuration. + +## Alternatives considered + +**Separate Team authority and tools.** This retains a second roster and recovery lifecycle for Agents already owned by the subagent runtime, plus same-name tools and a private delivery adapter. The shared parent relation supplies the required authority. + +**Unique names as sibling-routing opt-in.** Names add validation, reservation, persistence, and ambiguous resolution beside existing Session ids. Descriptions already explain responsibilities; stable ids distinguish repeated descriptions without another identity system. + +**Pluggable address books.** A registry would support groups spanning unrelated parents but add another authorization input. No current consumer needs this; revisit when collaboration must cross parent trees. + +## Consequences + +Ordinary continuable siblings can discover and message one another. Team retains task ownership, dependencies, waits, policy, and role identity. Its member and message-size limits and Team-owned teardown disappear; subagent capacity and lifecycle apply. Failed starts leave no Team roster row. No new Session payload generation is required, and historical member names remain decode-only data. + +Deeper descendants share tasks and waits but are not roster rows; their message routes remain limited to their own parent, children, and continuable siblings. Separate working directories and cross-process coordination remain unsupported. + +## Testing + +Verify sibling delivery and cold resume with actual sender attribution and repeated descriptions; reject unrelated targets. Verify Team lineage, inherited catalog isolation, historical reads, task CAS, waits, and registration disposal. Recorded Team sessions and the headless profile exercise the complete model-facing workflow and parent/fork request-prefix equality. diff --git a/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.zh.md b/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.zh.md new file mode 100644 index 0000000000..52d4f7d6eb --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.zh.md @@ -0,0 +1,39 @@ +# Agent Note: Agent Teams 队友是子代理 + +Status: implemented + +[English](2026-10-15-agent-teams-as-subagents.md) | 中文 + +## Problem + +队友已经使用子代理运行时完成创建、Session 身份、目录成员关系、容量、冷恢复、结算和中断。独立 Team 成员列表重复了身份、创建恢复和释放职责。Team 作用域内的消息及控制工具覆盖标准子代理工具,迫使 profile 禁用普通委派,并使分叉工具模式偏离父级。 + +## Decision + +统一使用子代理的 Session id 作为身份。现有 `description` 工具参数提供目录标签和展示文本;描述可以重复,不用于寻址代理。创建不接收成员名称、不预留名称,也不增加目录载荷版本。 + +确切在线的发送方可向直接可继续子级发消息。驻留的可继续子级还可按 id 联系直接父级或可继续同级。同级投递使用共同在线父级对子级的权限和冷恢复能力;消息保留实际发送方 id。`list_agents` 提供父级和同级 id。中断保留祖先权限和 id 寻址。 + +每个根 Agent 领导以自身 Session id 标识的 Team;其下所有在线子代理共享任务板和等待。成员列表投影根、其直接可继续目录子级和历史 `team/member` 记录。Team 不创建代理,也不注册消息、列举或中断工具。Team profile 只增加配置行。父级与分叉子级获得相同 Team 策略和工具模式,角色身份通过必需运行时上下文提供。 + +任务所有权使用 `ownerId`,重新分配接受 Session id。Team 只持久化任务变更。消息使用目标 Agent 收件箱;历史 `team/message/*` 记录仍可读取,但不会被投递或确认。 + +本决策取代 [Agent Teams](../feature/2026-08-05-agent-teams.zh.md) 的成员列表、创建、恢复和消息决策,以及 [Team 直接收件箱决策](../../archived/simplification/2026-09-26-team-direct-inbox.md)。任务板和共享工作树决策仍有效。[相邻 Agent 消息](2026-08-27-adjacent-agent-steer-messaging.zh.md)纳入同级路由,[必需运行时身份](2026-10-13-team-identity-runtime-context.zh.md)负责角色声明。[升级指南](../../../../docs/upgrade-guide/v0.2.1-alpha.3/agent-team-subagents/guide.zh.md)列出移除的工具、方法和配置。 + +## Alternatives considered + +**独立 Team 权限和工具。** 这会为子代理运行时已拥有的 Agent 保留第二套成员列表与恢复生命周期,以及同名工具和私有投递适配器。共同父级关系已经提供所需权限。 + +**以唯一名称选择性启用同级路由。** 名称会在现有 Session id 之外增加校验、预留、持久化和歧义解析。描述已能说明职责;稳定 id 可以区分重复描述,无需另一套身份系统。 + +**可插拔地址簿。** 注册表可支持跨不同父级的组,但会增加另一种授权输入。当前没有消费者需要此能力;协作必须跨父级树时再考虑。 + +## Consequences + +普通可继续同级可发现彼此并发消息。Team 保留任务所有权、依赖、等待、策略和角色身份。成员数量限制、消息大小限制和 Team 自有释放被移除,改用子代理容量与生命周期。启动失败不留下 Team 成员行。不需要新的 Session 载荷代际,历史成员名称只作为解码数据保留。 + +更深的后代共享任务和等待,但不显示成员行;消息路由仍限于自己的父级、子级和可继续同级。仍不支持独立工作目录和跨进程协调。 + +## Testing + +验证同级投递、冷恢复、实际发送方归属和重复描述,并拒绝无关目标。验证 Team 谱系、继承目录隔离、历史读取、任务 CAS、等待和注册释放。录制 Team 会话与 headless profile 验证完整模型工作流以及父级和分叉请求前缀相等。 diff --git a/.agents/notes/implemented/feature/2026-08-05-agent-teams.i18n.yaml b/.agents/notes/implemented/feature/2026-08-05-agent-teams.i18n.yaml index 68ef489fe0..f67fb624bb 100644 --- a/.agents/notes/implemented/feature/2026-08-05-agent-teams.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-05-agent-teams.i18n.yaml @@ -9,32 +9,32 @@ en: bab62798c2dc07b4 zh: fe42c5abf9127d37 /agent-note-durable-agent-teams-over-continuable-children/decision: - en: 35650a30529c01f8 - zh: 4f9ee01b568d2fed + en: 0f0503a9270b3581 + zh: dcc82dd0f6aab0e6 /agent-note-durable-agent-teams-over-continuable-children/profile-delegation: - en: 4835fb8c0c0d8a0d - zh: 5f9438a0d58104c4 + en: 5e938a21b2785b4c + zh: 4e85e7e2cdbcf7e0 /agent-note-durable-agent-teams-over-continuable-children/team-identity: - en: f82812d6ac0ac78d - zh: 7dd80a6ad13674ca + en: d79f4425f2c1929f + zh: 6586410db4a02cd1 /agent-note-durable-agent-teams-over-continuable-children/provisioning-and-recovery: - en: 4345ac9a6ac7ad04 - zh: 1d19ecbb128a75a3 + en: 624e9d42af1af859 + zh: 020dcbb26211ea67 /agent-note-durable-agent-teams-over-continuable-children/mailbox-and-task-transactions: - en: bfc3063536837c0e - zh: 6b8e69ba85cc2bb1 + en: 059e9f7ba2af0035 + zh: 637bdf3a12aa3194 /agent-note-durable-agent-teams-over-continuable-children/shared-checkout-boundary: en: 34920da68b5763e0 zh: 7604392594e4b8a8 /agent-note-durable-agent-teams-over-continuable-children/web-projection: - en: e6348054f024725b - zh: 8740636e99940308 + en: e5d6f8a34b35fa34 + zh: a2e9ab3c8986b6ff /agent-note-durable-agent-teams-over-continuable-children/alternatives-considered: - en: 8cb2fd8839f5f8b7 - zh: e302ca03e569de65 + en: 0c9a57bf8150ad71 + zh: b91de2f431796f0a /agent-note-durable-agent-teams-over-continuable-children/testing: - en: 092ac3c2b6f552aa - zh: c688c1bec5ad8a07 + en: d1cb9d713b6d09a6 + zh: 908cf68b1fa856f3 /agent-note-durable-agent-teams-over-continuable-children/consequences: - en: da9cd22b1185a793 - zh: e20d7ef9226f237f + en: c071fa2ccdb911b5 + zh: 3b8cf0d3303a4d40 diff --git a/.agents/notes/implemented/feature/2026-08-05-agent-teams.md b/.agents/notes/implemented/feature/2026-08-05-agent-teams.md index b93e2716f9..10ce048b60 100644 --- a/.agents/notes/implemented/feature/2026-08-05-agent-teams.md +++ b/.agents/notes/implemented/feature/2026-08-05-agent-teams.md @@ -14,39 +14,35 @@ Agent Teams needs an explicit opt-in composition while its public contracts rema ## Decision -Every ordinary runtime root is the implicit Lead of a Team identified by that root's `SessionId`. The Team has no creation event: its Lead pseudo-row exists by identity, while durable state begins with the first member, message, or task event. A roster is flat and contains at most the configured number of immutable lowercase-kebab-case names. Each teammate is a continuable direct child with a reserved Session id; only the Lead creates or interrupts teammates. Ordinary provider-owned subagents outside the roster are not Team members, and an ordinary fork is a new root whose inherited Team records are excluded by their ancestor `TeamId`. +Every ordinary root leads the Team identified by its `SessionId`. The Lead row exists by identity; durable task state begins with the first task event. [Subagent teammates](../architecture/2026-10-15-agent-teams-as-subagents.md) owns creation, messaging, and interruption. Every live subagent below the root is a member; the root’s direct continuable children form the roster. An ordinary fork leads a new Team and excludes inherited Team records. -The implementation is split into `@deepseek-ai/dsh-experimental-agent-team`, which owns `ctx.agentTeams` and durable semantics, and `@deepseek-ai/dsh-experimental-tool-agent-team`, which owns scoped schemas and model guidance. Every Team tool declares its complete result schema and renders that value as compact JSON, so the compiler checks each `execute` against what the model is promised and no result spends tokens on indentation. Deployments mount both plugins explicitly and may disable legacy continuable controls with the same model-visible names. The shared policy announces Team availability and describes coordination responsibilities. Both packages are public members of `packages/experimental/`; the [experimental package reference](../../../../packages/experimental/AGENTS.md) owns publication, dependency isolation, and promotion. +The implementation is split into `@deepseek-ai/dsh-experimental-agent-team`, which owns `ctx.agentTeams` and durable semantics, and `@deepseek-ai/dsh-experimental-tool-agent-team`, which owns scoped schemas and model guidance. Every Team tool declares its complete result schema and renders that value as compact JSON, so the compiler checks each `execute` against what the model is promised and no result spends tokens on indentation. Deployments mount both plugins explicitly; Team registers no tool that shares a name with the subagent controls. The shared policy announces Team availability and describes coordination responsibilities. Both packages are public members of `packages/experimental/`; the [experimental package reference](../../../../packages/experimental/AGENTS.md) owns publication, dependency isolation, and promotion. The Lead must wait for required work before its final answer. Process teardown remains the final lifecycle owner and drains continuation Activations; a Team task owner is durable state and is not automatically released by idle, interruption, or process exit. ## Profile delegation -The [Team profile](../../../../packages/experimental/agent-team-profile/README.md) disables `subagent` and `subagent_fork` together with the overlapping global controls. Direct model delegation uses `spawn_teammate` with fresh or fork context, keeping those children in the durable roster. Workflow remains available through the base profile’s fresh `spawn` provider for scripted orchestration; it cannot inherit a teammate’s conversation identity through the model tool. The Subagent services and providers remain shared infrastructure. Ordinary Session forks retain their history; [current identity handling](../architecture/2026-10-13-team-identity-runtime-context.md) supplies the fork’s own role. +The [Team profile](../../../../packages/experimental/agent-team-profile/README.md) inserts the Team rows and keeps the base delegation and control tools; teammates are started with those tools as described in [subagents](../architecture/2026-10-15-agent-teams-as-subagents.md). Ordinary Session forks retain their history; [current identity handling](../architecture/2026-10-13-team-identity-runtime-context.md) supplies the fork's own role. ## Team identity -Shared system policy and all tool schemas stay uniform across members; execution owns role restrictions. Team tools resolve the caller’s Team and accept member names, so the model needs no Team id. [Required runtime-context identity](../architecture/2026-10-13-team-identity-runtime-context.md) owns identity injection, retention, and fork handling. +Shared system policy and tool schemas stay uniform across members; execution enforces role restrictions. Team tools resolve the caller’s Team and use Session ids as task owners. [Required runtime-context identity](../architecture/2026-10-13-team-identity-runtime-context.md) owns identity injection, retention, and fork handling. ## Provisioning and recovery -Creation first appends and flushes a `team/member` provisioning snapshot in the Lead Session, then starts the reserved continuable child through the selected fresh or fork provider. Failure before initial inbox acceptance appends a failed snapshot. Success flushes the child's accepted inbox item before appending active. Recovery recognizes that initial message while it is still pending or after it enters user-message history. Names are reserved by the first provisioning record and never reused, including after failure. Disposal closes admission, aborts and awaits admitted creation and send operations, then stops every live child recorded by the roster; a failed child remains cleanup-owned until its Activation exits, and cleanup rejection fails disposal. - -A root recovery reconciles an unterminated provisioning record against the child's independently persisted Session. Matching direct-parent and continuable descriptors plus a recorded initial user message prove successful admission and produce active; absence, corruption, mismatched provider/lineage, or a missing admitted message produces failed. The creator re-reads the terminal phase under the same Lead-log serializer; if recovery marked failed while creation succeeded, it drains the child and reports a provisioning conflict instead of retaining an orphan. This avoids reconstructing an initial prompt that was never retained in the Team log and contains plugin-reload races. - -Fresh children have no inherited conversation. Fork children capture the Lead's completed-turn prefix once and retain it as their own durable seed. The current delegation turn remains excluded, matching the existing fork provider contract. +Teammate creation and cold resume belong to the subagent runtime ([subagent teammates](../architecture/2026-10-15-agent-teams-as-subagents.md)). Historical `team/member` records remain readable with their phases and descriptions; Team writes no new membership records. ## Mailbox and task transactions -The historical Lead-log mailbox remains a readable format: queued and delivered records keep their stable identity and sender, and Team payload validation still applies to them. Team no longer delivers or acknowledges them; new sends follow the [direct inbox decision](../simplification/2026-09-26-team-direct-inbox.md). +The historical Lead-log mailbox remains a readable format: queued and delivered records keep their stable identity and sender, and Team payload validation still applies to them. Team never delivers or acknowledges them; messages between members use the target Agent inbox through the subagent operation ([subagents](../architecture/2026-10-15-agent-teams-as-subagents.md)). Unknown mailbox content stays as decoded JSON during event projection and checkpoint admission. Validation inspects its type without rebuilding its fields, because generic object parsing can omit a valid own `__proto__` key. Locally declared content variants undergo structural validation; opaque plugin fields do not gain Team semantics. Cache invalidation is necessary because parser correction alone cannot recover keys omitted from cached state. The [Team documentation](../../../../packages/experimental/agent-team/README.md) owns the restore behavior. -`send_message` uses Steer for running, idle, and cold targets. New sends return inbox acceptance. The [Team Steer messaging decision](../../archived/simplification/2026-08-30-team-send-message-steer.md) records the single-tool scheduling rationale. +Member messages use the subagent `send_message` operation, which steers running, idle, and cold targets and returns inbox acceptance. Shared tasks are complete snapshots with Team-local ids and monotonic revisions. Every mutation carries `expectedRevision`. Any member creates, reads, or claims a ready unowned task; the owner or Lead edits and transitions it, while only the Lead assigns another member. Numeric task ids remain within the safe-integer allocation range, and exhaustion fails without reusing an id. Dependencies must name non-deleted tasks and form a complete DAG. Deleted tasks are retained tombstones. `writeScopes` are normalized path prefixes that produce overlap diagnostics but never block claim or authorize a write. -`wait_agent` blocks on one roster, mailbox, task, or live-status edge registered after the call starts instead of encouraging model polling. It does not replay an earlier edge, so callers re-read authoritative state after wakeup or timeout. Lead-only interruption cancels the current turn with inbox preservation and does not alter mailbox or task ownership. +`wait_agent` blocks on one task, member-status, or accepted member-message edge registered after the call starts instead of encouraging model polling. It does not replay an earlier edge, so callers re-read authoritative state after wakeup or timeout. Interruption is the subagent ancestor operation, so only the Lead interrupts teammates; it does not alter task ownership. ## Shared checkout boundary @@ -58,13 +54,13 @@ The Team domain does not infer branches, create worktrees, merge changes, or sil The Web panel reads the Lead Session's `agentTeam` wire projection because the existing projection stream keeps browser state current without a separate Team read API. Both Lead and teammate conversations select values from the shared cross-Session store. The panel owns no projection reads: Session loading, cached list values, and control baselines supply the data. -The wire view carries durable roster phases, non-deleted task views with shared readiness and overlap derivation, and `failure` beside the last valid state. Live activity comes from Session status. Model selection has its own durable projection; the panel displays the shared store's available values without opening member conversations. A missing cached value remains absent until Session loading or a live update supplies it. +The wire view carries the roster of the Lead, its continuable catalog children, and historical members with their recorded phases, non-deleted task views with shared readiness and overlap derivation, and `failure` beside the last valid state. Live activity comes from Session status. Model selection has its own durable projection; the panel displays the shared store's available values without opening member conversations. A missing cached value remains absent until Session loading or a live update supplies it. -Publication requires `apply` to return a new state object per applied Team event and replace only the touched collection. Mailbox-only events retain the view reference and publish nothing. The existing carrier sends each changed complete view to every connected browser; the package README records that cost. +Publication requires `apply` to return a new state per applied event and replace only the touched collection. Mailbox-only events retain the client view and publish nothing. The existing carrier sends each changed complete view to connected browsers; the package README records that cost. ## Alternatives considered -**Extend direct-child subagent tools with peer ids.** Rejected because parent/child authority and Team peer membership are different domains. Adding peer access to the continuation seam would weaken its exact-parent authorization and still leave roster and tasks without a persistence owner. +**Give every child access across the Team tree.** Shared task membership does not grant messaging authority across parents. [Subagent teammates](../architecture/2026-10-15-agent-teams-as-subagents.md) uses the shared parent’s authority for continuable siblings; tasks retain the Lead log as persistence owner. **Store mail in each target Session before Lead-log enqueue.** Rejected because target materialization and acceptance can fail after the Team has committed the send. The always-live Lead Session is the transaction home; target recording is the acknowledgement and de-duplication boundary. @@ -72,22 +68,22 @@ Publication requires `apply` to return a new state object per applied Team event **Create isolated worktrees automatically.** Rejected because worktree creation, branch naming, merge policy, ignored files, build artifacts, and cleanup are deployment choices. It also changes the same-world behavior existing subagents and sandboxes expose. -**Enable Teams in the default catalog.** Rejected because scoped Team controls would shadow same-named legacy globals and unsolicited delegation would add latency and token cost to simple tasks. An opt-in profile bundle inserts Team and disables the legacy controls without adding Team packages to shipped dependency graphs. +**Enable Teams in the default catalog.** Rejected because unsolicited coordination and Team tool schemas would add latency and token cost to simple tasks. An opt-in profile bundle inserts Team without adding Team packages to shipped dependency graphs. -**Use an in-memory board and mailbox.** Roster and task state must survive settlement, HMR, and interruption. Historical mailbox records stay readable; message ownership follows the target inbox. +**Use an in-memory board and mailbox.** Task state must survive settlement, HMR, and interruption. Historical mailbox records stay readable; message ownership follows the target inbox. -**Return Team tool results as untyped JSON.** Rejected because an undeclared result type lets `execute` drift from the value the model is promised without a compiler error, and it invites indentation that costs tokens on every roster, task, and receipt. Each Team tool therefore declares its complete result schema and one shared helper renders it compactly. +**Return Team tool results as untyped JSON.** Rejected because an undeclared result type lets `execute` drift from the value the model is promised without a compiler error, and it invites indentation that costs tokens on every task and wait result. Each Team tool therefore declares its complete result schema and one shared helper renders it compactly. ## Testing -Package tests cover Team authority, provisioning durability, task transitions, direct inbox acceptance, sender attribution, lifecycle cancellation, and historical mailbox readability. The keyless Team profile snapshot pins the policy, tools, and workflow; the CLI composition test checks persisted Team and child logs. +Package tests cover Team membership, catalog-backed roster projection, task transitions, waits, and historical roster and mailbox readability. The keyless Team profile snapshot pins the policy, tools, and workflow; the CLI composition test checks persisted Team and child logs. Model-facing member identities and availability follow the [tool projection reference](../../../../packages/experimental/tool-agent-team/README.md); service residency and durable identity remain distinct. ## Consequences -The Lead Session retains whole task/member snapshots and historical mailbox records. This favors inspectable recovery over compact deltas; deleted and delivered history remains append-only until broader Session retention applies. +The Lead Session retains whole task snapshots and historical member and mailbox records. This favors inspectable recovery over compact deltas; deleted and delivered history remains append-only until broader Session retention applies. -An active roster member can be non-resident, so `inactive` is not failure and sends can incur cold-resume latency. Admission failures reject new sends. Historical pending mail is never delivered. Failed provisioning permanently consumes its name and member slot. +A teammate can be non-resident, so `inactive` is not failure and sends can incur cold-resume latency. Historical pending mail is never delivered. Coordination reduces likely checkout conflicts but cannot eliminate writes outside filesystem compare-and-set tools. The final diff and tests remain the Lead's integration boundary. diff --git a/.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md b/.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md index f38c6f68c4..26d8c76e84 100644 --- a/.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md +++ b/.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md @@ -14,39 +14,35 @@ Agent Teams 的公开约定仍处于实验阶段,因此需要显式启用的 ## Decision -每个普通运行时 Root 都是一个隐式 Team 的 Lead,Team id 等于该 Root 的 `SessionId`。Team 没有 creation event:Lead pseudo-row 由身份直接存在,持久状态从第一条 member、message 或 task event 开始。roster 是扁平结构,最多包含配置数量、不可变且采用小写 kebab-case 的名字。每个 teammate 都是使用预留 Session id 的 continuable 直接 child;只有 Lead 可以创建或 interrupt teammate。roster 外由 provider 管理的普通 subagent 不是 Team member;普通 fork 是新的 Root,继承的 Team 记录会因 ancestor `TeamId` 被排除。 +每个普通根领导以其 `SessionId` 标识的 Team。Lead 行由身份确定;持久任务状态从首个任务事件开始。[子代理队友](../architecture/2026-10-15-agent-teams-as-subagents.zh.md)负责创建、消息和中断。根下所有在线子代理都是成员;根的直接可继续子级形成成员列表。普通分叉领导新的 Team,并排除继承的 Team 记录。 -实现拆分为 `@deepseek-ai/dsh-experimental-agent-team` 与 `@deepseek-ai/dsh-experimental-tool-agent-team`:前者负责 `ctx.agentTeams` 和持久语义,后者负责 scoped schema 与模型指引。每个 Team 工具都声明完整的结果 schema,并把该值渲染为紧凑 JSON,因此编译器会检查每个 `execute` 是否符合对模型的承诺,也没有结果把 token 花在缩进上。部署显式挂载两个插件,并可禁用具有相同模型可见名称的旧 continuable control。共享策略声明 Team 可用,并说明协调责任。两个包都是 `packages/experimental/` 的公开成员;[实验性包参考](../../../../packages/experimental/AGENTS.md)负责发布、依赖隔离与 promotion。 +实现拆分为 `@deepseek-ai/dsh-experimental-agent-team` 与 `@deepseek-ai/dsh-experimental-tool-agent-team`:前者负责 `ctx.agentTeams` 和持久语义,后者负责 scoped schema 与模型指引。每个 Team 工具都声明完整的结果 schema,并把该值渲染为紧凑 JSON,因此编译器会检查每个 `execute` 是否符合对模型的承诺,也没有结果把 token 花在缩进上。部署显式挂载两个插件;Team 不注册与 subagent control 同名的工具。共享策略声明 Team 可用,并说明协调责任。两个包都是 `packages/experimental/` 的公开成员;[实验性包参考](../../../../packages/experimental/AGENTS.md)负责发布、依赖隔离与 promotion。 Lead 必须等待所需工作后才能给出最终答案。进程 teardown 仍是最终生命周期 owner,并会 drain continuation Activation;Team task owner 是持久状态,不会因 idle、interrupt 或进程退出自动释放。 ## Profile delegation -[Team profile](../../../../packages/experimental/agent-team-profile/README.zh.md) 禁用 `subagent`、`subagent_fork` 以及名称重叠的全局控件。模型直接委派使用支持 fresh 或 fork 上下文的 `spawn_teammate`,使这些子代理进入持久 roster。Workflow 仍通过 base profile 的 fresh `spawn` 提供方执行脚本编排;经模型工具创建的 workflow 子代理不会继承 teammate 的对话身份。Subagent 服务和提供方仍是共享基础设施。普通 Session fork 保留历史;[当前身份处理](../architecture/2026-10-13-team-identity-runtime-context.zh.md)提供 fork 自己的角色。 +[Team profile](../../../../packages/experimental/agent-team-profile/README.zh.md) 插入 Team 行,并保留 base 的委派与控制工具;teammate 按[subagent](../architecture/2026-10-15-agent-teams-as-subagents.zh.md)所述使用这些工具启动。普通 Session fork 保留历史;[当前身份处理](../architecture/2026-10-13-team-identity-runtime-context.zh.md)提供 fork 自己的角色。 ## Team identity -共享 system 策略和全部工具 schema 在成员间保持一致;执行时检查角色权限。Team 工具根据调用者确定 Team,并接受成员名字,因此模型不需要 Team id。[必需 runtime-context 身份](../architecture/2026-10-13-team-identity-runtime-context.zh.md)负责身份注入、保留与 fork 处理。 +成员共享统一系统策略和工具模式;执行时强制角色权限。Team 工具解析调用方所属 Team,并使用 Session id 作为任务负责人。[必需运行时身份](../architecture/2026-10-13-team-identity-runtime-context.zh.md)负责身份注入、保留和分叉处理。 ## Provisioning and recovery -创建操作先在 Lead Session 中追加并 flush `team/member` provisioning 快照,再通过选定 fresh 或 fork provider 启动预留的 continuable child。初始 inbox 获准前的失败会追加 failed 快照;成功会先 flush child 中已接受的 inbox 条目,再追加 active。恢复会在初始消息仍处于 pending 或已进入用户消息历史时识别它。名字由第一条 provisioning 记录永久保留,包括失败后也不能复用。dispose 会关闭准入,中止并等待已获准的创建与发送操作,再停止 roster 记录的所有 live child;failed child 在 Activation 退出前仍由 cleanup 拥有,cleanup 拒绝会让 dispose 失败。 - -Root 恢复时会把未终结 provisioning 记录与独立持久 child Session 对账。直接 parent 与 continuable descriptor 匹配,并且已经记录初始用户消息,才能证明准入成功并转为 active;缺失、损坏、provider/lineage 不匹配或缺少已准入消息都会转为 failed。creator 会在同一 Lead 日志 serializer 内重读终态;如果 recovery 在创建成功时先标记 failed,creator 会 drain child 并报告 provisioning conflict,而不是遗留孤儿。这样既无需重建从未保存在 Team 日志中的初始 prompt,也能约束插件 reload 竞争。 - -fresh child 不继承对话。fork child 只捕获一次 Lead 已完成 turn 前缀,并保留为自己的持久 seed。当前 delegation turn 保持排除,与既有 fork provider 契约一致。 +队友创建和冷恢复属于子代理运行时([子代理队友](../architecture/2026-10-15-agent-teams-as-subagents.zh.md))。历史 `team/member` 记录保留阶段和描述并继续可读;Team 不写入新成员记录。 ## Mailbox and task transactions -历史 Lead-log mailbox 仍是可读格式:queued 与 delivered 记录保留稳定身份与发送者,Team payload 校验仍适用于它们。Team 不再投递或确认这些记录;新发送遵循[直接 inbox 决策](../simplification/2026-09-26-team-direct-inbox.zh.md)。 +历史 Lead-log mailbox 仍是可读格式:queued 与 delivered 记录保留稳定身份与发送者,Team payload 校验仍适用于它们。Team 从不投递或确认这些记录;member 之间的消息通过 subagent 操作进入目标 Agent inbox([subagent](../architecture/2026-10-15-agent-teams-as-subagents.zh.md))。 事件投影和 checkpoint 准入期间,未知 mailbox 内容保持为已解码 JSON。校验只检查其 type,不重建字段,因为通用对象解析可能省略有效的自有 `__proto__` 键。本地声明的内容变体接受结构校验;不透明的插件字段不获得 Team 语义。缓存失效是必要的,因为只修正解析器无法恢复缓存状态中省略的键。[Team 文档](../../../../packages/experimental/agent-team/README.zh.md)拥有恢复行为的说明。 -`send_message` 对 running、idle 和 cold target 使用 Steer。新发送返回 inbox 接收结果。[Team Steer 消息决策](../../archived/simplification/2026-08-30-team-send-message-steer.md)记录单一工具调度的理由。 +member 消息使用 subagent 的 `send_message` 操作,它对 running、idle 和 cold target 使用 Steer,并返回 inbox 接收结果。 共享 task 是带 Team-local id 与单调 revision 的完整快照。每次变更都携带 `expectedRevision`。任意 member 可以创建、读取或 claim ready 且无 owner 的任务;Owner 或 Lead 可以编辑和转换;只有 Lead 可以分配给另一个 member。数字 task id 保持在安全整数分配范围内;该范围耗尽时会失败,不会复用 id。依赖必须指向未删除任务,并形成完整 DAG。删除任务保留为 tombstone。`writeScopes` 是规范化路径前缀,只产生重叠诊断,绝不会阻止 claim 或授予写权限。 -`wait_agent` 等待调用注册后发生的下一条 roster、mailbox、task 或实时 status 边,避免模型轮询。它不会回放更早的边,因此调用方需要在唤醒或超时后重新读取权威状态。仅限 Lead 的 interrupt 使用 inbox preservation 取消当前 turn,不改变 mailbox 或 task owner。 +`wait_agent` 等待调用注册后发生的下一条 task、member status 或已接收 member 消息边,避免模型轮询。它不会回放更早的边,因此调用方需要在唤醒或超时后重新读取权威状态。interrupt 是 subagent 的祖先操作,因此只有 Lead 能 interrupt teammate;它不改变 task owner。 ## Shared checkout boundary @@ -58,13 +54,13 @@ Team 领域不会推断 branch、创建 worktree、merge 变更或静默改变 c Web panel 读取 Lead Session 的 `agentTeam` 传输投影,因为现有投影流能持续更新浏览器状态,无需独立的 Team 读取 API。Lead 与 teammate 会话均从共享的跨 Session store 选择值。面板不负责投影读取:Session 加载、列表缓存值与控制基线提供数据。 -传输视图携带持久 roster phase、使用共享就绪与重叠派生逻辑的未删除任务视图,以及最后有效状态旁的 `failure`。实时活动来自 Session 状态。模型选择有自己的持久投影;面板展示共享 store 中可用的值,不打开成员会话。缺少的缓存值保持缺失,直到 Session 加载或实时更新提供它。 +传输视图携带由 Lead、其可继续目录 child 与带记录 phase 的历史 member 组成的 roster、使用共享就绪与重叠派生逻辑的未删除任务视图,以及最后有效状态旁的 `failure`。实时活动来自 Session 状态。模型选择有自己的持久投影;面板展示共享 store 中可用的值,不打开成员会话。缺少的缓存值保持缺失,直到 Session 加载或实时更新提供它。 -发布要求 `apply` 对每个已应用的 Team 事件返回新的 state 对象,并只替换被触及的集合。仅邮箱的事件保留视图引用,不发布 frame。现有传输层将每个变化后的完整视图发送给所有已连接浏览器;包 README 记录了这项开销。 +发布要求 `apply` 为每个应用的事件返回新状态,且只替换触及的集合。仅邮箱事件保留客户端视图,不发布更新。现有载体向已连接浏览器发送变更后的完整视图;包 README 记录该成本。 ## Alternatives considered -**用 peer id 扩展 direct-child subagent tool。** 拒绝,因为 parent/child 权限与 Team peer membership 是不同领域。向 continuation seam 增加 peer access 会削弱 exact-parent authorization,仍无法为 roster 与 task 提供持久 owner。 +**允许子级跨整棵 Team 树通信。** 共享任务成员身份不授予跨父级消息权限。[子代理队友](../architecture/2026-10-15-agent-teams-as-subagents.zh.md)使用共同父级权限实现可继续同级通信;任务仍由 Lead 日志持久化。 **在 Lead 日志入队前把 mail 存入每个 target Session。** 拒绝,因为 Team 提交发送之后,target materialization 与准入仍可能失败。始终 live 的 Lead Session 是事务 owner;target recording 是 acknowledgement 与去重边界。 @@ -72,22 +68,22 @@ Web panel 读取 Lead Session 的 `agentTeam` 传输投影,因为现有投影 **自动创建隔离 worktree。** 拒绝,因为 worktree 创建、branch 命名、merge 策略、ignored file、构建产物与 cleanup 都是 deployment 选择;它也会改变既有 subagent 与 sandbox 暴露的 same-world 行为。 -**在默认工具目录中启用 Team。** 拒绝,因为 scoped Team control 会覆盖同名旧全局工具,主动 delegation 也会给简单任务增加延迟和 token 成本。opt-in profile bundle 会插入 Team 并禁用旧 control,同时不向随附依赖图添加 Team 包。 +**在默认工具目录中启用 Team。** 拒绝,因为主动协调与 Team 工具 schema 会给简单任务增加延迟和 token 成本。opt-in profile bundle 会插入 Team,同时不向随附依赖图添加 Team 包。 -**使用内存 board 与 mailbox。** roster 与任务状态必须经受 settlement、HMR 与中断。历史 mailbox 记录保持可读;消息归属目标 inbox。 +**使用内存 board 与 mailbox。** 任务状态必须经受 settlement、HMR 与中断。历史 mailbox 记录保持可读;消息归属目标 inbox。 -**让 Team 工具返回未类型化 JSON。** 拒绝,因为未声明的结果类型会让 `execute` 在没有编译错误的情况下偏离对模型的承诺,也会引入在每份 roster、task 与回执上都消耗 token 的缩进。因此每个 Team 工具都声明完整的结果 schema,并由一个共享 helper 紧凑渲染。 +**让 Team 工具返回未类型化 JSON。** 拒绝,因为未声明的结果类型会让 `execute` 在没有编译错误的情况下偏离对模型的承诺,也会引入在每份 task 与 wait 结果上都消耗 token 的缩进。因此每个 Team 工具都声明完整的结果 schema,并由一个共享 helper 紧凑渲染。 ## Testing -包测试覆盖 Team 权限、provisioning 持久性、任务转换、直接 inbox 接收、发送者归属、生命周期取消和历史 mailbox 可读性。无密钥 Team profile 快照固定 policy、工具与工作流;CLI 组合测试检查持久 Team 和 child 日志。 +包测试覆盖 Team 成员关系、以目录为来源的 roster 投影、任务转换、等待以及历史 roster 与 mailbox 的可读性。无密钥 Team profile 快照固定 policy、工具与工作流;CLI 组合测试检查持久 Team 和 child 日志。 模型可见的成员身份与可用状态遵循[工具投影说明](../../../../packages/experimental/tool-agent-team/README.zh.md);服务驻留状态和持久身份仍保持区分。 ## Consequences -Lead Session 保留完整 task/member 快照和历史 mailbox 记录。这优先支持可检查的恢复,而不是紧凑增量;deleted 与 delivered 历史在更广泛的 Session 保留策略生效前仍只追加。 +Lead Session 保留完整 task 快照以及历史 member 与 mailbox 记录。这优先支持可检查的恢复,而不是紧凑增量;deleted 与 delivered 历史在更广泛的 Session 保留策略生效前仍只追加。 -active roster 成员可能不驻留,因此 `inactive` 不表示失败,发送可能产生冷恢复延迟。准入失败会拒绝新发送。历史 pending 邮件永远不会被投递。provisioning 失败永久占用其名称和成员槽位。 +teammate 可能不驻留,因此 `inactive` 不表示失败,发送可能产生冷恢复延迟。历史 pending 邮件永远不会被投递。 协调可以降低 checkout 冲突概率,但无法消除文件系统 CAS 工具之外的写入。最终 diff 与测试仍是 Lead 的集成边界。 diff --git a/.agents/notes/implemented/simplification/2026-09-26-team-direct-inbox.i18n.yaml b/.agents/notes/implemented/simplification/2026-09-26-team-direct-inbox.i18n.yaml deleted file mode 100644 index f830391e86..0000000000 --- a/.agents/notes/implemented/simplification/2026-09-26-team-direct-inbox.i18n.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# Bilingual-pair consistency record for 2026-09-26-team-direct-inbox.md (docs/i18n/README.md): per heading -# section, a hash of its English and Chinese blocks outside code blocks and generated regions. -# After editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-09-26-team-direct-inbox.md -/agent-note-team-messages-use-the-target-inbox: - en: ae17b789652784b5 - zh: fa462ace95c8cdaa -/agent-note-team-messages-use-the-target-inbox/problem: - en: 83296699f94c77d3 - zh: e681a426bf0a703a -/agent-note-team-messages-use-the-target-inbox/decision: - en: 8d5fed34f19c7302 - zh: 25417793d3797775 -/agent-note-team-messages-use-the-target-inbox/alternatives-considered: - en: e847556d8e9f1071 - zh: bf1abe2f0e201eac -/agent-note-team-messages-use-the-target-inbox/consequences: - en: 24085acb09438dc8 - zh: 27c92e8de8cf7dbe diff --git a/apps/cli/tests/agent-team-headless.e2e.ts b/apps/cli/tests/agent-team-headless.e2e.ts index dd4e456252..66fb2c4ab0 100644 --- a/apps/cli/tests/agent-team-headless.e2e.ts +++ b/apps/cli/tests/agent-team-headless.e2e.ts @@ -17,7 +17,7 @@ function records(content: string): Record[] { } describe('dsh run with Agent Teams enabled', () => { - it('runs two teammates, direct peer messages, dependent tasks, waiting, and final aggregation', async () => { + it('runs two named teammates, sibling messages, dependent tasks, waiting, and final aggregation', async () => { const cwd = await mkdtemp(join(tmpdir(), 'dsh-agent-team-headless-')) try { const home = join(cwd, '.dsh') @@ -102,7 +102,10 @@ describe('dsh run with Agent Teams enabled', () => { }) expect(root).toBeDefined() const eventTypes = root!.map(record => record.type) - expect(eventTypes.filter(type => type === 'team/member')).toHaveLength(4) + expect(eventTypes).not.toContain('team/member') + expect(root!.filter(record => record.type === 'subagent/catalog') + .flatMap(record => (record.data as { name?: string }).name ?? [])) + .toEqual(['implementer', 'researcher']) expect(eventTypes).not.toContain('team/message/queued') expect(eventTypes).not.toContain('team/message/delivered') const peerMessages = parsed.flatMap(log => log.filter(record => record.type === 'user/message' diff --git a/apps/cli/tests/profiles/headless/team-snapshot.patch.yml b/apps/cli/tests/profiles/headless/team-snapshot.patch.yml index 290714194e..20dd97ba42 100644 --- a/apps/cli/tests/profiles/headless/team-snapshot.patch.yml +++ b/apps/cli/tests/profiles/headless/team-snapshot.patch.yml @@ -21,35 +21,14 @@ root: './.sessions' compression: none -- id: tool-subagent-control - disabled: true - -- id: tool-subagent-list-agents - disabled: true - -- id: tool-subagent-report - disabled: true - -- id: tool-subagent - disabled: true - -- id: tool-subagent-fork - disabled: true - - insert: - id: agent-team name: '@deepseek-ai/dsh-experimental-agent-team' config: - maxMembers: 8 maxTasks: 256 - maxMessageBytes: 65536 - disposalTimeoutMs: 5000 - id: tool-agent-team name: '@deepseek-ai/dsh-experimental-tool-agent-team' - config: - freshProvider: spawn - forkProvider: fork - id: team-fixture-llm name: './tests/fixtures/team-llm.mjs' diff --git a/apps/cli/tests/profiles/headless/tests/fixtures/team-llm.mjs b/apps/cli/tests/profiles/headless/tests/fixtures/team-llm.mjs index 9c1bedd00e..a61376043d 100644 --- a/apps/cli/tests/profiles/headless/tests/fixtures/team-llm.mjs +++ b/apps/cli/tests/profiles/headless/tests/fixtures/team-llm.mjs @@ -63,6 +63,11 @@ function textChunks(text) { function researcher(messages) { const names = calls(messages) + if (!names.includes('list_agents')) return toolChunks([{ name: 'list_agents', args: {} }]) + const sibling = messages.filter(message => message.role === 'tool') + .flatMap(message => message.content.filter(block => block.type === 'text').flatMap(block => block.text.split('\n'))) + .find(line => line.endsWith('— sibling: Own deterministic implementation.'))?.split(' ')[0] + if (sibling === undefined) throw new Error('list_agents did not expose the implementation sibling') if (!names.includes('team_task_create')) { return toolChunks([{ name: 'team_task_create', args: { subject: 'Research', description: 'Collect the deterministic finding.', write_scopes: ['research'], @@ -76,13 +81,13 @@ function researcher(messages) { if (!names.includes('send_message')) { return toolChunks([ { name: 'team_task_update', args: { task_id: 'task-1', expected_revision: 2, action: 'complete' } }, - { name: 'send_message', args: { target: 'implementer', message: 'Research complete: use the deterministic finding.' } }, + { name: 'send_message', args: { agent_id: sibling, message: 'Research complete: use the deterministic finding.' } }, ]) } return textChunks('Research teammate complete.') } -function implementer(messages) { +function implementer(messages, parentId) { const names = calls(messages) const last = latestAssistantCalls(messages) const text = latestToolText(messages) @@ -123,7 +128,7 @@ function implementer(messages) { if (!names.includes('send_message')) { return toolChunks([ { name: 'team_task_update', args: { task_id: 'task-2', expected_revision: 2, action: 'complete' } }, - { name: 'send_message', args: { target: 'lead', message: 'Implementation complete and verified.' } }, + { name: 'send_message', args: { agent_id: parentId, message: 'Implementation complete and verified.' } }, ]) } return textChunks('Implementation teammate complete.') @@ -138,26 +143,22 @@ function lead(messages) { script: 'return await agent("TEAM_WORKFLOW_CHILD");', } }]) } - const spawned = names.filter(name => name === 'spawn_teammate').length + const spawned = names.filter(name => name === 'subagent').length if (spawned === 0) { return toolChunks([{ - name: 'spawn_teammate', + name: 'subagent', args: { - name: 'implementer', description: 'Own deterministic implementation.', prompt: 'IMPLEMENTER_MARK: wait for research, complete dependent task 2, report to lead.', - context: 'fresh', }, }]) } if (spawned === 1) { return toolChunks([{ - name: 'spawn_teammate', + name: 'subagent', args: { - name: 'researcher', description: 'Own deterministic research.', prompt: 'RESEARCHER_MARK: complete research task 1, message implementer, then finish.', - context: 'fresh', }, }]) } @@ -168,7 +169,8 @@ function lead(messages) { return toolChunks([{ name: 'team_task_list', args: {} }]) } if (last.includes('list_agents')) { - const inactive = result.match(/"status":"inactive"/gu)?.length ?? 0 + const inactive = result.split('\n') + .filter(row => row.includes('— Own deterministic ') && row.includes('[inactive]')).length if (inactive >= 2) return textChunks('TEAM_WORKFLOW_OK: both teammates and dependent tasks completed.') return toolChunks([{ name: 'list_agents', args: {} }]) } @@ -179,21 +181,21 @@ function lead(messages) { class TeamFixtureAdapter extends LlmAdapter { async * stream(options) { const tools = options.tools.map(tool => tool.name) - if (tools.includes('subagent') || tools.includes('subagent_fork')) { - throw new Error('Team profile exposes a direct subagent tool') - } const initial = options.messages.findLast(message => message.role === 'user' && message.source.kind === 'user') const workflowChild = initial?.content[0]?.text === 'TEAM_WORKFLOW_CHILD' const context = options.messages.findLast(message => message.source?.kind === 'runtime-context') - const identity = context?.source.sections?.find(section => section.name === 'team:identity')?.text - if (!workflowChild && (!tools.includes('spawn_teammate') || !tools.includes('workflow'))) { - throw new Error('Team profile is missing teammate or workflow tools') + const section = name => context?.source.sections?.find(candidate => candidate.name === name)?.text + const role = section('team:identity') + const parentId = role?.match(/led by agent "([^"]+)"/u)?.[1] + const task = initial?.content.filter(block => block.type === 'text').map(block => block.text).join('\n') + if (!workflowChild && (!tools.includes('subagent') || !tools.includes('workflow') || !tools.includes('team_task_create'))) { + throw new Error('Team profile is missing delegation, workflow, or task tools') } let chunks if (workflowChild) chunks = textChunks('Fresh workflow child complete.') - else if (identity?.startsWith('You are teammate "researcher".')) chunks = researcher(options.messages) - else if (identity?.startsWith('You are teammate "implementer".')) chunks = implementer(options.messages) - else if (identity?.startsWith('You are the Team Lead named "lead".')) chunks = lead(options.messages) + else if (task?.startsWith('RESEARCHER_MARK:')) chunks = researcher(options.messages) + else if (task?.startsWith('IMPLEMENTER_MARK:')) chunks = implementer(options.messages, parentId) + else if (role?.startsWith('You are the Team Lead.')) chunks = lead(options.messages) else throw new Error('Team runtime context is missing a recognized identity') for (const chunk of chunks) { options.signal?.throwIfAborted() diff --git a/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts b/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts index 2e45acbb6c..a7b1472f88 100644 --- a/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts +++ b/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts @@ -905,7 +905,7 @@ describe('headless stream-json snapshots', () => { } }, LOADER_SMOKE_TEST_TIMEOUT_MS) - it('runs a keyless Agent Team with peer mail, dependent tasks, waiting, and Lead aggregation', async () => { + it('runs a keyless Agent Team with described teammates, sibling messages, dependent tasks, waiting, and Lead aggregation', async () => { let projection: unknown const result = await runLoaderSmoke({ label: 'Agent Teams headless snapshot', @@ -937,8 +937,8 @@ describe('headless stream-json snapshots', () => { expect(workflowRows.filter(row => row.type === 'user/message' && ((row.data as JsonObject).source as JsonObject).kind === 'user').map(row => row.data)) .toEqual([expect.objectContaining({ content: [{ type: 'text', text: 'TEAM_WORKFLOW_CHILD' }] })]) - const members = rows.filter(row => row.type === 'team/member') - .map(row => ((row.data as JsonObject).member as JsonObject)) + const teammates = rows.filter(row => row.type === 'subagent/catalog') + .flatMap(row => typeof (row.data as JsonObject).label === 'string' && ((row.data as JsonObject).label as string).startsWith('Own deterministic ') ? [(row.data as JsonObject).label as string] : []) const tasks = rows.filter(row => row.type === 'team/task') .map(row => ((row.data as JsonObject).task as JsonObject)) const latestTasks = Object.values(Object.fromEntries(tasks.map(task => [String(task.subject), task]))) @@ -991,21 +991,26 @@ describe('headless stream-json snapshots', () => { && ((row.data as JsonObject).source as JsonObject).kind === 'user') if (initial === undefined) throw new Error('Teammate Session has no initial task') const content = (initial.data as JsonObject).content as JsonObject[] - expect(content).toHaveLength(1) + // The task block is followed by the standard return guidance naming the Lead. + expect(content).toHaveLength(2) const context = parseJsonl(log.content).find(row => row.type === 'user/message' && ((row.data as JsonObject).source as JsonObject).kind === 'runtime-context') const source = (context?.data as JsonObject)?.source as JsonObject const sections = source?.sections as JsonObject[] | undefined - const identity = sections?.find(section => section.name === 'team:identity')?.text - if (typeof identity !== 'string') throw new Error('Teammate runtime context has no identity') - return [identity] + const leadId = parent.header.id as string + const text = (name: string) => { + const value = sections?.find(section => section.name === name)?.text + if (typeof value !== 'string') throw new Error(`Teammate runtime context has no ${name}`) + return value.replaceAll(leadId, '') + } + expect(sections?.some(section => section.name === 'subagent:identity')).toBe(false) + return [text('team:identity')] }).sort() projection = { sessions: logs.length, workflowStopReason: (rows.find(row => row.type === 'tool-workflow/run-end')?.data as JsonObject)?.stopReason, - memberEdges: members.length, identities, - activeMembers: members.filter(member => member.phase === 'active').map(member => member.name).sort(), + teammates: [...teammates].sort(), tasks: latestTasks.map(task => ({ subject: task.subject, revision: task.revision, @@ -1033,25 +1038,12 @@ describe('headless stream-json snapshots', () => { }) expect(projection).toMatchInlineSnapshot(` { - "activeMembers": [ - "implementer", - "researcher", - ], "checkedRoster": true, "deliveredMessages": 0, "identities": [ - "You are teammate "implementer". - Your Team Lead is named "lead". - To message your Team Lead, use send_message({ target: "lead", message: "..." }). - Use list_agents({}) to find your teammates and their names. - To message another teammate, use send_message({ target: "", message: "..." }).", - "You are teammate "researcher". - Your Team Lead is named "lead". - To message your Team Lead, use send_message({ target: "lead", message: "..." }). - Use list_agents({}) to find your teammates and their names. - To message another teammate, use send_message({ target: "", message: "..." }).", + "You are a teammate in the Team led by agent "".", + "You are a teammate in the Team led by agent "".", ], - "memberEdges": 4, "queuedMessages": 0, "sessions": 4, "steerEvidence": { @@ -1072,6 +1064,10 @@ describe('headless stream-json snapshots', () => { "subject": "Research", }, ], + "teammates": [ + "Own deterministic implementation.", + "Own deterministic research.", + ], "waited": true, "workflowStopReason": "completed", } diff --git a/apps/web/tests/agent-team-panel.e2e.ts b/apps/web/tests/agent-team-panel.e2e.ts index bd7dcff720..2068e47a88 100644 --- a/apps/web/tests/agent-team-panel.e2e.ts +++ b/apps/web/tests/agent-team-panel.e2e.ts @@ -88,7 +88,7 @@ describe('web e2e: Agent Teams panel', () => { await page.locator('[data-team-action]').getByRole('button', { name: /Agent Team/iu }).click() const action = page.getByRole('dialog', { name: 'Agent Team', exact: true }) await action.getByText('No shared tasks yet. Create them through the conversation.').waitFor() - await action.getByText('lead').waitFor() + await action.getByText('Lead').waitFor() expect(projectionReads).not.toHaveBeenCalled() expect(await action.getByRole('button', { name: 'New task' }).count()).toBe(0) @@ -105,7 +105,7 @@ describe('web e2e: Agent Teams panel', () => { taskId: task.id, expectedRevision: task.revision, action: 'claim', }) await action.getByText('In progress', { exact: true }).waitFor() - await action.getByText('Owner: lead', { exact: true }).waitFor() + await action.getByText('Owner: Lead', { exact: true }).waitFor() expect(await action.getByRole('button', { name: /^(New task|Edit|Complete|Reopen|Delete)$/u }).count()).toBe(0) expect(await action.locator('input, select, textarea').count()).toBe(0) @@ -123,7 +123,7 @@ describe('web e2e: Agent Teams panel', () => { const trigger = page.locator('[data-team-action]').getByRole('button', { name: /Agent Team/iu }) const viewport = page.viewportSize()! await trigger.click() - await panel.getByText('lead', { exact: true }).waitFor() + await panel.getByText('Lead', { exact: true }).waitFor() try { for (const size of [{ width: 760, height: 540 }, { width: 600, height: 420 }]) { await page.setViewportSize(size) diff --git a/apps/web/tests/agent-team-panel.overlay.yml b/apps/web/tests/agent-team-panel.overlay.yml index ecca62985b..1c552e8c1f 100644 --- a/apps/web/tests/agent-team-panel.overlay.yml +++ b/apps/web/tests/agent-team-panel.overlay.yml @@ -1,30 +1,12 @@ # Equivalent to the Agent Teams bundle over the stable base and Web bundles. -- id: tool-subagent-control - disabled: true - -- id: tool-subagent-list-agents - disabled: true - -- id: tool-subagent - disabled: true - -- id: tool-subagent-fork - disabled: true - - insert: - id: agent-team name: '@deepseek-ai/dsh-experimental-agent-team' config: - maxMembers: 8 maxTasks: 256 - maxMessageBytes: 65536 - disposalTimeoutMs: 5000 - id: tool-agent-team name: '@deepseek-ai/dsh-experimental-tool-agent-team' - config: - freshProvider: spawn - forkProvider: fork - id: ui-agent-team name: '@deepseek-ai/dsh-experimental-client-ui-agent-team' diff --git a/apps/web/tests/snapshots/agent-team-panel/resize.expected.md b/apps/web/tests/snapshots/agent-team-panel/resize.expected.md index 19f90be9a3..b11d6c8e32 100644 --- a/apps/web/tests/snapshots/agent-team-panel/resize.expected.md +++ b/apps/web/tests/snapshots/agent-team-panel/resize.expected.md @@ -1,12 +1,12 @@ - dialog "Agent Team": - heading "Members" [level=3] - - button "lead Current chat Inactive" [disabled] + - button "Lead Current chat Inactive" [disabled] - heading "Shared tasks 2" [level=3] - article: - strong: Agent task - text: In progress - paragraph: Created by the Team Lead - - text: "task-1Owner: leadWrite scopes: src/web" + - text: "task-1Owner: LeadWrite scopes: src/web" - article: - strong: Responsive task - text: Pending diff --git a/apps/web/tests/snapshots/agent-team-panel/task.expected.md b/apps/web/tests/snapshots/agent-team-panel/task.expected.md index 9e6f63532e..b526c3a74c 100644 --- a/apps/web/tests/snapshots/agent-team-panel/task.expected.md +++ b/apps/web/tests/snapshots/agent-team-panel/task.expected.md @@ -1,9 +1,9 @@ - dialog "Agent Team": - heading "Members" [level=3] - - button "lead Current chat Inactive" [disabled] + - button "Lead Current chat Inactive" [disabled] - heading "Shared tasks 1" [level=3] - article: - strong: Agent task - text: In progress - paragraph: Created by the Team Lead - - text: "task-1Owner: leadWrite scopes: src/web" + - text: "task-1Owner: LeadWrite scopes: src/web" diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index 8206f3e6dc..d2f02cfead 100644 --- a/docs/architecture.i18n.yaml +++ b/docs/architecture.i18n.yaml @@ -30,8 +30,8 @@ en: a2e35c43438fec6d zh: e27bdac8e9feff32 /deepseek-harness-architecture/capability-seams: - en: a5bc20c3f2a1b370 - zh: 91773a281410b2f4 + en: 8f9ab91984125fca + zh: 4ed6b2c510478574 /deepseek-harness-architecture/where-new-behavior-goes: en: 9b2eb0539a9cd0d6 zh: 4f575277b91d4e5c diff --git a/docs/architecture.md b/docs/architecture.md index 03d88148c7..d2ca59ec4b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -133,7 +133,7 @@ A **seam** is a swappable capability with three roles: a **Service Definition** Seams are why one provider swap changes the whole product. Filesystem and subprocess providers share one execution world, so pointing them at a remote sandbox moves Bash, PTY, and LSP with them, with no provider forks. [Subagent providers](subsystems/subagent.md) vary just as widely behind one interface, from a fresh child agent to a delegated turn in another product. -[Experimental Agent Teams](subsystems/agent-team.md) exposes opt-in `ctx.agentTeams` coordination with durable roster and task state plus direct inbox messaging over continuable subagents. +[Experimental Agent Teams](subsystems/agent-team.md) adds opt-in `ctx.agentTeams` task coordination and change waits over subagent lineage; subagents own creation, messaging, and lifecycle. ## Where new behavior goes diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index 870fa1c996..8c67a7b5e5 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -137,7 +137,7 @@ Session 消费方只了解当前逻辑格式。仅 header 的 `stat` 与 `list` seam 正是替换一个提供方就能改变整个产品的原因。文件系统与进程提供方共享同一个执行世界,因此把它们指向远程沙箱,也就把 Bash、PTY 和 LSP 一并搬了过去,无需提供方专用 fork。[subagent 提供方](subsystems/subagent.zh.md)在同一个接口之后同样千差万别,从新建一个子 agent,到把一个轮次委派给另一个产品。 -[实验性 Agent Teams](subsystems/agent-team.zh.md) 通过显式启用的 `ctx.agentTeams`,在可继续 subagent 之上提供持久 roster、任务状态和直接 inbox 消息协作。 +[实验性 Agent Teams](subsystems/agent-team.zh.md) 通过显式启用的 `ctx.agentTeams`,基于子代理谱系提供任务协调和变更等待;创建、消息和生命周期由子代理负责。 ## 新行为的归属位置 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 5240eec77a..5b7ed380d6 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -840,20 +840,14 @@ export interface Config { ## `@deepseek-ai/dsh-experimental-agent-team` -- `inject`: `agents` · `sessions` · `sessionPersistence` · `sessionProjections` · `subagents` -- `source`: [`packages/experimental/agent-team/src/types.ts:153`](../packages/experimental/agent-team/src/types.ts) +- `inject`: `agents` · `sessions` · `sessionProjections` +- `source`: [`packages/experimental/agent-team/src/types.ts:152`](../packages/experimental/agent-team/src/types.ts) ```ts config-catalog /** Team-service deployment limits. */ export interface Config { - /** Maximum immutable teammate names retained by one Team. */ - readonly maxMembers?: number /** Maximum non-deleted tasks retained by one Team. */ readonly maxTasks?: number - /** Maximum UTF-8 bytes in one complete sender-framed delivery. */ - readonly maxMessageBytes?: number - /** Maximum milliseconds for shared operation settlement and for each Team child drain. */ - readonly disposalTimeoutMs?: number } ``` @@ -1270,15 +1264,8 @@ export interface Config { - `source`: [`packages/experimental/tool-agent-team/src/index.ts:17`](../packages/experimental/tool-agent-team/src/index.ts) ```ts config-catalog -/** Tool routing configuration. */ -export interface Config { - /** Register `interrupt_agent` and its policy guidance. Defaults to true; false leaves the domain API available. */ - readonly interrupt?: boolean - /** Continuable-subagent provider used for fresh teammates. */ - readonly freshProvider?: string - /** Continuable-subagent provider used for completed-prefix fork teammates. */ - readonly forkProvider?: string -} +/** The Team tool plugin takes no configuration. */ +export type Config = Record ``` @@ -3325,7 +3312,7 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist' - `inject`: `workingDirectory` - `refs`: `Volatile` (`@deepseek-ai/cordis`) -- `source`: [`packages/subagent/subagent/src/index.ts:186`](../packages/subagent/subagent/src/index.ts) +- `source`: [`packages/subagent/subagent/src/index.ts:187`](../packages/subagent/subagent/src/index.ts) ```ts config-catalog /** Host configuration for continuable subagent capacity. */ diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index bf66626c51..625c4ddb99 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -842,20 +842,14 @@ export interface Config { ## `@deepseek-ai/dsh-experimental-agent-team` -- `inject`: `agents` · `sessions` · `sessionPersistence` · `sessionProjections` · `subagents` -- `source`: [`packages/experimental/agent-team/src/types.ts:153`](../packages/experimental/agent-team/src/types.ts) +- `inject`: `agents` · `sessions` · `sessionProjections` +- `source`: [`packages/experimental/agent-team/src/types.ts:152`](../packages/experimental/agent-team/src/types.ts) ```ts config-catalog /** Team-service deployment limits. */ export interface Config { - /** Maximum immutable teammate names retained by one Team. */ - readonly maxMembers?: number /** Maximum non-deleted tasks retained by one Team. */ readonly maxTasks?: number - /** Maximum UTF-8 bytes in one complete sender-framed delivery. */ - readonly maxMessageBytes?: number - /** Maximum milliseconds for shared operation settlement and for each Team child drain. */ - readonly disposalTimeoutMs?: number } ``` @@ -1272,15 +1266,8 @@ export interface Config { - `source`: [`packages/experimental/tool-agent-team/src/index.ts:17`](../packages/experimental/tool-agent-team/src/index.ts) ```ts config-catalog -/** Tool routing configuration. */ -export interface Config { - /** Register `interrupt_agent` and its policy guidance. Defaults to true; false leaves the domain API available. */ - readonly interrupt?: boolean - /** Continuable-subagent provider used for fresh teammates. */ - readonly freshProvider?: string - /** Continuable-subagent provider used for completed-prefix fork teammates. */ - readonly forkProvider?: string -} +/** The Team tool plugin takes no configuration. */ +export type Config = Record ``` @@ -3327,7 +3314,7 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist' - `inject`: `workingDirectory` - `refs`: `Volatile` (`@deepseek-ai/cordis`) -- `source`: [`packages/subagent/subagent/src/index.ts:186`](../packages/subagent/subagent/src/index.ts) +- `source`: [`packages/subagent/subagent/src/index.ts:187`](../packages/subagent/subagent/src/index.ts) ```ts config-catalog /** Host configuration for continuable subagent capacity. */ diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 659b9d53a5..61f51dfbb8 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -11,12 +11,12 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:249`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | | `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:69`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` | | `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` | -| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `browser-use-runtime`, `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | | `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | | `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:393`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) | | `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) | | `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) | -| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) | +| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | `agent-team`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) | | `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) | | `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/experimental/webhook) | | `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) | @@ -63,15 +63,15 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:478`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` | | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - | | `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`permission-presets`](../packages/interaction/permission-presets), [`schedule`](../packages/schedule/schedule), `server`, `session-controller`, [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry) | -| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:63`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`workspace-changes`](../packages/deliverables/workspace-changes) | -| `session/event` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:63`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `session/event` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:82`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) | | `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:75`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`emit`) | `remotes` | | `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:296`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - | -| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:166`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), `server` | -| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:142`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:148`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:158`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | +| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:167`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), `server` | +| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:143`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:149`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:159`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | | `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), `browser-use-runtime`, [`session-reference`](../packages/context/session-reference), [`working-directory`](../packages/session/working-directory) | | `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - | | `tools/change` | `emit` | [`packages/core/tools/src/index.ts:209`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index 19d1da2409..9b16b69863 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -13,12 +13,12 @@ | `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:249`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | | `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:69`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` | | `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` | -| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `browser-use-runtime`, `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | | `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | | `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:393`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) | | `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) | | `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) | -| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) | +| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | `agent-team`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) | | `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) | | `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/experimental/webhook) | | `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) | @@ -65,15 +65,15 @@ | `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:478`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` | | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - | | `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`permission-presets`](../packages/interaction/permission-presets), [`schedule`](../packages/schedule/schedule), `server`, `session-controller`, [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry) | -| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:63`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`workspace-changes`](../packages/deliverables/workspace-changes) | -| `session/event` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:63`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `session/event` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:82`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) | | `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:75`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`emit`) | `remotes` | | `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:296`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - | -| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:166`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), `server` | -| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:142`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:148`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:158`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | +| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:167`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), `server` | +| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:143`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:149`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:159`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | | `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), `browser-use-runtime`, [`session-reference`](../packages/context/session-reference), [`working-directory`](../packages/session/working-directory) | | `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - | | `tools/change` | `emit` | [`packages/core/tools/src/index.ts:209`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) | diff --git a/docs/module-graph.md b/docs/module-graph.md index 00bc4da738..284116e967 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -1329,7 +1329,6 @@ flowchart TD pkg_experimental_agent_team --> pkg_agent pkg_experimental_agent_team --> pkg_llm pkg_experimental_agent_team --> pkg_session - pkg_experimental_agent_team --> pkg_session_persistence pkg_experimental_agent_team --> pkg_session_projection pkg_experimental_agent_team --> pkg_subagent pkg_hooks_claude_code --> pkg_agent @@ -1750,7 +1749,7 @@ flowchart TD | [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`subagent`](../packages/subagent/subagent) | | [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | | [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`spill`](../packages/spill/spill), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) | -| [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent) | +| [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent) | | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | `experimental` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/experimental/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`tool-ralph`](../packages/experimental/tool-ralph) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index c452bebf03..165641d138 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -1331,7 +1331,6 @@ flowchart TD pkg_experimental_agent_team --> pkg_agent pkg_experimental_agent_team --> pkg_llm pkg_experimental_agent_team --> pkg_session - pkg_experimental_agent_team --> pkg_session_persistence pkg_experimental_agent_team --> pkg_session_projection pkg_experimental_agent_team --> pkg_subagent pkg_hooks_claude_code --> pkg_agent @@ -1752,7 +1751,7 @@ flowchart TD | [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`subagent`](../packages/subagent/subagent) | | [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | | [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`spill`](../packages/spill/spill), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) | -| [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent) | +| [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent) | | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | `experimental` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/experimental/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`tool-ralph`](../packages/experimental/tool-ralph) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index 7d6496f3b6..5cd163ac76 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -207,8 +207,8 @@ en: 9630752e97ddbfbc zh: 9630752e97ddbfbc /session-persistence-event-catalog/events/subagent/subagent-catalog-log-only: - en: e6ce4b5a98ff2f52 - zh: 9a8e112754679aef + en: 0ef7504e93df1ac0 + zh: d8bf76a1b1e7c362 /session-persistence-event-catalog/events/subagent/subagent-descriptor-log-only: en: f5d53639dc5afdfc zh: 8c553e7397d08832 @@ -225,17 +225,17 @@ en: a640800a38b3c874 zh: a640800a38b3c874 /session-persistence-event-catalog/events/team/team-member-log-only: - en: a2fe5b8582ef5f46 - zh: 42bfffcde3f3d886 + en: 99f18a2de16fd363 + zh: 2b163e019d464cac /session-persistence-event-catalog/events/team/team-message-delivered-log-only: - en: 4a716b14e33eab18 - zh: dcde26e4dc40b62e + en: 5bfe87b1a9b64556 + zh: 61746aba1ccc41af /session-persistence-event-catalog/events/team/team-message-queued-log-only: - en: 05bf873244099da5 - zh: 8a0b62912087f4aa + en: bf19aa4094eb7a9b + zh: 38c4f3a86da39d4e /session-persistence-event-catalog/events/team/team-task-log-only: - en: 5ffeb64225feda2a - zh: d58129e189a726fb + en: 94d42a4f196539ac + zh: 95521a242d363b94 /session-persistence-event-catalog/events/todo: en: 308d4bb4b6b96e35 zh: 308d4bb4b6b96e35 @@ -1224,8 +1224,8 @@ en: 6a9cc39c2df1b476 zh: 697ea4d14d77f59c /session-persistence-event-catalog/resolved-persistence-types/subagentcatalogevent: - en: 725539dcd5c335ed - zh: 5ca08d3667f84ff9 + en: 4da876322ab7a590 + zh: 67992071915ab925 /session-persistence-event-catalog/resolved-persistence-types/subagentdescriptordata: en: c56821e57987ae9e zh: 0364f59a472d51da @@ -1242,23 +1242,23 @@ en: a3eabd0e9fc565dd zh: fa763aefdbc1bdb8 /session-persistence-event-catalog/resolved-persistence-types/teammemberphase: - en: 2bf8f06fe328ab33 - zh: ded602f18e0deadf + en: 562d6349db44cc77 + zh: 5a6839b217a3fa34 /session-persistence-event-catalog/resolved-persistence-types/teammembersnapshot: - en: 8a37845006830548 - zh: b8e7caf6189b92df + en: fa7208409888557c + zh: 01f948c369fd2973 /session-persistence-event-catalog/resolved-persistence-types/teammessagesnapshot: - en: 9b94faac43ba068f - zh: a83582be6a6a3c1a + en: 31fb7e4f07b49012 + zh: 15fd933a217dadee /session-persistence-event-catalog/resolved-persistence-types/teammessagesource: - en: 269f0a4fcbea1c0c - zh: 5f2c50c03c951995 + en: 11bec13073c76f30 + zh: e90deecb47471c74 /session-persistence-event-catalog/resolved-persistence-types/teamtasksnapshot: - en: cfa8230ef5d661b2 - zh: cb40ce86d6778f09 + en: dc1ef3c62e071b07 + zh: 3d647cdc465a4139 /session-persistence-event-catalog/resolved-persistence-types/teamtaskstatus: - en: 9bff57038f472770 - zh: 4eec0e793ae16d53 + en: 80e26a96e24847e4 + zh: 038ca1e72e795520 /session-persistence-event-catalog/resolved-persistence-types/textblock: en: c5fcd2cdd37ea387 zh: 9c678fbdea0bf25c @@ -1344,8 +1344,8 @@ en: 2786fef560851bb4 zh: 5d787475356b2606 /session-persistence-event-catalog/resolved-persistence-types/string: - en: 009c7ecb8c7d3bb5 - zh: 7f56d672f376b22f + en: 41cbdc9d8adc6bfc + zh: 7160a3199b6808b0 /session-persistence-event-catalog/resolved-persistence-types/string~2: en: 0089620bf4638263 zh: 1922bef1713094bb @@ -1866,17 +1866,17 @@ en: afedf4a31a67eb14 zh: 98d3bc882f2b61c3 /session-persistence-event-catalog/resolved-persistence-types/member-teamid-version: - en: 82b0989dad6bbfc0 - zh: 0789b63f1d59b2ed + en: ac4ee9f0fcee4c1a + zh: fb437a84244237af /session-persistence-event-catalog/resolved-persistence-types/message-step-turn: en: dcaa676f106cfdfd zh: e3941fea97c943fb /session-persistence-event-catalog/resolved-persistence-types/message-teamid-version: - en: f778769acb264dd5 - zh: eba9b81d04e9f57b + en: 704dd6cdbb709924 + zh: 391a59bb937ba29d /session-persistence-event-catalog/resolved-persistence-types/messageid-targetid-teamid-version: - en: 91a948ede28ebcb5 - zh: 8a2b0ac6bb68a09e + en: 710f8046f611bdf3 + zh: 8643906e9ec6bdae /session-persistence-event-catalog/resolved-persistence-types/mode-source: en: 3028b222670ce5ad zh: cae4c9437e560d61 @@ -1908,8 +1908,8 @@ en: 5360af614fe753e3 zh: 96427c489cc71de6 /session-persistence-event-catalog/resolved-persistence-types/task-teamid-version: - en: a2fcd46e54b14120 - zh: d9a6e0e50bbff9ad + en: 3f893a77a7b25a01 + zh: 06721be3c9a30e82 /session-persistence-event-catalog/resolved-persistence-types/todos: en: 06e6c1837e2b5d50 zh: 37cebfe3e41294ec diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index ec7e3240da..0d4a8c566b 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -938,7 +938,7 @@ Source: [`packages/core/session/src/types.ts:300`](../packages/core/session/src/ 'subagent/catalog': SubagentCatalogEvent ``` -Source: [`packages/subagent/subagent/src/catalog.ts:44`](../packages/subagent/subagent/src/catalog.ts) +Source: [`packages/subagent/subagent/src/catalog.ts:45`](../packages/subagent/subagent/src/catalog.ts) @@ -1007,13 +1007,13 @@ Source: [`packages/core/session/src/types.ts:331`](../packages/core/session/src/ #### `team/member` — log-only ```ts persistence-catalog -/** Whole teammate lifecycle value, stored only in the Team Lead Session. */ +/** Historical teammate lifecycle value; new teammates are continuable entries in the Lead's subagent catalog. */ 'team/member': { version: 2; teamId: TeamId; member: TeamMemberSnapshot } ``` Types: [TeamId](subsystems/agent-team.md) · [TeamMemberSnapshot](subsystems/agent-team.md) -Source: [`packages/experimental/agent-team/src/types.ts:230`](../packages/experimental/agent-team/src/types.ts) +Source: [`packages/experimental/agent-team/src/types.ts:196`](../packages/experimental/agent-team/src/types.ts) @@ -1031,7 +1031,7 @@ Source: [`packages/experimental/agent-team/src/types.ts:230`](../packages/experi Types: [TeamId](subsystems/agent-team.md) · [TeamMessageId](subsystems/agent-team.md) -Source: [`packages/experimental/agent-team/src/types.ts:236`](../packages/experimental/agent-team/src/types.ts) +Source: [`packages/experimental/agent-team/src/types.ts:202`](../packages/experimental/agent-team/src/types.ts) @@ -1044,7 +1044,7 @@ Source: [`packages/experimental/agent-team/src/types.ts:236`](../packages/experi Types: [TeamId](subsystems/agent-team.md) · [TeamMessageSnapshot](subsystems/agent-team.md) -Source: [`packages/experimental/agent-team/src/types.ts:234`](../packages/experimental/agent-team/src/types.ts) +Source: [`packages/experimental/agent-team/src/types.ts:200`](../packages/experimental/agent-team/src/types.ts) @@ -1057,7 +1057,7 @@ Source: [`packages/experimental/agent-team/src/types.ts:234`](../packages/experi Types: [TeamId](subsystems/agent-team.md) · [TeamTaskSnapshot](subsystems/agent-team.md) -Source: [`packages/experimental/agent-team/src/types.ts:232`](../packages/experimental/agent-team/src/types.ts) +Source: [`packages/experimental/agent-team/src/types.ts:198`](../packages/experimental/agent-team/src/types.ts) ### `todo/*` @@ -4820,7 +4820,7 @@ One of: SHA-256: `f731ffe80810b0289b3e694d8f40af6f01865c02a622ee457be90ca808b66e87` -Sources: [`packages/subagent/subagent/src/catalog.ts:28`](../packages/subagent/subagent/src/catalog.ts) +Sources: [`packages/subagent/subagent/src/catalog.ts:29`](../packages/subagent/subagent/src/catalog.ts) One of: @@ -4929,7 +4929,7 @@ Sources: [`packages/llm/llm/src/message.ts:35`](../packages/llm/llm/src/message. SHA-256: `dfce5db4aa42b7ebe0bb79eca92553587287e18c06b9eba85cbfb9d332e26acd` -Sources: [`packages/experimental/agent-team/src/types.ts:45`](../packages/experimental/agent-team/src/types.ts) +Sources: [`packages/experimental/agent-team/src/types.ts:47`](../packages/experimental/agent-team/src/types.ts) One of: @@ -4947,7 +4947,7 @@ One of: SHA-256: `6598cd6da253948d6b207c6374a45335804c79b576866d6215649c2d1bc85e25` -Sources: [`packages/experimental/agent-team/src/types.ts:48`](../packages/experimental/agent-team/src/types.ts) +Sources: [`packages/experimental/agent-team/src/types.ts:50`](../packages/experimental/agent-team/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -4969,7 +4969,7 @@ Sources: [`packages/experimental/agent-team/src/types.ts:48`](../packages/experi SHA-256: `2eafefdc6f79a4922d68a787a09eaf4dd2b2369c3777fba4c05e800b5678ac8e` -Sources: [`packages/experimental/agent-team/src/types.ts:129`](../packages/experimental/agent-team/src/types.ts) +Sources: [`packages/experimental/agent-team/src/types.ts:128`](../packages/experimental/agent-team/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -4989,7 +4989,7 @@ Sources: [`packages/experimental/agent-team/src/types.ts:129`](../packages/exper SHA-256: `2152caf1a71e588e4df2358a806dea24936c1c47131134f01a12939d12cdc0c0` -Sources: [`packages/experimental/agent-team/src/types.ts:138`](../packages/experimental/agent-team/src/types.ts) +Sources: [`packages/experimental/agent-team/src/types.ts:137`](../packages/experimental/agent-team/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -5009,7 +5009,7 @@ Sources: [`packages/experimental/agent-team/src/types.ts:138`](../packages/exper SHA-256: `51edec100659da3a9bd9c3f6ba1180f5e64ec9a1585e5057080df45c180bd357` -Sources: [`packages/experimental/agent-team/src/types.ts:75`](../packages/experimental/agent-team/src/types.ts) +Sources: [`packages/experimental/agent-team/src/types.ts:70`](../packages/experimental/agent-team/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -5032,7 +5032,7 @@ Sources: [`packages/experimental/agent-team/src/types.ts:75`](../packages/experi SHA-256: `b2d2a01e51341a3bfec809198952c8c830a7899f4cb6ecd3c1dc0b5d9a2af943` -Sources: [`packages/experimental/agent-team/src/types.ts:72`](../packages/experimental/agent-team/src/types.ts) +Sources: [`packages/experimental/agent-team/src/types.ts:67`](../packages/experimental/agent-team/src/types.ts) One of: @@ -5575,7 +5575,7 @@ Array of `number`. SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b` -Sources: [`packages/api/session-controller/src/types.ts:409`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:21`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:33`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:9`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts) +Sources: [`packages/api/session-controller/src/types.ts:409`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:20`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:32`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:8`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts) `string` @@ -7698,7 +7698,7 @@ Sources: [`packages/web/web-search-deepseek/src/provider.ts:61`](../packages/web SHA-256: `b1ebd5cda47d90d980bb05bffc3f970c51537b9aff15b3bc3d639811727741b1` -Sources: [`packages/experimental/agent-team/src/types.ts:230`](../packages/experimental/agent-team/src/types.ts) +Sources: [`packages/experimental/agent-team/src/types.ts:196`](../packages/experimental/agent-team/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -7726,7 +7726,7 @@ Sources: [`packages/core/session/src/types.ts:331`](../packages/core/session/src SHA-256: `ea08ec1df0c33108fd03345b9af199cf04dea450aa045d3c683f216311403403` -Sources: [`packages/experimental/agent-team/src/types.ts:234`](../packages/experimental/agent-team/src/types.ts) +Sources: [`packages/experimental/agent-team/src/types.ts:200`](../packages/experimental/agent-team/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -7740,7 +7740,7 @@ Sources: [`packages/experimental/agent-team/src/types.ts:234`](../packages/exper SHA-256: `0bcfaf5566d271028429ac7b562cb59053c9d78b8ae14708c35fac835108d1b1` -Sources: [`packages/experimental/agent-team/src/types.ts:236`](../packages/experimental/agent-team/src/types.ts) +Sources: [`packages/experimental/agent-team/src/types.ts:202`](../packages/experimental/agent-team/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -7886,7 +7886,7 @@ Sources: [`packages/compaction/compaction-image-offload/src/projection.ts:25`](. SHA-256: `a40d12070f6f4a124f32fb3cc86e7857554702e32f0eda3080a25c4b9ac9b18c` -Sources: [`packages/experimental/agent-team/src/types.ts:232`](../packages/experimental/agent-team/src/types.ts) +Sources: [`packages/experimental/agent-team/src/types.ts:198`](../packages/experimental/agent-team/src/types.ts) | Property | Presence | Type | |---|---|---| diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index 15d1af1d58..aeffdcd496 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -940,7 +940,7 @@ export type SessionEvent = { 'subagent/catalog': SubagentCatalogEvent ``` -来源:[`packages/subagent/subagent/src/catalog.ts:44`](../packages/subagent/subagent/src/catalog.ts) +来源:[`packages/subagent/subagent/src/catalog.ts:45`](../packages/subagent/subagent/src/catalog.ts) @@ -1009,13 +1009,13 @@ export type SessionEvent = { #### `team/member` — log-only ```ts persistence-catalog -/** Whole teammate lifecycle value, stored only in the Team Lead Session. */ +/** Historical teammate lifecycle value; new teammates are continuable entries in the Lead's subagent catalog. */ 'team/member': { version: 2; teamId: TeamId; member: TeamMemberSnapshot } ``` 类型:[TeamId](subsystems/agent-team.zh.md) · [TeamMemberSnapshot](subsystems/agent-team.zh.md) -来源:[`packages/experimental/agent-team/src/types.ts:230`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/experimental/agent-team/src/types.ts:196`](../packages/experimental/agent-team/src/types.ts) @@ -1033,7 +1033,7 @@ export type SessionEvent = { 类型:[TeamId](subsystems/agent-team.zh.md) · [TeamMessageId](subsystems/agent-team.zh.md) -来源:[`packages/experimental/agent-team/src/types.ts:236`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/experimental/agent-team/src/types.ts:202`](../packages/experimental/agent-team/src/types.ts) @@ -1046,7 +1046,7 @@ export type SessionEvent = { 类型:[TeamId](subsystems/agent-team.zh.md) · [TeamMessageSnapshot](subsystems/agent-team.zh.md) -来源:[`packages/experimental/agent-team/src/types.ts:234`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/experimental/agent-team/src/types.ts:200`](../packages/experimental/agent-team/src/types.ts) @@ -1059,7 +1059,7 @@ export type SessionEvent = { 类型:[TeamId](subsystems/agent-team.zh.md) · [TeamTaskSnapshot](subsystems/agent-team.zh.md) -来源:[`packages/experimental/agent-team/src/types.ts:232`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/experimental/agent-team/src/types.ts:198`](../packages/experimental/agent-team/src/types.ts) ### `todo/*` @@ -4822,7 +4822,7 @@ SHA-256: `47e9095591ae36e7c39ee62553097772f2bfa16304536a72dbbc3f691f605180` SHA-256: `f731ffe80810b0289b3e694d8f40af6f01865c02a622ee457be90ca808b66e87` -来源:[`packages/subagent/subagent/src/catalog.ts:28`](../packages/subagent/subagent/src/catalog.ts) +来源:[`packages/subagent/subagent/src/catalog.ts:29`](../packages/subagent/subagent/src/catalog.ts) 以下类型之一: @@ -4931,7 +4931,7 @@ SHA-256: `65a8400e451e667e5b6e34df72ceb81fca8c12330330fa72083ad40279b189ad` SHA-256: `dfce5db4aa42b7ebe0bb79eca92553587287e18c06b9eba85cbfb9d332e26acd` -来源:[`packages/experimental/agent-team/src/types.ts:45`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/experimental/agent-team/src/types.ts:47`](../packages/experimental/agent-team/src/types.ts) 以下类型之一: @@ -4949,7 +4949,7 @@ SHA-256: `dfce5db4aa42b7ebe0bb79eca92553587287e18c06b9eba85cbfb9d332e26acd` SHA-256: `6598cd6da253948d6b207c6374a45335804c79b576866d6215649c2d1bc85e25` -来源:[`packages/experimental/agent-team/src/types.ts:48`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/experimental/agent-team/src/types.ts:50`](../packages/experimental/agent-team/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -4971,7 +4971,7 @@ SHA-256: `6598cd6da253948d6b207c6374a45335804c79b576866d6215649c2d1bc85e25` SHA-256: `2eafefdc6f79a4922d68a787a09eaf4dd2b2369c3777fba4c05e800b5678ac8e` -来源:[`packages/experimental/agent-team/src/types.ts:129`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/experimental/agent-team/src/types.ts:128`](../packages/experimental/agent-team/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -4991,7 +4991,7 @@ SHA-256: `2eafefdc6f79a4922d68a787a09eaf4dd2b2369c3777fba4c05e800b5678ac8e` SHA-256: `2152caf1a71e588e4df2358a806dea24936c1c47131134f01a12939d12cdc0c0` -来源:[`packages/experimental/agent-team/src/types.ts:138`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/experimental/agent-team/src/types.ts:137`](../packages/experimental/agent-team/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -5011,7 +5011,7 @@ SHA-256: `2152caf1a71e588e4df2358a806dea24936c1c47131134f01a12939d12cdc0c0` SHA-256: `51edec100659da3a9bd9c3f6ba1180f5e64ec9a1585e5057080df45c180bd357` -来源:[`packages/experimental/agent-team/src/types.ts:75`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/experimental/agent-team/src/types.ts:70`](../packages/experimental/agent-team/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -5034,7 +5034,7 @@ SHA-256: `51edec100659da3a9bd9c3f6ba1180f5e64ec9a1585e5057080df45c180bd357` SHA-256: `b2d2a01e51341a3bfec809198952c8c830a7899f4cb6ecd3c1dc0b5d9a2af943` -来源:[`packages/experimental/agent-team/src/types.ts:72`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/experimental/agent-team/src/types.ts:67`](../packages/experimental/agent-team/src/types.ts) 以下类型之一: @@ -5577,7 +5577,7 @@ SHA-256: `5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78` SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b` -来源:[`packages/api/session-controller/src/types.ts:409`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:21`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:33`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:9`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts) +来源:[`packages/api/session-controller/src/types.ts:409`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:20`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:32`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:8`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts) `string` @@ -7700,7 +7700,7 @@ SHA-256: `930a6567a10bb62ddd157bd8abdf4b182810c8c3b49d91309b3b009a5fed9731` SHA-256: `b1ebd5cda47d90d980bb05bffc3f970c51537b9aff15b3bc3d639811727741b1` -来源:[`packages/experimental/agent-team/src/types.ts:230`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/experimental/agent-team/src/types.ts:196`](../packages/experimental/agent-team/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -7728,7 +7728,7 @@ SHA-256: `6bc8e51124d0874bc89e1d0ceff381a9c3cd0fc240667d69f841ce4f60f65fe5` SHA-256: `ea08ec1df0c33108fd03345b9af199cf04dea450aa045d3c683f216311403403` -来源:[`packages/experimental/agent-team/src/types.ts:234`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/experimental/agent-team/src/types.ts:200`](../packages/experimental/agent-team/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -7742,7 +7742,7 @@ SHA-256: `ea08ec1df0c33108fd03345b9af199cf04dea450aa045d3c683f216311403403` SHA-256: `0bcfaf5566d271028429ac7b562cb59053c9d78b8ae14708c35fac835108d1b1` -来源:[`packages/experimental/agent-team/src/types.ts:236`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/experimental/agent-team/src/types.ts:202`](../packages/experimental/agent-team/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -7888,7 +7888,7 @@ SHA-256: `477614bca595a9e7adb5a1870be446b9a8684f3ba8950259d6289f2d3b74f028` SHA-256: `a40d12070f6f4a124f32fb3cc86e7857554702e32f0eda3080a25c4b9ac9b18c` -来源:[`packages/experimental/agent-team/src/types.ts:232`](../packages/experimental/agent-team/src/types.ts) +来源:[`packages/experimental/agent-team/src/types.ts:198`](../packages/experimental/agent-team/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| diff --git a/docs/persistence-schema.json b/docs/persistence-schema.json index a77fe9f24b..09652d65bd 100644 --- a/docs/persistence-schema.json +++ b/docs/persistence-schema.json @@ -22537,7 +22537,7 @@ "digest": "0bcfaf5566d271028429ac7b562cb59053c9d78b8ae14708c35fac835108d1b1", "names": [], "sources": [ - "packages/experimental/agent-team/src/types.ts:236" + "packages/experimental/agent-team/src/types.ts:202" ] }, { @@ -22917,7 +22917,7 @@ "packages/experimental/agent-team/src/types.ts#TeamMessageSource" ], "sources": [ - "packages/experimental/agent-team/src/types.ts:138" + "packages/experimental/agent-team/src/types.ts:137" ] }, { @@ -23106,7 +23106,7 @@ "packages/experimental/agent-team/src/types.ts#TeamMessageSnapshot" ], "sources": [ - "packages/experimental/agent-team/src/types.ts:129" + "packages/experimental/agent-team/src/types.ts:128" ] }, { @@ -23550,9 +23550,9 @@ "packages/attachment/attachment/src/brand.ts:6", "packages/compaction/compaction/src/brand.ts:4", "packages/core/session/src/types.ts:21", - "packages/experimental/agent-team/src/types.ts:21", - "packages/experimental/agent-team/src/types.ts:33", - "packages/experimental/agent-team/src/types.ts:9", + "packages/experimental/agent-team/src/types.ts:20", + "packages/experimental/agent-team/src/types.ts:32", + "packages/experimental/agent-team/src/types.ts:8", "packages/experimental/webhook/src/brand.ts:12", "packages/experimental/webhook/src/brand.ts:6", "packages/experimental/webhook/src/brand.ts:9", @@ -23644,7 +23644,7 @@ "packages/experimental/agent-team/src/types.ts#TeamTaskSnapshot" ], "sources": [ - "packages/experimental/agent-team/src/types.ts:75" + "packages/experimental/agent-team/src/types.ts:70" ] }, { @@ -23933,7 +23933,7 @@ "packages/experimental/agent-team/src/types.ts#TeamMemberSnapshot" ], "sources": [ - "packages/experimental/agent-team/src/types.ts:48" + "packages/experimental/agent-team/src/types.ts:50" ] }, { @@ -24851,7 +24851,7 @@ "digest": "a40d12070f6f4a124f32fb3cc86e7857554702e32f0eda3080a25c4b9ac9b18c", "names": [], "sources": [ - "packages/experimental/agent-team/src/types.ts:232" + "packages/experimental/agent-team/src/types.ts:198" ] }, { @@ -25089,7 +25089,7 @@ "digest": "b1ebd5cda47d90d980bb05bffc3f970c51537b9aff15b3bc3d639811727741b1", "names": [], "sources": [ - "packages/experimental/agent-team/src/types.ts:230" + "packages/experimental/agent-team/src/types.ts:196" ] }, { @@ -25118,7 +25118,7 @@ "packages/experimental/agent-team/src/types.ts#TeamTaskStatus" ], "sources": [ - "packages/experimental/agent-team/src/types.ts:72" + "packages/experimental/agent-team/src/types.ts:67" ] }, { @@ -25658,7 +25658,7 @@ "packages/experimental/agent-team/src/types.ts#TeamMemberPhase" ], "sources": [ - "packages/experimental/agent-team/src/types.ts:45" + "packages/experimental/agent-team/src/types.ts:47" ] }, { @@ -25799,7 +25799,7 @@ "digest": "ea08ec1df0c33108fd03345b9af199cf04dea450aa045d3c683f216311403403", "names": [], "sources": [ - "packages/experimental/agent-team/src/types.ts:234" + "packages/experimental/agent-team/src/types.ts:200" ] }, { @@ -26012,7 +26012,7 @@ "packages/subagent/subagent/src/catalog.ts#SubagentCatalogEvent" ], "sources": [ - "packages/subagent/subagent/src/catalog.ts:28" + "packages/subagent/subagent/src/catalog.ts:29" ] }, { diff --git a/docs/subsystems/agent-team.i18n.yaml b/docs/subsystems/agent-team.i18n.yaml index a4c599562f..4e71bf1a98 100644 --- a/docs/subsystems/agent-team.i18n.yaml +++ b/docs/subsystems/agent-team.i18n.yaml @@ -3,20 +3,20 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/agent-team.md /agent-teams: - en: f571e223220bb616 - zh: dd7e37fdf359eeae + en: 7e3218b25e4d4311 + zh: afd2eb735d492881 /agent-teams/identity-and-roster: - en: df72d9409b89714d - zh: 982934dd18864ca2 + en: 68b948fcbe85ea02 + zh: 19e6bb9728093bdd /agent-teams/messages-and-historical-mailbox: - en: 2aed7dd369f47747 - zh: 4f080e50c584ad37 + en: a0c73ac2780bb180 + zh: 74b47a59ace8b255 /agent-teams/shared-task-dag: - en: 4515005d07e4d2a3 - zh: 26811b31f69009d6 + en: c70f0fd199fc7db5 + zh: c6b77cb5a5cfd1c2 /agent-teams/web-projection: - en: 6af99eff8036597a - zh: f172f994e01f12c1 + en: a8c66eecffebe6dd + zh: 8d6d12b2e75074c0 /agent-teams/replay: - en: fe32f71c6d799ebc - zh: aa2b6ccddba8cfd5 + en: 9aa7742f40182a02 + zh: 118a463f69a83ab4 diff --git a/docs/subsystems/agent-team.md b/docs/subsystems/agent-team.md index c5c6a0f775..581cb6a309 100644 --- a/docs/subsystems/agent-team.md +++ b/docs/subsystems/agent-team.md @@ -2,14 +2,14 @@ English | [中文](agent-team.zh.md) -Types shared by the experimental implicit-root Team domain, model tools, and host adapters. The [Agent Teams Agent Note](../../.agents/notes/implemented/feature/2026-08-05-agent-teams.md) owns identity, mailbox, task, and shared-checkout decisions; this page records the durable and client-visible forms from [`packages/experimental/agent-team/src/types.ts`](../../packages/experimental/agent-team/src/types.ts). +Types shared by the experimental implicit-root Team domain, model tools, and host adapters. The [subagent teammates Agent Note](../../.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md) owns the membership, roster, and messaging decisions; this page records the durable and client-visible forms from [`packages/experimental/agent-team/src/types.ts`](../../packages/experimental/agent-team/src/types.ts). ## Identity and roster -`TeamId` is the root `SessionId` under a distinct [brand](core.md#branded-ids). `TeamTaskId` is allocated as `task-`. New sends return the target inbox `MessageId`; `TeamMessageId` is retained only for historical mailbox records. Teammate names are immutable labels. +`TeamId` brands the root `SessionId`; `TeamTaskId` uses `task-`. `TeamMessageId` remains only for historical mailbox records. Team writes no roster records: the view includes the Lead, its direct continuable children as `TeamChild` values with `id` and `description`, and historical members. Descriptions may repeat; ids address members. ```ts type-equiv -/** Whole durable value written on every teammate lifecycle change. */ +/** Historical roster value written by earlier Team builds on every teammate lifecycle change. */ interface TeamMemberSnapshot { readonly id: SessionId readonly name: string @@ -21,11 +21,11 @@ interface TeamMemberSnapshot { } ``` -Every member starts in `provisioning` and reaches exactly one terminal roster phase, `active` or `failed`. Roster `running`/`inactive` status is derived separately and never rewrites this record. +Historical records keep their `provisioning`, `active`, or `failed` phase; direct continuable children are `active`. Turn activity (`running` or `inactive`) is derived separately. ## Messages and historical mailbox -New sends return inbox acceptance or reject. The target stores the existing `agent-message` source with the real sender Session id and a sender-name content prefix. Team has no new-message outbox or resend deduplication. The [package contract](../../packages/experimental/agent-team/README.md#understand-the-implementation) defines persistence. The following mailbox types remain readable so historical Lead logs still replay; Team does not deliver their pending records. +Members exchange ordinary [subagent messages](subagent.md): the target stores the `agent-message` source with the real sender Session id. Team has no outbox, mailbox, or resend deduplication. The following mailbox types remain readable so historical Lead logs still replay; Team never delivers their pending records. ```ts type-equiv /** One peer message recorded by the historical Team mailbox. */ @@ -38,8 +38,6 @@ interface TeamMessageSnapshot { } ``` -Every message attempts Steer delivery. A running target receives it at the nearest step boundary; an inactive target starts a turn if loaded or cold-resumes otherwise. Scheduling is not stored in the durable record because callers cannot select another mode. - Historical `team-message` sources retain the old message id and sender attribution; the client uses them for the message title and icon. ```ts type-equiv @@ -71,21 +69,25 @@ interface TeamTaskSnapshot { } ``` -`pending` is unstarted or released, `in_progress` carries an owner, `completed` satisfies blockers, and `deleted` is a retained tombstone. Views add owner name, readiness, and write-scope overlap warnings without changing the durable snapshot. +`pending` is unstarted or released, `in_progress` carries an owner, `completed` satisfies blockers, and `deleted` is a retained tombstone. Views expose `ownerId`, readiness, and advisory write-scope overlap without changing the durable snapshot. ## Web projection -The Lead Session publishes `SessionProjectionMap.agentTeam` with durable roster rows and non-deleted task views. `failure` reports a rejected persisted record beside the last valid state. Member activity comes from Session status; model labels come from each member's `modelSelection` projection. +The Lead Session publishes `SessionProjectionMap.agentTeam` with roster rows and non-deleted task views. `failure` reports a rejected persisted record beside the last valid state. Member activity comes from Session status; model labels come from each member's `modelSelection` projection. ```ts type-equiv -/** One durable roster row published through the `agentTeam` Session projection. */ +/** + * One roster row published through the `agentTeam` Session projection: the + * Lead, each continuable direct child of the Lead, and each historical roster member. + */ interface TeamMemberProjection { readonly id: SessionId - readonly name: string + /** Delegated responsibility; absent for the Lead. Never used as an address. */ + readonly description?: string readonly role: 'lead' | 'teammate' - /** Durable lifecycle; the Lead row is always `active`. Turn activity comes from Session status. */ + /** The Lead and continuable children are always `active`. Turn activity comes from Session status. */ readonly phase: TeamMemberPhase readonly error?: string } @@ -101,7 +103,7 @@ interface TeamTaskView { readonly status: TeamTaskStatus readonly blockedBy: TeamTaskId[] readonly writeScopes: string[] - readonly ownerName?: string + readonly ownerId?: SessionId readonly ready: boolean readonly writeScopeWarnings: string[] } @@ -122,7 +124,7 @@ interface TeamProjection { ## Replay -The `agentTeam` Session projection replays one root Session into the roster, task board, and historical queued-minus-delivered mailbox. It selects records by `TeamId`, so events inherited by an ordinary fork retain the ancestor id and never enter the new root's state. Session event `seq` and `time` remain the ordering and timing record; Team snapshots do not duplicate them. Roster and task reads reach callers as views; pending mail is not exposed to callers. The package [README](../../packages/experimental/agent-team/README.md) owns operation, authorization, recovery, and limit behavior. +The `agentTeam` projection replays the root’s continuable catalog children, historical roster, tasks, and historical mailbox. Team records are selected by `TeamId`; inherited catalog facts are skipped. Descriptions do not participate in identity checks. Session event `seq` and `time` own ordering and timing. Callers receive roster and task views, not pending mail. The [package README](../../packages/experimental/agent-team/README.md) defines operations, authorization, and limits. @@ -142,32 +144,23 @@ Agent Teams service backed by the exact live Lead Session log. /** * Resolve one exact live Agent's Team role. * @param agent - exact live Agent used as the authority credential. - * @returns its root, Team identity, role, and model-facing name. + * @returns its Lead, Team identity, and role. */ membership(agent: Agent): TeamMembership /** - * List the runtime-enriched roster visible to one Team member. - * @param agent - exact live Team member. - * @returns Lead and teammate rows in creation order. + * Resolve a caller without throwing, used by scoped-tool installation and observers. + * @param agent - candidate exact live Agent. + * @returns Team membership, or undefined for stale identities and children whose parent left the registry. */ -listMembers(agent: Agent): TeamMemberView[] +tryMembership(agent: Agent): TeamMembership | undefined /** - * Create one named, continuable direct child of the Team Lead. - * @param caller - exact live Lead Agent. - * @param request - immutable name, description, prompt, context mode, provider, and cancellation. - * @returns the active roster row. + * Whether another live member of the caller's Team is currently running a turn. + * @param caller - exact live Team member. + * @returns whether a peer can still produce a Team change. */ -async spawnTeammate(caller: Agent, request: SpawnTeammateRequest): Promise - -/** - * Steer one peer message into the target inbox or reject the attempt. - * @param caller - exact live sending Team member. - * @param request - target name, content, and cancellation before acceptance. - * @returns accepted inbox identity; acceptance follows normal Agent persistence and does not await model processing. - */ -async sendMessage(caller: Agent, request: SendTeamMessageRequest): Promise +hasActivePeer(caller: Agent): boolean /** * Create one unowned pending task in the Team Lead log. @@ -201,28 +194,13 @@ listTasks(caller: Agent): TeamTaskView[] async updateTask(caller: Agent, request: UpdateTeamTaskRequest): Promise /** - * Wait for the next Team-domain or member-status change. + * Wait for the next task change, member status change, or message a member's inbox accepts. * @param caller - exact live Team member waiting for activity. * @param timeoutMs - bounded wait duration from ten seconds through one hour. * @param signal - caller cancellation for the wait only. * @returns one observed change or a timeout result. */ async waitForChange(caller: Agent, timeoutMs: number, signal: AbortSignal): Promise - -/** - * Interrupt one live teammate turn without clearing its pending inbox. - * @param caller - exact live Lead Agent. - * @param targetName - durable teammate name. - * @returns the target status sampled before cancellation. - */ -interrupt(caller: Agent, targetName: string): { previousStatus: 'running' | 'inactive' } - -/** - * Resolve a caller without throwing, used by scoped-tool installation and observers. - * @param agent - candidate exact live Agent. - * @returns Team membership, or undefined for non-Team subagents and stale identities. - */ -tryMembership(agent: Agent): TeamMembership | undefined ``` Types: [Agent](core.md) diff --git a/docs/subsystems/agent-team.zh.md b/docs/subsystems/agent-team.zh.md index 2e3c0b040e..d1438d08cb 100644 --- a/docs/subsystems/agent-team.zh.md +++ b/docs/subsystems/agent-team.zh.md @@ -2,14 +2,14 @@ [English](agent-team.md) | 中文 -实验性隐式 Root Team 领域、模型工具与宿主适配器共享的类型。[Agent Teams Agent Note](../../.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md)负责身份、mailbox、task 与共享 checkout 决策;本页记录 [`packages/experimental/agent-team/src/types.ts`](../../packages/experimental/agent-team/src/types.ts) 中的持久与客户端可见形式。 +实验性隐式 Root Team 领域、模型工具与宿主适配器共享的类型。[subagent teammate Agent Note](../../.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.zh.md)负责成员关系、roster 与消息决策;本页记录 [`packages/experimental/agent-team/src/types.ts`](../../packages/experimental/agent-team/src/types.ts) 中的持久与客户端可见形式。 ## 身份与 roster -`TeamId` 是使用独立[品牌类型](core.zh.md#branded-ids)的 root `SessionId`。`TeamTaskId` 按 `task-` 分配。新发送返回目标 inbox 的 `MessageId`;`TeamMessageId` 仅供历史 mailbox 记录保留。teammate 名称是不可变标签。 +`TeamId` 为根 `SessionId` 添加品牌;`TeamTaskId` 使用 `task-`。`TeamMessageId` 仅供历史邮箱记录保留。Team 不写成员记录:视图包含 Lead、以 `id` 和 `description` 构成 `TeamChild` 的直接可继续子级,以及历史成员。描述可以重复;id 用于寻址成员。 ```ts type-equiv -/** Whole durable value written on every teammate lifecycle change. */ +/** Historical roster value written by earlier Team builds on every teammate lifecycle change. */ interface TeamMemberSnapshot { readonly id: SessionId readonly name: string @@ -21,11 +21,11 @@ interface TeamMemberSnapshot { } ``` -每个 member 都从 `provisioning` 开始,并且只到达一个终态 roster phase:`active` 或 `failed`。roster 的 `running`/`inactive` 状态单独派生,绝不会重写该记录。 +历史记录保留 `provisioning`、`active` 或 `failed` 阶段;直接可继续子级为 `active`。轮次活动(`running` 或 `inactive`)单独派生。 ## 消息与历史 mailbox -新发送返回 inbox 接收结果或拒绝。目标保存现有 `agent-message` source,包含真实发送者 Session id 和发送者名称正文前缀。Team 不为新消息提供 outbox 或重发去重。[包约定](../../packages/experimental/agent-team/README.zh.md#understand-the-implementation)定义持久化。以下 mailbox 类型继续可读,以便历史 Lead 日志仍能回放;Team 不投递其中的 pending 记录。 +成员之间交换普通的 [subagent 消息](subagent.zh.md):目标保存包含真实发送者 Session id 的 `agent-message` source。Team 没有 outbox、mailbox 或重发去重。以下 mailbox 类型继续可读,以便历史 Lead 日志仍能回放;Team 从不投递其中的 pending 记录。 ```ts type-equiv /** One peer message recorded by the historical Team mailbox. */ @@ -38,8 +38,6 @@ interface TeamMessageSnapshot { } ``` -每条消息都会尝试 Steer 投递。running target 在最近的步骤边界收到消息,inactive target 在已加载时启动一个轮次,否则冷恢复。调用方不能选择其他模式,因此持久记录不存储调度方式。 - 历史 `team-message` source 保留旧消息 id 与发送者归属;客户端用它们展示消息标题和图标。 ```ts type-equiv @@ -71,21 +69,25 @@ interface TeamTaskSnapshot { } ``` -`pending` 表示尚未开始或已经释放,`in_progress` 携带 owner,`completed` 满足 blocker,`deleted` 是保留的 tombstone。view 会添加 owner name、readiness 和 write-scope 重叠警告,但不会改变持久快照。 +`pending` 表示未开始或已释放,`in_progress` 携带负责人,`completed` 满足依赖,`deleted` 是保留的墓碑记录。视图提供 `ownerId`、就绪性和写入范围重叠提示,不改变持久快照。 ## Web 投影 -Lead Session 通过 `SessionProjectionMap.agentTeam` 发布持久 roster 行与未删除任务视图。`failure` 在最后有效状态旁报告被拒绝的持久记录。成员活动来自 Session 状态;模型标签来自各成员的 `modelSelection` 投影。 +Lead Session 通过 `SessionProjectionMap.agentTeam` 发布 roster 行与未删除任务视图。`failure` 在最后有效状态旁报告被拒绝的持久记录。成员活动来自 Session 状态;模型标签来自各成员的 `modelSelection` 投影。 ```ts type-equiv -/** One durable roster row published through the `agentTeam` Session projection. */ +/** + * One roster row published through the `agentTeam` Session projection: the + * Lead, each continuable direct child of the Lead, and each historical roster member. + */ interface TeamMemberProjection { readonly id: SessionId - readonly name: string + /** Delegated responsibility; absent for the Lead. Never used as an address. */ + readonly description?: string readonly role: 'lead' | 'teammate' - /** Durable lifecycle; the Lead row is always `active`. Turn activity comes from Session status. */ + /** The Lead and continuable children are always `active`. Turn activity comes from Session status. */ readonly phase: TeamMemberPhase readonly error?: string } @@ -101,7 +103,7 @@ interface TeamTaskView { readonly status: TeamTaskStatus readonly blockedBy: TeamTaskId[] readonly writeScopes: string[] - readonly ownerName?: string + readonly ownerId?: SessionId readonly ready: boolean readonly writeScopeWarnings: string[] } @@ -122,7 +124,7 @@ interface TeamProjection { ## 回放 -`agentTeam` Session 投影把一个 Root Session 回放成roster、任务板与历史 queued-minus-delivered mailbox。它按 `TeamId` 选取记录,因此普通 fork 继承的 event 保留 ancestor id,绝不会进入新 Root 的状态。Session event 的 `seq` 与 `time` 继续负责顺序和时间记录,Team snapshot 不再重复保存它们。roster 与 task 读取以 view 形式到达调用方,而 pending 邮件不对调用方公开。包 [README](../../packages/experimental/agent-team/README.zh.md)负责 operation、authorization、recovery 和限制行为。 +`agentTeam` 投影回放根的可继续目录子级、历史成员、任务和历史邮箱。Team 记录按 `TeamId` 选取;继承的目录事实被跳过。描述不参与身份校验。Session 事件的 `seq` 和 `time` 负责顺序与时间。调用方获得成员和任务视图,不获得待投递邮件。[包 README](../../packages/experimental/agent-team/README.zh.md)定义操作、授权和限制。 @@ -142,32 +144,23 @@ Agent Teams service backed by the exact live Lead Session log. /** * Resolve one exact live Agent's Team role. * @param agent - exact live Agent used as the authority credential. - * @returns its root, Team identity, role, and model-facing name. + * @returns its Lead, Team identity, and role. */ membership(agent: Agent): TeamMembership /** - * List the runtime-enriched roster visible to one Team member. - * @param agent - exact live Team member. - * @returns Lead and teammate rows in creation order. + * Resolve a caller without throwing, used by scoped-tool installation and observers. + * @param agent - candidate exact live Agent. + * @returns Team membership, or undefined for stale identities and children whose parent left the registry. */ -listMembers(agent: Agent): TeamMemberView[] +tryMembership(agent: Agent): TeamMembership | undefined /** - * Create one named, continuable direct child of the Team Lead. - * @param caller - exact live Lead Agent. - * @param request - immutable name, description, prompt, context mode, provider, and cancellation. - * @returns the active roster row. + * Whether another live member of the caller's Team is currently running a turn. + * @param caller - exact live Team member. + * @returns whether a peer can still produce a Team change. */ -async spawnTeammate(caller: Agent, request: SpawnTeammateRequest): Promise - -/** - * Steer one peer message into the target inbox or reject the attempt. - * @param caller - exact live sending Team member. - * @param request - target name, content, and cancellation before acceptance. - * @returns accepted inbox identity; acceptance follows normal Agent persistence and does not await model processing. - */ -async sendMessage(caller: Agent, request: SendTeamMessageRequest): Promise +hasActivePeer(caller: Agent): boolean /** * Create one unowned pending task in the Team Lead log. @@ -201,28 +194,13 @@ listTasks(caller: Agent): TeamTaskView[] async updateTask(caller: Agent, request: UpdateTeamTaskRequest): Promise /** - * Wait for the next Team-domain or member-status change. + * Wait for the next task change, member status change, or message a member's inbox accepts. * @param caller - exact live Team member waiting for activity. * @param timeoutMs - bounded wait duration from ten seconds through one hour. * @param signal - caller cancellation for the wait only. * @returns one observed change or a timeout result. */ async waitForChange(caller: Agent, timeoutMs: number, signal: AbortSignal): Promise - -/** - * Interrupt one live teammate turn without clearing its pending inbox. - * @param caller - exact live Lead Agent. - * @param targetName - durable teammate name. - * @returns the target status sampled before cancellation. - */ -interrupt(caller: Agent, targetName: string): { previousStatus: 'running' | 'inactive' } - -/** - * Resolve a caller without throwing, used by scoped-tool installation and observers. - * @param agent - candidate exact live Agent. - * @returns Team membership, or undefined for non-Team subagents and stale identities. - */ -tryMembership(agent: Agent): TeamMembership | undefined ``` Types: [Agent](core.zh.md) diff --git a/docs/subsystems/subagent.i18n.yaml b/docs/subsystems/subagent.i18n.yaml index 21fa5dbbde..4a92a3473e 100644 --- a/docs/subsystems/subagent.i18n.yaml +++ b/docs/subsystems/subagent.i18n.yaml @@ -12,8 +12,8 @@ en: 6f87231d18d82916 zh: 9f064b23058f8766 /subagents/messages-and-interruption: - en: 623b06b3c12540bc - zh: f56c6341e1c62768 + en: f1921c4d9f85890f + zh: c16d41aaa3697171 /subagents/durable-enumeration: en: 30c3607a969696ce zh: e6359cdf22b1e235 diff --git a/docs/subsystems/subagent.md b/docs/subsystems/subagent.md index 30e064f25c..21d3b167b1 100644 --- a/docs/subsystems/subagent.md +++ b/docs/subsystems/subagent.md @@ -173,7 +173,7 @@ interface ContinuableCreateSpec { ## Messages and interruption -sendMessage authorizes the exact live sender and permits only adjacent local agents. Running children receive steering input; absent continuable children resume from persistence. External executions reject follow-ups. Both local and external executions automatically report their final text, structured result, diagnostic, and status in the parent settlement notice. Local children can also send messages through send_message. caller delivery suppresses settlement notices. drainDescendants and drainChildren await owned work teardown for local and external activations. +sendMessage requires the exact live sender and a target Session id. Every sender can reach a direct continuable child; a resident continuable child can also reach its direct parent and continuable siblings. Sibling delivery uses the shared live parent’s authority, preserving the actual sender’s id. Running targets receive Steer; absent children and siblings cold-resume. External executions reject follow-ups. Local and external executions report final text, structured output, diagnostics, and status through parent settlement notices; caller delivery suppresses those notices. drainDescendants and drainChildren await owned teardown. ```ts type-equiv /** Durable attribution for one model-authored message between Agents. */ @@ -453,13 +453,13 @@ startActivation(spec: SubagentActivationSpec): Promise async waitForChildren(parent: Agent): Promise /** - * Steer one model-authored message to the sender's direct parent or direct - * continuable child. A running target admits it at the nearest step boundary; + * Steer one model-authored message to the sender's direct parent, direct + * continuable child, or continuable sibling under their shared live parent. A running target admits it at the nearest step boundary; * an idle target starts a turn, and an absent direct child cold-resumes from * persistence. The service derives durable sender attribution from the exact * live sender. Caller cancellation stops only pre-acceptance work. * @param sender - exact live Agent authorizing and originating the message. - * @param targetId - durable direct-parent or direct-child session id. + * @param targetId - durable direct-parent, direct-child, or sibling session id. * @param content - model-authored content to deliver. * @param options - caller cancellation before inbox acceptance. * @returns the accepted message's inbox id. diff --git a/docs/subsystems/subagent.zh.md b/docs/subsystems/subagent.zh.md index c6d7119a25..c33562e186 100644 --- a/docs/subsystems/subagent.zh.md +++ b/docs/subsystems/subagent.zh.md @@ -173,7 +173,7 @@ interface ContinuableCreateSpec { ## 消息与中断 -sendMessage 根据确切的在线发送者实例授权,仅允许相邻本地代理通信。运行中的子代理接收引导输入;不在线的可继续子代理从持久化恢复。外部执行拒绝后续消息。本地与外部执行都会通过父级结束通知自动回传最终文本、结构化结果、诊断和状态。本地子代理也可以通过 send_message 发送消息。caller 回传会抑制结束通知。drainDescendants 与 drainChildren 对本地和外部 activation 都等待所属任务清理完成。 +sendMessage 要求确切在线发送方和目标 Session id。每个发送方可联系直接可继续子级;驻留的可继续子级还可联系直接父级和可继续同级。同级投递使用共同在线父级的权限,保留实际发送方 id。运行中目标接收 Steer;不在线的子级和同级冷恢复。外部执行拒绝后续输入。本地和外部执行通过父级结算通知报告最终文本、结构化输出、诊断和状态;caller 投递抑制通知。drainDescendants 和 drainChildren 等待所属资源释放。 ```ts type-equiv /** Durable attribution for one model-authored message between Agents. */ @@ -455,13 +455,13 @@ startActivation(spec: SubagentActivationSpec): Promise async waitForChildren(parent: Agent): Promise /** - * Steer one model-authored message to the sender's direct parent or direct - * continuable child. A running target admits it at the nearest step boundary; + * Steer one model-authored message to the sender's direct parent, direct + * continuable child, or continuable sibling under their shared live parent. A running target admits it at the nearest step boundary; * an idle target starts a turn, and an absent direct child cold-resumes from * persistence. The service derives durable sender attribution from the exact * live sender. Caller cancellation stops only pre-acceptance work. * @param sender - exact live Agent authorizing and originating the message. - * @param targetId - durable direct-parent or direct-child session id. + * @param targetId - durable direct-parent, direct-child, or sibling session id. * @param content - model-authored content to deliver. * @param options - caller cancellation before inbox acceptance. * @returns the accepted message's inbox id. diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml index e5249f25da..3b76b88253 100644 --- a/docs/tool-catalog.i18n.yaml +++ b/docs/tool-catalog.i18n.yaml @@ -9,8 +9,8 @@ en: 6cc2cd9ced70bb09 zh: 1c0e6cb4ba2ed637 /tool-schema-catalog/tool-package-map: - en: e72ad7af437fa362 - zh: d8f3683a4efe01b0 + en: aff10b6cad96bfcb + zh: 800df4738667d1cc /tool-schema-catalog/deepseek-ai-dsh-plugin-manager: en: e0143916bb634762 zh: e0143916bb634762 @@ -243,11 +243,11 @@ en: 9190a32df5e10ddd zh: 48e1b05956d63e26 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control/list-agents: - en: ae22e8543d123c39 - zh: 0991a73ef49bcdea + en: 90e684158bcd0b55 + zh: 23cdd90a05f4909f /tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control/send-message: en: 69c1c0a80c24e3a4 - zh: 04f4ac5ec365aa63 + zh: 8d3e8b8b1137e5c5 /tool-schema-catalog/deepseek-ai-dsh-tool-jobs: en: 9be673b1604d0114 zh: 9be673b1604d0114 @@ -263,18 +263,6 @@ /tool-schema-catalog/deepseek-ai-dsh-experimental-tool-agent-team: en: 6b1106f417b50db6 zh: 6b1106f417b50db6 -/tool-schema-catalog/deepseek-ai-dsh-experimental-tool-agent-team/interrupt-agent: - en: c3c7af05ceb9f8c9 - zh: ec379e0b42110c6a -/tool-schema-catalog/deepseek-ai-dsh-experimental-tool-agent-team/list-agents: - en: f9457f033f75f00a - zh: a2d60ec44118c34c -/tool-schema-catalog/deepseek-ai-dsh-experimental-tool-agent-team/send-message: - en: ec50e6a0b86aecc6 - zh: 29c8728536a5adf1 -/tool-schema-catalog/deepseek-ai-dsh-experimental-tool-agent-team/spawn-teammate: - en: 02d6cc746e234b99 - zh: 49bc60ac098bca48 /tool-schema-catalog/deepseek-ai-dsh-experimental-tool-agent-team/team-task-create: en: 91e84febe063c3bd zh: 550555d0f44f603b @@ -288,8 +276,8 @@ en: 2eac7360ba68b1bd zh: b5d676bf288c245e /tool-schema-catalog/deepseek-ai-dsh-experimental-tool-agent-team/wait-agent: - en: e70c77a45832bb28 - zh: 7d03bec294b65452 + en: 7b4bbc819b990d3d + zh: 0c2c1c680870b4ac /tool-schema-catalog/deepseek-ai-dsh-tool-todo: en: dea62d26fb18ff68 zh: dea62d26fb18ff68 diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index e3c9b9df36..811595b41f 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -41,7 +41,7 @@ This table connects model-visible tool names to the plugin package and service s | `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`, `subagent` | `ctx.tools`, `ctx.subagents`, `ctx.systemPrompt`, `ctx.llm for model discovery and selected-route validation` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion available in an enabled Session. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` remains fixed-route. Each instance selects its backend through `provider` and reads model-selection settings through `modelSelectionSettings`. Every delegation returns a child id promptly; local children accept follow-ups, while external backends report their single execution automatically. | | `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`, `list_agents`, `send_message` | `ctx.tools`, `ctx.subagents`, `ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`, `tool/result`, `child session events through ctx.subagents` | - | The globally named discovery, interruption, and local-message tools over managed subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` and `interrupt_agent` once, plus `list_agents` from its separately loaded `/list-agents` plugin (whose catalog rows use the sessionProjections and live Agent registries). | | `@deepseek-ai/dsh-tool-jobs` | `job_kill`, `job_list`, `job_output` | `ctx.tools`, `ctx.jobs`, `ctx.systemPrompt` | `tool/call`, `tool/result`, `user/message via agent.inject() for background completion notices` | - | The kind-agnostic background-job controller: background bash commands and PTY sends are read, listed, and killed through the same three tools. Loading the plugin attaches the controller that arms producers' `ctx.jobs.start()`. | -| `@deepseek-ai/dsh-experimental-tool-agent-team` | `interrupt_agent`, `list_agents`, `send_message`, `spawn_teammate`, `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update`, `wait_agent` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `an exact live Team member Agent` | `tool/call`, `team/member`, `team/task`, `agent/inbox/spliced`, `user/message on inbox claim`, `tool/result` | - | All nine tools are scoped to implicit Team Leads and durable teammates. The shipped dsh-base bundle keeps the package disabled; the documented Agent Teams profile patch enables it while disabling the legacy continuable-child control names. | +| `@deepseek-ai/dsh-experimental-tool-agent-team` | `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update`, `wait_agent` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `an exact live Team member Agent` | `tool/call`, `team/task`, `tool/result` | - | Five task-board and wait tools are scoped to the Lead and its live subagents. The optional Agent Teams profile adds this package while retaining the standard subagent delegation and control tools. | | `@deepseek-ai/dsh-tool-todo` | `todo_write` | `ctx.tools`, `owning Agent session` | `tool/call`, `todo/write`, `tool/result` | - | todo_write is session-owned state; UIs render the latest todo/write event as a checklist. `allowParallelInProgress` is required with no default, so the catalog states its choice: `true`, whose description invites several `in_progress` items. A deployment choosing `false` receives the same tool with a description asking for exactly one active task. | | `@deepseek-ai/dsh-tool-working-directory` | `working_directory` | `ctx.tools`, `ctx.workingDirectory` | `tool/call`, `working-directory/change`, `user context for directory changes`, `tool/result` | - | Reads or changes the calling Session directory. Existing shells and processes retain their own directories; origin metadata and permission roots stay fixed. | | `@deepseek-ai/dsh-experimental-tool-worktree` | `create_worktree` | `ctx.tools`, `ctx.worktrees`, `a calling Agent` | `tool/call`, `Git branch and checkout`, `working-directory/change`, `tool/result` | - | Explicit experimental composition only. Creates a new branch and checkout from a pinned local commit under existing write permissions, then changes the Session directory. Leaving retains the branch and checkout. | @@ -2147,7 +2147,7 @@ Source: [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/sub ### `list_agents` -List subagents you started, with their ids, labels, and status. running means it is working; inactive means it is not currently working. You will be notified when a subagent finishes; there is no need to keep checking its status. Use send_message to continue the conversation. +List agents you can message: subagents you started and, when you are a subagent, your parent and your continuable siblings. Rows carry ids, descriptions, and status. running means it is working; inactive means it is not currently working. You will be notified when a subagent finishes; there is no need to keep checking its status. Use send_message to continue the conversation. ```json { @@ -2155,7 +2155,7 @@ List subagents you started, with their ids, labels, and status. running means it "properties": { "scope": { "type": "string", - "description": "children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent.", + "description": "children (default) lists your parent, continuable siblings, and direct children; each accepts send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent.", "enum": [ "children", "descendants" @@ -2177,7 +2177,7 @@ Send a message to an agent. A working agent receives it at its next step; an idl "properties": { "agent_id": { "type": "string", - "description": "The agent id of your direct continuable child, or your direct parent when you are a resident continuable child." + "description": "The agent id of your direct continuable child, or your direct parent or continuable sibling when you are a resident continuable child." }, "message": { "type": "string", @@ -2272,105 +2272,6 @@ The kind-agnostic background-job controller: background bash commands and PTY se ## `@deepseek-ai/dsh-experimental-tool-agent-team` -### `interrupt_agent` - -Interrupt one teammate's current turn while preserving its pending inbox. Team Lead only. - -```json -{ - "type": "object", - "properties": { - "target": { - "type": "string", - "description": "Teammate target returned by spawn_teammate or list_agents." - } - }, - "required": [ - "target" - ] -} -``` - -Source: [`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) - -### `list_agents` - -List the Lead and every durable teammate with an addressable target and current availability. inactive means no turn is executing, not a task result. provisioning and failed describe member creation. - -```json -{ - "type": "object", - "properties": {} -} -``` - -Source: [`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) - -### `send_message` - -Send one message to another Team member. A running target receives it at the nearest step boundary; an inactive target starts or resumes a turn. - -```json -{ - "type": "object", - "properties": { - "target": { - "type": "string", - "description": "Member target returned by spawn_teammate or list_agents, including lead." - }, - "message": { - "type": "string", - "description": "Self-contained message for the target." - } - }, - "required": [ - "target", - "message" - ] -} -``` - -Source: [`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) - -### `spawn_teammate` - -Create one named, durable teammate. Only the Team Lead may call this tool. - -```json -{ - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Unique lower-kebab-case teammate name." - }, - "description": { - "type": "string", - "description": "Short description of the delegated responsibility." - }, - "prompt": { - "type": "string", - "description": "Complete initial task for the teammate." - }, - "context": { - "type": "string", - "description": "fresh starts without Lead history; fork inherits completed Lead turns. Defaults to fresh.", - "enum": [ - "fresh", - "fork" - ] - } - }, - "required": [ - "name", - "description", - "prompt" - ] -} -``` - -Source: [`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) - ### `team_task_create` Create one unowned pending task on the shared Team task board. @@ -2451,7 +2352,7 @@ List shared tasks, including readiness, owner, revision, blockers, and write-sco }, "owner": { "type": "string", - "description": "Optional member target from spawn_teammate or list_agents, matching ownerName; use unowned for tasks without an owner." + "description": "Optional exact ownerId filter; use unowned for tasks without an owner." }, "ready": { "type": "boolean", @@ -2525,7 +2426,7 @@ Compare-and-set a shared task action using the latest revision from team_task_ge }, "owner": { "type": "string", - "description": "Member target from spawn_teammate or list_agents for Lead-only reassign; omit to unassign." + "description": "Agent id of the Lead or one of its direct continuable children for Lead-only reassign; omit to unassign." } }, "required": [ @@ -2540,7 +2441,7 @@ Source: [`packages/experimental/tool-agent-team/src/index.ts`](../packages/exper ### `wait_agent` -Wait for the next teammate status, message, or shared-task change after this call starts. This never wakes inactive members and returns noProgress immediately when no other member is running or provisioning. Re-list after wakeup or timeout instead of polling. +Wait for the next teammate status, message, or shared-task change after this call starts. This never wakes inactive members and returns noProgress immediately when no other member is running. Re-list after wakeup or timeout instead of polling. ```json { @@ -2556,7 +2457,7 @@ Wait for the next teammate status, message, or shared-task change after this cal Source: [`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) -All nine tools are scoped to implicit Team Leads and durable teammates. The shipped dsh-base bundle keeps the package disabled; the documented Agent Teams profile patch enables it while disabling the legacy continuable-child control names. +Five task-board and wait tools are scoped to the Lead and its live subagents. The optional Agent Teams profile adds this package while retaining the standard subagent delegation and control tools. diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md index 467fcae8da..780df7e476 100644 --- a/docs/tool-catalog.zh.md +++ b/docs/tool-catalog.zh.md @@ -45,7 +45,7 @@ | `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述默认 schema 关闭模型选择,而发现 schema 则展示为已启用 Session 中可用的固定配套工具。Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;`subagent_fork` 始终使用固定路由。每个实例通过 `provider` 选择后端,通过 `modelSelectionSettings` 读取模型选择设置。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 | | `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`、`list_agents`、`send_message` | `ctx.tools`、`ctx.subagents`、`ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`、`tool/result`、`child session events through ctx.subagents` | - | 这些是用于发现、中断托管 subagent 和向本地子代理发送消息的全局命名工具:绑定提供方的 `tool-subagent` 实例注册不同的委派工具;本包注册一次 `send_message` 和 `interrupt_agent`,另由 `list_agents` 通过单独加载的 `/list-agents` 插件提供,其目录行使用 sessionProjections 和实时 Agent 注册表。 | | `@deepseek-ai/dsh-tool-jobs` | `job_kill`、`job_list`、`job_output` | `ctx.tools`、`ctx.jobs`、`ctx.systemPrompt` | `tool/call`、`tool/result`、`user/message via agent.inject() for background completion notices` | - | 与任务种类无关的后台任务控制器:后台 bash 命令与 PTY 发送 都通过相同的 3 个工具读取、列出和终止。加载该插件会挂接控制器,从而启用生产方的 `ctx.jobs.start()`。 | -| `@deepseek-ai/dsh-experimental-tool-agent-team` | `interrupt_agent`、`list_agents`、`send_message`、`spawn_teammate`、`team_task_create`、`team_task_get`、`team_task_list`、`team_task_update`、`wait_agent` | `ctx.tools`、`ctx.systemPrompt`、`ctx.agentTeams`、`an exact live Team member Agent` | `tool/call`、`team/member`、`team/task`、`agent/inbox/spliced`、`user/message on inbox claim`、`tool/result` | - | 这 9 个工具限定于隐式 Team Lead 与持久 teammate 作用域。随产品发布的 dsh-base bundle 默认禁用该包;文档中的 Agent Teams profile patch 会启用它,并禁用旧 continuable child 的同名控制工具。 | +| `@deepseek-ai/dsh-experimental-tool-agent-team` | `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update`, `wait_agent` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `an exact live Team member Agent` | `tool/call`, `team/task`, `tool/result` | - | 五个任务板和等待工具作用于 Lead 及其在线子代理。可选 Agent Teams profile 增加本包,同时保留标准子代理委派和控制工具。 | | `@deepseek-ai/dsh-tool-todo` | `todo_write` | `ctx.tools`、`owning Agent session` | `tool/call`、`todo/write`、`tool/result` | - | todo_write 是会话所有的状态;UI 将最新的 todo/write 事件渲染为检查清单。`allowParallelInProgress` 是没有默认值的必填项,因此本目录明确选择 `true`,对应描述允许同时存在多个 `in_progress` 项。选择 `false` 的部署会获得同一工具,但描述会要求只能有 1 个活动任务。 | | `@deepseek-ai/dsh-tool-working-directory` | `working_directory` | `ctx.tools`, `ctx.workingDirectory` | `tool/call`, `working-directory/change`, `user context for directory changes`, `tool/result` | - | 读取或改变调用方 Session 的目录。已有 shell 和进程保留自己的目录;原始元数据和权限根目录保持不变。 | | `@deepseek-ai/dsh-experimental-tool-worktree` | `create_worktree` | `ctx.tools`, `ctx.worktrees`, `a calling Agent` | `tool/call`, `Git branch and checkout`, `working-directory/change`, `tool/result` | - | 仅通过显式实验组合加载。在现有写权限下从固定的本地提交创建新分支与检出目录,再改变 Session 工作目录。离开时保留分支与检出目录。 | @@ -2153,7 +2153,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, ### `list_agents` -按持久 ID 和标签列出调用方目录中可继续的直接子代理。使用此工具回忆已启动的子代理,不要轮询完成状态;完成时会收到通知。状态来自在线注册表:running 表示当前正在工作;inactive 表示当前没有执行轮次,无论子代理已加载还是需要恢复,并不表示任务完成、成功、失败或正在等待其他代理。`send_message` 可在最近的步骤边界引导正在运行的子代理,或启动、恢复非活跃子代理的轮次;任何状态下的直接子级都可作为 `send_message` 的候选目标。列表快照不保证投递成功,`send_message` 仍进行权威检查,且可能失败。 +列出可联系的代理:你启动的子代理,以及你作为子代理时的父级和可继续同级。每行包含 id、描述和状态。running 表示正在工作,inactive 表示当前未工作。子代理完成时会通知你,无需反复检查;使用 send_message 继续对话。 ```json { @@ -2161,7 +2161,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, "properties": { "scope": { "type": "string", - "description": "children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent.", + "description": "children (default) lists your parent, continuable siblings, and direct children; each accepts send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent.", "enum": [ "children", "descendants" @@ -2175,7 +2175,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, ### `send_message` -根据 agent id 向直接可继续 child 发送消息。如果你是驻留的可继续 child,也可以把自己的直接 parent 作为目标。如果目标仍在工作,消息会 steer 其最近的 step;如果目标处于 idle,消息会启动一个轮次。此调用不会返回该 agent 的答案,只会确认消息已投递。调用失败表示消息**未**投递。 +向代理发消息。工作中的代理在下一步骤接收消息,空闲代理用消息启动新轮次。返回投递确认,不返回代理答案。 ```json { @@ -2183,7 +2183,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, "properties": { "agent_id": { "type": "string", - "description": "The agent id of your direct continuable child, or your direct parent when you are a resident continuable child." + "description": "The agent id of your direct continuable child, or your direct parent or continuable sibling when you are a resident continuable child." }, "message": { "type": "string", @@ -2278,105 +2278,6 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, ## `@deepseek-ai/dsh-experimental-tool-agent-team` -### `interrupt_agent` - -中断一名 teammate 的当前 turn,同时保留其待处理 inbox。仅 Team Lead 可用。 - -```json -{ - "type": "object", - "properties": { - "target": { - "type": "string", - "description": "Teammate target returned by spawn_teammate or list_agents." - } - }, - "required": [ - "target" - ] -} -``` - -来源:[`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) - -### `list_agents` - -列出 Lead 与所有持久 teammate,以及可用于寻址的 target 和当前可用状态。inactive 表示没有轮次在执行,不表示任务结果。provisioning 与 failed 描述成员创建状态。 - -```json -{ - "type": "object", - "properties": {} -} -``` - -来源:[`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) - -### `send_message` - -向另一名 Team member 发送一条消息。running target 会在最近的步骤边界收到消息;inactive target 会启动或恢复一个 turn。 - -```json -{ - "type": "object", - "properties": { - "target": { - "type": "string", - "description": "Member target returned by spawn_teammate or list_agents, including lead." - }, - "message": { - "type": "string", - "description": "Self-contained message for the target." - } - }, - "required": [ - "target", - "message" - ] -} -``` - -来源:[`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) - -### `spawn_teammate` - -创建一名具名、持久的 teammate。只有 Team Lead 可以调用此工具。 - -```json -{ - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Unique lower-kebab-case teammate name." - }, - "description": { - "type": "string", - "description": "Short description of the delegated responsibility." - }, - "prompt": { - "type": "string", - "description": "Complete initial task for the teammate." - }, - "context": { - "type": "string", - "description": "fresh starts without Lead history; fork inherits completed Lead turns. Defaults to fresh.", - "enum": [ - "fresh", - "fork" - ] - } - }, - "required": [ - "name", - "description", - "prompt" - ] -} -``` - -来源:[`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) - ### `team_task_create` 在共享 Team 任务板上创建一个无 owner 的 pending task。 @@ -2457,7 +2358,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, }, "owner": { "type": "string", - "description": "Optional member target from spawn_teammate or list_agents, matching ownerName; use unowned for tasks without an owner." + "description": "Optional exact ownerId filter; use unowned for tasks without an owner." }, "ready": { "type": "boolean", @@ -2531,7 +2432,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, }, "owner": { "type": "string", - "description": "Member target from spawn_teammate or list_agents for Lead-only reassign; omit to unassign." + "description": "Agent id of the Lead or one of its direct continuable children for Lead-only reassign; omit to unassign." } }, "required": [ @@ -2546,7 +2447,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, ### `wait_agent` -等待本次调用开始后下一次 teammate 状态、消息或共享任务变更。它绝不会唤醒 inactive member;若没有其他 member 正在 running 或 provisioning,则立即返回 noProgress。唤醒或超时后应重新列出状态,而不是轮询。 +等待调用开始之后的下一次队友状态、消息或共享任务变更。此工具不唤醒不活跃成员;没有其他运行成员时立即返回 noProgress。唤醒或超时后重新列举,不要轮询。 ```json { @@ -2562,8 +2463,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, 来源:[`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts) -这 10 个工具限定于隐式 Team Lead 与持久 teammate 作用域。随产品发布的 dsh-base bundle 默认禁用该包;文档中的 Agent Teams profile patch 会启用它,并禁用旧 continuable child 的同名控制工具。 - +五个任务板和等待工具作用于 Lead 及其在线子代理。可选 Agent Teams profile 增加本包,同时保留标准子代理委派和控制工具。 diff --git a/docs/upgrade-guide/v0.2.1-alpha.3/agent-team-subagents/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.3/agent-team-subagents/guide.i18n.yaml new file mode 100644 index 0000000000..de503a5d4e --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.3/agent-team-subagents/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-alpha.3/agent-team-subagents/guide.md +/: + en: 1f073189776760d8 + zh: cfbb0403aea96f84 +/agent-teams-uses-subagents-and-session-ids: + en: 32ee389e58de25fe + zh: 420fd4344f8f1214 +/agent-teams-uses-subagents-and-session-ids/change: + en: fb96ccedefaa5eee + zh: 7598b8e1f2cffac7 +/agent-teams-uses-subagents-and-session-ids/migration: + en: 0ed5e22412074192 + zh: 60deae7d46a21c75 diff --git a/docs/upgrade-guide/v0.2.1-alpha.3/agent-team-subagents/guide.md b/docs/upgrade-guide/v0.2.1-alpha.3/agent-team-subagents/guide.md new file mode 100644 index 0000000000..278d432b62 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.3/agent-team-subagents/guide.md @@ -0,0 +1,26 @@ +--- +kind: upgrade-guide +description: "Agent Teams uses the standard subagent tools and Session ids; Team-specific creation, messaging, listing, interruption, and their configuration keys are removed." +--- + +# Agent Teams uses subagents and Session ids + +English | [中文](guide.zh.md) + +## Change + +The experimental Agent Teams plugins share agent creation, discovery, messaging, and interruption with subagents. Start a teammate with `subagent` or `subagent_fork`, describe its responsibility with `description`, and use its returned agent id for messages and task assignment. Descriptions may repeat and never identify a recipient. + +- `spawn_teammate` and the Team-scoped `send_message`, `list_agents`, and `interrupt_agent` are removed. The standard control tools use `agent_id`, not `target`. Continuable children of one live parent can message each other by id. +- The Team profile no longer disables the subagent tools. Team adds only task tools, `wait_agent`, and collaboration guidance. +- `agent-team` rejects `maxMembers`, `maxMessageBytes`, and `disposalTimeoutMs`; `tool-agent-team` rejects `interrupt`, `freshProvider`, and `forkProvider`. +- `ctx.agentTeams` removes `spawnTeammate()`, `sendMessage()`, `interrupt()`, and `listMembers()`. Task views expose `ownerId` instead of `ownerName`; task ownership uses Session ids, including the Lead's id. +- Historical Team roster and mailbox records remain readable. Names in those records are historical data and do not resolve message recipients or task owners. Subagent catalog payloads are unchanged. + +## Migration + +1. Remove the obsolete configuration keys and any patch rows that disable subagent tools solely for Team support. +2. Replace `spawn_teammate({ name, description, prompt, context })` with `subagent({ description, prompt })`, or `subagent_fork` for inherited conversation history. Save the returned agent id. +3. Use `send_message({ agent_id, message })` and `interrupt_agent({ agent_id })`. Discover parent and sibling ids with `list_agents`. +4. Pass member Session ids to task `owner` fields and read `ownerId` in results. Use the Lead's Session id instead of the alias `lead`. +5. Replace removed service calls with `ctx.subagents.startActivation()`, `sendMessage()`, `interrupt()`, and `listChildren()`. Start a Team-enabled profile and verify that a child's description appears in the Team panel and that an id-addressed message reaches it. diff --git a/docs/upgrade-guide/v0.2.1-alpha.3/agent-team-subagents/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.3/agent-team-subagents/guide.zh.md new file mode 100644 index 0000000000..408763c21d --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.3/agent-team-subagents/guide.zh.md @@ -0,0 +1,26 @@ +--- +kind: upgrade-guide +description: "Agent Teams 使用标准 subagent 工具和 Session id;移除 Team 专属创建、消息、列举、中断操作及其配置键。" +--- + +# Agent Teams 使用子代理和 Session id + +[English](guide.md) | 中文 + +## 变更 + +实验性 Agent Teams 插件与 subagent 共用代理创建、发现、消息和中断能力。使用 `subagent` 或 `subagent_fork` 创建队友,以 `description` 描述职责,并用返回的 agent id 发送消息和分配任务。描述可以重复,不能作为收件人标识。 + +- 移除 `spawn_teammate` 以及 Team 作用域的 `send_message`、`list_agents` 和 `interrupt_agent`。标准控制工具使用 `agent_id` 而非 `target`。同一个在线父代理的可继续子代理可以通过 id 互发消息。 +- Team profile 不再禁用 subagent 工具。Team 只增加任务工具、`wait_agent` 和协作提示。 +- `agent-team` 拒绝 `maxMembers`、`maxMessageBytes` 和 `disposalTimeoutMs`;`tool-agent-team` 拒绝 `interrupt`、`freshProvider` 和 `forkProvider`。 +- `ctx.agentTeams` 移除 `spawnTeammate()`、`sendMessage()`、`interrupt()` 和 `listMembers()`。任务视图以 `ownerId` 取代 `ownerName`;包括 Lead 在内,任务所有者均使用 Session id。 +- 历史 Team 名册和邮箱记录仍可读取。其中的名字仅作为历史数据,不用于解析消息收件人或任务所有者。Subagent catalog 载荷不变。 + +## 迁移 + +1. 删除废弃配置键,以及仅为支持 Team 而禁用 subagent 工具的 patch 行。 +2. 将 `spawn_teammate({ name, description, prompt, context })` 替换为 `subagent({ description, prompt })`;继承对话历史时使用 `subagent_fork`。保存返回的 agent id。 +3. 使用 `send_message({ agent_id, message })` 和 `interrupt_agent({ agent_id })`。通过 `list_agents` 查询父代理和兄弟代理的 id。 +4. 任务的 `owner` 字段传入成员 Session id,结果读取 `ownerId`。以 Lead 的 Session id 取代 `lead` 别名。 +5. 将已移除的服务调用替换为 `ctx.subagents.startActivation()`、`sendMessage()`、`interrupt()` 和 `listChildren()`。启动启用 Team 的 profile,确认面板显示子代理描述,且通过 id 发送的消息到达该子代理。 diff --git a/packages/core/tools/tests/gen-tool-catalog.spec.ts b/packages/core/tools/tests/gen-tool-catalog.spec.ts index bc0b2ba97d..f048c92f0e 100644 --- a/packages/core/tools/tests/gen-tool-catalog.spec.ts +++ b/packages/core/tools/tests/gen-tool-catalog.spec.ts @@ -29,12 +29,12 @@ describe('gen-tool-catalog collectToolCatalog', () => { 'ask_user_question', 'bash', 'bash', 'cordis_inspect_list', 'cordis_inspect_query', 'create_goal', 'create_worktree', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep', - 'interrupt_agent', 'interrupt_agent', 'job_kill', 'job_list', 'job_output', - 'list_agents', 'list_agents', 'list_mcp_resource_templates', 'list_mcp_resources', + 'interrupt_agent', 'job_kill', 'job_list', 'job_output', + 'list_agents', 'list_mcp_resource_templates', 'list_mcp_resources', 'list_subagent_models', 'load_workspace_dependencies', 'lsp', 'plugin_manager', 'plugin_market', 'present', 'pwsh', 'pwsh', 'ralph', 'read', 'read_image', 'read_mcp_resource', 'run_code', 'schedule_create', 'schedule_delete', - 'schedule_list', 'schedule_update', 'send_message', 'send_message', 'session_event_read', 'session_event_search', - 'session_event_trace', 'session_search', 'session_trace', 'skill', 'spawn_teammate', + 'schedule_list', 'schedule_update', 'send_message', 'session_event_read', 'session_event_search', + 'session_event_trace', 'session_search', 'session_trace', 'skill', 'stagehand_act', 'stagehand_extract', 'stagehand_navigate', 'stagehand_observe', 'stagehand_screenshot', 'stagehand_tabs', 'str_replace_editor', 'subagent', 'team_task_create', 'team_task_get', 'team_task_list', 'team_task_update', 'terminal_close', 'terminal_list', diff --git a/packages/experimental/agent-team-profile/README.i18n.yaml b/packages/experimental/agent-team-profile/README.i18n.yaml index cf0a7e983a..26c1a08b06 100644 --- a/packages/experimental/agent-team-profile/README.i18n.yaml +++ b/packages/experimental/agent-team-profile/README.i18n.yaml @@ -9,8 +9,8 @@ en: 3deb4b95d9e58c8d zh: 0ba043a54428445e /deepseek-ai-dsh-experimental-agent-team-profile/summary: - en: 3ca34df3ee31841d - zh: a413140d89be2d05 + en: 7b33807cde38a48b + zh: 276de3022ce193c8 /deepseek-ai-dsh-experimental-agent-team-profile/table-of-contents: en: d152484eb41ac6b4 zh: 09388d293f9be9cb @@ -18,17 +18,17 @@ en: bbff250eba0f1b89 zh: 0a618f05cc88ee2b /deepseek-ai-dsh-experimental-agent-team-profile/use-this-package/install-into-a-profile: - en: 97ef5916c04bf2a1 - zh: 51f9d2bc3fca7936 + en: a8a37bd633498df0 + zh: c72a802f909e97df /deepseek-ai-dsh-experimental-agent-team-profile/use-this-package/what-you-get: - en: f0e95e60095fc5cb - zh: 8c36603b423475d0 + en: 9494ef152ee1ee43 + zh: e9c5129490a1fae1 /deepseek-ai-dsh-experimental-agent-team-profile/understand-the-implementation: - en: cac23b0774e00717 - zh: 4446c09b6525587a + en: 55a4275896c1ca29 + zh: 8c106158d71ba3cf /deepseek-ai-dsh-experimental-agent-team-profile/further-exploration: - en: 2513c5cb830db597 - zh: de3df83cb9fd2340 + en: 5aff65d55fb1251c + zh: 0e933df3f1bcd802 /deepseek-ai-dsh-experimental-agent-team-profile/model-experience: en: 215e7ba838619b7b zh: a311da3843709f90 @@ -36,17 +36,17 @@ en: 8e1e365ff3b0dfb2 zh: c6c91fbf1e1df934 /deepseek-ai-dsh-experimental-agent-team-profile/model-experience/team-policy-and-tools/what-the-model-sees: - en: f54dc1725b90365b - zh: 5a505996f7684231 + en: df8299bd58899fb4 + zh: c324bff39de3dc94 /deepseek-ai-dsh-experimental-agent-team-profile/model-experience/team-policy-and-tools/token-effect: - en: da8c178179ba50ba - zh: c53d5863bc14be79 + en: 59ae85f91bb01b7a + zh: 26401da9392f5dbb /deepseek-ai-dsh-experimental-agent-team-profile/model-experience/team-policy-and-tools/kv-cache-effect: - en: ac4c8d0ba492fa6a - zh: 16d4a05f2e719651 + en: 5d78a48233aaa341 + zh: 03ea892500980ff0 /deepseek-ai-dsh-experimental-agent-team-profile/known-limitations-and-deferred-work: - en: 0413821c2fc20bea - zh: 53e135425dd82e12 + en: 21d147bb86e0c285 + zh: 94d516e5f5671f15 /deepseek-ai-dsh-experimental-agent-team-profile/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/experimental/agent-team-profile/README.md b/packages/experimental/agent-team-profile/README.md index 08cde7a032..ce32c31cdc 100644 --- a/packages/experimental/agent-team-profile/README.md +++ b/packages/experimental/agent-team-profile/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -`dsh-experimental-agent-team-profile` enables [Agent Teams](../agent-team/README.md) collaboration, tools, and Web UI with one bundle. Delegate work to teammates and view members, shared tasks, and teammate sessions in Web. Ordinary subagent delegation and overlapping global child controls are disabled; Workflow can still create fresh children. The bundle ships with dsh switched off. Enable it from the Plugins page or add it to an initialized profile. +`dsh-experimental-agent-team-profile` enables [Agent Teams](../agent-team/README.md) collaboration, tools, and Web UI with one bundle. The Lead starts subagents as teammates that share a task board and wait for each other; Web shows members, shared tasks, and teammate sessions. The base profile's delegation and messaging tools stay available. The bundle ships with dsh switched off. Enable it from the Plugins page or add it to an initialized profile. ## Table of Contents @@ -34,7 +34,7 @@ dsh plugin --profile headless add @deepseek-ai/dsh-experimental-agent-team-profi dsh --profile headless "Use Agent Teams to split this task between two teammates, wait, and summarize." ``` -The profile must already contain `@deepseek-ai/dsh-base`, whose Subagent services and provider rows this layer consumes. Removing the package with `dsh plugin --profile remove @deepseek-ai/dsh-experimental-agent-team-profile` removes the bundle from the profile's ordered layer list. +The profile must already contain `@deepseek-ai/dsh-base`, whose Subagent services, providers, and tools this layer relies on. Removing the package with `dsh plugin --profile remove @deepseek-ai/dsh-experimental-agent-team-profile` removes the bundle from the profile's ordered layer list. Enable Agent Teams on the Web or Desktop Plugins page to activate both tools and UI. For a CLI Web profile, use: @@ -42,11 +42,11 @@ Enable Agent Teams on the Web or Desktop Plugins page to activate both tools and dsh plugin --profile web add @deepseek-ai/dsh-experimental-agent-team-profile ``` -In an existing profile’s `package.json`, keep `@deepseek-ai/dsh-experimental-agent-team-profile` in `dsh.profile.bundles` and remove the separate `@deepseek-ai/dsh-experimental-agent-team-web-profile` entry. User patches targeting `ui-agent-team` still apply. +In an existing profile’s `package.json`, keep `@deepseek-ai/dsh-experimental-agent-team-profile` in `dsh.profile.bundles` and remove the separate `@deepseek-ai/dsh-experimental-agent-team-web-profile` entry. User patches targeting `ui-agent-team` still apply. A user patch that sets a removed Team key keeps that Team row from activating; the [upgrade guide](../../../docs/upgrade-guide/v0.2.1-alpha.3/agent-team-subagents/guide.md) lists the removed keys and tools. ### What you get -The layer adds the Agent Teams domain and its scoped creation, roster, messaging, interruption, waiting, and task-board tools. Direct delegation uses `spawn_teammate`, which supports fresh and fork context. The `subagent` and `subagent_fork` tools and overlapping global child controls are disabled. Workflow retains the base profile’s `spawn` provider, while the underlying Subagent services and both providers remain available to teammates and workflow. +The layer adds the Team domain, task-board and wait tools, and UI, without disabling base rows. The Lead uses `subagent` for fresh context or `subagent_fork` for inherited history, describing the task with `description`. Standard controls use agent ids. In Web and Desktop conversations, the [Team UI](../client-ui-agent-team/README.md) displays the member roster and shared task board and opens teammate sessions. The same bundle switch controls the tools and browser UI. The Plugins page reads the bundle's [icon](icon.svg) from its `package.json.icon` declaration, including while the bundle is disabled. @@ -58,7 +58,7 @@ In Web and Desktop conversations, the [Team UI](../client-ui-agent-team/README.m
Implementation internals — click to expand -The package's runtime content is [`cordis.patch.yml`](cordis.patch.yml). Applied after `dsh-base`, the patch disables `tool-subagent-control`, `tool-subagent-list-agents`, `tool-subagent`, and `tool-subagent-fork`, and inserts the Team service, tools, and UI with explicit providers and limits. The UI plugin has an inert Host entry; only the Web Client loader mounts its browser entry, so headless needs no Web server. +The package's runtime content is [`cordis.patch.yml`](cordis.patch.yml). Applied after `dsh-base`, the patch only inserts rows: the Team service with `maxTasks: 256`, the Team tools, and the UI. The UI plugin has an inert Host entry; only the Web Client loader mounts its browser entry, so headless needs no Web server. | File | Role | |---|---| @@ -73,8 +73,8 @@ The package's runtime content is [`cordis.patch.yml`](cordis.patch.yml). Applied ## Further Exploration - [Experimental packages](../README.md) — incubation status and publication policy. -- [Agent Teams service](../agent-team/README.md) — durable roster and task board, plus direct messaging. -- [Agent Teams tools](../tool-agent-team/README.md) — the Team-scoped model tool surface. +- [Agent Teams service](../agent-team/README.md) — Team membership, roster, and the durable task board. +- [Agent Teams tools](../tool-agent-team/README.md) — the task and wait tools, policy, and identity every member receives. - [Agent Teams browser UI](../client-ui-agent-team/README.md) — roster, task board, and teammate session navigation. - [Base bundle](../../bundle/base/README.md) — the profile layer this patch extends. @@ -87,25 +87,24 @@ The package's runtime content is [`cordis.patch.yml`](cordis.patch.yml). Applied #### What the model sees -The Team policy and schemas belong to [`@deepseek-ai/dsh-experimental-tool-agent-team`](../tool-agent-team/README.md). This bundle changes composition only: Team-scoped `list_agents`, `send_message`, and `interrupt_agent` replace the disabled global continuable-child controls. `spawn_teammate` is the direct delegation tool. Workflow’s `agent()` calls create fresh one-shot children; their prompts must contain the context needed for their tasks. +The Team policy, identity, and schemas belong to [`@deepseek-ai/dsh-experimental-tool-agent-team`](../tool-agent-team/README.md). This bundle changes composition only: every Team member gains the five task and wait tools, while `subagent`, `subagent_fork`, `send_message`, `list_agents`, and `interrupt_agent` keep their base-profile schemas. #### Token effect -The bundle adds the Team policy and tool schemas described by `@deepseek-ai/dsh-experimental-tool-agent-team`; it adds no prompt text of its own. +The bundle adds the Team policy section, the identity statement, and the five tool schemas described by `@deepseek-ai/dsh-experimental-tool-agent-team`; it adds no prompt text of its own. #### KV Cache effect -The shared system prompt and tool schemas remain prefix-stable while the bundle patch and configured tool schemas remain unchanged. Team identity is appended through runtime-context snapshots; [Team tools](../tool-agent-team/README.md#model-experience) define snapshot reuse. +Every member, including `subagent_fork` children, receives the same Team policy and tool schemas, so a fork keeps its parent's request prefix; enabling or disabling the bundle changes both for every member. Team identity is appended through runtime-context snapshots; [Team tools](../tool-agent-team/README.md#model-experience) define snapshot reuse. ## Known Limitations and Deferred Work - **Opt-in only** — the package ships with the installation switched off; no shipped CLI, Web, SDK, ACP, or Python profile enables it. -- **Workflow child tools** — the [Team tool visibility limitation](../tool-agent-team/README.md#known-limitations-and-deferred-work) also applies to workflow children. +- **Workflow children join the Team** — continuable Workflow `agent()` children receive the same Team tools, policy, and sibling message routes. Direct children of the Lead appear in its roster. - **Shared checkout** — every teammate observes the same working directory; this bundle adds no worktree isolation or filesystem locking. -- **Preset-scoped child controls** — Web presets can still mount continuable Subagent controls in their own scope; this top-level bundle does not replace those registrations. -- **Base profile required** — the patch depends on row ids and Subagent providers supplied by `dsh-base`; it is not a standalone profile. +- **Base profile required** — teammates are created and reached through the Subagent services, providers, and tools that `dsh-base` mounts; the layer is not a standalone profile. ### Dev Note diff --git a/packages/experimental/agent-team-profile/README.zh.md b/packages/experimental/agent-team-profile/README.zh.md index 96979fdf3d..32068d8739 100644 --- a/packages/experimental/agent-team-profile/README.zh.md +++ b/packages/experimental/agent-team-profile/README.zh.md @@ -9,7 +9,7 @@ kind: "package-bundle" ## 概述 -`dsh-experimental-agent-team-profile` 让 [Agent Teams](../agent-team/README.zh.md) 的团队协作、工具和 Web 界面通过一个组合包启用。开启后可委派 teammate,并在 Web 中查看成员、任务看板和成员会话。普通 subagent 委派及名称重叠的全局 child control 会被禁用;Workflow 仍可创建 fresh 子代理。本包随 dsh 安装提供,默认关闭,可在插件页开启或添加到已初始化的 profile。 +`dsh-experimental-agent-team-profile` 让 [Agent Teams](../agent-team/README.zh.md) 的团队协作、工具和 Web 界面通过一个组合包启用。Lead 把subagent 作为 teammate 启动,它们共享任务看板并互相等待;Web 显示成员、共享任务和成员会话。base profile 的委派与消息工具保持可用。本包随 dsh 安装提供,默认关闭,可在插件页开启或添加到已初始化的 profile。 ## 目录 @@ -34,7 +34,7 @@ dsh plugin --profile headless add @deepseek-ai/dsh-experimental-agent-team-profi dsh --profile headless "Use Agent Teams to split this task between two teammates, wait, and summarize." ``` -profile 必须已经包含 `@deepseek-ai/dsh-base`,本层会使用其中的 Subagent 服务与提供方配置行。执行 `dsh plugin --profile remove @deepseek-ai/dsh-experimental-agent-team-profile` 移除本包时,bundle 也会从 profile 的有序层列表中移除。 +profile 必须已经包含 `@deepseek-ai/dsh-base`,本层依赖其中的 Subagent 服务、提供方与工具。执行 `dsh plugin --profile remove @deepseek-ai/dsh-experimental-agent-team-profile` 移除本包时,bundle 也会从 profile 的有序层列表中移除。 在 Web 或 Desktop 的插件页开启「智能体团队」,即可同时启用工具与界面。CLI 的 Web profile 也可使用以下命令: @@ -42,11 +42,11 @@ profile 必须已经包含 `@deepseek-ai/dsh-base`,本层会使用其中的 Su dsh plugin --profile web add @deepseek-ai/dsh-experimental-agent-team-profile ``` -已有 profile 的 `package.json` 中,`dsh.profile.bundles` 应只保留 `@deepseek-ai/dsh-experimental-agent-team-profile`,删除独立的 `@deepseek-ai/dsh-experimental-agent-team-web-profile` 条目。用户 patch 中的 `ui-agent-team` 配置仍然有效。 +已有 profile 的 `package.json` 中,`dsh.profile.bundles` 应只保留 `@deepseek-ai/dsh-experimental-agent-team-profile`,删除独立的 `@deepseek-ai/dsh-experimental-agent-team-web-profile` 条目。用户 patch 中的 `ui-agent-team` 配置仍然有效。设置已移除 Team 键的用户 patch 会使对应 Team 配置行无法激活;[升级指南](../../../docs/upgrade-guide/v0.2.1-alpha.3/agent-team-subagents/guide.zh.md)列出已移除的键与工具。 ### 获得的功能 -本层会添加 Agent Teams domain,以及 Team-scoped 创建、roster、消息、interrupt、等待与任务板工具。直接委派使用支持 fresh 和 fork 上下文的 `spawn_teammate`。`subagent`、`subagent_fork` 工具和名称重叠的全局 child control 均被禁用。Workflow 保留 base profile 的 `spawn` 提供方,底层 Subagent 服务和两个提供方仍供 teammate 与 workflow 使用。 +该层增加 Team 领域、任务板和等待工具以及 UI,不禁用基础配置行。Lead 使用 `subagent` 创建全新上下文,或使用 `subagent_fork` 继承历史,通过 `description` 描述任务。标准控制工具使用代理 id。 在 Web 和 Desktop 会话中,[Team UI](../client-ui-agent-team/README.zh.md) 显示成员列表与共享任务看板,并可打开成员会话。同一个组合包开关控制工具与浏览器 UI。插件页通过组合包的 `package.json.icon` 声明读取其[图标](icon.svg),组合包禁用时也会显示。 @@ -58,7 +58,7 @@ dsh plugin --profile web add @deepseek-ai/dsh-experimental-agent-team-profile
实现细节——点击展开 -本包的运行时内容是 [`cordis.patch.yml`](cordis.patch.yml)。在 `dsh-base` 之后应用时,patch 会禁用 `tool-subagent-control`、`tool-subagent-list-agents`、`tool-subagent` 和 `tool-subagent-fork`,并以显式 provider 和限制插入 Team 服务、工具和 UI 行。UI 插件的 Host 入口不执行逻辑;只有 Web 客户端加载器会挂载其浏览器入口,因此 headless 无需启动 Web 服务。 +本包的运行时内容是 [`cordis.patch.yml`](cordis.patch.yml)。在 `dsh-base` 之后应用时,patch 只插入配置行:带 `maxTasks: 256` 的 Team 服务、Team 工具和 UI。UI 插件的 Host 入口不执行逻辑;只有 Web 客户端加载器会挂载其浏览器入口,因此 headless 无需启动 Web 服务。 | 文件 | 职责 | |---|---| @@ -73,8 +73,8 @@ dsh plugin --profile web add @deepseek-ai/dsh-experimental-agent-team-profile ## 进一步探索 - [实验性包](../README.zh.md)——孵化状态与发布规则。 -- [Agent Teams service](../agent-team/README.zh.md)——持久 roster 与任务板,以及直接消息行为。 -- [Agent Teams 工具](../tool-agent-team/README.zh.md)——Team-scoped 模型工具表层。 +- [Agent Teams service](../agent-team/README.zh.md)——Team 成员关系、roster 与持久任务板。 +- [Agent Teams 工具](../tool-agent-team/README.zh.md)——每个成员获得的任务与等待工具、策略和身份。 - [Agent Teams 浏览器 UI](../client-ui-agent-team/README.zh.md)——成员列表、任务看板与成员会话导航。 - [Base bundle](../../bundle/base/README.zh.md)——本 patch 扩展的 profile 层。 @@ -87,25 +87,24 @@ dsh plugin --profile web add @deepseek-ai/dsh-experimental-agent-team-profile #### 模型会看到什么 -Team 策略与 schema 由 [`@deepseek-ai/dsh-experimental-tool-agent-team`](../tool-agent-team/README.zh.md) 所有。本 bundle 只改变 composition:Team-scoped `list_agents`、`send_message` 与 `interrupt_agent` 会替代已禁用的全局 continuable-child control。`spawn_teammate` 是直接委派工具。Workflow 的 `agent()` 调用创建 fresh 一次性子代理;其提示词必须包含任务所需的上下文。 +Team 策略、身份与 schema 由 [`@deepseek-ai/dsh-experimental-tool-agent-team`](../tool-agent-team/README.zh.md) 所有。本 bundle 只改变 composition:每个 Team 成员获得五个任务与等待工具,而 `subagent`、`subagent_fork`、`send_message`、`list_agents` 与 `interrupt_agent` 保持 base profile 的 schema。 #### Token 影响 -本 bundle 会加入 `@deepseek-ai/dsh-experimental-tool-agent-team` 描述的 Team 策略与工具 schema;它自身不增加提示词文本。 +本 bundle 会加入 `@deepseek-ai/dsh-experimental-tool-agent-team` 描述的 Team 策略段落、身份声明与五个工具 schema;它自身不增加提示词文本。 #### KV Cache 影响 -只要 bundle patch 与配置的工具 schema 不变,共享 system prompt 和工具 schema 就保持前缀稳定。Team 身份通过 runtime-context 快照追加;[Team 工具](../tool-agent-team/README.zh.md#model-experience)定义快照复用方式。 +每个成员(包括 `subagent_fork` 子级)都收到相同的 Team 策略与工具 schema,因此 fork 保留父级请求前缀;启用或禁用本 bundle 会为每个成员同时改变两者。Team 身份通过 runtime-context 快照追加;[Team 工具](../tool-agent-team/README.zh.md#model-experience)定义快照复用方式。 ## 已知限制与延期工作 - **仅显式启用**——本包随安装提供但默认关闭;随附 CLI、Web、SDK、ACP 与 Python profile 都不会启用它。 -- **Workflow 子代理工具**——[Team 工具可见性限制](../tool-agent-team/README.zh.md#known-limitations-and-deferred-work)也适用于 workflow 子代理。 +- **Workflow 子级加入 Team**——可继续的 Workflow `agent()` 子级获得相同的 Team 工具、策略和同级消息路由。Lead 的直接子级出现在其成员列表中。 - **共享 checkout**——所有 teammate 都观察同一个工作目录;本 bundle 不提供 worktree 隔离或文件系统锁。 -- **预设内的子代理控件**——Web 预设仍可在预设作用域挂载 continuable Subagent 控件;顶层组合包不会替换这些注册。 -- **需要 base profile**——本 patch 依赖 `dsh-base` 提供的配置行 id 与 Subagent 提供方;它不是独立 profile。 +- **需要 base profile**——teammate 通过 `dsh-base` 挂载的 Subagent 服务、提供方与工具创建和联系;本层不是独立 profile。 ### 开发备注 diff --git a/packages/experimental/agent-team-profile/cordis.patch.yml b/packages/experimental/agent-team-profile/cordis.patch.yml index e75aae058a..33dc638d94 100644 --- a/packages/experimental/agent-team-profile/cordis.patch.yml +++ b/packages/experimental/agent-team-profile/cordis.patch.yml @@ -1,32 +1,15 @@ -# Experimental Agent Teams profile layer. Apply after dsh-base so these replacements -# keep direct delegation and coordination on the Team tools. - -- id: tool-subagent-control - disabled: true - -- id: tool-subagent-list-agents - disabled: true - -- id: tool-subagent - disabled: true - -- id: tool-subagent-fork - disabled: true +# Experimental Agent Teams profile layer. Apply after dsh-base: teammates are +# subagents, so the base delegation and messaging tools stay mounted and +# this layer only adds the Team task board, wait tool, and panel. - insert: - id: agent-team name: '@deepseek-ai/dsh-experimental-agent-team' config: - maxMembers: 8 maxTasks: 256 - maxMessageBytes: 65536 - disposalTimeoutMs: 5000 - id: tool-agent-team name: '@deepseek-ai/dsh-experimental-tool-agent-team' - config: - freshProvider: spawn - forkProvider: fork - id: ui-agent-team name: '@deepseek-ai/dsh-experimental-client-ui-agent-team' diff --git a/packages/experimental/agent-team-profile/tests/profile.spec.ts b/packages/experimental/agent-team-profile/tests/profile.spec.ts index b75da63252..55ca7418fa 100644 --- a/packages/experimental/agent-team-profile/tests/profile.spec.ts +++ b/packages/experimental/agent-team-profile/tests/profile.spec.ts @@ -1,4 +1,4 @@ -/** The experimental bundle must carry one parseable, explicit Team layer. */ +/** The experimental bundle must carry one parseable, insert-only Team layer. */ import { readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' @@ -36,18 +36,16 @@ describe('Agent Teams profile bundle', () => { config?: Record insert?: { id?: string; name?: string; config?: Record }[] }[] - expect(patches.find(patch => patch.id === 'tool-subagent-control')).toMatchObject({ disabled: true }) - expect(patches.find(patch => patch.id === 'tool-subagent-list-agents')).toMatchObject({ disabled: true }) - expect(patches.find(patch => patch.id === 'tool-subagent')).toMatchObject({ disabled: true }) - expect(patches.find(patch => patch.id === 'tool-subagent-fork')).toMatchObject({ disabled: true }) + // Teammates are subagents: the layer keeps every delegation and messaging row mounted. + expect(patches.filter(patch => patch.disabled === true)).toEqual([]) const inserted = patches.flatMap(patch => patch.insert ?? []) expect(inserted.find(entry => entry.id === 'agent-team')).toMatchObject({ name: '@deepseek-ai/dsh-experimental-agent-team', - config: { maxMembers: 8 }, + config: { maxTasks: 256 }, }) - expect(inserted.find(entry => entry.id === 'tool-agent-team')).toMatchObject({ + expect(inserted.find(entry => entry.id === 'tool-agent-team')).toEqual({ + id: 'tool-agent-team', name: '@deepseek-ai/dsh-experimental-tool-agent-team', - config: { freshProvider: 'spawn', forkProvider: 'fork' }, }) expect(inserted.find(entry => entry.id === 'ui-agent-team')).toMatchObject({ name: '@deepseek-ai/dsh-experimental-client-ui-agent-team', diff --git a/packages/experimental/agent-team/README.i18n.yaml b/packages/experimental/agent-team/README.i18n.yaml index 017a9d603e..8a665b241e 100644 --- a/packages/experimental/agent-team/README.i18n.yaml +++ b/packages/experimental/agent-team/README.i18n.yaml @@ -3,92 +3,95 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/experimental/agent-team/README.md /: - en: 8b11ad84b831ec01 - zh: 01acc9f40e64b8d4 + en: bd0e1e830bb15925 + zh: fa1c2de84d4b7749 /deepseek-ai-dsh-experimental-agent-team: en: b28084855adcdabd zh: 59e3d49c3d63baf9 /deepseek-ai-dsh-experimental-agent-team/summary: - en: 3deae0f15c9acf3a - zh: 31bcf4cb0b1bb59b + en: 829b0c91f9659037 + zh: 58031bda875765e5 /deepseek-ai-dsh-experimental-agent-team/table-of-contents: en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-experimental-agent-team/use-this-package: - en: 3f28de40b688e5fd - zh: 16a13edf360a35eb + en: 5b53e0488d3c197d + zh: c25f3b556e668846 /deepseek-ai-dsh-experimental-agent-team/use-this-package/when-to-choose-it: - en: fc789f82c1b86394 - zh: c6dff674d537dc26 + en: 12d03bedc2a7be57 + zh: 27fc0dc8fef2340b /deepseek-ai-dsh-experimental-agent-team/use-this-package/smallest-working-setup: - en: a820f021cfea5d87 - zh: 7cbaadaf808971d3 + en: 037e98afeb998730 + zh: 224b568c2d54cd92 /deepseek-ai-dsh-experimental-agent-team/use-this-package/teammates: - en: 5a314d1f80ecd31b - zh: c34e6a93dd1e2045 + en: 6946c12e1482c65c + zh: 9320041928c49519 /deepseek-ai-dsh-experimental-agent-team/use-this-package/messages-between-teammates: - en: 7d55ac289b8da711 - zh: 6a15f941bb44356f + en: 3bcd3a00b3d9de9b + zh: 00a404f4cf033a16 /deepseek-ai-dsh-experimental-agent-team/use-this-package/shared-task-board: - en: 73682f0d2001b517 - zh: 3744d82eb3b7fcb9 + en: 21623c0bb323b8bd + zh: 13012bad307d26f3 /deepseek-ai-dsh-experimental-agent-team/use-this-package/waiting-and-interruption: - en: f8e38ccf6295b375 - zh: 44f008e23613c0af + en: 494f4d031fc69dec + zh: ebaf87dbca8c4346 /deepseek-ai-dsh-experimental-agent-team/use-this-package/what-success-and-failure-look-like: - en: a7255a6e1b5c0e96 - zh: 0551f949a8fa16ee + en: 9d08c60797350708 + zh: 7c13e0c7391371e5 /deepseek-ai-dsh-experimental-agent-team/understand-the-implementation: en: 6a18034f1ccc247a zh: e08b0eb0ccb41ea8 /deepseek-ai-dsh-experimental-agent-team/understand-the-implementation/design-philosophy: - en: 9034f3b96f68dee4 - zh: 4ea926474d3a8c24 + en: e87c3075fa0dfa2a + zh: ea26b8865ca2ecef /deepseek-ai-dsh-experimental-agent-team/understand-the-implementation/source-map: - en: d96ba512b8327751 - zh: 21481c29cf5af19a -/deepseek-ai-dsh-experimental-agent-team/understand-the-implementation/team-identity-and-roster: - en: 2389048c1a494ed3 - zh: 7e72acd6b83ea248 -/deepseek-ai-dsh-experimental-agent-team/understand-the-implementation/direct-messages-and-historical-compatibility: - en: 67cc4e92adda8baa - zh: 5abdb2415e414b9d + en: 135c4922b544404c + zh: aa99a7ad3b5d6c34 +/deepseek-ai-dsh-experimental-agent-team/understand-the-implementation/team-identity-and-membership: + en: c2712be658c8bfe2 + zh: 7748d32a9e87ff87 +/deepseek-ai-dsh-experimental-agent-team/understand-the-implementation/roster: + en: 3e06e4fadbe0cae7 + zh: 89ef62f80d3b3bf0 +/deepseek-ai-dsh-experimental-agent-team/understand-the-implementation/messages-and-historical-mailbox: + en: 7ab4daa9ce7989b9 + zh: 4eec246570ffef35 /deepseek-ai-dsh-experimental-agent-team/understand-the-implementation/shared-task-board: - en: 2dc37ba97a8a7178 - zh: 96bd24033030e19d -/deepseek-ai-dsh-experimental-agent-team/understand-the-implementation/waiting-and-interruption: - en: 29eebf85f25d8d8b - zh: 6976a482f65b4989 + en: f6e2b30909a25d53 + zh: f4c6937dd4402f7c +/deepseek-ai-dsh-experimental-agent-team/understand-the-implementation/waiting: + en: babe6d5ed5b72077 + zh: 8d112b5d6ce8235e /deepseek-ai-dsh-experimental-agent-team/understand-the-implementation/durability-model: - en: 6922e307e18696b4 - zh: 083f4d95587a844b + en: 831d6826d4280399 + zh: d846efb1cd13a60a /deepseek-ai-dsh-experimental-agent-team/understand-the-implementation/disposal: - en: a12fb358183cc6d8 - zh: 7fdb0434d3c407fe + en: f6272e8a4cab330e + zh: 6dbf43d487dc1c6e +/deepseek-ai-dsh-experimental-agent-team/understand-the-implementation/browser-projection: + en: 35f9e2968ab635f5 + zh: f599a75e88228d83 /deepseek-ai-dsh-experimental-agent-team/further-exploration: - en: 067ed50203315774 - zh: 7b4dfd41eea2208b -/deepseek-ai-dsh-experimental-agent-team/further-exploration/browser-projection: - en: 4b3033806e6db528 - zh: 53e0dfef7309840e + en: 4ac0e25391358fd3 + zh: bd0c9414fbddf654 /deepseek-ai-dsh-experimental-agent-team/model-experience: en: 215e7ba838619b7b zh: a311da3843709f90 -/deepseek-ai-dsh-experimental-agent-team/model-experience/peer-messages: - en: 7a72088145dd45ec - zh: 6763071c604c2edd -/deepseek-ai-dsh-experimental-agent-team/model-experience/peer-messages/what-the-model-sees: - en: 2fe9bc2d8c22e886 - zh: 79bb29e4dca59372 -/deepseek-ai-dsh-experimental-agent-team/model-experience/peer-messages/token-effect: - en: 37ec43beac34a6ad - zh: 6cdcd89ccceea815 -/deepseek-ai-dsh-experimental-agent-team/model-experience/peer-messages/kv-cache-effect: - en: af2993419835b838 - zh: b2a4fea055d5cdf4 +/deepseek-ai-dsh-experimental-agent-team/model-experience/task-views-and-errors: + en: 8b29cb821f80db40 + zh: 43e402736a553ce5 +/deepseek-ai-dsh-experimental-agent-team/model-experience/task-views-and-errors/what-the-model-sees: + en: 2353fe861b9fa710 + zh: 34a92639c01f57f5 +/deepseek-ai-dsh-experimental-agent-team/model-experience/task-views-and-errors/token-effect: + en: d5f77a18bfca1051 + zh: b988301e31491408 +/deepseek-ai-dsh-experimental-agent-team/model-experience/task-views-and-errors/kv-cache-effect: + en: e5c406af31bf0c67 + zh: 987e49e0f3e5af69 /deepseek-ai-dsh-experimental-agent-team/known-limitations-and-deferred-work: - en: 2b6bbe3e4242d932 - zh: c1b33474c62f9674 + en: e101287168ed5875 + zh: 909da68681f2b408 /deepseek-ai-dsh-experimental-agent-team/known-limitations-and-deferred-work/dev-note: en: 498e4cf9fe5741a7 zh: 9d2bab630e655c99 @@ -96,5 +99,5 @@ en: 3d126e3b1015e4ce zh: 22f0d18e2ce5c30f /deepseek-ai-dsh-experimental-agent-team/known-limitations-and-deferred-work/dev-note/future-directions: - en: 38eadeacd73b9c02 - zh: 05c5b55022e0b8d9 + en: 462ce9bcdfee79d8 + zh: 9b178a7b0f64f264 diff --git a/packages/experimental/agent-team/README.md b/packages/experimental/agent-team/README.md index 69d602f079..045b7ea5ff 100644 --- a/packages/experimental/agent-team/README.md +++ b/packages/experimental/agent-team/README.md @@ -1,5 +1,5 @@ --- -description: "Run named teammates with direct inbox messages and a durable shared task board in experimental Team compositions." +description: "Coordinate subagents through a durable shared task board and change waits in experimental Team compositions." kind: "package-reference" --- @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -`dsh-experimental-agent-team` gives one session a Lead, named teammates, direct messages, and a durable shared task board. Sends use each target Agent’s inbox and can cold-resume stored teammates. Team retains roster and task state; message persistence follows the target Agent’s normal policy. Mount `dsh-experimental-tool-agent-team` for model tools. The package is experimental and requires durable session storage. +`dsh-experimental-agent-team` turns an agent and its subagents into a Team with a durable shared task board. Standard subagent tools create and message teammates by id. Team adds compare-and-set task ownership, progress waits, and a roster for the Web panel. Mount `dsh-experimental-tool-agent-team` for model tools. The package is experimental, and members share one checkout. ## Table of Contents @@ -25,67 +25,59 @@ English | [中文](README.zh.md) ## Use this package -Add this package when one agent needs named helpers in a shared working directory, direct peer messages, and durable roster and task state. Mount it with `@deepseek-ai/dsh-experimental-tool-agent-team` to expose the model tools. +Add this package when subagents need a shared working directory, durable task board, and change waits. Mount it with `@deepseek-ai/dsh-experimental-tool-agent-team` for model tools. ### When to choose it -Choose it for cooperation in one shared workspace with durable roster and task state. Separate working directories, coordination across processes, and automatic task-owner release are unsupported. Durable session storage is required. +Choose it when delegated agents must divide and track shared work in one checkout. Plain subagents suffice when delegated work needs no shared board. Separate working directories, coordination across processes, and automatic task-owner release are unsupported. ### Smallest working setup -The smallest addition to an existing composition is durable session storage plus both Team packages: +Mount both Team packages in a composition that already provides continuable subagents, their messaging tools, and durable session storage, such as a profile built on `@deepseek-ai/dsh-base`: ```yaml -# smallest team setup — durable storage plus both Team packages -- name: '@deepseek-ai/dsh-session-persistence-jsonl' +# smallest Team addition over a composition with continuable subagents - name: '@deepseek-ai/dsh-experimental-agent-team' - name: '@deepseek-ai/dsh-experimental-tool-agent-team' ``` -With the tools installed, the model does the rest on request — for example, "create a teammate named reviewer to check the diff", then "send reviewer the change summary". All limits are optional and validated at startup: +For example, ask the model to start a subagent to review the diff and track its work on the task board. The one limit is optional and validated at startup: | Field | Default | Meaning | |---|---|---| -| `maxMembers` | `16` | Maximum teammates a team may ever create, including failed ones | -| `maxTasks` | `256` | Maximum active tasks on the board | -| `maxMessageBytes` | `65,536` | Maximum size of one sent message | -| `disposalTimeoutMs` | `5,000` | Time allowed for shutdown cleanup | +| `maxTasks` | `256` | Maximum non-deleted tasks on the board | -The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-experimental-agent-team) is the exhaustive source for every accepted field and its JSDoc. +The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-experimental-agent-team) is the exhaustive source for every accepted field and its JSDoc. A row that sets `maxMembers`, `maxMessageBytes`, or `disposalTimeoutMs` does not activate, and dsh names the key in its startup warning; the [upgrade guide](../../../docs/upgrade-guide/v0.2.1-alpha.3/agent-team-subagents/guide.md) lists the replacements. ### Teammates -Ask the Lead to create a teammate: give it a unique lowercase name such as `reviewer` and describe its job. A teammate starts fresh with no memory of the Lead's conversation, or as a fork that inherits the Lead's completed turns; the creation request chooses which. Teammate names are permanent — even a teammate whose creation failed keeps its name, and no name is ever reused. +Ask the Lead to start a subagent with a `description` of its work: `subagent` starts fresh, while `subagent_fork` inherits completed turns. The returned Session id addresses the teammate; descriptions are display text and may repeat. -The roster shows every member with its role (`lead` or `teammate`) and current status: `running`, `inactive` (no turn is executing, whether loaded or stored), `provisioning`, or `failed`. A member that is not loaded receives its messages when it wakes. - -Only the Lead can create teammates or interrupt them. +Every root agent leads its own Team, and every live subagent below it, at any depth, receives Team tools. The roster lists the Lead, its direct continuable children, and historical `team/member` records. Deeper descendants work on the same board under their Session ids but have no roster row. ### Messages between teammates -Any member can send to another member or the Lead. Each attempt either returns the target inbox `MessageId` or throws. Team does not retain unaccepted send intent, retry new messages after restart, or deduplicate explicit resends. - -Steer gives a running target the message at its nearest step boundary and starts or cold-resumes an inactive target. Success means inbox acceptance, not model processing or a separate synchronous storage flush. Accepted messages follow ordinary Agent persistence and inbox recovery. +Members use standard `send_message` with Session ids: the Lead reaches direct children, and resident continuable children reach their parent and continuable siblings. Steer delivers into the target inbox and can cold-resume children or siblings. Team stores no messages; accepted messages follow ordinary Agent persistence. ### Shared task board Any member can add a task with a title, details, optional dependencies on other tasks, and optional hints about which files it will touch. A task is claimable only when everything it depends on is complete. -Tasks have an owner: a member claims a task to start work, completes it when done, releases it back, or reopens it; the Lead can assign a task to any member. Every change is compare-and-set: an update based on an outdated copy is rejected, so two members cannot silently overwrite each other's work. +A member claims a task to start work, completes it, releases it, or reopens it. The Lead can reassign a task by Session id to itself, a direct continuable child, or an active historical member. Other members claim tasks themselves. Every change is compare-and-set, so stale updates cannot overwrite newer work. File hints produce warnings when two in-progress tasks plan to touch overlapping paths — they never block anything. Deleted tasks remain in history but disappear from the active list. ### Waiting and interruption -A member can wait for the next team change — a teammate's status, an incoming message, or a task update — instead of polling repeatedly; the wait reports only whether it timed out, and the caller re-reads the current state afterward. +A member can wait for the next Team change — a task update, a member's status change, or a message a member's inbox accepts — instead of polling repeatedly; the wait reports only whether it timed out, and the caller re-reads the current state afterward. -The Lead can stop a teammate's current turn without deleting its queued messages; task ownership is unchanged. +An ancestor stops a teammate's current turn with the standard `interrupt_agent` tool, so only the Lead interrupts its direct teammates. Interruption keeps unclaimed queued messages and leaves task ownership unchanged. ### What success and failure look like -Success returns a teammate roster row, an accepted inbox message id, or an updated task revision. Invalid member names, unready tasks, stale revisions, exhausted limits, and failed message admission report errors. +Success returns an updated task revision or a wait result. Unknown member ids, unready tasks, stale revisions, unauthorized mutations, and an exhausted task limit report a `TeamError`, such as `TEAM_TASK_STALE_REVISION`. A caller outside a live Team receives `TEAM_NOT_MEMBER`. ----- @@ -99,57 +91,62 @@ This section explains the design decisions behind the service and points at the ### Design philosophy -The service is built on one separation and three commitments: +The service is built on four commitments: -- **Durable log, derived state.** The Lead log owns roster and tasks; each target Agent owns its accepted messages. -- **Process-local ownership.** Continuation delivery owns target locking, cold recovery, and lifecycle authorization. -- **Explicit authority.** Every service method takes the exact live calling `Agent`; only the Lead spawns, reassigns, or interrupts. -- **Bounds that fail loud.** Every limit is a validated deployment value, and exhaustion reports a typed error instead of reusing an id or name. +- **Membership from lineage.** The live Agent registry and the Lead's subagent catalog define the Team; Team stores no membership record of its own. +- **Durable log, derived state.** The Lead log owns tasks and child catalog facts; each target Agent owns accepted messages. +- **Explicit authority.** Every service method takes the exact live calling `Agent`; a task owner or the Lead mutates a task, and only the Lead reassigns one. +- **Bounds that fail loud.** `maxTasks` is a validated deployment value, and exhaustion reports a typed error instead of reusing an id. -The [Agent Teams Agent Note](../../../.agents/notes/implemented/feature/2026-08-05-agent-teams.md) owns the roster, mailbox, task, and shared-checkout decisions. The [runtime-context identity decision](../../../.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.md) owns model-facing identity. +The [subagent teammates Agent Note](../../../.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md) owns the membership, roster, and messaging decisions. The [runtime-context identity decision](../../../.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.md) owns model-facing identity. ### Source map | File | Role | |---|---| -| [`src/index.ts`](src/index.ts) | Plugin entry: `Config` schema, service registration, recovery scheduling | -| [`src/roster.ts`](src/roster.ts) | Team identity, membership resolution, provisioning, and roster teardown | +| [`src/index.ts`](src/index.ts) | Plugin entry: `Config` schema, removed-key rejection, service registration, change notification | +| [`src/membership.ts`](src/membership.ts) | Lead and teammate resolution through the live Agent registry, and task-assignee lookup | | [`src/task-board.ts`](src/task-board.ts) | Task CAS commands, DAG validation, and derived views | | [`src/journal.ts`](src/journal.ts) | Serialized Lead-log transactions and commit notification | -| [`src/projection.ts`](src/projection.ts) | Strict replay projection that decodes and validates Team events and publishes the `agentTeam` client view | -| [`src/task-view.ts`](src/task-view.ts) | Pure task readiness, owner-name, and write-overlap derivation shared by the task board and the client view | +| [`src/projection.ts`](src/projection.ts) | Strict replay projection of the Lead’s continuable children and Team events, publishing the `agentTeam` client view | +| [`src/task-view.ts`](src/task-view.ts) | Pure task readiness and write-overlap derivation shared by the task board and client view | | [`src/activity.ts`](src/activity.ts) | One-shot change waiters and disposal release | -| [`src/lifecycle.ts`](src/lifecycle.ts) | Shared admission cutoff and bounded settlement | -### Team identity and roster +### Team identity and membership -Every ordinary runtime root is the implicit Lead of a Team whose `TeamId` equals its `SessionId`; there is no creation event, and durable Team state begins with the first member or task record. `spawnTeammate()` first appends and flushes a `provisioning` member record, then asks the configured provider to create the reserved child; a provider failure appends a durable `failed` member. A fresh child starts with no Lead history; a fork child captures the Lead's completed-turn prefix once. Recovery reconciles an unterminated provisioning record against the child's independently persisted Session: a matching direct-parent and continuable descriptor plus a recorded initial user message produces `active`, and anything else produces `failed`. If recovery wins a same-process race, the creator accepts the terminal state or reports `TEAM_PROVISIONING_CONFLICT` and drains the child. Names are reserved by the first provisioning record and never reused. +A root Agent whose own log carries no subagent descriptor leads the Team identified by its Session id. `tryMembership()` walks `parentSession` through the live registry; a missing parent leaves the subagent without membership. An ordinary fork carries at most an inherited descriptor, so it leads a new Team. Membership records only the root, Team id, and role. -### Direct messages and historical compatibility +### Roster -`sendMessage()` checks exact caller membership, rejects self-messaging, and bounds the complete sender-framed UTF-8 content. It returns the existing inbox identity and emits Team activity only after acceptance. The target stores `agent-message` source with the actual `senderSessionId`; the first content block is `Team message from :`. No new `team/message/queued` or `team/message/delivered` records are written for these sends. Team adds no per-target send ordering; delivery order belongs to the target inbox. The client presents these messages with the ordinary Agent title and icon; the message body retains the sender name. +The projection folds the Lead’s own `subagent/catalog` facts into rows keyed by child id, using the catalog label as description. Continuable children are included; inherited facts and other modes are skipped. Descriptions need not be unique. Historical `team/member` records retain their phases and descriptions and add only ids absent from the catalog. Team writes no membership records. -Lead delivery calls `Agent.steer()` directly. Teammate delivery uses the continuation owner's host-only Steer path, which preserves the Team sender source while authorizing the Lead-to-child edge and cold-resuming inactive targets. Sibling messages never impersonate the Lead through the public adjacent-Agent messaging operation. +### Messages and historical mailbox + +Team sends no messages. Routes, Steer delivery, cold resume, and sender framing for `send_message` belong to the subagent runtime. Historical `team/message/queued` and `team/message/delivered` records and `team-message` sources remain decodable; Team never delivers or acknowledges them, so a historical message its target had not recorded stays undelivered. ### Shared task board -Tasks are complete versioned snapshots; every mutation carries `expectedRevision`, and a stale caller receives `TEAM_TASK_STALE_REVISION` instead of overwriting a newer value. Numeric `task-` ids require a safe-integer suffix, and id-space exhaustion reports `TEAM_TASK_LIMIT` instead of reusing the final id. Deleted tasks remain tombstones for replay and id stability but do not consume `maxTasks` or appear in `listTasks()`. `writeScopes` are normalized workspace-relative prefixes; views warn on overlap with in-progress tasks but never block claim or authorize writes. +Tasks are complete versioned snapshots; every mutation carries `expectedRevision`, and a stale caller receives `TEAM_TASK_STALE_REVISION` instead of overwriting a newer value. Numeric `task-` ids require a safe-integer suffix, and id-space exhaustion reports `TEAM_TASK_LIMIT` instead of reusing the final id. Deleted tasks remain tombstones for replay and id stability but do not consume `maxTasks` or appear in `listTasks()`. `writeScopes` are normalized workspace-relative prefixes; views warn on overlap with in-progress tasks but never block claim or authorize writes. Lead reassignment resolves the root, a direct continuable child, or an active historical member by Session id, and reports `TEAM_MEMBER_NOT_FOUND` for anything else. -### Waiting and interruption +### Waiting -`waitForChange()` waits for one roster, task, message-acceptance, or live-status edge that occurs after registration, from ten seconds through one hour, and reports only whether it timed out; runtime disposal releases current waits. Cancellation preserves an Error reason or reports a non-Error reason through `TEAM_WAIT_ABORTED`. `interrupt()` is Lead-only and delegates to the continuable-subagent interrupt path, which cancels only a live teammate's current turn with `keepInbox`; it neither releases task ownership nor deletes durable mail. +`waitForChange()` waits for one task commit, member `agent/status` change, or member `agent/inbox/inserted` edge that occurs after registration, from ten seconds through one hour, and reports only whether it timed out; unloading the service releases current waits. Cancellation preserves an Error reason or reports a non-Error reason through `TEAM_WAIT_ABORTED`. `hasActivePeer()` reports whether another live member is running a turn, which lets `wait_agent` skip a wait that no member can end. ### Durability model -Roster and task mutations append `team/member` and `team/task` to the exact live Lead Session and flush before reporting success or waking waiters. Historical `team/message/queued` and `team/message/delivered` records remain readable; Team writes neither. All four event types are log-only: they never enter the conversation surface, so derived model history is untouched by coordination records. Session event `seq` and `time` own ordering and timing; snapshots do not duplicate them. +Task mutations append `team/task` to the exact live Lead Session and flush before reporting success or waking waiters. Team events are log-only: they never enter the conversation surface, so derived model history is untouched by coordination records. Session event `seq` and `time` own ordering and timing; snapshots do not duplicate them. Teammate ids and descriptions live in the Lead's `subagent/catalog`, which the subagent runtime writes. -Historical mailbox events and `team-message` sources remain readable, and the projection still reports queued-minus-delivered records. Team never delivers or acknowledges them: a historical message that its target had not recorded stays undelivered. - -Mailbox projection and checkpoint admission preserve every decoded JSON field of accepted content outside the locally declared validators, including an own `__proto__` key. Local field checks cover `text`, `reasoning`, `image`, and `tool-call`; accepted unknown tags remain opaque. Team projection cache version 4 rebuilds checkpoints from earlier cache versions from the Session log; the Session format version is unchanged. +Mailbox projection and checkpoint admission preserve every decoded JSON field of accepted content outside the locally declared validators, including an own `__proto__` key. Local field checks cover `text`, `reasoning`, `image`, and `tool-call`; accepted unknown tags remain opaque. Team projection cache version 5 rebuilds checkpoints from earlier cache versions from the Session log; the Session format version is unchanged. ### Disposal -The runtime lifecycle tracks sends and complete creation transactions in one operation set. Disposal closes admission, aborts and awaits those operations, then releases the roster’s live children and descendants. Non-Team children remain untouched. Admitted operations share one `disposalTimeoutMs` settlement deadline; each subsequent Team child drain has its own deadline. This value is not a whole-service shutdown bound. Cleanup failures are reported. +Unloading the service releases current waits and unregisters the projection. Team owns no Agent: teammates keep running under the subagent runtime, which owns their capacity, settlement, and teardown. + +### Browser projection + +The `agentTeam` Session projection publishes the Lead Session's roster rows with their phases and errors, non-deleted task views, and any `failure` beside the last valid state. Its `apply` replaces only the touched collection; mailbox-only changes retain the client view reference and produce no frame. The [subsystem reference](../../../docs/subsystems/agent-team.md#web-projection) defines the wire types. + +The [Web UI](../client-ui-agent-team/README.md) reads the shared Session projections and overlays activity from Session status. Task creation and updates belong to Team agents through the service and model tools. The `./client` export supplies browser-safe roster, task, and projection types.
@@ -161,35 +158,30 @@ The runtime lifecycle tracks sends and complete creation transactions in one ope Read these pages when the package-level contract is not enough. They move from the shared subsystem types to the tool surface and the decisions behind the design. - [Agent Teams subsystem](../../../docs/subsystems/agent-team.md) — durable Team types and the `ctx.agentTeams` service API. -- [tool-agent-team package](../tool-agent-team/README.md) — the tools that let the model create, message, and coordinate teammates. -- [Agent Teams Agent Note](../../../.agents/notes/implemented/feature/2026-08-05-agent-teams.md) — roster, mailbox, task, and shared-checkout decisions. +- [tool-agent-team package](../tool-agent-team/README.md) — the task and wait tools, policy, and identity every member receives. +- [Subagent runtime](../../subagent/subagent/README.md) — continuable children, messaging, interruption, and lifecycle that teammates use. +- [Subagent teammates Agent Note](../../../.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md) — why teammates are subagents and what Team keeps. +- [Upgrade guide](../../../docs/upgrade-guide/v0.2.1-alpha.3/agent-team-subagents/guide.md) — replacements for the removed Team tools, service methods, and configuration keys. - [Experimental package reference](../AGENTS.md) — placement, publication, and dependency isolation. ----- - -### Browser projection - -The `agentTeam` Session projection publishes the Lead Session's durable roster identities and phases, member errors, non-deleted task views, and any `failure` beside the last valid state. Its `apply` replaces only the touched collection; mailbox-only changes retain the client view reference and produce no frame. The [subsystem reference](../../../docs/subsystems/agent-team.md#web-projection) defines the wire types. - -The [Web UI](../client-ui-agent-team/README.md) reads the shared Session projections and overlays activity from Session status. Task creation and updates belong to Team agents through the service and model tools. The `./client` export supplies browser-safe roster, task, and projection types. - ## Model Experience -### Peer messages +### Task views and errors #### What the model sees -Each peer message is a user-role message prefixed with its sender name, followed by the original content blocks. Roster, task, and historical mailbox records remain log-only. +The model sees Team state through task-tool results; coordination events never enter the conversation. Task views identify their owner through `ownerId`. Failures surface as tool errors with the `TeamError` message, including the current revision for stale updates. #### Token effect -Each peer delivery adds the sender prefix plus message content to the target history. Task and roster mutations add no model tokens; their model-facing representation belongs to `@deepseek-ai/dsh-experimental-tool-agent-team` results. +The domain adds no context of its own; each task-tool result costs one compact task view per returned task. Policy, identity, and tool schemas belong to `@deepseek-ai/dsh-experimental-tool-agent-team`. #### KV Cache effect -Peer messages append after the target's reusable history prefix. Cold resume reuses the persisted conversation before appending the new message. +Task commits append only log-only events to the Lead Session, so they never change any member's request prefix; task results append after it. ## Known Limitations and Deferred Work @@ -202,9 +194,10 @@ These limits describe what a team cannot do yet or what needs special operationa - **Experimental prototype with no stability promise** — the package is public, but its contracts can change freely while it incubates. - **One process and one shared checkout** — members share cwd and observe edits immediately; this package provides no worktree, remote member, merge, or filesystem lock. - **Advisory write scopes** — Bash, formatters, code generators, and direct external writers can bypass filesystem version checks; Leads must coordinate ownership and review the final diff. -- **Flat immutable roster** — only the Lead creates direct teammates; there is no nested Team, rename, deletion, or name reuse. +- **Roster covers direct continuable children** — deeper descendants share tasks and waits but have no roster row; they can claim tasks under their own ids and message only their direct parent, children, and continuable siblings. +- **Membership needs a live lineage** — a subagent whose parent Agent is not loaded is outside every Team, so its Team calls fail with `TEAM_NOT_MEMBER`. - **No automatic ownership release** — inactivity, interruption, process exit, and failed work do not release a task owner. -- **No send retry guarantee** — Team does not retain or deduplicate new attempts; concurrent processes over one Team are unsupported. Historical queued messages that never reached their target are not delivered. +- **Historical queued messages stay undelivered** — `team/message/queued` records without a matching delivery remain readable in the Lead log, but no member receives them. ### Dev Note @@ -220,6 +213,6 @@ Promotion to a product-role group requires reviewing the public contract, limita #### Future directions -Undecided directions include nested Teams, automatic ownership release policies, cross-process mailbox transactions, and filesystem isolation via worktrees; none of these are committed. +Undecided directions include nested Teams, automatic ownership release policies, and filesystem isolation via worktrees; none of these are committed.
diff --git a/packages/experimental/agent-team/README.zh.md b/packages/experimental/agent-team/README.zh.md index 37bcdafded..69e9bfb875 100644 --- a/packages/experimental/agent-team/README.zh.md +++ b/packages/experimental/agent-team/README.zh.md @@ -1,5 +1,5 @@ --- -description: "在实验性 Team 组合中运行具名 teammate,使用直接 inbox 消息和持久共享任务板。" +description: "在实验性 Team 组合中,通过持久共享任务板和变更等待协调子代理。" kind: "package-reference" --- @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -`dsh-experimental-agent-team` 为一个会话提供 Lead、具名 teammate、直接消息和持久共享任务板。发送使用目标 agent(智能体)的 inbox,并可冷恢复已存储的 teammate。Team 保留 roster 与任务状态,消息持久化遵循目标 Agent 的常规策略。挂载 `dsh-experimental-tool-agent-team` 可提供模型工具。本包为实验性功能,需要持久会话存储。 +`dsh-experimental-agent-team` 将代理及其子代理组成共享持久任务板的 Team。标准 subagent 工具负责创建队友并按 id 发消息。Team 增加比较并设置的任务所有权、进度等待和 Web 面板成员列表。模型工具由 `dsh-experimental-tool-agent-team` 提供。该包为实验性功能,成员共享同一工作树。 ## 目录 @@ -25,67 +25,59 @@ kind: "package-reference" ## 使用本包 -当一个 agent 需要共享工作目录中的具名助手、直接 peer 消息以及持久 roster 与任务状态时,加入本包。与 `@deepseek-ai/dsh-experimental-tool-agent-team` 一起挂载可提供模型工具。 +当子代理需要共享工作目录、持久任务板和变更等待时使用此包。与 `@deepseek-ai/dsh-experimental-tool-agent-team` 一起挂载以提供模型工具。 ### 何时选择 -适用于在同一共享工作区协作并保留持久 roster 和任务状态。不支持独立工作目录、跨进程协调和自动释放任务 owner。需要持久会话存储。 +当被委派的 agent 必须在同一个 checkout 中划分并跟踪共享工作时,选择本包。被委派的工作不需要共享任务板时,普通 subagent 就足够。不支持独立工作目录、跨进程协调和自动释放任务 owner。 ### 最小工作配置 -对现有组合的最小增量是持久会话存储加两个 Team 包: +在已经提供 continuable subagent、其消息工具和持久会话存储的组合(例如基于 `@deepseek-ai/dsh-base` 的 profile)中挂载两个 Team 包: ```yaml -# smallest team setup — durable storage plus both Team packages -- name: '@deepseek-ai/dsh-session-persistence-jsonl' +# smallest Team addition over a composition with continuable subagents - name: '@deepseek-ai/dsh-experimental-agent-team' - name: '@deepseek-ai/dsh-experimental-tool-agent-team' ``` -工具安装后,模型会按请求完成其余工作——例如先「创建一个名为 reviewer 的 teammate 检查 diff」,再「把变更摘要发给 reviewer」。所有限制都是可选的,并在启动时校验: +例如,让模型启动子代理审查差异,并在任务板上跟踪工作。唯一的限制是可选项,在启动时校验: | 字段 | 默认值 | 含义 | |---|---|---| -| `maxMembers` | `16` | 一支团队最多可创建的 teammate 数,包括失败的 | -| `maxTasks` | `256` | 任务板上最多的活动任务数 | -| `maxMessageBytes` | `65,536` | 单条发送消息的最大尺寸 | -| `disposalTimeoutMs` | `5,000` | 关闭清理允许的时间 | +| `maxTasks` | `256` | 任务板上最多的未删除任务数 | -生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-experimental-agent-team)是每个受支持字段及其 JSDoc 的穷尽式真源。 +生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-experimental-agent-team)是每个受支持字段及其 JSDoc 的穷尽式真源。设置 `maxMembers`、`maxMessageBytes` 或 `disposalTimeoutMs` 的配置行不会激活,dsh 会在启动警告中指出该键;[升级指南](../../../docs/upgrade-guide/v0.2.1-alpha.3/agent-team-subagents/guide.zh.md)列出替代方式。 ### Teammate -请 Lead 创建 teammate:给它一个唯一的小写名字(例如 `reviewer`)并描述其职责。teammate 可以 fresh 启动(不携带 Lead 对话的任何记忆),也可以作为 fork 启动(继承 Lead 已完成的轮次);创建请求决定用哪种。teammate 名字是永久的——即使创建失败的 teammate 也保留其名字,任何名字都不会被复用。 +让 Lead 使用 `description` 描述工作并启动子代理:`subagent` 从全新上下文开始,`subagent_fork` 继承已完成轮次。返回的 Session id 用于寻址队友;描述用于展示,可以重复。 -roster 显示每个成员的职责(`lead` 或 `teammate`)与当前状态:`running`、`inactive`(当前没有执行轮次,包括已加载和仅存储的成员)、`provisioning` 或 `failed`。未加载的成员会在唤醒后收到其消息。 - -只有 Lead 可以创建 teammate 或中断它们。 +每个根代理领导自己的 Team,其下任意深度的在线子代理都获得 Team 工具。成员列表包含 Lead、其直接可继续子级以及历史 `team/member` 记录。更深的后代按 Session id 使用同一任务板,但不显示为成员行。 ### teammate 之间的消息 -任何成员都可向其他成员或 Lead 发送消息。每次尝试要么返回目标 inbox 的 `MessageId`,要么抛出错误。Team 不保留尚未接收的发送意图,不在重启后重试新消息,也不对显式重发去重。 - -Steer 让 running target 在最近的步骤边界接收消息,并启动或冷恢复 inactive target。成功表示 inbox 接收,不表示模型已处理或已完成单独的同步存储 flush。获准消息遵循普通 Agent 的持久化和 inbox 恢复策略。 +成员使用标准 `send_message` 按 Session id 发消息:Lead 联系直接子级,驻留的可继续子级联系父级和可继续同级。Steer 投递到目标收件箱,并可冷恢复子级或同级。Team 不存储消息;已接受消息遵循普通 Agent 持久化。 ### 共享任务板 任何成员都可以添加任务,包含标题、详情、对其他任务的可选依赖,以及可选的文件触及提示。只有其全部依赖完成后,任务才可 claim。 -任务有 owner:成员 claim 任务开始工作,完成后标记完成、释放回板或重新打开;Lead 可以把任务分配给任意成员。每次变更都是 compare-and-set:基于过期副本的更新会被拒绝,因此两个成员不会悄悄覆盖彼此的成果。 +成员认领任务后开始工作,可以完成、释放或重新打开任务。Lead 可按 Session id 将任务重新分配给自身、直接可继续子级或活跃的历史成员。其他成员自行认领任务。每次变更都使用比较并设置,防止过期更新覆盖新工作。 当两个 in-progress 任务计划触及重叠路径时,文件提示会产生警告——它们绝不阻止任何操作。已删除任务保留在历史中,但从活动列表中消失。 ### 等待与中断 -成员可以等待下一次团队变化——teammate 的状态、新消息或任务更新——而不必反复轮询;等待只报告是否超时,调用方随后重新读取当前状态。 +成员可以等待下一次 Team 变化——任务更新、成员状态变化,或成员 inbox 接收的消息——而不必反复轮询;等待只报告是否超时,调用方随后重新读取当前状态。 -Lead 可以停止 teammate 的当前轮次,而不会删除其排队的消息;任务归属不变。 +祖先使用标准 `interrupt_agent` 工具停止 teammate 的当前轮次,因此只有 Lead 能中断其直接 teammate。中断保留尚未领取的排队消息,任务归属不变。 ### 成功与失败的表现 -成功会返回 teammate roster 行、获准 inbox 消息 id 或更新后的任务 revision。无效成员名称、未就绪任务、过期 revision、上限耗尽和消息准入失败均报告错误。 +成功返回更新后的任务修订或等待结果。未知成员 id、未就绪任务、过期修订、未授权变更和任务上限耗尽均报告 `TeamError`,例如 `TEAM_TASK_STALE_REVISION`。不属于在线 Team 的调用方收到 `TEAM_NOT_MEMBER`。 ----- @@ -99,57 +91,62 @@ Lead 可以停止 teammate 的当前轮次,而不会删除其排队的消息 ### 设计理念 -本服务建立在一个分离与三项承诺之上: +本服务建立在四项承诺之上: -- **持久日志,派生状态。** Lead 日志拥有 roster 与任务,各目标 Agent 拥有其接收的消息。 -- **进程内归属。** Continuation 投递拥有目标锁、冷恢复和生命周期授权。 -- **显式权限。** 每个服务方法都接收确切的实时调用方 `Agent`;只有 Lead 可以 spawn、reassign 或 interrupt。 -- **超出上限时明确失败。** 每个限制都是经过校验的部署值,耗尽时报告类型化错误,而不是复用 id 或名字。 +- **成员关系来自谱系。** live Agent registry 与 Lead 的 subagent catalog 定义 Team;Team 自己不存储成员记录。 +- **持久日志、派生状态。** Lead 日志拥有任务和子级目录事实;每个目标 Agent 拥有已接受的消息。 +- **显式权限。** 每个服务方法都接收确切的 live 调用方 `Agent`;任务 owner 或 Lead 可以变更任务,只有 Lead 可以 reassign。 +- **超出上限时明确失败。** `maxTasks` 是经过校验的部署值,耗尽时报告类型化错误,而不是复用 id。 -[Agent Teams Agent Note](../../../.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md)负责 roster、mailbox、任务与共享 checkout 决策。[runtime-context 身份决策](../../../.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.zh.md)负责面向模型的身份。 +[subagent teammate Agent Note](../../../.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.zh.md)负责成员关系、roster 与消息决策。[runtime-context 身份决策](../../../.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.zh.md)负责面向模型的身份。 ### 源码地图 | 文件 | 职责 | |---|---| -| [`src/index.ts`](src/index.ts) | 插件入口:`Config` schema、服务注册、恢复调度 | -| [`src/roster.ts`](src/roster.ts) | Team 身份、成员关系解析、provisioning 与 roster 拆除 | +| [`src/index.ts`](src/index.ts) | 插件入口:`Config` schema、已移除键的拒绝、服务注册、变更通知 | +| [`src/membership.ts`](src/membership.ts) | 通过 live Agent registry 解析 Lead 与 teammate,以及查找任务分配对象 | | [`src/task-board.ts`](src/task-board.ts) | 任务 CAS 命令、DAG 校验与派生视图 | | [`src/journal.ts`](src/journal.ts) | 串行化的 Lead 日志事务与提交通知 | -| [`src/projection.ts`](src/projection.ts) | 解码并校验 Team 事件、发布 `agentTeam` 客户端视图的严格回放投影 | -| [`src/task-view.ts`](src/task-view.ts) | 任务板与客户端视图共用的纯任务派生:就绪状态、owner 名称与写入范围重叠 | +| [`src/projection.ts`](src/projection.ts) | 严格回放 Lead 的可继续子级和 Team 事件,发布 `agentTeam` 客户端视图 | +| [`src/task-view.ts`](src/task-view.ts) | 任务板与客户端视图共用的纯任务就绪性和写入重叠推导 | | [`src/activity.ts`](src/activity.ts) | 一次性变更等待者与 dispose(资源释放)时的等待解除 | -| [`src/lifecycle.ts`](src/lifecycle.ts) | 共享准入截止与有界结算 | -### Team 身份与 roster +### Team 身份与成员关系 -每个普通运行时 root 都是一个隐式 Team 的 Lead,其 `TeamId` 等于 `SessionId`;不存在创建事件,持久 Team 状态从第一条成员或任务记录开始。`spawnTeammate()` 先追加并 flush 一条 `provisioning` 成员记录,再要求配置的提供方创建预留 child;提供方失败会追加一条持久的 `failed` 成员。fresh child 不携带 Lead 历史;fork child 只捕获一次 Lead 的已完成 turn 前缀。恢复把未终结的 provisioning 记录对照 child 独立持久化的会话进行对账:直接 parent 与 continuable descriptor 匹配、且初始用户消息已记录则产生 `active`,其他任何情况都产生 `failed`。如果恢复在同进程竞争中先完成,creator 会接受终态,或报告 `TEAM_PROVISIONING_CONFLICT` 并 drain 该 child。名字由第一条 provisioning 记录保留,且永不复用。 +自身日志不含子代理描述符的根 Agent 领导以其 Session id 标识的 Team。`tryMembership()` 沿在线注册表中的 `parentSession` 查找根;父级缺失时,子代理不属于任何 Team。普通分叉最多携带继承的描述符,因此领导新的 Team。成员身份仅记录根、Team id 和角色。 -### 直接消息与历史兼容 +### Roster -`sendMessage()` 检查确切调用方的成员身份,拒绝自发消息,并限制包含发送者前缀的完整 UTF-8 内容大小。它返回现有 inbox 身份,并仅在接收后发出 Team activity。目标保存带真实 `senderSessionId` 的 `agent-message` source;首个内容块为 `Team message from :`。这些发送不写入新的 `team/message/queued` 或 `team/message/delivered` 记录。Team 不额外保证同目标发送顺序;投递顺序由目标 inbox 管理。客户端使用普通 Agent 消息标题和图标展示这些消息;正文保留发送者名称。 +投影将 Lead 自有的 `subagent/catalog` 事实转为按子级 id 标识的行,目录标签作为描述。可继续子级被纳入;继承的事实和其他模式被跳过。描述不要求唯一。历史 `team/member` 记录保留阶段和描述,仅补充目录中不存在的 id。Team 不写入成员记录。 -投递给 Lead 时直接调用 `Agent.steer()`。投递给 teammate 时使用 continuation owner 的 host-only Steer 路径;该路径会保留 Team 发送者 source,同时授权 Lead-to-child edge 并冷恢复 inactive target。sibling 消息绝不会通过公开的相邻 Agent 消息操作伪装成 Lead。 +### 消息与历史 mailbox + +Team 不发送消息。`send_message` 的路由、Steer 投递、冷恢复和发送者前缀都属于 subagent 运行时。历史 `team/message/queued` 与 `team/message/delivered` 记录以及 `team-message` source 仍可解码;Team 从不投递或确认它们,因此目标尚未记录的历史消息保持未投递。 ### 共享任务板 -任务是完整版本化快照;每次变更都携带 `expectedRevision`,陈旧调用方会收到 `TEAM_TASK_STALE_REVISION`,而不会覆盖更新的值。数字 `task-` id 的后缀必须是安全整数,id 空间耗尽时报告 `TEAM_TASK_LIMIT`,而不是复用最后一个 id。已删除任务作为 tombstone 保留以供回放与维持 id 稳定,但不占用 `maxTasks`,也不出现在 `listTasks()` 中。`writeScopes` 是规范化后的 workspace 相对前缀;视图会对与 in-progress 任务的重叠发出警告,但绝不阻止 claim 或授予写权限。 +任务是完整版本化快照;每次变更都携带 `expectedRevision`,陈旧调用方会收到 `TEAM_TASK_STALE_REVISION`,而不会覆盖更新的值。数字 `task-` id 的后缀必须是安全整数,id 空间耗尽时报告 `TEAM_TASK_LIMIT`,而不是复用最后一个 id。已删除任务作为 tombstone 保留以供回放与维持 id 稳定,但不占用 `maxTasks`,也不出现在 `listTasks()` 中。`writeScopes` 是规范化后的 workspace 相对前缀;视图会对与 in-progress 任务的重叠发出警告,但绝不阻止 claim 或授予写权限。Lead reassign 按 Session id 解析根、直接可继续子级或 active 历史成员,其他目标报告 `TEAM_MEMBER_NOT_FOUND`。 -### 等待与中断 +### 等待 -`waitForChange()` 等待注册之后发生的下一条 roster、task、消息接收或实时状态边,时长从 10 秒到 1 小时,并且只报告是否超时;运行时 dispose 会释放当前等待。取消会保留 Error reason;非 Error reason 则通过 `TEAM_WAIT_ABORTED` 报告。`interrupt()` 仅限 Lead,委托 continuable-subagent 的 interrupt 路径,以 `keepInbox` 只取消 live teammate 的当前 turn;它既不释放任务 owner,也不删除持久 mail。 +`waitForChange()` 等待注册之后发生的下一次任务提交、成员 `agent/status` 变化或成员 `agent/inbox/inserted` 边,时长从 10 秒到 1 小时,并且只报告是否超时;卸载服务会释放当前等待。取消会保留 Error reason;非 Error reason 则通过 `TEAM_WAIT_ABORTED` 报告。`hasActivePeer()` 报告是否有另一个 live 成员正在运行轮次,`wait_agent` 据此跳过没有成员能结束的等待。 ### 持久性模型 -roster 与任务变更将 `team/member` 和 `team/task` 追加到精确的 live Lead 会话,并在报告成功或唤醒等待者之前 flush。历史 `team/message/queued` 与 `team/message/delivered` 记录仍可读取;Team 不再写入这两种记录。这四种事件仅存在于日志:它们从不进入会话表面,因此派生模型历史不受协作记录影响。顺序与时间由会话事件的 `seq` 与 `time` 负责,快照不重复保存。 +任务变更将 `team/task` 追加到精确的 live Lead 会话,并在报告成功或唤醒等待者之前 flush。Team 事件仅存在于日志:它们从不进入会话表面,因此派生模型历史不受协作记录影响。顺序与时间由会话事件的 `seq` 与 `time` 负责,快照不重复保存。teammate id 与描述保存在 Lead 的 `subagent/catalog` 中,由 subagent 运行时写入。 -历史 mailbox 事件与 `team-message` source 仍可读取,投影仍报告 queued-minus-delivered 记录。Team 不会投递或确认这些记录:目标尚未记录的历史消息保持未投递。 - -Mailbox 投影与 checkpoint 准入保留本地声明的校验器之外获准内容中全部已解码 JSON 字段,包括自有 `__proto__` 键。本地字段检查覆盖 `text`、`reasoning`、`image` 和 `tool-call`;获准的未知标签保持不透明。Team 投影缓存版本 4 从 Session 日志重建较早缓存版本的 checkpoint;Session 格式版本保持不变。 +Mailbox 投影与 checkpoint 准入保留本地声明的校验器之外获准内容中全部已解码 JSON 字段,包括自有 `__proto__` 键。本地字段检查覆盖 `text`、`reasoning`、`image` 和 `tool-call`;获准的未知标签保持不透明。Team 投影缓存版本 5 从 Session 日志重建较早缓存版本的 checkpoint;Session 格式版本保持不变。 ### Dispose -运行时生命周期用同一个操作集合跟踪发送和完整创建事务。dispose 关闭准入,中止并等待这些操作,然后释放 roster 中的 live child 及其后代。非 Team child 不受影响。已获准操作共享一个 `disposalTimeoutMs` 等待期限;随后每个 Team 的 child drain 各有独立期限。此值不是整个服务关闭的总时限。清理失败会明确报告。 +卸载服务会释放当前等待并注销投影。Team 不拥有任何 Agent:teammate 继续在 subagent 运行时下运行,由后者负责其容量、结算与拆除。 + +### 浏览器投影 + +`agentTeam` Session 投影发布 Lead Session 的 roster 行(含 phase 与错误)、未删除任务视图,以及最后有效状态旁的 `failure`。其 `apply` 只替换被触及的集合;仅邮箱的变化保留客户端视图引用,不产生 frame。[子系统参考](../../../docs/subsystems/agent-team.zh.md#web-projection) 定义传输类型。 + +[Web UI](../client-ui-agent-team/README.zh.md) 读取共享 Session 投影,并从 Session 状态叠加活动信息。任务创建与更新由 Team agent 通过服务和模型工具完成。`./client` 导出可供浏览器使用的 roster、任务与投影类型。 @@ -161,35 +158,30 @@ Mailbox 投影与 checkpoint 准入保留本地声明的校验器之外获准内 当包级约定不够用时阅读以下页面。它们从共享子系统类型逐步进入工具表面与设计背后的决策。 - [Agent Teams 子系统](../../../docs/subsystems/agent-team.zh.md)——持久 Team 类型与 `ctx.agentTeams` 服务 API。 -- [tool-agent-team 包](../tool-agent-team/README.zh.md)——让模型创建 teammate、向其发送消息并进行协调的工具。 -- [Agent Teams Agent Note](../../../.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md)——roster、mailbox、任务与共享 checkout 决策。 +- [tool-agent-team 包](../tool-agent-team/README.zh.md)——每个成员获得的任务与等待工具、策略和身份。 +- [Subagent 运行时](../../subagent/subagent/README.zh.md)——teammate 使用的可继续子级、消息、中断与生命周期。 +- [subagent teammate Agent Note](../../../.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.zh.md)——为何 teammate 是subagent,以及 Team 保留了什么。 +- [升级指南](../../../docs/upgrade-guide/v0.2.1-alpha.3/agent-team-subagents/guide.zh.md)——已移除的 Team 工具、服务方法与配置键的替代方式。 - [实验包参考](../AGENTS.md)——位置、公开发布与依赖隔离。 ----- - -### 浏览器投影 - -`agentTeam` Session 投影发布 Lead Session 的持久成员身份与阶段、成员错误、未删除任务视图,以及最后有效状态旁的 `failure`。其 `apply` 只替换被触及的集合;仅邮箱的变化保留客户端视图引用,不产生 frame。[子系统参考](../../../docs/subsystems/agent-team.zh.md#web-projection) 定义传输类型。 - -[Web UI](../client-ui-agent-team/README.zh.md) 读取共享 Session 投影,并从 Session 状态叠加活动信息。任务创建与更新由 Team agent 通过服务和模型工具完成。`./client` 导出可供浏览器使用的 roster、任务与投影类型。 - ## 模型体验 -### Peer 消息 +### 任务视图与错误 #### 模型看到什么 -每条 peer 消息都是以发送者名称为前缀的用户角色消息,后接原始内容块。roster、task 和历史 mailbox 记录仍仅存在于日志。 +模型通过任务工具结果查看 Team 状态;协调事件不进入对话。任务视图通过 `ownerId` 标识负责人。失败以携带 `TeamError` 消息的工具错误显示,过期更新错误包含当前修订。 #### Token 影响 -每次 peer 投递都会把发送者前缀与消息内容加入 target 历史。任务与 roster 变更不增加模型 token;其面向模型的呈现属于 `@deepseek-ai/dsh-experimental-tool-agent-team` 结果。 +本领域包自身不增加任何上下文;每个任务工具结果按返回的任务数,每个任务计一个紧凑任务视图。策略、身份与工具 schema 属于 `@deepseek-ai/dsh-experimental-tool-agent-team`。 #### KV Cache 影响 -Peer 消息追加在 target 可复用历史前缀之后。冷恢复会先复用持久对话,再追加新消息。 +任务提交只向 Lead 会话追加仅日志事件,因此从不改变任何成员的请求前缀;任务结果追加在前缀之后。 ## 已知限制与延期工作 @@ -202,9 +194,10 @@ Peer 消息追加在 target 可复用历史前缀之后。冷恢复会先复用 - **实验原型,无稳定性承诺**——本包公开发布,但孵化期间约定仍可自由变更。 - **单进程、共享 checkout**——成员共享 cwd,修改立即可见;本包不提供 worktree、远端成员、merge 或文件锁。 - **write scope 仅作提示**——Bash、formatter、代码生成器与直接外部写入可以绕过文件版本检查;Lead 必须协调 owner 并检查最终 diff。 -- **扁平且不可变的 roster**——只有 Lead 可以创建直接 teammate;不支持嵌套 Team、重命名、删除或名字复用。 +- **成员列表覆盖直接可继续子级**——更深的后代共享任务和等待,但不显示成员行;它们可按自身 id 认领任务,并且只能联系直接父级、子级和可继续同级。 +- **成员关系需要 live 谱系**——parent Agent 未加载的 subagent 不属于任何 Team,因此其 Team 调用以 `TEAM_NOT_MEMBER` 失败。 - **不会自动释放 owner**——成员不活动、interrupt、进程退出与工作失败都不会释放任务 owner。 -- **无发送重试保证**——Team 不保留新尝试,也不对其去重;不支持多个进程同时操作一个 Team。从未到达目标的历史排队消息不会被投递。 +- **历史排队消息保持未投递**——没有对应投递记录的 `team/message/queued` 记录仍可在 Lead 日志中读取,但不会有成员收到它们。 ### 开发备注 @@ -220,6 +213,6 @@ promotion 到产品角色组需要按[实验子树规则](../AGENTS.md)审查公 #### 未来方向 -尚未决定的探索方向包括嵌套 Team、自动释放 owner 的策略、跨进程 mailbox 事务,以及通过 worktree 实现文件系统隔离;这些都没有承诺。 +尚未决定的探索方向包括嵌套 Team、自动释放 owner 的策略,以及通过 worktree 实现文件系统隔离;这些都没有承诺。 diff --git a/packages/experimental/agent-team/locale/en.json b/packages/experimental/agent-team/locale/en.json index 5d93c64811..8126dd88bc 100644 --- a/packages/experimental/agent-team/locale/en.json +++ b/packages/experimental/agent-team/locale/en.json @@ -1,6 +1,6 @@ { "meta": { "title": "Team Collaboration", - "description": "Manage team members, member messages, and shared tasks." + "description": "Track named teammates and manage the shared task board." } } diff --git a/packages/experimental/agent-team/locale/zh.json b/packages/experimental/agent-team/locale/zh.json index 5c27bbbf75..e8df5f1e62 100644 --- a/packages/experimental/agent-team/locale/zh.json +++ b/packages/experimental/agent-team/locale/zh.json @@ -1,6 +1,6 @@ { "meta": { "title": "团队协作", - "description": "管理团队成员、成员消息和共享任务。" + "description": "跟踪具名队友并管理共享任务看板。" } } diff --git a/packages/experimental/agent-team/package.json b/packages/experimental/agent-team/package.json index 2e2f35e1a0..9dadfae197 100644 --- a/packages/experimental/agent-team/package.json +++ b/packages/experimental/agent-team/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-experimental-agent-team", - "description": "Implicit-root Agent Teams roster, direct peer messaging, and shared task DAG", + "description": "Implicit-root Agent Teams roster of subagents, shared task DAG, and change waits", "version": "0.2.1-alpha.3", "publishConfig": { "access": "public" @@ -47,7 +47,6 @@ "@deepseek-ai/dsh-agent": "workspace:*", "@deepseek-ai/dsh-llm": "workspace:*", "@deepseek-ai/dsh-session": "workspace:*", - "@deepseek-ai/dsh-session-persistence": "workspace:*", "@deepseek-ai/dsh-session-projection": "workspace:*", "@deepseek-ai/dsh-subagent": "workspace:*" }, diff --git a/packages/experimental/agent-team/src/activity.ts b/packages/experimental/agent-team/src/activity.ts index 3b33f3320b..9ea826d8ee 100644 --- a/packages/experimental/agent-team/src/activity.ts +++ b/packages/experimental/agent-team/src/activity.ts @@ -1,7 +1,13 @@ /** One-shot Team change waiters independent of durable state projection. */ import type { TeamId, TeamWaitResult } from './types.ts' -import { errorMessage, TeamError } from './error.ts' +import { inspect } from 'node:util' +import { TeamError } from './error.ts' + +/** Render a non-Error abort reason in one bounded line. */ +function abortDescription(reason: unknown): string { + return typeof reason === 'string' ? reason : inspect(reason, { breakLength: Infinity, compact: true, depth: 4 }) +} interface Waiter { readonly resolve: () => void @@ -10,7 +16,6 @@ interface Waiter { /** Owns current Team change waiters and releases each at most once. */ export class TeamActivity { private readonly waiters = new Map>() - private closed = false /** * Wait for one later Team-domain or member-status change. @@ -24,7 +29,6 @@ export class TeamActivity { throw new TeamError('timeoutMs must be an integer from 10000 through 3600000', 'TEAM_INVALID_TIMEOUT') } signal.throwIfAborted() - if (this.closed) return { timedOut: false } const changed = await new Promise((resolve, reject) => { let waiters = this.waiters.get(id) if (waiters === undefined) { @@ -47,7 +51,7 @@ export class TeamActivity { const reason: unknown = signal.reason reject(reason instanceof Error ? reason - : new TeamError(`wait_agent aborted: ${errorMessage(reason)}`, 'TEAM_WAIT_ABORTED')) + : new TeamError(`wait_agent aborted: ${abortDescription(reason)}`, 'TEAM_WAIT_ABORTED')) }) } const waiter: Waiter = { @@ -76,9 +80,8 @@ export class TeamActivity { for (const waiter of waiters) waiter.resolve() } - /** Close admission and wake every current waiter during runtime disposal. */ + /** Wake every current waiter during runtime disposal; the disposed service admits no later wait. */ close(): void { - this.closed = true for (const waiters of this.waiters.values()) { for (const waiter of waiters) waiter.resolve() } diff --git a/packages/experimental/agent-team/src/client.ts b/packages/experimental/agent-team/src/client.ts index c56076fa7c..60475d318b 100644 --- a/packages/experimental/agent-team/src/client.ts +++ b/packages/experimental/agent-team/src/client.ts @@ -3,7 +3,6 @@ export type { TeamMemberPhase, TeamMemberProjection, - TeamMemberView, TeamProjection, TeamTaskId, TeamTaskStatus, diff --git a/packages/experimental/agent-team/src/error.ts b/packages/experimental/agent-team/src/error.ts index e0b89b72bd..51fab6f81c 100644 --- a/packages/experimental/agent-team/src/error.ts +++ b/packages/experimental/agent-team/src/error.ts @@ -1,6 +1,5 @@ /** Typed Agent Teams failures. */ -import { inspect } from 'node:util' import { HarnessError } from '@deepseek-ai/dsh-llm' /** Stable failure raised by the Team domain. */ @@ -10,14 +9,3 @@ export class TeamError extends HarnessError { this.name = 'TeamError' } } - -/** - * Render an arbitrary thrown value without replacing the original rejection. - * @param error - caught value used in a diagnostic or durable failure record. - * @returns one bounded single-line description. - */ -export function errorMessage(error: unknown): string { - if (error instanceof Error) return error.message - if (typeof error === 'string') return error - return inspect(error, { breakLength: Infinity, compact: true, depth: 4 }) -} diff --git a/packages/experimental/agent-team/src/index.ts b/packages/experimental/agent-team/src/index.ts index 2d35933776..f8c7d7b5a9 100644 --- a/packages/experimental/agent-team/src/index.ts +++ b/packages/experimental/agent-team/src/index.ts @@ -1,35 +1,26 @@ -/** Agent Teams service façade over roster, messaging, task, and runtime lifecycle owners. */ +/** Agent Teams service façade over membership, the shared task board, and change waits. */ import { Context, Service } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import type { Agent } from '@deepseek-ai/dsh-agent' -import type {} from '@deepseek-ai/dsh-session-persistence' -import { createUserMessage } from '@deepseek-ai/dsh-llm' -import { steerHostSubagentPrompt } from '@deepseek-ai/dsh-subagent/internal' import { TeamActivity } from './activity.ts' -import { errorMessage, TeamError } from './error.ts' +import { TeamError } from './error.ts' import { TeamJournal } from './journal.ts' -import { TeamRuntimeLifecycle } from './lifecycle.ts' +import { TeamMembers } from './membership.ts' +import type { TeamMembership } from './membership.ts' import { teamProjectionDefinition } from './projection.ts' -import { resolveActiveMember, TeamRoster } from './roster.ts' -import type { TeamMembership } from './roster.ts' import { TeamTaskBoard } from './task-board.ts' import { TeamId, TeamTaskId } from './types.ts' import type { Config, CreateTeamTaskRequest, - SendTeamMessageRequest, - SendTeamMessageResult, - SpawnTeammateRequest, - SpawnTeammateResult, - TeamMemberView, TeamTaskView, TeamWaitResult, UpdateTeamTaskRequest, } from './types.ts' export type * from './types.ts' -export type { TeamMembership } from './roster.ts' +export type { TeamMembership } from './membership.ts' export { TeamId, TeamMessageId, TeamTaskId } from './types.ts' export { TeamError } from './error.ts' @@ -39,10 +30,10 @@ declare module '@deepseek-ai/cordis' { } } -const DEFAULT_MAX_MEMBERS = 16 const DEFAULT_MAX_TASKS = 256 -const DEFAULT_MAX_MESSAGE_BYTES = 65_536 -const DEFAULT_DISPOSAL_TIMEOUT_MS = 5_000 + +/** Configuration keys removed when Team reused subagent creation and messaging. */ +const REMOVED_CONFIG_KEYS = ['maxMembers', 'maxMessageBytes', 'disposalTimeoutMs'] as const /** Validate one positive safe-integer deployment limit. */ function positiveLimit(name: string, value: number): number { @@ -54,100 +45,74 @@ function positiveLimit(name: string, value: number): number { /** Agent Teams service backed by the exact live Lead Session log. */ export class TeamService extends Service { - static inject = ['agents', 'sessions', 'sessionPersistence', 'sessionProjections', 'subagents'] + static inject = ['agents', 'sessions', 'sessionProjections'] static Config: z = z.object({ - maxMembers: z.number().step(1).min(1).default(DEFAULT_MAX_MEMBERS), maxTasks: z.number().step(1).min(1).default(DEFAULT_MAX_TASKS), - maxMessageBytes: z.number().step(1).min(1).default(DEFAULT_MAX_MESSAGE_BYTES), - disposalTimeoutMs: z.number().step(1).min(1).default(DEFAULT_DISPOSAL_TIMEOUT_MS), }) - /** Validated deployment limits used by every Team operation. */ - private readonly config: Required - private readonly activity: TeamActivity - private readonly lifecycle: TeamRuntimeLifecycle private readonly journal: TeamJournal - private readonly roster: TeamRoster + private readonly members: TeamMembers private readonly tasks: TeamTaskBoard constructor(ctx: Context, config: Config = {}) { super(ctx, 'agentTeams') - this.config = { - maxMembers: positiveLimit('maxMembers', config.maxMembers ?? DEFAULT_MAX_MEMBERS), - maxTasks: positiveLimit('maxTasks', config.maxTasks ?? DEFAULT_MAX_TASKS), - maxMessageBytes: positiveLimit('maxMessageBytes', config.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES), - disposalTimeoutMs: positiveLimit( - 'disposalTimeoutMs', - config.disposalTimeoutMs ?? DEFAULT_DISPOSAL_TIMEOUT_MS, - ), + for (const key of REMOVED_CONFIG_KEYS) { + if (Object.hasOwn(config, key)) { + throw new TeamError( + `agent-team: ${key} is no longer supported; teammates are subagents, so subagent limits and inbox delivery apply`, + 'TEAM_INVALID_CONFIG', + ) + } } + const maxTasks = positiveLimit('maxTasks', config.maxTasks ?? DEFAULT_MAX_TASKS) this.activity = new TeamActivity() - this.lifecycle = new TeamRuntimeLifecycle(this.config.disposalTimeoutMs) this.journal = new TeamJournal(ctx, (root) => { this.activity.notify(TeamId(root.id)) }) - this.roster = new TeamRoster(ctx, this.journal, this.lifecycle, this.config.maxMembers) - this.tasks = new TeamTaskBoard(this.journal, this.config.maxTasks) + this.members = new TeamMembers(ctx) + this.tasks = new TeamTaskBoard(this.journal, maxTasks) - ctx.on('agent/created', ({ agent }) => { this.scheduleRecovery(agent) }) - ctx.on('agent/status', ({ agent }) => { - const membership = this.roster.tryMembership(agent) + const notifyMember = ({ agent }: { agent: Agent }): void => { + const membership = this.members.tryMembership(agent) if (membership !== undefined) this.activity.notify(membership.id) - }) + } + ctx.on('agent/status', notifyMember) + ctx.on('agent/inbox/inserted', notifyMember) ctx.effect(() => { const disposeProjection = ctx.root.sessionProjections.register(teamProjectionDefinition) - return async () => { - try { - await this.disposeRuntime() - } finally { - disposeProjection() - } + return () => { + this.activity.close() + disposeProjection() } }, 'agentTeams.runtimeLifecycle()') - for (const agent of ctx.agents.list()) this.scheduleRecovery(agent) } /** * Resolve one exact live Agent's Team role. * @param agent - exact live Agent used as the authority credential. - * @returns its root, Team identity, role, and model-facing name. + * @returns its Lead, Team identity, and role. */ membership(agent: Agent): TeamMembership { - return this.roster.membership(agent) + return this.members.membership(agent) } /** - * List the runtime-enriched roster visible to one Team member. - * @param agent - exact live Team member. - * @returns Lead and teammate rows in creation order. + * Resolve a caller without throwing, used by scoped-tool installation and observers. + * @param agent - candidate exact live Agent. + * @returns Team membership, or undefined for stale identities and children whose parent left the registry. */ - listMembers(agent: Agent): TeamMemberView[] { - return this.roster.list(this.roster.membership(agent)) + tryMembership(agent: Agent): TeamMembership | undefined { + return this.members.tryMembership(agent) } /** - * Create one named, continuable direct child of the Team Lead. - * @param caller - exact live Lead Agent. - * @param request - immutable name, description, prompt, context mode, provider, and cancellation. - * @returns the active roster row. + * Whether another live member of the caller's Team is currently running a turn. + * @param caller - exact live Team member. + * @returns whether a peer can still produce a Team change. */ - async spawnTeammate(caller: Agent, request: SpawnTeammateRequest): Promise { - return await this.roster.spawn(caller, request) - } - - /** - * Steer one peer message into the target inbox or reject the attempt. - * @param caller - exact live sending Team member. - * @param request - target name, content, and cancellation before acceptance. - * @returns accepted inbox identity; acceptance follows normal Agent persistence and does not await model processing. - */ - async sendMessage(caller: Agent, request: SendTeamMessageRequest): Promise { - if (this.lifecycle.disposed) throw new TeamError('Agent Teams service is disposing', 'TEAM_DISPOSED') - return await this.lifecycle.track(this.sendAdmitted(caller, { - ...request, - signal: AbortSignal.any([request.signal, this.lifecycle.signal]), - })) + hasActivePeer(caller: Agent): boolean { + return this.members.hasActivePeer(caller) } /** @@ -157,7 +122,7 @@ export class TeamService extends Service { * @returns the revision-one task view. */ async createTask(caller: Agent, request: CreateTeamTaskRequest): Promise { - return await this.tasks.create(this.roster.membership(caller), request) + return await this.tasks.create(this.members.membership(caller), request) } /** @@ -167,7 +132,7 @@ export class TeamService extends Service { * @returns the latest task value and derived readiness diagnostics. */ getTask(caller: Agent, id: TeamTaskId): TeamTaskView { - return this.tasks.get(this.roster.membership(caller), id) + return this.tasks.get(this.members.membership(caller), id) } /** @@ -176,7 +141,7 @@ export class TeamService extends Service { * @returns detached current task views. */ listTasks(caller: Agent): TeamTaskView[] { - return this.tasks.list(this.roster.membership(caller)) + return this.tasks.list(this.members.membership(caller)) } /** @@ -186,94 +151,20 @@ export class TeamService extends Service { * @returns the committed next task revision. */ async updateTask(caller: Agent, request: UpdateTeamTaskRequest): Promise { - return await this.tasks.update(caller, this.roster.membership(caller), request) + return await this.tasks.update(caller, this.members.membership(caller), request) } /** - * Wait for the next Team-domain or member-status change. + * Wait for the next task change, member status change, or message a member's inbox accepts. * @param caller - exact live Team member waiting for activity. * @param timeoutMs - bounded wait duration from ten seconds through one hour. * @param signal - caller cancellation for the wait only. * @returns one observed change or a timeout result. */ async waitForChange(caller: Agent, timeoutMs: number, signal: AbortSignal): Promise { - const membership = this.roster.membership(caller) + const membership = this.members.membership(caller) return await this.activity.wait(membership.id, timeoutMs, signal) } - - /** - * Interrupt one live teammate turn without clearing its pending inbox. - * @param caller - exact live Lead Agent. - * @param targetName - durable teammate name. - * @returns the target status sampled before cancellation. - */ - interrupt(caller: Agent, targetName: string): { previousStatus: 'running' | 'inactive' } { - return this.roster.interrupt(caller, targetName) - } - - /** - * Resolve a caller without throwing, used by scoped-tool installation and observers. - * @param agent - candidate exact live Agent. - * @returns Team membership, or undefined for non-Team subagents and stale identities. - */ - tryMembership(agent: Agent): TeamMembership | undefined { - return this.roster.tryMembership(agent) - } - - /** Deliver an admitted send using the Lead's authority and the actual sender's identity. */ - private async sendAdmitted(caller: Agent, request: SendTeamMessageRequest): Promise { - const membership = this.roster.membership(caller) - request.signal.throwIfAborted() - const { root } = membership - const target = resolveActiveMember(root, this.journal.state(root), request.target) - if (target.id === caller.id) throw new TeamError('a Team member cannot message itself', 'TEAM_SELF_MESSAGE') - const content = [ - { type: 'text' as const, text: `Team message from ${membership.name}:` }, - ...structuredClone(request.content), - ] - if (Buffer.byteLength(JSON.stringify(content), 'utf8') > this.config.maxMessageBytes) { - throw new TeamError(`team message exceeds ${this.config.maxMessageBytes} bytes`, 'TEAM_MESSAGE_TOO_LARGE') - } - const source = { kind: 'agent-message' as const, form: 'relay' as const, senderSessionId: caller.id } - let messageId - if (target.id === root.id) { - const input = createUserMessage({ content, source }) - root.steer(input) - messageId = input.id - } else { - messageId = await steerHostSubagentPrompt(this.ctx.subagents, root, target.id, content, source, request.signal) - } - this.activity.notify(membership.id) - return { messageId } - } - - /** Queue one contained provisioning reconciliation after publication has unwound. */ - private scheduleRecovery(agent: Agent): void { - queueMicrotask(() => { - if (this.lifecycle.disposed) return - void this.roster.recoverFor(agent, this.lifecycle.signal).catch((error: unknown) => { - if (this.lifecycle.disposed) return - this.ctx.logger.warn(`Agent Teams recovery for "${agent.id}" failed: ${errorMessage(error)}`) - }) - }) - } - - /** Stop Team-owned live branches and release every waiter before service disposal completes. */ - private async disposeRuntime(): Promise { - this.lifecycle.close() - this.activity.close() - - const failures: unknown[] = [] - await this.lifecycle.settle(this.lifecycle.pending(), failures) - for (const [root, childIds] of this.roster.liveChildrenByRoot()) { - try { - await this.roster.stopTeammates(root, childIds) - } catch (error: unknown) { - failures.push(error) - } - } - if (failures.length > 0) throw new AggregateError(failures, 'Agent Teams runtime disposal failed') - } } export default TeamService diff --git a/packages/experimental/agent-team/src/lifecycle.ts b/packages/experimental/agent-team/src/lifecycle.ts deleted file mode 100644 index 079d627168..0000000000 --- a/packages/experimental/agent-team/src/lifecycle.ts +++ /dev/null @@ -1,107 +0,0 @@ -/** Shared admission cutoff and bounded settlement for the Team runtime. */ - -import { TeamError } from './error.ts' - -/** Owns the single Team runtime cancellation fact and disposal timeout. */ -export class TeamRuntimeLifecycle { - private readonly operations = new Set>() - private readonly controller = new AbortController() - - /** - * @param disposalTimeoutMs - maximum wait for one disposal settlement operation. - */ - constructor(private readonly disposalTimeoutMs: number) {} - - /** Signal aborted exactly when Team runtime admission closes. */ - get signal(): AbortSignal { - return this.controller.signal - } - - /** Whether Team runtime admission is closed. */ - get disposed(): boolean { - return this.signal.aborted - } - - /** The exact cancellation reason used to distinguish expected disposal rejection. */ - get reason(): unknown { - const reason: unknown = this.signal.reason - return reason - } - - /** Whether a rejection is the runtime cancellation, directly or through an Error cause chain. */ - private isCancellation(reason: unknown): boolean { - const seen = new Set() - let current = reason - while (!seen.has(current)) { - if (this.disposed && current === this.reason) return true - if (this.disposed && current instanceof TeamError && current.code === 'TEAM_DISPOSED') return true - if (!(current instanceof Error)) return false - seen.add(current) - current = current.cause - } - return false - } - - /** Close Team runtime admission and cancel admitted interruptible work. */ - close(): void { - this.controller.abort(new TeamError('Agent Teams service disposed', 'TEAM_DISPOSED')) - } - - /** - * Track an admitted send or complete creation transaction until it settles. - * @param operation - asynchronous work owned by this runtime. - * @returns the same operation. - */ - track(operation: Promise): Promise { - this.operations.add(operation) - void operation.then(() => { this.operations.delete(operation) }, () => { this.operations.delete(operation) }) - return operation - } - - /** - * Capture outstanding operations after closing admission. - * @returns admitted work still awaiting settlement. - */ - pending(): readonly Promise[] { - return [...this.operations] - } - - /** - * Await admitted operations and retain failures other than runtime cancellation. - * @param operations - admitted operations captured after the admission cutoff. - * @param failures - aggregate destination for unexpected rejection or timeout. - */ - async settle(operations: readonly Promise[], failures: unknown[]): Promise { - if (operations.length === 0) return - try { - const outcomes = await this.withTimeout(Promise.allSettled(operations)) - for (const outcome of outcomes) { - if (outcome.status === 'rejected' && !this.isCancellation(outcome.reason)) failures.push(outcome.reason) - } - } catch (error: unknown) { - failures.push(error) - } - } - - /** - * Bound one runtime settlement operation. - * @param operation - settlement that may otherwise block HMR or process shutdown. - * @returns the operation result. - */ - async withTimeout(operation: Promise): Promise { - let timer!: ReturnType - const timeout = new Promise((_resolve, reject) => { - timer = setTimeout(() => { - reject(new TeamError( - `Agent Teams runtime disposal exceeded ${this.disposalTimeoutMs}ms`, - 'TEAM_DISPOSAL_TIMEOUT', - )) - }, this.disposalTimeoutMs) - }) - try { - return await Promise.race([operation, timeout]) - } finally { - clearTimeout(timer) - } - } -} diff --git a/packages/experimental/agent-team/src/membership.ts b/packages/experimental/agent-team/src/membership.ts new file mode 100644 index 0000000000..e90f0fc6b4 --- /dev/null +++ b/packages/experimental/agent-team/src/membership.ts @@ -0,0 +1,99 @@ +/** Team membership derived from live subagent lineage and durable Session ids. */ + +import type { Context } from '@deepseek-ai/cordis' +import type { Agent } from '@deepseek-ai/dsh-agent' +import type { SessionId } from '@deepseek-ai/dsh-session' +import type {} from '@deepseek-ai/dsh-subagent' +import { TeamError } from './error.ts' +import type { TeamState } from './projection.ts' +import { TeamId } from './types.ts' + +/** Caller identity inside one implicit Team. */ +export interface TeamMembership { + /** Exact live Lead: the root of the caller's live subagent lineage. */ + readonly root: Agent + readonly id: TeamId + readonly role: 'lead' | 'teammate' +} + +/** + * Resolve one member Session id for task assignment. + * @param root - exact live Team Lead. + * @param state - current Team state. + * @param target - member Session id. + * @returns the member's durable id. + */ +export function resolveMember(root: Agent, state: TeamState, target: SessionId): SessionId { + if (target === root.id) return root.id + const child = state.children.find(candidate => candidate.id === target) + if (child !== undefined) return child.id + const member = state.members.find(candidate => candidate.phase === 'active' + && candidate.id === target) + if (member === undefined) throw new TeamError(`teammate "${target}" not found`, 'TEAM_MEMBER_NOT_FOUND') + return member.id +} + +/** Resolves Team membership from the live Agent registry. */ +export class TeamMembers { + /** + * @param ctx - Team service context with Agent and projection services. + */ + constructor( + private readonly ctx: Context, + ) {} + + /** + * Resolve one exact live Agent's Team role. + * @param agent - exact live Agent used as the authority credential. + * @returns its Lead, Team identity, and role. + */ + membership(agent: Agent): TeamMembership { + const membership = this.tryMembership(agent) + if (membership === undefined) { + throw new TeamError(`agent "${agent.id}" is not a member of an active Agent Team`, 'TEAM_NOT_MEMBER') + } + return membership + } + + /** + * Resolve a caller without throwing for scoped installation and lifecycle observers. + * Every root Agent leads its own Team; every subagent below it, at any + * depth, is a teammate of that root while its lineage remains live. + * @param agent - candidate exact live Agent. + * @returns Team membership, or undefined for stale identities and children whose parent left the registry. + */ + tryMembership(agent: Agent): TeamMembership | undefined { + if (this.ctx.agents.get(agent.id) !== agent) return undefined + let root = agent + const seen = new Set() + while (this.isSubagent(root)) { + seen.add(root.id) + const parentId = root.session.header.parentSession + const parent = parentId === undefined ? undefined : this.ctx.agents.get(parentId) + // A continuation can briefly outlive its parent during child-first teardown; + // do not reinterpret that durable child as a new implicit root Team. + if (parent === undefined || seen.has(parent.id)) return undefined + root = parent + } + if (root === agent) return { root, id: TeamId(root.id), role: 'lead' } + return { root, id: TeamId(root.id), role: 'teammate' } + } + + /** + * Whether another live member of the caller's Team is currently running a turn. + * @param caller - exact live Team member. + * @returns whether a peer can still produce a Team change. + */ + hasActivePeer(caller: Agent): boolean { + const { id } = this.membership(caller) + return this.ctx.agents.list().some(agent => agent !== caller + && agent.status === 'running' + && this.tryMembership(agent)?.id === id) + } + + /** Whether a Session's own log identifies it as a subagent child. */ + private isSubagent(agent: Agent): boolean { + const identity = this.ctx.sessionProjections.stateOf(agent.session, 'subagent')?.identity + return identity !== undefined && agent.session.isOwnSeq(identity.seq) + } +} diff --git a/packages/experimental/agent-team/src/persisted.ts b/packages/experimental/agent-team/src/persisted.ts deleted file mode 100644 index e7fcb20441..0000000000 --- a/packages/experimental/agent-team/src/persisted.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** Short-lived read-handle access to persisted Team member Sessions. */ - -import type { SessionEvent, SessionHeader, SessionId , SessionLogOffset } from '@deepseek-ai/dsh-session' -import type { SessionPersistence } from '@deepseek-ai/dsh-session-persistence' - -/** One persisted Session's detached header and complete committed event log. */ -export interface PersistedSessionView { - /** Exact fork-inherited event count paired with `header`. */ - readonly inheritedEventCount: SessionLogOffset - readonly header: SessionHeader - readonly events: readonly SessionEvent[] -} - -/** - * Read one stored session's header and complete event log through a - * short-lived read handle, closing the handle before returning. - * @param persistence - the durable session store. - * @param id - the stored session to read. - * @param signal - cancellation observed by open and read. - * @returns the stored header and every committed event. - */ -export async function readPersistedSession( - persistence: SessionPersistence, - id: SessionId, - signal: AbortSignal, -): Promise { - const handle = await persistence.open(id, 'read', { signal }) - try { - const { events } = await handle.read(0, undefined, { signal }) - return { header: handle.header, inheritedEventCount: handle.inheritedEventCount, events } - } finally { - await handle.close() - } -} diff --git a/packages/experimental/agent-team/src/projection.ts b/packages/experimental/agent-team/src/projection.ts index bc9239d3d8..579d73a4bc 100644 --- a/packages/experimental/agent-team/src/projection.ts +++ b/packages/experimental/agent-team/src/projection.ts @@ -3,9 +3,12 @@ import { z } from 'zod' import { brandString } from '@deepseek-ai/dsh-brand' import type { ContentBlock } from '@deepseek-ai/dsh-llm' +import { SessionLogOffset } from '@deepseek-ai/dsh-session' import type { SessionEvent, SessionEventMap, SessionId } from '@deepseek-ai/dsh-session' import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection' +import type {} from '@deepseek-ai/dsh-subagent' import type { + TeamChild, TeamId, TeamMemberProjection, TeamMemberSnapshot, @@ -116,6 +119,19 @@ const teamMessageQueuedEventSchema = z.object({ message: teamMessageSnapshotSchema, }).strict() as z.ZodType +const teamChildSchema = z.object({ + id: sessionIdSchema, + description: z.string(), +}).strict() as z.ZodType + +/** Catalog payloads that identify continuable children of the Lead. */ +const continuableCatalogEventSchema = z.object({ + version: z.union([z.literal(0), z.literal(1)]), + childId: sessionIdSchema, + mode: z.literal('continuable'), + label: z.string(), +}).loose() + const teamMessageDeliveredEventSchema = z.object({ version: z.literal(2), teamId: teamIdSchema, @@ -130,6 +146,11 @@ const teamMessageDeliveredEventSchema = z.object({ */ export interface TeamState { readonly id: TeamId + /** Fork-inherited prefix length; catalog facts before it belong to another Lead. */ + readonly inheritedEventCount: SessionLogOffset + /** Continuable direct children of the Lead, in catalog order. */ + readonly children: readonly TeamChild[] + /** Historical roster records written by earlier Team builds. */ readonly members: readonly TeamMemberSnapshot[] readonly tasks: readonly TeamTaskSnapshot[] readonly messages: readonly TeamMessageSnapshot[] @@ -140,11 +161,14 @@ export interface TeamState { /** * Construct empty state for one Team identity. * @param rootId - root Session identity. + * @param inheritedEventCount - fork-inherited prefix length of the root Session. * @returns empty Team state. */ -export function emptyTeamState(rootId: SessionId): TeamProjectionState { +export function emptyTeamState(rootId: SessionId, inheritedEventCount: SessionLogOffset): TeamProjectionState { return { id: toTeamId(rootId), + inheritedEventCount, + children: [], members: [], tasks: [], messages: [], @@ -166,6 +190,8 @@ declare module '@deepseek-ai/dsh-session-projection/types' { const teamProjectionEntrySchema = z.object({ id: teamIdSchema, + inheritedEventCount: nonNegativeSafeInteger.transform(value => SessionLogOffset(value)), + children: z.array(teamChildSchema), members: z.array(teamMemberSnapshotSchema), tasks: z.array(teamTaskSnapshotSchema), messages: z.array(teamMessageSnapshotSchema), @@ -197,7 +223,7 @@ export function isTeamEvent(event: SessionEvent): event is TeamSessionEvent { } /** Decode one persisted Team value and retain the schema failure as its cause. */ -function parsePersisted(type: TeamEventType, schema: z.ZodType, value: unknown): T { +function parsePersisted(type: TeamEventType | 'subagent/catalog', schema: z.ZodType, value: unknown): T { try { return schema.parse(value) } catch (error: unknown) { @@ -222,8 +248,23 @@ function parseCurrentTeamEvent(event: TeamSessionEvent): TeamSessionEvent { } } +/** Add one continuable catalog child of the projected Lead, excluding fork-inherited facts. */ +function applyCatalogEvent(state: TeamProjectionState, event: SessionEvent<'subagent/catalog'>): TeamProjectionState { + if (event.seq < state.inheritedEventCount || event.data.mode !== 'continuable') return state + const child = parsePersisted(event.type, continuableCatalogEventSchema, event.data) + return { ...state, children: [...state.children, { id: child.childId, description: child.label }] } +} + function applyProjectionEvent(state: TeamProjectionState, event: SessionEvent): TeamProjectionState { if (state.failure !== undefined) return state + if (event.type === 'subagent/catalog') { + try { + return applyCatalogEvent(state, event) + } catch (error: unknown) { + /* v8 ignore next -- the catalog transition throws Error instances. */ + return { ...state, failure: error instanceof Error ? error.message : String(error) } + } + } if (!isTeamEvent(event)) return state try { const selector = parsePersisted(event.type, teamEventSelectorSchema, event.data) @@ -311,7 +352,7 @@ function applyCurrentTeamEvent(state: TeamProjectionState, event: TeamSessionEve const teamMemberProjectionSchema = z.object({ id: sessionIdSchema, - name: z.string(), + description: z.string().optional(), role: z.enum(['lead', 'teammate']), phase: z.enum(['provisioning', 'active', 'failed']), error: z.string().optional(), @@ -325,7 +366,7 @@ const teamTaskViewSchema = z.object({ status: z.enum(['pending', 'in_progress', 'completed', 'deleted']), blockedBy: z.array(teamTaskIdSchema), writeScopes: z.array(z.string()), - ownerName: z.string().optional(), + ownerId: sessionIdSchema.optional(), ready: z.boolean(), writeScopeWarnings: z.array(z.string()), }).strict() as z.ZodType @@ -336,16 +377,23 @@ const teamProjectionSchema = z.object({ failure: z.string().optional(), }).strict() as z.ZodType -/** Client views keyed by the member and task collections they were derived from. */ -const teamProjectionViews = new WeakMap>() +/** Client views keyed by the child, member, and task collections they were derived from. */ +const teamProjectionViews = new WeakMap< + readonly TeamChild[], + WeakMap> +>() function buildTeamProjection(state: TeamProjectionState): TeamProjection { const rootId = brandString(state.id) - const members: TeamMemberProjection[] = [{ id: rootId, name: 'lead', role: 'lead', phase: 'active' }] + const members: TeamMemberProjection[] = [{ id: rootId, role: 'lead', phase: 'active' }] + for (const child of state.children) { + members.push({ id: child.id, description: child.description, role: 'teammate', phase: 'active' }) + } for (const member of state.members) { + if (state.children.some(child => child.id === member.id)) continue members.push({ id: member.id, - name: member.name, + description: member.description, role: 'teammate', phase: member.phase, ...member.error === undefined ? {} : { error: member.error }, @@ -370,10 +418,15 @@ function buildTeamProjection(state: TeamProjectionState): TeamProjection { */ export function teamProjectionView(state: TeamProjectionState): TeamProjection { if (state.failure !== undefined) return buildTeamProjection(state) - let byTasks = teamProjectionViews.get(state.members) + let byMembers = teamProjectionViews.get(state.children) + if (byMembers === undefined) { + byMembers = new WeakMap() + teamProjectionViews.set(state.children, byMembers) + } + let byTasks = byMembers.get(state.members) if (byTasks === undefined) { byTasks = new WeakMap() - teamProjectionViews.set(state.members, byTasks) + byMembers.set(state.members, byTasks) } let view = byTasks.get(state.tasks) if (view === undefined) { @@ -383,12 +436,16 @@ export function teamProjectionView(state: TeamProjectionState): TeamProjection { return view } -/** Team projection selected by the projected Session identity; the wire view carries durable roster and task state only. */ +/** + * Team projection selected by the projected Session identity; the wire view + * carries the roster and task board. Named children come from the Lead's own + * `subagent/catalog` facts, so the roster has no Team-written copy. + */ export const teamProjectionDefinition = { key: 'agentTeam', - stateVersion: 4, + stateVersion: 5, stateSchema: teamProjectionEntrySchema, - init: header => emptyTeamState(header.id), + init: (header, inheritedEventCount) => emptyTeamState(header.id, inheritedEventCount), apply: applyProjectionEvent, wire: { viewSchema: teamProjectionSchema, view: teamProjectionView }, } satisfies ProjectionDefinition<'agentTeam', TeamProjectionState> diff --git a/packages/experimental/agent-team/src/roster.ts b/packages/experimental/agent-team/src/roster.ts deleted file mode 100644 index 7b1047a0a2..0000000000 --- a/packages/experimental/agent-team/src/roster.ts +++ /dev/null @@ -1,477 +0,0 @@ -/** Team membership, continuable-child provisioning, and roster-owned teardown. */ - -import { randomUUID } from 'node:crypto' -import type { Context } from '@deepseek-ai/cordis' -import { brandString } from '@deepseek-ai/dsh-brand' -import type { Agent } from '@deepseek-ai/dsh-agent' -import type { MessageId } from '@deepseek-ai/dsh-llm' -import type { SessionId } from '@deepseek-ai/dsh-session' -import { foldSubagentDescriptor } from '@deepseek-ai/dsh-subagent' -import { errorMessage, TeamError } from './error.ts' -import type { TeamJournal } from './journal.ts' -import type { TeamRuntimeLifecycle } from './lifecycle.ts' -import { readPersistedSession } from './persisted.ts' -import type { TeamState } from './projection.ts' -import { messageAccepted } from './session-message.ts' -import { TeamId } from './types.ts' -import type { - SpawnTeammateRequest, - SpawnTeammateResult, - TeamMemberSnapshot, - TeamMemberView, -} from './types.ts' -import { requiredText } from './validation.ts' - -const MEMBER_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u - -/** Caller identity inside one implicit Team. */ -export interface TeamMembership { - readonly root: Agent - readonly id: TeamId - readonly role: 'lead' | 'teammate' - readonly name: string -} - -/** - * Resolve one active Team member by model-facing name, including the Lead pseudo-row. - * @param root - exact live Team Lead. - * @param state - current Team state. - * @param rawName - candidate member name. - * @returns resolved durable id and normalized name. - */ -export function resolveActiveMember( - root: Agent, - state: TeamState, - rawName: string, -): { id: SessionId; name: string } { - const name = rawName.trim() - if (name === 'lead') return { id: root.id, name } - const member = state.members.find(candidate => candidate.name === name) - if (member === undefined || member.phase !== 'active') { - throw new TeamError(`active teammate "${name}" not found`, 'TEAM_MEMBER_NOT_FOUND') - } - return { id: member.id, name } -} - -/** Owns Team identities and the lifecycle of rostered continuable children. */ -export class TeamRoster { - /** - * @param ctx - Team service context with Agent, Session, persistence, and subagent services. - * @param journal - authoritative Lead-log transaction owner. - * @param lifecycle - shared Team runtime admission cutoff. - * @param maxMembers - maximum immutable roster entries per Team. - */ - constructor( - private readonly ctx: Context, - private readonly journal: TeamJournal, - private readonly lifecycle: TeamRuntimeLifecycle, - private readonly maxMembers: number, - ) {} - - /** - * Resolve one exact live Agent's Team role. - * @param agent - exact live Agent used as the authority credential. - * @returns its root, Team identity, role, and model-facing name. - */ - membership(agent: Agent): TeamMembership { - const membership = this.tryMembership(agent) - if (membership === undefined) { - throw new TeamError(`agent "${agent.id}" is not a member of an active Agent Team`, 'TEAM_NOT_MEMBER') - } - return membership - } - - /** - * Resolve a caller without throwing for scoped installation and lifecycle observers. - * @param agent - candidate exact live Agent. - * @returns Team membership, or undefined for non-Team subagents and stale identities. - */ - tryMembership(agent: Agent): TeamMembership | undefined { - if (this.ctx.agents.get(agent.id) !== agent) return undefined - try { - const parentId = agent.session.header.parentSession - if (parentId !== undefined) { - const root = this.ctx.agents.get(parentId) - if (root !== undefined) { - const member = this.journal.state(root).members.find(candidate => candidate.id === agent.id) - if (member?.phase === 'active' || member?.phase === 'provisioning') { - return { root, id: TeamId(root.id), role: 'teammate', name: member.name } - } - // A direct child outside the durable roster is not a teammate. Ordinary - // host forks are independent roots; subagent descriptors distinguish - // provider-owned workers that must not receive a nested Team identity. - if (this.subagentDescriptor(agent)) return undefined - return { root: agent, id: TeamId(agent.id), role: 'lead', name: 'lead' } - } - } - // A continuation can briefly outlive its parent during child-first teardown. - // Do not reinterpret that durable child as a new implicit root Team. A host- - // resumed ordinary fork has no descriptor in its own suffix and remains a - // valid new root whose inherited Team records stay outside its projected Team state. - if (this.subagentDescriptor(agent)) return undefined - return { root: agent, id: TeamId(agent.id), role: 'lead', name: 'lead' } - } catch { - // This method is used by lifecycle observers and teardown discovery. A - // malformed durable stream is surfaced by authoritative Team operations; - // the non-throwing probe must not veto unrelated Agent lifecycle edges. - return undefined - } - } - - /** - * List the runtime-enriched roster visible to one Team member. - * @param membership - exact caller membership resolved by this roster. - * @returns Lead and teammate rows in creation order. - */ - list(membership: TeamMembership): TeamMemberView[] { - const { root } = membership - const state = this.journal.state(root) - const result: TeamMemberView[] = [{ - id: root.id, - name: 'lead', - role: 'lead', - status: availability(root), - ...root.options.model === undefined ? {} : { model: root.options.model }, - diagnostics: [], - }] - for (const member of state.members) { - const live = this.ctx.agents.get(member.id) - const model = live?.options.model ?? root.options.model - result.push({ - id: member.id, - name: member.name, - role: 'teammate', - status: member.phase === 'failed' - ? 'failed' - : member.phase === 'provisioning' - ? 'provisioning' - : availability(live), - description: member.description, - provider: member.provider, - context: member.context, - ...model === undefined ? {} : { model }, - diagnostics: member.error === undefined ? [] : [member.error], - }) - } - return result - } - - /** - * Create one named, continuable direct child of the Team Lead. - * @param caller - exact live Lead Agent. - * @param request - immutable name, description, prompt, context mode, provider, and cancellation. - * @returns the active roster row. - */ - async spawn(caller: Agent, request: SpawnTeammateRequest): Promise { - if (this.lifecycle.disposed) throw new TeamError('Agent Teams service is disposing', 'TEAM_DISPOSED') - return await this.lifecycle.track(this.spawnAdmitted(caller, request)) - } - - /** - * Reconcile provisioning state when one Team member Session starts. - * @param agent - newly started exact live Agent. - * @param signal - shared runtime cancellation. - */ - async recoverFor(agent: Agent, signal: AbortSignal): Promise { - signal.throwIfAborted() - const membership = this.tryMembership(agent) - if (membership?.role === 'lead') await this.reconcileProvisioning(membership.root, signal) - } - - /** - * Interrupt one live teammate turn without clearing its pending inbox. - * @param caller - exact live Lead Agent. - * @param targetName - durable teammate name. - * @returns the target status sampled before cancellation. - */ - interrupt(caller: Agent, targetName: string): { previousStatus: 'running' | 'inactive' } { - const membership = this.membership(caller) - if (membership.role !== 'lead') throw new TeamError('only the Team Lead can interrupt teammates', 'TEAM_LEAD_REQUIRED') - const state = this.journal.state(membership.root) - const target = resolveActiveMember(membership.root, state, targetName) - if (target.id === membership.root.id) throw new TeamError('the Team Lead cannot interrupt itself', 'TEAM_INVALID_TARGET') - const live = this.ctx.agents.get(target.id) - if (live === undefined) return { previousStatus: 'inactive' } - const previousStatus = availability(live) - this.ctx.subagents.interrupt(target.id, { kind: 'ancestor', agent: caller }) - return { previousStatus } - } - - /** - * Group exact live roster children by their current Lead for runtime teardown. - * @returns each live Lead and the roster child ids currently in the Agent registry. - */ - liveChildrenByRoot(): Map { - const teams = new Map() - for (const agent of this.ctx.agents.list()) { - const rootId = agent.session.header.parentSession - if (rootId === undefined) continue - const root = this.ctx.agents.get(rootId) - if (root === undefined - || !this.journal.state(root).members.some(member => member.id === agent.id)) continue - const children = teams.get(root) ?? [] - children.push(agent.id) - teams.set(root, children) - } - return teams - } - - /** - * Release exact teammate Activations through the continuation lifecycle owner. - * @param root - exact live Team Lead authorizing release. - * @param childIds - selected roster child ids. - */ - async stopTeammates(root: Agent, childIds: readonly SessionId[]): Promise { - await this.lifecycle.withTimeout(this.ctx.subagents.drainChildren(root, childIds)) - } - - /** Perform one creation admitted before the Team runtime disposal cutoff. */ - private async spawnAdmitted( - caller: Agent, - request: SpawnTeammateRequest, - ): Promise { - const membership = this.membership(caller) - if (membership.role !== 'lead') { - throw new TeamError('only the Team Lead can create teammates', 'TEAM_LEAD_REQUIRED') - } - const signal = AbortSignal.any([request.signal, this.lifecycle.signal]) - signal.throwIfAborted() - const root = membership.root - const name = this.memberName(request.name) - const description = requiredText(request.description, 'description', 200) - const childId = brandString(randomUUID()) - const member: TeamMemberSnapshot = { - id: childId, - name, - description, - provider: requiredText(request.provider, 'provider', 200), - context: request.context, - phase: 'provisioning', - } - - await this.journal.transact(root.id, async () => { - const state = this.journal.state(root) - if (state.members.some(member => member.name === name)) { - throw new TeamError(`teammate name "${name}" was already used in this Team`, 'TEAM_MEMBER_NAME_TAKEN') - } - if (state.members.length >= this.maxMembers) { - throw new TeamError(`Team member limit ${this.maxMembers} reached`, 'TEAM_MEMBER_LIMIT') - } - await this.journal.appendAndFlush(root, 'team/member', { version: 2, teamId: TeamId(root.id), member }) - }) - - try { - const started = await this.ctx.subagents.startActivation({ - delivery: 'parent', - childId, - provider: request.provider, - label: description, - request: { - prompt: request.prompt, - parent: root, - }, - signal, - }) - await this.checkpointInitialPrompt(childId, started.messageId, signal) - } catch (error: unknown) { - const failed: TeamMemberSnapshot = { - ...member, - phase: 'failed', - error: errorMessage(error), - } - try { - const phase = await this.settleProvisioning(root, failed) - await this.stopTeammates(root, [childId]) - if (phase === 'active') { - throw new TeamError( - `teammate "${name}" became active while its creator reported failure`, - 'TEAM_PROVISIONING_CONFLICT', - { cause: error }, - ) - } - } catch (recordError: unknown) { - throw new AggregateError([error, recordError], 'teammate creation and durable failure recording both failed') - } - throw error - } - const active = { - ...member, - phase: 'active' as const, - } satisfies TeamMemberSnapshot - // Once the continuation accepted its first prompt, it is a real child. If - // this checkpoint fails, keep the in-memory active edge instead of inventing - // an impossible active -> failed transition; restart reconciliation covers - // the provisioning-only durable prefix. - const settledPhase = await this.settleProvisioning(root, active) - if (settledPhase === 'failed') { - const conflict = new TeamError( - `teammate "${name}" was reconciled as failed while creation was in progress`, - 'TEAM_PROVISIONING_CONFLICT', - ) - try { - await this.stopTeammates(root, [childId]) - } catch (cleanupError: unknown) { - /* v8 ignore next -- requires the independently tested HMR settlement conflict and cleanup failure together. */ - throw new AggregateError([conflict, cleanupError], 'provisioning conflict cleanup failed') - } - throw conflict - } - return { member: this.memberView(active) } - } - - /** Flush the accepted initial inbox item before the Lead can commit `active`. */ - private async checkpointInitialPrompt( - childId: SessionId, - messageId: MessageId, - signal: AbortSignal, - ): Promise { - while (true) { - signal.throwIfAborted() - const session = this.ctx.sessions.get(childId) - if (session === undefined) { - const stored = await readPersistedSession(this.ctx.sessionPersistence, childId, signal) - const suffix = stored.events.slice(stored.inheritedEventCount) - if (messageAccepted(suffix, message => message.id === messageId)) return - throw new TeamError( - `teammate "${childId}" initial prompt was not durably accepted`, - 'TEAM_PROVISIONING_CONFLICT', - ) - } - - const progress = Promise.withResolvers() - // Abort can win while the durability flush is still pending; mark the - // later-awaited rejection handled without changing its eventual result. - void progress.promise.catch(() => undefined) - const stopEvent = this.ctx.on('session/event', (candidate) => { - if (candidate === session) progress.resolve() - }) - const stopDisposed = this.ctx.on('session/disposed', (candidate) => { - if (candidate === session) progress.resolve() - }) - const onAbort = (): void => { - const reason: unknown = signal.reason - progress.reject(reason instanceof Error - ? reason - : new TeamError(`teammate creation aborted: ${errorMessage(reason)}`, 'TEAM_DISPOSED')) - } - signal.addEventListener('abort', onAbort, { once: true }) - try { - signal.throwIfAborted() - await this.ctx.sessions.flush(session) - // oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred. - const suffix = session.snapshotEvents(session.inheritedEventCount) - if (messageAccepted(suffix, message => message.id === messageId)) return - if (this.ctx.sessions.get(childId) !== session) continue - await progress.promise - } finally { - signal.removeEventListener('abort', onAbort) - stopDisposed() - stopEvent() - } - } - } - - /** Settle provisioning-only members from their independently durable child Sessions. */ - private async reconcileProvisioning(root: Agent, signal: AbortSignal): Promise { - const provisioning = this.journal.state(root).members.filter(member => member.phase === 'provisioning') - for (const member of provisioning) { - signal.throwIfAborted() - // A live child means creation is still completing in this process. Its - // creator owns the terminal member edge. - if (this.ctx.agents.get(member.id) !== undefined) continue - let phase: 'active' | 'failed' = 'failed' - let failure = 'provisioning did not leave a resumable child Session' - try { - const loaded = await readPersistedSession(this.ctx.sessionPersistence, member.id, signal) - const suffix = loaded.events.slice(loaded.inheritedEventCount) - const descriptor = foldSubagentDescriptor(suffix) - const acceptedInitialPrompt = messageAccepted(suffix, message => message.source.kind === 'user') - if (loaded.header.parentSession === root.id - && descriptor?.mode === 'continuable' - && descriptor.provider === member.provider - && acceptedInitialPrompt) { - phase = 'active' - } else { - failure = 'persisted child Session does not match the provisioned continuation' - } - } catch (error: unknown) { - failure = `child Session recovery failed: ${errorMessage(error)}` - } - signal.throwIfAborted() - await this.journal.transact(root.id, async () => { - signal.throwIfAborted() - const current = this.journal.state(root).members.find(candidate => candidate.id === member.id) - if (current?.phase !== 'provisioning') return - const settled: TeamMemberSnapshot = { - ...current, - phase, - ...phase === 'failed' ? { error: failure } : {}, - } - await this.journal.appendAndFlush(root, 'team/member', { - version: 2, - teamId: TeamId(root.id), - member: settled, - }) - }) - } - } - - /** Build one runtime member row after successful creation. */ - private memberView(member: TeamMemberSnapshot & { readonly phase: 'active' }): TeamMemberView { - const live = this.ctx.agents.get(member.id) - return { - id: member.id, - name: member.name, - role: 'teammate', - status: availability(live), - description: member.description, - provider: member.provider, - context: member.context, - ...live?.options.model === undefined ? {} : { model: live.options.model }, - diagnostics: [], - } - } - - /** Validate a never-reused model-facing teammate name. */ - private memberName(value: string): string { - if (!MEMBER_NAME.test(value) || value.length > 64 || value === 'lead') { - throw new TeamError( - 'teammate name must be lower-kebab-case, at most 64 characters, and not "lead"', - 'TEAM_INVALID_MEMBER_NAME', - ) - } - return value - } - - /** Append one terminal provisioning edge unless recovery already settled it. */ - private async settleProvisioning( - root: Agent, - terminal: TeamMemberSnapshot, - ): Promise<'active' | 'failed'> { - return this.journal.transact(root.id, async () => { - const current = this.journal.state(root).members.find(member => member.id === terminal.id) - /* v8 ignore next 3 -- the append-only provisioning event is committed by this operation before settlement. */ - if (current === undefined) { - throw new TeamError(`provisioned teammate "${terminal.id}" disappeared`, 'TEAM_PROVISIONING_CONFLICT') - } - if (current.phase !== 'provisioning') return current.phase - await this.journal.appendAndFlush(root, 'team/member', { - version: 2, - teamId: TeamId(root.id), - member: terminal, - }) - return terminal.phase === 'active' ? 'active' : 'failed' - }) - } - - /** Whether a Session's own suffix identifies a provider-owned subagent child. */ - private subagentDescriptor(agent: Agent): boolean { - // oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred. - return foldSubagentDescriptor(agent.session.snapshotEvents(agent.session.inheritedEventCount)) !== undefined - } -} - -/** Turn availability is independent of whether the Agent is loaded. */ -function availability(agent: Agent | undefined): 'running' | 'inactive' { - return agent?.status === 'running' ? 'running' : 'inactive' -} diff --git a/packages/experimental/agent-team/src/session-message.ts b/packages/experimental/agent-team/src/session-message.ts deleted file mode 100644 index 8ac5245967..0000000000 --- a/packages/experimental/agent-team/src/session-message.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** Durable Session-message acceptance checks for teammate provisioning. */ - -import type { UserMessage } from '@deepseek-ai/dsh-llm' -import type { SessionEvent } from '@deepseek-ai/dsh-session' - -type InboxProjection = Record<'next-turn' | 'next-step', UserMessage[]> - -/** Fold the durable inbox suffix into the messages still awaiting a claim. */ -function pendingInboxMessages(events: readonly SessionEvent[]): UserMessage[] { - const inbox: InboxProjection = { 'next-turn': [], 'next-step': [] } - for (const event of events) { - if (event.type !== 'agent/inbox/spliced') continue - const pending = inbox[event.data.target] - pending.splice(event.data.start, event.data.removedCount ?? 0, ...event.data.inserted) - } - return [...inbox['next-turn'], ...inbox['next-step']] -} - -/** - * Test whether one message is model-visible or still durably pending. - * @param events - one Session's non-inherited event suffix. - * @param predicate - identity check for the accepted message. - * @returns whether history or the current inbox contains a match. - */ -export function messageAccepted( - events: readonly SessionEvent[], - predicate: (message: UserMessage) => boolean, -): boolean { - return events.some(event => event.type === 'user/message' && predicate(event.data)) - || pendingInboxMessages(events).some(predicate) -} diff --git a/packages/experimental/agent-team/src/task-board.ts b/packages/experimental/agent-team/src/task-board.ts index 4b6132526a..fc90c5a1cf 100644 --- a/packages/experimental/agent-team/src/task-board.ts +++ b/packages/experimental/agent-team/src/task-board.ts @@ -1,11 +1,11 @@ /** Shared Team task DAG commands and runtime-enriched views. */ import type { Agent } from '@deepseek-ai/dsh-agent' -import type { TeamMembership } from './roster.ts' +import type { TeamMembership } from './membership.ts' import { TeamError } from './error.ts' import type { TeamJournal } from './journal.ts' import type { TeamState } from './projection.ts' -import { resolveActiveMember } from './roster.ts' +import { resolveMember } from './membership.ts' import { assertTaskGraphCandidate, TeamTaskGraphError } from './task-graph.ts' import type { TeamTaskGraphViolation } from './task-graph.ts' import { TeamId, TeamTaskId } from './types.ts' @@ -37,7 +37,7 @@ export class TeamTaskBoard { /** * Create one unowned pending task in the Team Lead log. - * @param membership - exact caller membership resolved by the Team roster. + * @param membership - exact caller membership. * @param request - task text, blockers, and advisory write scopes. * @returns the revision-one task view. */ @@ -70,7 +70,7 @@ export class TeamTaskBoard { /** * Return one task, including a deleted tombstone. - * @param membership - exact caller membership resolved by the Team roster. + * @param membership - exact caller membership. * @param id - Team-local task identity. * @returns the latest task value and derived readiness diagnostics. */ @@ -84,7 +84,7 @@ export class TeamTaskBoard { /** * List current non-deleted tasks in numeric creation order. - * @param membership - exact caller membership resolved by the Team roster. + * @param membership - exact caller membership. * @returns detached current task views. */ list(membership: TeamMembership): TeamTaskView[] { @@ -177,13 +177,12 @@ export class TeamTaskBoard { 'TEAM_TASK_INVALID_TRANSITION', ) } - if (request.owner === undefined || request.owner.trim().length === 0) { + if (request.owner === undefined) { next = this.withoutOwner({ ...current, status: 'pending' }) break } if (!taskReady(state, current)) throw new TeamError(`team task "${current.id}" is blocked`, 'TEAM_TASK_BLOCKED') - const assignee = resolveActiveMember(root, state, request.owner) - next = { ...current, status: 'in_progress', ownerId: assignee.id } + next = { ...current, status: 'in_progress', ownerId: resolveMember(root, state, request.owner) } break } case 'delete': { diff --git a/packages/experimental/agent-team/src/task-view.ts b/packages/experimental/agent-team/src/task-view.ts index 569ff38261..fbee689617 100644 --- a/packages/experimental/agent-team/src/task-view.ts +++ b/packages/experimental/agent-team/src/task-view.ts @@ -1,7 +1,5 @@ /** Pure task-view derivation shared by the task board and the client projection. */ -import { brandString } from '@deepseek-ai/dsh-brand' -import type { SessionId } from '@deepseek-ai/dsh-session' import type { TeamState } from './projection.ts' import type { TeamTaskSnapshot, TeamTaskView } from './types.ts' @@ -26,20 +24,15 @@ export function taskReady(state: TeamState, task: TeamTaskSnapshot): boolean { } /** - * Derive one task view with owner name, readiness, and advisory write overlaps. + * Derive one task view with owner id, readiness, and advisory write overlaps. * A committing caller may pass its pre-append state because `task` supplies the - * new value explicitly; owner names, blocker readiness, and other task scopes + * new value explicitly; blocker readiness, and other task scopes * do not change when that snapshot is appended. - * @param state - Team state supplying members and sibling tasks. + * @param state - Team state supplying sibling tasks for readiness and overlap checks. * @param task - durable task snapshot to view. * @returns a detached task view. */ export function projectTaskView(state: TeamState, task: TeamTaskSnapshot): TeamTaskView { - const ownerName = task.ownerId === undefined - ? undefined - : task.ownerId === brandString(state.id) - ? 'lead' - : state.members.find(member => member.id === task.ownerId)?.name const warnings = new Set() for (const other of state.tasks) { if (other.id === task.id || other.status !== 'in_progress') continue @@ -55,7 +48,7 @@ export function projectTaskView(state: TeamState, task: TeamTaskSnapshot): TeamT status: task.status, blockedBy: [...task.blockedBy], writeScopes: [...task.writeScopes], - ...ownerName === undefined ? {} : { ownerName }, + ...task.ownerId === undefined ? {} : { ownerId: task.ownerId }, ready: task.status === 'pending' && taskReady(state, task), writeScopeWarnings: [...warnings], } diff --git a/packages/experimental/agent-team/src/types.ts b/packages/experimental/agent-team/src/types.ts index 465f6bbf12..9edb85add2 100644 --- a/packages/experimental/agent-team/src/types.ts +++ b/packages/experimental/agent-team/src/types.ts @@ -1,6 +1,5 @@ /** Public Agent Teams identities, durable records, and service request values. */ -import type { MessageId } from '@deepseek-ai/dsh-llm/brand' import type { Branded } from '@deepseek-ai/dsh-brand' import type { ContentBlock } from '@deepseek-ai/dsh-llm/types' import type { SessionId } from '@deepseek-ai/dsh-session/types' @@ -41,10 +40,13 @@ export function TeamMessageId(id: string): TeamMessageId { return id as TeamMessageId } -/** Durable teammate lifecycle. */ +/** + * Teammate lifecycle. Continuable children of the Lead are always `active`; + * `provisioning` and `failed` appear only in historical roster records. + */ export type TeamMemberPhase = 'provisioning' | 'active' | 'failed' -/** Whole durable value written on every teammate lifecycle change. */ +/** Historical roster value written by earlier Team builds on every teammate lifecycle change. */ export interface TeamMemberSnapshot { readonly id: SessionId readonly name: string @@ -55,17 +57,10 @@ export interface TeamMemberSnapshot { readonly error?: string } -/** Current runtime-enriched roster row. */ -export interface TeamMemberView { +/** A continuable direct child of the Lead, read from the Lead's subagent catalog. */ +export interface TeamChild { readonly id: SessionId - readonly name: string - readonly role: 'lead' | 'teammate' - readonly status: 'running' | 'inactive' | 'provisioning' | 'failed' - readonly description?: string - readonly provider?: string - readonly context?: 'fresh' | 'fork' - readonly model?: string - readonly diagnostics: string[] + readonly description: string } /** Durable task lifecycle. */ @@ -92,17 +87,21 @@ export interface TeamTaskView { readonly status: TeamTaskStatus readonly blockedBy: TeamTaskId[] readonly writeScopes: string[] - readonly ownerName?: string + readonly ownerId?: SessionId readonly ready: boolean readonly writeScopeWarnings: string[] } -/** One durable roster row published through the `agentTeam` Session projection. */ +/** + * One roster row published through the `agentTeam` Session projection: the + * Lead, each continuable direct child of the Lead, and each historical roster member. + */ export interface TeamMemberProjection { readonly id: SessionId - readonly name: string + /** Delegated responsibility; absent for the Lead. Never used as an address. */ + readonly description?: string readonly role: 'lead' | 'teammate' - /** Durable lifecycle; the Lead row is always `active`. Turn activity comes from Session status. */ + /** The Lead and continuable children are always `active`. Turn activity comes from Session status. */ readonly phase: TeamMemberPhase readonly error?: string } @@ -151,41 +150,8 @@ declare module '@deepseek-ai/dsh-llm' { /** Team-service deployment limits. */ export interface Config { - /** Maximum immutable teammate names retained by one Team. */ - readonly maxMembers?: number /** Maximum non-deleted tasks retained by one Team. */ readonly maxTasks?: number - /** Maximum UTF-8 bytes in one complete sender-framed delivery. */ - readonly maxMessageBytes?: number - /** Maximum milliseconds for shared operation settlement and for each Team child drain. */ - readonly disposalTimeoutMs?: number -} - -/** Input for creating one durable teammate. */ -export interface SpawnTeammateRequest { - readonly name: string - readonly description: string - readonly prompt: ContentBlock[] - readonly context: 'fresh' | 'fork' - readonly provider: string - readonly signal: AbortSignal -} - -/** Result after one teammate reaches a durable active or failed edge. */ -export interface SpawnTeammateResult { - readonly member: TeamMemberView -} - -/** Input for one direct peer-message attempt. */ -export interface SendTeamMessageRequest { - readonly target: string - readonly content: ContentBlock[] - readonly signal: AbortSignal -} - -/** Target inbox acceptance under ordinary Agent persistence; no Team retry or deduplication. */ -export interface SendTeamMessageResult { - readonly messageId: MessageId } /** Input for creating one shared task. */ @@ -216,7 +182,7 @@ export interface UpdateTeamTaskRequest { readonly description?: string readonly blockedBy?: readonly TeamTaskId[] readonly writeScopes?: readonly string[] - readonly owner?: string + readonly owner?: SessionId } /** Result of waiting for Team activity. */ @@ -226,7 +192,7 @@ export interface TeamWaitResult { declare module '@deepseek-ai/dsh-session/types' { interface SessionEventMap { - /** Whole teammate lifecycle value, stored only in the Team Lead Session. */ + /** Historical teammate lifecycle value; new teammates are continuable entries in the Lead's subagent catalog. */ 'team/member': { version: 2; teamId: TeamId; member: TeamMemberSnapshot } /** Whole shared-task value, stored only in the Team Lead Session. */ 'team/task': { version: 2; teamId: TeamId; task: TeamTaskSnapshot } diff --git a/packages/experimental/agent-team/tests/persistence.spec.ts b/packages/experimental/agent-team/tests/persistence.spec.ts index a0cbf244e4..fa9b4b3406 100644 --- a/packages/experimental/agent-team/tests/persistence.spec.ts +++ b/packages/experimental/agent-team/tests/persistence.spec.ts @@ -7,15 +7,13 @@ import { Context } from '@deepseek-ai/cordis' import type { Agent } from '@deepseek-ai/dsh-agent' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' -import { createUserMessage } from '@deepseek-ai/dsh-llm' -import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session' +import { SessionId } from '@deepseek-ai/dsh-session' import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' -import SubagentService, { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent' +import SubagentService from '@deepseek-ai/dsh-subagent' import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process' import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts' import { mountWorkingDirectoryFixture } from '../../../subagent/subagent/tests/working-directory-fixture.ts' import TeamService, { TeamId, TeamMessageId } from '../src/index.ts' -import { readPersistedSession } from '../src/persisted.ts' import { teamProjectionDefinition } from '../src/projection.ts' import type { TeamMemberSnapshot, TeamMessageSnapshot, TeamTaskSnapshot } from '../src/index.ts' import { TestSessionQuery } from './test-session-query.ts' @@ -31,7 +29,7 @@ function durable(agent: Agent): { tasks: readonly TeamTaskSnapshot[] pendingMessages: readonly TeamMessageSnapshot[] } { - let projected = teamProjectionDefinition.init(agent.session.header) + let projected = teamProjectionDefinition.init(agent.session.header, agent.session.inheritedEventCount) for (const event of agent.session.snapshotEvents()) projected = teamProjectionDefinition.apply(projected, event) if (projected.failure !== undefined) throw new Error(projected.failure) const state = projected @@ -42,16 +40,6 @@ function durable(agent: Agent): { } } -/** Read one stored session's full event log through a short-lived read handle. */ -async function storedEvents(ctx: Context, id: SessionId): Promise { - const handle = await ctx.sessionPersistence.open(id, 'read') - try { - return (await handle.read()).events - } finally { - await handle.close() - } -} - async function disposeContext(ctx: Context): Promise { try { await ctx.fiber.dispose() @@ -118,181 +106,27 @@ async function stack( } } -function provisioning(childId: SessionId, name: string): TeamMemberSnapshot { - return { - id: childId, - name, - description: `${name} recovery`, - provider: 'spawn', - context: 'fresh', - phase: 'provisioning', - } -} - -async function persistedChild( - ctx: Context, - rootId: SessionId, - childId: SessionId, - message: ReturnType, -) { - const descriptor = snapshotSubagentDescriptor({ - mode: 'continuable', - provider: 'spawn', - label: 'persisted child fixture', - agentProvider: 'mock', - agentModel: 'mock', - }) - const child = ctx.sessions.create(childId, { - meta: { parentSession: rootId, origin: 'subagent' }, - }) - child.append('subagent/descriptor', descriptor) - child.append('agent/inbox/spliced', { - target: 'next-turn', - start: 0, - inserted: [message], - }) - // Live sessions persist only through an attached agent-loop writer; this - // bare fixture session seeds its durable log directly for the cold restart. - const handle = await ctx.sessionPersistence.create(child.header) - await handle.append(child.snapshotEvents()) - await handle.close() - return child -} - for (const backend of backends) { describe(`${backend.name} Agent Teams recovery`, () => { - it('restores a directly accepted target inbox from its flushed crash prefix without Team intent', async () => { - const liveRoot = mkdtempSync(join(tmpdir(), 'dsh-team-live-')) - const crashRoot = mkdtempSync(join(tmpdir(), 'dsh-team-crash-')) - roots.push(liveRoot, crashRoot) - const first = await stack(backend, liveRoot, ['hang']) - const lead = await first.ctx.agentLoop.create(SessionId('direct-root'), { provider: 'mock', model: 'mock' }) - const target = await first.ctx.agentTeams.spawnTeammate(lead, { - name: 'worker', description: 'pending inbox recovery', prompt: [{ type: 'text', text: 'initial' }], - context: 'fresh', provider: 'spawn', signal: SIGNAL, - }) - const child = first.ctx.agents.get(target.member.id)! - const receipt = await first.ctx.agentTeams.sendMessage(lead, { - target: 'worker', content: [{ type: 'text', text: 'accepted before crash' }], signal: SIGNAL, - }) - await first.ctx.sessions.flush(child.session) - const prefix = await readPersistedSession(first.ctx.sessionPersistence, child.id, SIGNAL) - expect(prefix.events.some(event => event.type === 'agent/inbox/spliced' - && event.data.inserted.some(message => message.id === receipt.messageId))).toBe(true) - expect(lead.session.snapshotEvents().some(event => event.type.startsWith('team/message/'))).toBe(false) - await first.dispose() - const second = await stack(backend, crashRoot, [textResponse('recovered')]) - const writer = await second.ctx.sessionPersistence.create(prefix.header) - try { await writer.append(prefix.events) } finally { await writer.close() } - const resumed = await second.ctx.agents.resume({ - resumeSessionId: child.id, agentOptions: { provider: 'mock', model: 'mock' }, - }) - expect(resumed.agent.inbox.nextStep.find(message => message.id === receipt.messageId)).toMatchObject({ - source: { kind: 'agent-message', form: 'relay', senderSessionId: lead.id }, - content: [{ type: 'text', text: 'Team message from lead:' }, { type: 'text', text: 'accepted before crash' }], - }) - await resumed.dispose() - await second.dispose() - }) - - it('reconciles a persisted child to active and a missing child to durable failed', { + it('restores continuable teammates and tasks from the Lead log after restart', { timeout: PERSISTENCE_TEST_TIMEOUT_MS, }, async () => { - const storageRoot = mkdtempSync(join(tmpdir(), `dsh-team-${backend.name.toLowerCase()}-`)) + const storageRoot = mkdtempSync(join(tmpdir(), `dsh-team-restart-${backend.name.toLowerCase()}-`)) roots.push(storageRoot) - const first = await stack(backend, storageRoot, [textResponse('initial child answer')]) - const activeRootId = SessionId(`${backend.name.toLowerCase()}-active-root`) - const failedRootId = SessionId(`${backend.name.toLowerCase()}-failed-root`) - const childId = SessionId(`${backend.name.toLowerCase()}-child`) - const activeRoot = await first.ctx.agentLoop.create(activeRootId, { provider: 'mock', model: 'mock' }) - const failedRoot = await first.ctx.agentLoop.create(failedRootId, { provider: 'mock', model: 'mock' }) - // Let each root's startup recovery observe the empty initial log before - // simulating the crash-only provisioning prefix. - await Promise.resolve() - await Promise.resolve() + const rootId = SessionId(`${backend.name.toLowerCase()}-restart-root`) - activeRoot.session.append('team/member', { - version: 2, - teamId: TeamId(activeRoot.id), - member: provisioning(childId, 'recoverable'), - }) - failedRoot.session.append('team/member', { - version: 2, - teamId: TeamId(failedRoot.id), - member: provisioning(SessionId(`${backend.name}-missing`), 'missing'), - }) - await Promise.all([ - first.ctx.sessions.flush(activeRoot.session), - first.ctx.sessions.flush(failedRoot.session), - ]) - await first.ctx.subagents.startActivation({ - delivery: 'parent', - childId, + const first = await stack(backend, storageRoot, [textResponse('initial teammate answer')]) + const firstLead = await first.ctx.agentLoop.create(rootId, { provider: 'mock', model: 'mock' }) + const started = await first.ctx.subagents.startActivation({ provider: 'spawn', - label: 'recoverable recovery', - request: { - prompt: [{ type: 'text', text: 'persist before active edge' }], - parent: activeRoot, - }, + label: 'restart worker', + request: { prompt: [{ type: 'text', text: 'finish before restart' }], parent: firstLead }, signal: SIGNAL, + delivery: 'caller', }) - await vi.waitFor(() => { expect(first.ctx.agents.get(childId)).toBeUndefined() }, { timeout: 5_000 }) - expect((await storedEvents(first.ctx, childId)) - .some(event => event.type === 'user/message')).toBe(true) - await first.dispose() - - const second = await stack(backend, storageRoot, [textResponse('cold resumed answer')]) - const activeHandle = await second.ctx.agents.resume({ - resumeSessionId: activeRootId, - agentOptions: { provider: 'mock', model: 'mock' }, - }) - const failedHandle = await second.ctx.agents.resume({ - resumeSessionId: failedRootId, - agentOptions: { provider: 'mock', model: 'mock' }, - }) - await vi.waitFor(() => { - expect(durable(activeHandle.agent).members[0]?.phase).toBe('active') - const failedMember = durable(failedHandle.agent).members[0] - expect(failedMember?.phase).toBe('failed') - expect(failedMember?.error).toContain('child Session recovery failed') - }, { timeout: 5_000 }) - - const receipt = await second.ctx.agentTeams.sendMessage(activeHandle.agent, { - target: 'recoverable', - content: [{ type: 'text', text: 'resume after reconciliation' }], - signal: SIGNAL, - }) - expect(receipt.messageId).toEqual(expect.any(String)) - await vi.waitFor(() => { expect(second.ctx.agents.get(childId)).toBeUndefined() }, { timeout: 5_000 }) - await vi.waitFor(() => { expect(durable(activeHandle.agent).pendingMessages).toEqual([]) }) - - await activeHandle.dispose() - await failedHandle.dispose() - await second.dispose() - }) - - it('reconciles a provisioning child whose initial prompt is durably pending', { - timeout: PERSISTENCE_TEST_TIMEOUT_MS, - }, async () => { - const storageRoot = mkdtempSync(join(tmpdir(), `dsh-team-pending-${backend.name.toLowerCase()}-`)) - roots.push(storageRoot) - const rootId = SessionId(`${backend.name.toLowerCase()}-pending-root`) - const childId = SessionId(`${backend.name.toLowerCase()}-pending-child`) - const first = await stack(backend, storageRoot, []) - const root = await first.ctx.agentLoop.create(rootId, { provider: 'mock', model: 'mock' }) - await Promise.resolve() - await Promise.resolve() - root.session.append('team/member', { - version: 2, - teamId: TeamId(root.id), - member: provisioning(childId, 'pending-worker'), - }) - const initial = createUserMessage({ - content: [{ type: 'text', text: 'durably pending initial task' }], - source: { kind: 'user' }, - }) - await persistedChild(first.ctx, rootId, childId, initial) - await first.ctx.sessions.flush(root.session) + await vi.waitFor(() => { expect(first.ctx.agents.get(started.childId)).toBeUndefined() }, { timeout: 5_000 }) + const created = await first.ctx.agentTeams.createTask(firstLead, { subject: 'carry over', description: 'survives restart' }) + await first.ctx.sessions.flush(firstLead.session) await first.dispose() const second = await stack(backend, storageRoot, []) @@ -300,40 +134,42 @@ for (const backend of backends) { resumeSessionId: rootId, agentOptions: { provider: 'mock', model: 'mock' }, }) - await vi.waitFor(() => { - expect(durable(rootHandle.agent).members[0]?.phase).toBe('active') + expect(second.ctx.sessionProjections.snapshot(rootHandle.agent.session).values['agentTeam']).toMatchObject({ + members: [ + { id: rootId, role: 'lead', phase: 'active' }, + { id: started.childId, description: 'restart worker', role: 'teammate', phase: 'active' }, + ], + tasks: [{ id: created.id, subject: 'carry over' }], }) expect(second.adapter.requests).toEqual([]) - const stored = await storedEvents(second.ctx, childId) - expect(stored.some(event => event.type === 'agent/inbox/spliced' - && event.data.inserted.some(message => message.id === initial.id))).toBe(true) - await rootHandle.dispose() await second.dispose() }) - it('keeps historical queued mail readable without delivering it after restart', { + it('keeps historical roster records and queued mail readable without delivering them after restart', { timeout: PERSISTENCE_TEST_TIMEOUT_MS, }, async () => { const storageRoot = mkdtempSync(join(tmpdir(), `dsh-team-mail-${backend.name.toLowerCase()}-`)) roots.push(storageRoot) const rootId = SessionId(`${backend.name.toLowerCase()}-mail-root`) + const childId = SessionId('historical-worker') - const first = await stack(backend, storageRoot, [textResponse('initial teammate answer')]) + const first = await stack(backend, storageRoot, []) const firstLead = await first.ctx.agentLoop.create(rootId, { provider: 'mock', model: 'mock' }) - const started = await first.ctx.agentTeams.spawnTeammate(firstLead, { + const historical: TeamMemberSnapshot = { + id: childId, name: 'mail-worker', description: 'historical mailbox target', - prompt: [{ type: 'text', text: 'finish before restart' }], - context: 'fresh', provider: 'spawn', - signal: SIGNAL, - }) - await vi.waitFor(() => { expect(first.ctx.agents.get(started.member.id)).toBeUndefined() }, { timeout: 5_000 }) + context: 'fresh', + phase: 'provisioning', + } const message = { - id: TeamMessageId('legacy-pending'), senderId: rootId, senderName: 'lead', targetId: started.member.id, + id: TeamMessageId('legacy-pending'), senderId: rootId, senderName: 'lead', targetId: childId, content: [{ type: 'text' as const, text: 'historical pending context' }], } + firstLead.session.append('team/member', { version: 2, teamId: TeamId(rootId), member: historical }) + firstLead.session.append('team/member', { version: 2, teamId: TeamId(rootId), member: { ...historical, phase: 'active' } }) firstLead.session.append('team/message/queued', { version: 2, teamId: TeamId(rootId), message }) await first.ctx.sessions.flush(firstLead.session) await first.dispose() @@ -343,11 +179,17 @@ for (const backend of backends) { resumeSessionId: rootId, agentOptions: { provider: 'mock', model: 'mock' }, }) - expect(durable(rootHandle.agent).pendingMessages).toEqual([message]) - expect(second.ctx.agents.get(started.member.id)).toBeUndefined() - const child = await storedEvents(second.ctx, started.member.id) - expect(child.some(event => event.type === 'user/message' - && event.data.source.kind === 'team-message')).toBe(false) + expect(durable(rootHandle.agent)).toMatchObject({ + members: [{ ...historical, phase: 'active' }], + pendingMessages: [message], + }) + expect(second.ctx.sessionProjections.snapshot(rootHandle.agent.session).values['agentTeam']).toMatchObject({ + members: [ + { id: rootId, role: 'lead' }, + { id: childId, description: 'historical mailbox target', role: 'teammate', phase: 'active' }, + ], + }) + expect(second.ctx.agents.get(childId)).toBeUndefined() expect(second.adapter.requests).toEqual([]) await rootHandle.dispose() diff --git a/packages/experimental/agent-team/tests/projection-events.spec.ts b/packages/experimental/agent-team/tests/projection-events.spec.ts index 2266b41483..edfda3f900 100644 --- a/packages/experimental/agent-team/tests/projection-events.spec.ts +++ b/packages/experimental/agent-team/tests/projection-events.spec.ts @@ -17,13 +17,13 @@ function event>(type: T, d return { type, data, seq, time: seq } as unknown as SessionEvent } -function project(rootId: SessionId, events: readonly SessionEvent[]): TeamProjectionState { +function project(rootId: SessionId, events: readonly SessionEvent[], inherited = 0): TeamProjectionState { let state = teamProjectionDefinition.init({ version: SESSION_FORMAT_VERSION, id: rootId, createdAt: 0, - isSeeded: false, - }) + isSeeded: inherited > 0, + }, SessionLogOffset(inherited)) for (const event of events) state = teamProjectionDefinition.apply(state, event) return state } @@ -73,6 +73,16 @@ function task(overrides: Partial = {}): TeamTaskSnapshot { } } +/** One parent catalog fact for a continuable child. */ +function catalog(childId: string, seq: number, description = `${childId} task`): SessionEvent<'subagent/catalog'> { + return { + type: 'subagent/catalog', + seq: SessionSeq(seq), + time: seq, + data: { version: 0, childId: SessionId(childId), childCreatedAt: seq, mode: 'continuable', label: description }, + } +} + function message(overrides: Partial = {}): TeamMessageSnapshot { return { id: TeamMessageId('message-1'), @@ -425,6 +435,44 @@ describe('Agent Teams projection events', () => { expect(isEmptyState(projectTeam(ROOT, [inherited]))).toBe(true) }) + it('projects own continuable catalog children as teammates and skips inherited children', () => { + const records: SessionEvent[] = [ + catalog('inherited', 0, 'inherited-name'), + catalog('plain', 1), + catalog('named', 2, 'reviewer'), + event('team/task', { version: 2, teamId: TEAM, task: task({ status: 'in_progress', revision: 1, ownerId: SessionId('named') }) }, SessionSeq(3)), + ] + const projected = project(ROOT, records, 1) + expect(teamState(projected).children).toEqual([{ id: SessionId('plain'), description: 'plain task' }, { id: SessionId('named'), description: 'reviewer' }]) + expect(teamProjectionDefinition.stateSchema.parse(JSON.parse(JSON.stringify(projected)))).toEqual(projected) + const view = teamProjectionView(projected) + expect(view.members).toEqual([ + { id: ROOT, role: 'lead', phase: 'active' }, + { id: SessionId('plain'), description: 'plain task', role: 'teammate', phase: 'active' }, + { id: SessionId('named'), description: 'reviewer', role: 'teammate', phase: 'active' }, + ]) + expect(view.tasks[0]).toMatchObject({ ownerId: SessionId('named') }) + expect(teamProjectionView(teamProjectionDefinition.apply(projected, catalog('second', 4, 'writer')))).not.toBe(view) + }) + + it('publishes one catalog row when a historical member has the same id', () => { + const state = project(ROOT, [ + event('team/member', { version: 2, teamId: TEAM, member: member() }, SessionSeq(0)), + catalog(CHILD, 1, 'current responsibility'), + ]) + expect(teamProjectionView(state).members).toEqual([ + { id: ROOT, role: 'lead', phase: 'active' }, + { id: CHILD, description: 'current responsibility', role: 'teammate', phase: 'active' }, + ]) + }) + + it('allows repeated descriptions and rejects malformed continuable catalog payloads', () => { + expect(project(ROOT, [catalog('a', 0, 'dup'), catalog('b', 1, 'dup')]).failure) + .toBeUndefined() + const malformed = JSON.parse(JSON.stringify({ ...catalog('a', 0, 'x'), data: { version: 0, mode: 'continuable', childId: 'a' } })) as SessionEvent<'subagent/catalog'> + expect(project(ROOT, [malformed]).failure).toMatch(/subagent\/catalog payload is invalid/) + }) + describe('copy-on-write state and client view', () => { const memberEvent = event('team/member', { version: 2, teamId: TEAM, member: member() }, SessionSeq(0)) const taskEvent = event('team/task', { version: 2, teamId: TEAM, task: task() }, SessionSeq(1)) @@ -495,13 +543,13 @@ describe('Agent Teams projection events', () => { expect(teamProjectionDefinition.wire.viewSchema.parse(view)).toEqual(view) expect(view).toEqual({ members: [ - { id: ROOT, name: 'lead', role: 'lead', phase: 'active' }, - { id: CHILD, name: 'worker-a', role: 'teammate', phase: 'active' }, + { id: ROOT, role: 'lead', phase: 'active' }, + { id: CHILD, description: 'worker', role: 'teammate', phase: 'active' }, ], tasks: [ { id: 'task-1', revision: 2, subject: 'subject', description: 'description', status: 'in_progress', - blockedBy: [], writeScopes: ['src'], ownerName: 'worker-a', ready: false, writeScopeWarnings: [], + blockedBy: [], writeScopes: ['src'], ownerId: CHILD, ready: false, writeScopeWarnings: [], }, { id: 'task-2', revision: 1, subject: 'subject', description: 'description', status: 'pending', @@ -520,7 +568,7 @@ describe('Agent Teams projection events', () => { const memberView = teamProjectionView(withMember) expect(memberView).not.toBe(view) expect(memberView.members.at(-1)).toEqual({ - id: 'child-b', name: 'worker-b', role: 'teammate', phase: 'provisioning', + id: 'child-b', description: 'worker', role: 'teammate', phase: 'provisioning', }) const failedMember = event('team/member', { @@ -554,7 +602,7 @@ describe('Agent Teams projection events', () => { const lead = ctx.sessions.create(ROOT) const other = ctx.sessions.create(SessionId('other-root')) expect(ctx.sessionProjections.snapshot(lead).values['agentTeam']).toEqual({ - members: [{ id: ROOT, name: 'lead', role: 'lead', phase: 'active' }], + members: [{ id: ROOT, role: 'lead', phase: 'active' }], tasks: [], }) @@ -574,7 +622,7 @@ describe('Agent Teams projection events', () => { expect(published).toHaveLength(2) expect(published[1]).toMatchObject({ session: other.id, - value: { members: [{ id: other.id, name: 'lead', role: 'lead', phase: 'active' }] }, + value: { members: [{ id: other.id, role: 'lead', phase: 'active' }] }, }) expect(ctx.sessionProjections.snapshot(lead).values['agentTeam']).toMatchObject({ tasks: [{ id: 'task-1' }], diff --git a/packages/experimental/agent-team/tests/team.spec.ts b/packages/experimental/agent-team/tests/team.spec.ts index 34a9a25afd..76ad64897d 100644 --- a/packages/experimental/agent-team/tests/team.spec.ts +++ b/packages/experimental/agent-team/tests/team.spec.ts @@ -7,18 +7,17 @@ import type { Agent } from '@deepseek-ai/dsh-agent' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' import { createUserMessage } from '@deepseek-ai/dsh-llm' -import { SessionLogOffset, SessionId, type Session, type SessionEvent } from '@deepseek-ai/dsh-session' +import { SessionLogOffset, SessionId } from '@deepseek-ai/dsh-session' import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' import SubagentService from '@deepseek-ai/dsh-subagent' -import { deliverSubagentPrompt, type HostPromptDeliverer } from '@deepseek-ai/dsh-subagent/internal' import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork-in-process' import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process' import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts' import { mountWorkingDirectoryFixture } from '../../../subagent/subagent/tests/working-directory-fixture.ts' -import TeamService, { TeamError, TeamId, TeamMessageId, TeamTaskId } from '../src/index.ts' -import { TeamRuntimeLifecycle } from '../src/lifecycle.ts' -import { teamProjectionDefinition } from '../src/projection.ts' -import type { TeamMemberSnapshot, TeamMessageSnapshot, TeamTaskSnapshot } from '../src/index.ts' +import TeamService, { TeamError, TeamId, TeamTaskId } from '../src/index.ts' +import { resolveMember } from '../src/membership.ts' +import { emptyTeamState, teamProjectionDefinition } from '../src/projection.ts' +import type { TeamProjectionState } from '../src/projection.ts' import { TestSessionQuery } from './test-session-query.ts' const SIGNAL = new AbortController().signal @@ -32,31 +31,12 @@ afterEach(async () => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) }) -/** Detached durable Team read through the same projection definition as the service. */ -function durable(agent: Agent): { - members: readonly TeamMemberSnapshot[] - tasks: readonly TeamTaskSnapshot[] - pendingMessages: readonly TeamMessageSnapshot[] -} { - let projected = teamProjectionDefinition.init(agent.session.header) +/** Detached Team state folded through the same projection definition as the service. */ +function durable(agent: Agent): TeamProjectionState { + let projected = teamProjectionDefinition.init(agent.session.header, agent.session.inheritedEventCount) for (const event of agent.session.snapshotEvents()) projected = teamProjectionDefinition.apply(projected, event) if (projected.failure !== undefined) throw new Error(projected.failure) - const state = projected - return { - members: state.members, - tasks: state.tasks, - pendingMessages: state.messages.filter(message => !state.delivered.includes(message.id)), - } -} - -/** Read one stored session's full event log through a short-lived read handle. */ -async function storedEvents(ctx: Context, id: SessionId): Promise { - const handle = await ctx.sessionPersistence.open(id, 'read') - try { - return (await handle.read()).events - } finally { - await handle.close() - } + return projected } async function setup( @@ -78,7 +58,7 @@ async function setup( const teamFiber = await ctx.plugin(TeamService, config) const adapter = new MockAdapter(script) ctx.llm.registerAdapter(['mock'], adapter) - const lead = await ctx.agentLoop.create(SessionId('lead'), { provider: 'mock', model: 'mock' }) + const lead = await ctx.agentLoop.create(SessionId('team-root'), { provider: 'mock', model: 'mock' }) return { ctx, lead, adapter, storageRoot, teamFiber } } @@ -87,18 +67,9 @@ function content(text: string) { } interface TeamServiceInternals { - readonly lifecycle: TeamRuntimeLifecycle - readonly roster: { - checkpointInitialPrompt(childId: SessionId, messageId: string, signal: AbortSignal): Promise - reconcileProvisioning(root: Agent, signal: AbortSignal): Promise - recoverFor(agent: Agent, signal: AbortSignal): Promise - liveChildrenByRoot(): Map - } readonly journal: { state(root: Agent): unknown } - disposeRuntime(): Promise - scheduleRecovery(agent: Agent): void } /** White-box access follows the runtime owners so coverage does not widen the service API. */ @@ -106,26 +77,16 @@ function teamInternals(ctx: Context): TeamServiceInternals { return ctx.agentTeams as unknown as TeamServiceInternals } -/** Access the existing host adapter for per-instance delivery barriers. */ -function hostDeliverer(ctx: Context): HostPromptDeliverer { - return ctx.subagents as unknown as HostPromptDeliverer -} - -function spawn( - ctx: Context, - lead: Agent, - name: string, - options: { context?: 'fresh' | 'fork'; provider?: string } = {}, -) { - const context = options.context ?? 'fresh' - return ctx.agentTeams.spawnTeammate(lead, { - name, - description: `${name} responsibility`, - prompt: content(`${name} initial`), - context, - provider: options.provider ?? (context === 'fork' ? 'fork' : 'spawn'), +/** Start one teammate: a continuable child of the Lead. */ +async function spawn(ctx: Context, lead: Agent, description: string | undefined, provider = 'spawn'): Promise { + const started = await ctx.subagents.startActivation({ + provider, + label: `${description ?? 'shared'} responsibility`, + request: { prompt: content(`${description ?? 'shared'} initial`), parent: lead }, signal: SIGNAL, + delivery: 'parent', }) + return started.childId } async function waitNoAgent(ctx: Context, id: SessionId): Promise { @@ -140,7 +101,7 @@ async function waitRunning(ctx: Context, id: SessionId): Promise { }, { timeout: 5_000 }) } -describe('Team identity and provisioning', () => { +describe('Team identity and membership', () => { it('rejects missing and failed authoritative Team projections', async () => { const first = await setup([]) const journal = teamInternals(first.ctx).journal @@ -150,333 +111,53 @@ describe('Team identity and provisioning', () => { )) expect(() => journal.state(first.lead)).toThrow('Agent Teams projection is not registered') stateOfSpy.mockImplementation((session, key) => key === 'agentTeam' - ? { ...teamProjectionDefinition.init(session.header), failure: 'failed Team projection' } + ? { ...teamProjectionDefinition.init(session.header, SessionLogOffset(0)), failure: 'failed Team projection' } : stateOf(session, key)) expect(() => journal.state(first.lead)).toThrow('failed Team projection') stateOfSpy.mockRestore() }) - it('rejects deployment limits that are not positive safe integers', async () => { - const fields = [ - 'maxMembers', - 'maxTasks', - 'maxMessageBytes', - 'disposalTimeoutMs', - ] as const - for (const field of fields) { - for (const value of [0, 1.5, Number.MAX_SAFE_INTEGER + 1]) { - await expect(setup([], { [field]: value })).rejects.toThrow() - } + it('rejects task limits that are not positive safe integers and removed configuration keys', async () => { + for (const value of [0, 1.5, Number.MAX_SAFE_INTEGER + 1]) { + await expect(setup([], { maxTasks: value })).rejects.toThrow() + } + for (const key of ['maxMembers', 'maxMessageBytes', 'disposalTimeoutMs']) { + await expect(setup([], { [key]: 1 })).rejects.toThrow(`${key} is no longer supported`) } }) - it('supports direct-constructor defaults and recovers roots that already exist', async () => { - const ctx = new Context() - contexts.push(ctx) - await mountAgentLoopTestDependencies(ctx) - await mountWorkingDirectoryFixture(ctx) - const storageRoot = mkdtempSync(join(tmpdir(), 'dsh-team-direct-')) - roots.push(storageRoot) - await ctx.plugin(JsonlSessionPersistence, { root: storageRoot }) - await ctx.plugin(AgentLoop, { agents: [] }) - await ctx.plugin(SubagentService) - const lead = await ctx.agentLoop.create(SessionId('preexisting-lead'), {}) - const service = new TeamService(ctx) + it('makes every root a Lead and every live subagent below it a teammate', async () => { + const { ctx, lead } = await setup(['hang', 'hang', 'hang']) + const named = await waitRunning(ctx, await spawn(ctx, lead, 'alpha')) + const unnamed = await waitRunning(ctx, await spawn(ctx, lead, undefined)) + const grandchildId = (await ctx.subagents.startActivation({ + provider: 'spawn', label: 'nested', delivery: 'caller', signal: SIGNAL, + request: { prompt: content('nested'), parent: named }, + })).childId + const grandchild = await waitRunning(ctx, grandchildId) - expect(service.listMembers(lead)).toEqual([expect.objectContaining({ - name: 'lead', - status: 'inactive', - diagnostics: [], - })]) - const provisioning = { - id: SessionId('preexisting-child'), - name: 'preexisting-worker', - description: 'preexisting responsibility', - provider: 'spawn', - context: 'fresh' as const, - phase: 'provisioning' as const, + const describe = (agent: Agent) => { + const { root, id, role } = ctx.agentTeams.membership(agent) + return { root: root.id, id, role } } - lead.session.append('team/member', { - version: 2, - teamId: TeamId(lead.id), - member: provisioning, - }) - expect(service.listMembers(lead)[1]).toEqual(expect.objectContaining({ - name: 'preexisting-worker', - status: 'provisioning', - diagnostics: [], - })) - expect(service.listMembers(lead)[1]).not.toHaveProperty('model') - await Promise.resolve(undefined) - }) + expect(describe(lead)).toEqual({ root: lead.id, id: TeamId(lead.id), role: 'lead' }) + expect(describe(named)).toEqual({ root: lead.id, id: TeamId(lead.id), role: 'teammate' }) + expect(describe(unnamed)).toEqual({ root: lead.id, id: TeamId(lead.id), role: 'teammate' }) + expect(describe(grandchild)).toEqual({ root: lead.id, id: TeamId(lead.id), role: 'teammate' }) + expect(durable(lead).children).toEqual([{ id: named.id, description: 'alpha responsibility' }, { id: unnamed.id, description: 'shared responsibility' }]) - it('creates fresh and fork teammates with immutable names and bounded roster size', async () => { - const { ctx, lead } = await setup([ - textResponse('lead answer'), - textResponse('fork answer'), - textResponse('fresh answer'), - ], { maxMembers: 2 }) - lead.followup(createUserMessage({ content: content('lead turn'), source: { kind: 'user' } })) - await lead.whenIdle() + const impostor = { ...lead } as Agent + expect(ctx.agentTeams.tryMembership(impostor)).toBeUndefined() + expect(() => ctx.agentTeams.membership(impostor)).toThrow(expect.objectContaining({ code: 'TEAM_NOT_MEMBER' })) - const forked = await spawn(ctx, lead, 'fork-worker', { context: 'fork' }) - await waitNoAgent(ctx, forked.member.id) - const fresh = await spawn(ctx, lead, 'fresh-worker') - await waitNoAgent(ctx, fresh.member.id) - - expect((await ctx.sessionPersistence.stat(forked.member.id))?.header.isSeeded).toBe(true) - expect((await ctx.sessionPersistence.stat(fresh.member.id))?.header.isSeeded).toBe(false) - expect(ctx.agentTeams.listMembers(lead).map(row => [row.name, row.context, row.status])).toEqual([ - ['lead', undefined, 'inactive'], - ['fork-worker', 'fork', 'inactive'], - ['fresh-worker', 'fresh', 'inactive'], - ]) - await expect(spawn(ctx, lead, 'third-worker')).rejects.toMatchObject({ code: 'TEAM_MEMBER_LIMIT' }) - await expect(spawn(ctx, lead, 'fresh-worker')).rejects.toMatchObject({ code: 'TEAM_MEMBER_NAME_TAKEN' }) - }) - - it('flushes the accepted child prompt before committing the active roster edge', async () => { - const { ctx, lead } = await setup([textResponse('checkpointed child answer')]) - const flush = ctx.sessions.flush.bind(ctx.sessions) - const order: string[] = [] - vi.spyOn(ctx.sessions, 'flush').mockImplementation(async (session) => { - if (session.id === lead.id && durable(lead).members[0]?.phase === 'active') { - order.push('lead-active') - } else if (session.id !== lead.id) { - order.push('child') - } - return flush(session) - }) - - const started = await spawn(ctx, lead, 'checkpoint-worker') - expect(order.indexOf('child')).toBeGreaterThanOrEqual(0) - expect(order.indexOf('child')).toBeLessThan(order.indexOf('lead-active')) - await waitNoAgent(ctx, started.member.id) - }) - - it('checkpoints live and detached inbox receipts and aborts an unresolved checkpoint', async () => { - const { ctx, lead } = await setup([]) - const internal = teamInternals(ctx).roster - let liveSession: Session | undefined - const liveFiber = await ctx.plugin(Object.assign(function checkpointFixture(childCtx: Context) { - liveSession = childCtx.sessions.create(SessionId('checkpoint-child')) - }, { inject: ['sessions'] })) - if (liveSession === undefined) throw new Error('checkpoint fixture did not create its Session') - const initial = createUserMessage({ content: content('checkpoint me'), source: { kind: 'user' } }) - const checkpoint = internal.checkpointInitialPrompt(liveSession.id, initial.id, SIGNAL) - await Promise.resolve(undefined) - lead.inject(createUserMessage({ content: content('unrelated progress'), source: { kind: 'user' } })) - const unrelatedFiber = await ctx.plugin(Object.assign(function unrelatedCheckpointFixture(childCtx: Context) { - childCtx.sessions.create(SessionId('unrelated-checkpoint-child')) - }, { inject: ['sessions'] })) - await unrelatedFiber.dispose() - liveSession.append('agent/inbox/spliced', { - target: 'next-turn', start: 0, inserted: [initial], - }) - await checkpoint - // Live sessions persist only through an attached agent-loop writer; this - // bare fixture session seeds its durable log directly for the cold reread. - const persisted = await ctx.sessionPersistence.create(liveSession.header) - await persisted.append(liveSession.snapshotEvents()) - await persisted.close() - await liveFiber.dispose() - - await expect(internal.checkpointInitialPrompt(liveSession.id, initial.id, SIGNAL)).resolves.toBeUndefined() - const missing = createUserMessage({ content: content('missing'), source: { kind: 'user' } }) - await expect(internal.checkpointInitialPrompt(liveSession.id, missing.id, SIGNAL)) - .rejects.toMatchObject({ code: 'TEAM_PROVISIONING_CONFLICT' }) - - let disposedSession: Session | undefined - const disposedFiber = await ctx.plugin(Object.assign(function disposedCheckpointFixture(childCtx: Context) { - disposedSession = childCtx.sessions.create(SessionId('disposed-checkpoint-child')) - }, { inject: ['sessions'] })) - if (disposedSession === undefined) throw new Error('disposed checkpoint fixture did not create its Session') - const disposed = internal.checkpointInitialPrompt(disposedSession.id, missing.id, SIGNAL) - const disposedResult = expect(disposed).rejects.toThrow('not found') - await Promise.resolve(undefined) - await disposedFiber.dispose() - await disposedResult - - let abortedSession: Session | undefined - const abortedFiber = await ctx.plugin(Object.assign(function abortedCheckpointFixture(childCtx: Context) { - abortedSession = childCtx.sessions.create(SessionId('aborted-checkpoint-child')) - }, { inject: ['sessions'] })) - if (abortedSession === undefined) throw new Error('aborted checkpoint fixture did not create its Session') - const controller = new AbortController() - const aborted = internal.checkpointInitialPrompt(abortedSession.id, missing.id, controller.signal) - await Promise.resolve(undefined) - controller.abort({ kind: 'test' }) - await expect(aborted).rejects.toMatchObject({ code: 'TEAM_DISPOSED' }) - - const errorController = new AbortController() - const errorAborted = internal.checkpointInitialPrompt(abortedSession.id, missing.id, errorController.signal) - const errorResult = expect(errorAborted).rejects.toThrow('checkpoint stopped') - await Promise.resolve(undefined) - errorController.abort(new Error('checkpoint stopped')) - await errorResult - await abortedFiber.dispose() - }) - - it('drains an accepted child when its initial durability checkpoint fails', async () => { - const { ctx, lead } = await setup(['hang']) - vi.spyOn(teamInternals(ctx).roster, 'checkpointInitialPrompt') - .mockRejectedValueOnce(new Error('checkpoint failed')) - - await expect(spawn(ctx, lead, 'checkpoint-failure')).rejects.toThrow('checkpoint failed') - const member = durable(lead).members[0] - expect(member).toMatchObject({ phase: 'failed', error: 'checkpoint failed' }) - if (member !== undefined) await waitNoAgent(ctx, member.id) - }) - - it('records failed provisioning durably, reserves its name, and counts it against the limit', async () => { - const { ctx, lead } = await setup([], { maxMembers: 1 }) - await expect(spawn(ctx, lead, 'failed-worker', { provider: 'missing' })).rejects.toThrow() - - expect(ctx.agentTeams.listMembers(lead)[1]).toMatchObject({ - name: 'failed-worker', - status: 'failed', - provider: 'missing', - }) - await expect(spawn(ctx, lead, 'failed-worker')).rejects.toMatchObject({ code: 'TEAM_MEMBER_NAME_TAKEN' }) - await expect(spawn(ctx, lead, 'other-worker')).rejects.toMatchObject({ code: 'TEAM_MEMBER_LIMIT' }) - }) - - it('records non-Error provider failures and contains a reversed provisioning settlement race', async () => { - const first = await setup([]) - vi.spyOn(first.ctx.subagents, 'startActivation').mockRejectedValueOnce('string provider failure') - await expect(spawn(first.ctx, first.lead, 'string-failure')).rejects.toBe('string provider failure') - expect(first.ctx.agentTeams.listMembers(first.lead)[1]).toMatchObject({ - status: 'failed', - diagnostics: ['string provider failure'], - }) - await expect(first.ctx.agentTeams.sendMessage(first.lead, { - target: 'string-failure', content: content('cannot deliver'), signal: SIGNAL, - })).rejects.toMatchObject({ code: 'TEAM_MEMBER_NOT_FOUND' }) - - const second = await setup([]) - vi.spyOn(second.ctx.subagents, 'startActivation').mockImplementationOnce(async () => { - const provisioning = durable(second.lead).members[0] - if (provisioning === undefined) throw new Error('missing provisioning edge') - second.lead.session.append('team/member', { - version: 2, - teamId: TeamId(second.lead.id), - member: { ...provisioning, phase: 'active' }, - }) - await second.ctx.sessions.flush(second.lead.session) - throw new Error('creator failed after recovery settled active') - }) - await expect(spawn(second.ctx, second.lead, 'reverse-race')).rejects.toBeInstanceOf(AggregateError) - expect(durable(second.lead).members[0]?.phase).toBe('active') - }) - - it('cleans up a child when recovery settles its provisioning record first', async () => { - const { ctx, lead } = await setup(['hang']) - const start = ctx.subagents.startActivation.bind(ctx.subagents) - const entered = Promise.withResolvers() - const release = Promise.withResolvers() - let childId: SessionId | undefined - vi.spyOn(ctx.subagents, 'startActivation').mockImplementation(async (spec) => { - childId = spec.childId - entered.resolve(undefined) - await release.promise - return start(spec) - }) - - const spawning = spawn(ctx, lead, 'racing-worker') - const rejected = expect(spawning).rejects.toMatchObject({ code: 'TEAM_PROVISIONING_CONFLICT' }) - await entered.promise - await teamInternals(ctx).roster.reconcileProvisioning(lead, SIGNAL) - expect(durable(lead).members[0]?.phase).toBe('failed') - - release.resolve(undefined) - await rejected - if (childId === undefined) throw new Error('reserved child id was not observed') - await waitNoAgent(ctx, childId) - }) - - it('handles a continuation that settles before the active roster view or conflict cleanup lookup', async () => { - const first = await setup([]) - vi.spyOn(teamInternals(first.ctx).roster, 'checkpointInitialPrompt').mockResolvedValueOnce() - vi.spyOn(first.ctx.subagents, 'startActivation').mockImplementationOnce(async spec => ({ - result: Promise.resolve({ output: [], stopReason: 'completed' as const }), - dispose: async () => {}, - childId: spec.childId!, - messageId: createUserMessage({ content: content('accepted'), source: { kind: 'user' } }).id, - })) - const inactive = await spawn(first.ctx, first.lead, 'instant-worker') - expect(inactive.member).toMatchObject({ status: 'inactive', diagnostics: [] }) - expect(inactive.member).not.toHaveProperty('model') - - const second = await setup([]) - vi.spyOn(teamInternals(second.ctx).roster, 'checkpointInitialPrompt').mockResolvedValueOnce() - const entered = Promise.withResolvers() - const release = Promise.withResolvers() - vi.spyOn(second.ctx.subagents, 'startActivation').mockImplementationOnce(async (spec) => { - entered.resolve(undefined) - await release.promise - return { - result: Promise.resolve({ output: [], stopReason: 'completed' as const }), - dispose: async () => {}, - childId: spec.childId!, - messageId: createUserMessage({ content: content('accepted'), source: { kind: 'user' } }).id, - } - }) - const spawning = spawn(second.ctx, second.lead, 'instant-conflict') - const rejected = expect(spawning).rejects.toMatchObject({ code: 'TEAM_PROVISIONING_CONFLICT' }) - await entered.promise - await teamInternals(second.ctx).roster.reconcileProvisioning(second.lead, SIGNAL) - release.resolve(undefined) - await rejected - }) - - it('validates names and permits only the Lead to create or interrupt teammates', async () => { - const { ctx, lead } = await setup(['hang']) - for (const name of ['Lead', 'lead', '-bad', 'bad-', 'bad_name', 'x'.repeat(65)]) { - await expect(spawn(ctx, lead, name)).rejects.toMatchObject({ code: 'TEAM_INVALID_MEMBER_NAME' }) - } - const started = await spawn(ctx, lead, 'worker') - const worker = await waitRunning(ctx, started.member.id) - await expect(spawn(ctx, worker, 'nested')).rejects.toMatchObject({ code: 'TEAM_LEAD_REQUIRED' }) - expect(() => ctx.agentTeams.interrupt(worker, 'worker')).toThrow(expect.objectContaining({ code: 'TEAM_LEAD_REQUIRED' })) - expect(ctx.agentTeams.interrupt(lead, 'worker')).toEqual({ previousStatus: 'running' }) - await waitNoAgent(ctx, worker.id) - expect(ctx.agentTeams.interrupt(lead, 'worker')).toEqual({ previousStatus: 'inactive' }) - expect(() => ctx.agentTeams.interrupt(lead, 'lead')).toThrow(expect.objectContaining({ code: 'TEAM_INVALID_TARGET' })) - }) - - it('validates teammate text fields and pre-provisioning cancellation', async () => { - const { ctx, lead } = await setup([]) - await expect(ctx.agentTeams.spawnTeammate(lead, { - name: 'empty-description', - description: ' ', - prompt: content('unused'), - context: 'fresh', - provider: 'spawn', - signal: SIGNAL, - })).rejects.toMatchObject({ code: 'TEAM_INVALID_ARGUMENT' }) - await expect(ctx.agentTeams.spawnTeammate(lead, { - name: 'empty-provider', - description: 'valid description', - prompt: content('unused'), - context: 'fresh', - provider: ' ', - signal: SIGNAL, - })).rejects.toMatchObject({ code: 'TEAM_INVALID_ARGUMENT' }) - const controller = new AbortController() - controller.abort(new TeamError('cancelled before provisioning', 'TEST_CANCELLED')) - await expect(ctx.agentTeams.spawnTeammate(lead, { - name: 'cancelled-worker', - description: 'never provisioned', - prompt: content('unused'), - context: 'fresh', - provider: 'spawn', - signal: controller.signal, - })).rejects.toMatchObject({ code: 'TEST_CANCELLED' }) - expect(durable(lead).members).toEqual([]) + for (const id of [grandchild.id, named.id, unnamed.id]) ctx.subagents.interrupt(id, { kind: 'ancestor', agent: lead }) + await Promise.all([waitNoAgent(ctx, grandchild.id), waitNoAgent(ctx, named.id), waitNoAgent(ctx, unnamed.id)]) }) it('treats an ordinary fork as a new Root Team and filters inherited Team state', async () => { - const { ctx, lead } = await setup([]) + const { ctx, lead } = await setup([textResponse('alpha done')]) + const alpha = await spawn(ctx, lead, 'alpha') + await waitNoAgent(ctx, alpha) await ctx.agentTeams.createTask(lead, { subject: 'parent task', description: 'belongs to parent' }) const handle = await ctx.agents.create({ sessionId: SessionId('ordinary-fork'), @@ -489,74 +170,71 @@ describe('Team identity and provisioning', () => { expect(ctx.agentTeams.membership(handle.agent)).toMatchObject({ id: TeamId(handle.agent.id), role: 'lead', - name: 'lead', }) - expect(durable(handle.agent)).toMatchObject({ members: [], tasks: [], pendingMessages: [] }) + expect(durable(handle.agent)).toMatchObject({ children: [], members: [], tasks: [] }) await handle.dispose() }) - it('rejects stale Agent identities and non-Team subagent children', async () => { - const { ctx, lead } = await setup([textResponse('done')]) - const started = await ctx.subagents.startActivation({ - delivery: 'parent', - provider: 'spawn', - label: 'ordinary worker', - request: { prompt: content('ordinary'), parent: lead }, - signal: SIGNAL, + it('resolves task assignees only by id, including historical members', async () => { + const { lead } = await setup([]) + const state = { + ...emptyTeamState(lead.id, SessionLogOffset(0)), + children: [{ id: SessionId('named-child'), description: 'reviewer' }], + members: [ + { id: SessionId('old-child'), name: 'legacy', description: 'd', provider: 'spawn', context: 'fresh' as const, phase: 'active' as const }, + { id: SessionId('failed-child'), name: 'broken', description: 'd', provider: 'spawn', context: 'fresh' as const, phase: 'failed' as const }, + ], + } + for (const id of [lead.id, SessionId('named-child'), SessionId('old-child')]) { + expect(resolveMember(lead, state, id)).toBe(id) + } + for (const alias of ['lead', 'reviewer', 'legacy', 'broken', 'failed-child']) { + expect(() => resolveMember(lead, state, SessionId(alias))).toThrow(expect.objectContaining({ code: 'TEAM_MEMBER_NOT_FOUND' })) + } + }) + + it('constructs with the default task limit and serializes concurrent task commits', async () => { + const { ctx, lead, teamFiber } = await setup([]) + await teamFiber.dispose() + const direct = new TeamService(ctx) + const [first, second] = await Promise.all([ + direct.createTask(lead, { subject: 'one', description: 'one' }), + direct.createTask(lead, { subject: 'two', description: 'two' }), + ]) + expect([first.id, second.id]).toEqual([TeamTaskId('task-1'), TeamTaskId('task-2')]) + }) + + it('does not treat a descriptor-backed Session without a parent as a Team member', async () => { + const { ctx, lead } = await setup([]) + lead.session.append('subagent/descriptor', { version: 3, mode: 'continuable', provider: 'spawn', label: 'forged' }) + expect(ctx.agentTeams.tryMembership(lead)).toBeUndefined() + }) + + it('does not reinterpret an orphaned subagent child as a Team root', async () => { + const { ctx } = await setup([textResponse('ordinary child done'), textResponse('orphan turn')]) + const parent = await ctx.agents.create({ + sessionId: SessionId('temporary-parent'), + agentOptions: { provider: 'mock', model: 'mock' }, }) - const live = ctx.agents.get(started.childId) - if (live !== undefined) expect(ctx.agentTeams.tryMembership(live)).toBeUndefined() - await waitNoAgent(ctx, started.childId) - expect(() => ctx.agentTeams.membership(lead)).not.toThrow() + const child = await spawn(ctx, parent.agent, 'orphan') + await waitNoAgent(ctx, child) + await parent.dispose() + const orphan = await ctx.agents.resume({ + resumeSessionId: child, + agentOptions: { provider: 'mock', model: 'mock' }, + }) + expect(ctx.agentTeams.tryMembership(orphan.agent)).toBeUndefined() + orphan.agent.followup(createUserMessage({ content: content('run without a Team'), source: { kind: 'user' } })) + await orphan.agent.whenIdle() + await orphan.dispose() - const impostor = { ...lead } as Agent - expect(ctx.agentTeams.tryMembership(impostor)).toBeUndefined() - expect(() => ctx.agentTeams.membership(impostor)).toThrow(expect.objectContaining({ code: 'TEAM_NOT_MEMBER' })) - - const orphanRoot = await ctx.agents.create({ + const rootWithOfflineSource = await ctx.agents.create({ sessionId: SessionId('orphan-ordinary-root'), meta: { parentSession: SessionId('absent-parent') }, agentOptions: { provider: 'mock', model: 'mock' }, }) - expect(ctx.agentTeams.membership(orphanRoot.agent)).toMatchObject({ role: 'lead', name: 'lead' }) - await orphanRoot.dispose() - }) - - it('does not reinterpret an orphaned provider child or malformed parent stream as a Team root', async () => { - const first = await setup([textResponse('ordinary child done')]) - const parent = await first.ctx.agents.create({ - sessionId: SessionId('temporary-parent'), - agentOptions: { provider: 'mock', model: 'mock' }, - }) - const started = await first.ctx.subagents.startActivation({ - delivery: 'parent', - provider: 'spawn', - label: 'ordinary child', - request: { prompt: content('finish'), parent: parent.agent }, - signal: SIGNAL, - }) - await waitNoAgent(first.ctx, started.childId) - await parent.dispose() - const orphan = await first.ctx.agents.resume({ - resumeSessionId: started.childId, - agentOptions: { provider: 'mock', model: 'mock' }, - }) - expect(first.ctx.agentTeams.tryMembership(orphan.agent)).toBeUndefined() - expect(teamInternals(first.ctx).roster.liveChildrenByRoot()).toEqual(new Map()) - await orphan.dispose() - - const second = await setup([]) - const child = await second.ctx.agents.create({ - sessionId: SessionId('malformed-parent-child'), - meta: { parentSession: second.lead.id }, - agentOptions: { provider: 'mock', model: 'mock' }, - }) - const journal = teamInternals(second.ctx).journal - const state = journal.state.bind(journal) - journal.state = () => { throw new Error('malformed Team stream') } - expect(second.ctx.agentTeams.tryMembership(child.agent)).toBeUndefined() - journal.state = state - await child.dispose() + expect(ctx.agentTeams.membership(rootWithOfflineSource.agent)).toMatchObject({ role: 'lead' }) + await rootWithOfflineSource.dispose() }) }) @@ -606,9 +284,9 @@ describe('Team shared task DAG', () => { it('enforces CAS, ownership, dependencies, transitions, and write-scope warnings', async () => { const { ctx, lead } = await setup(['hang', 'hang', textResponse('beta integrated update')]) const firstMember = await spawn(ctx, lead, 'alpha') - const alpha = await waitRunning(ctx, firstMember.member.id) + const alpha = await waitRunning(ctx, firstMember) const secondMember = await spawn(ctx, lead, 'beta') - const beta = await waitRunning(ctx, secondMember.member.id) + const beta = await waitRunning(ctx, secondMember) const first = await ctx.agentTeams.createTask(alpha, { subject: 'first', @@ -674,8 +352,8 @@ describe('Team shared task DAG', () => { expect(released).toMatchObject({ status: 'pending', ready: true }) expect('ownerId' in released).toBe(false) - ctx.agentTeams.interrupt(lead, 'alpha') - ctx.agentTeams.interrupt(lead, 'beta') + ctx.subagents.interrupt(alpha.id, { kind: 'ancestor', agent: lead }) + ctx.subagents.interrupt(beta.id, { kind: 'ancestor', agent: lead }) await Promise.all([waitNoAgent(ctx, alpha.id), waitNoAgent(ctx, beta.id)]) }) @@ -754,19 +432,19 @@ describe('Team shared task DAG', () => { it('supports Lead reassignment, completion, reopen, and deletion permissions', async () => { const { ctx, lead } = await setup(['hang']) const started = await spawn(ctx, lead, 'owner') - const owner = await waitRunning(ctx, started.member.id) + const owner = await waitRunning(ctx, started) const task = await ctx.agentTeams.createTask(owner, { subject: 'lifecycle', description: 'lifecycle' }) const assigned = await ctx.agentTeams.updateTask(lead, { taskId: task.id, expectedRevision: task.revision, action: 'reassign', - owner: 'owner', + owner: owner.id, }) await expect(ctx.agentTeams.updateTask(owner, { taskId: task.id, expectedRevision: assigned.revision, action: 'reassign', - owner: 'lead', + owner: lead.id, })).rejects.toMatchObject({ code: 'TEAM_LEAD_REQUIRED' }) const complete = await ctx.agentTeams.updateTask(owner, { taskId: task.id, @@ -777,7 +455,7 @@ describe('Team shared task DAG', () => { taskId: task.id, expectedRevision: complete.revision, action: 'reassign', - owner: 'lead', + owner: lead.id, })).rejects.toMatchObject({ code: 'TEAM_TASK_INVALID_TRANSITION' }) const reopened = await ctx.agentTeams.updateTask(owner, { taskId: task.id, @@ -802,14 +480,14 @@ describe('Team shared task DAG', () => { action: 'edit', subject: 'late', })).rejects.toMatchObject({ code: 'TEAM_TASK_DELETED' }) - ctx.agentTeams.interrupt(lead, 'owner') + ctx.subagents.interrupt(owner.id, { kind: 'ancestor', agent: lead }) await waitNoAgent(ctx, owner.id) }) it('covers partial edits, Lead ownership, unassignment, and blocked reassignment', async () => { const { ctx, lead } = await setup(['hang']) const started = await spawn(ctx, lead, 'editor') - const editor = await waitRunning(ctx, started.member.id) + const editor = await waitRunning(ctx, started) const blocker = await ctx.agentTeams.createTask(lead, { subject: 'blocker', description: 'blocker' }) const task = await ctx.agentTeams.createTask(lead, { subject: 'draft', @@ -820,19 +498,19 @@ describe('Team shared task DAG', () => { taskId: TeamTaskId('missing-update'), expectedRevision: 1, action: 'delete', })).rejects.toMatchObject({ code: 'TEAM_TASK_NOT_FOUND' }) await expect(ctx.agentTeams.updateTask(lead, { - taskId: task.id, expectedRevision: task.revision, action: 'reassign', owner: 'editor', + taskId: task.id, expectedRevision: task.revision, action: 'reassign', owner: editor.id, })).rejects.toMatchObject({ code: 'TEAM_TASK_BLOCKED' }) const leadClaim = await ctx.agentTeams.updateTask(lead, { taskId: blocker.id, expectedRevision: blocker.revision, action: 'claim', }) - expect(leadClaim.ownerName).toBe('lead') + expect(leadClaim.ownerId).toBe(lead.id) const completedBlocker = await ctx.agentTeams.updateTask(lead, { taskId: blocker.id, expectedRevision: leadClaim.revision, action: 'complete', }) expect(completedBlocker.status).toBe('completed') const assigned = await ctx.agentTeams.updateTask(lead, { - taskId: task.id, expectedRevision: task.revision, action: 'reassign', owner: 'editor', + taskId: task.id, expectedRevision: task.revision, action: 'reassign', owner: editor.id, }) const subject = await ctx.agentTeams.updateTask(editor, { taskId: task.id, expectedRevision: assigned.revision, action: 'edit', subject: 'edited subject', @@ -855,7 +533,7 @@ describe('Team shared task DAG', () => { writeScopes: ['src/nested'], }) const unassigned = await ctx.agentTeams.updateTask(lead, { - taskId: task.id, expectedRevision: scopes.revision, action: 'reassign', owner: ' ', + taskId: task.id, expectedRevision: scopes.revision, action: 'reassign', }) expect(unassigned).toMatchObject({ status: 'pending' }) expect('ownerId' in unassigned).toBe(false) @@ -881,183 +559,13 @@ describe('Team shared task DAG', () => { expect(ctx.agentTeams.getTask(lead, broad.id).writeScopeWarnings) .toEqual([`write scopes overlap with ${narrow.id}`]) - ctx.agentTeams.interrupt(lead, 'editor') + ctx.subagents.interrupt(editor.id, { kind: 'ancestor', agent: lead }) await waitNoAgent(ctx, editor.id) }) }) -describe('Team direct messages and waiting', () => { - it('accepts peer and Lead messages with actual sender identity and wakes Team waits', async () => { - const { ctx, lead, teamFiber } = await setup(['hang', 'hang', 'hang']) - const alpha = await waitRunning(ctx, (await spawn(ctx, lead, 'alpha')).member.id) - const beta = await waitRunning(ctx, (await spawn(ctx, lead, 'beta')).member.id) - lead.followup(createUserMessage({ content: content('busy lead'), source: { kind: 'user' } })) - await waitRunning(ctx, lead.id) - for (const [target, agent] of [['beta', beta], ['lead', lead]] as const) { - const waiting = ctx.agentTeams.waitForChange(alpha, 10_000, SIGNAL) - const result = await ctx.agentTeams.sendMessage(alpha, { target, content: content('update'), signal: SIGNAL }) - await expect(waiting).resolves.toEqual({ timedOut: false }) - expect(Object.keys(result)).toEqual(['messageId']) - expect(result.messageId).toBeTypeOf('string') - expect(agent.inbox.nextStep.find(message => message.id === result.messageId)).toMatchObject({ - source: { kind: 'agent-message', form: 'relay', senderSessionId: alpha.id }, - content: [{ type: 'text', text: 'Team message from alpha:' }, { type: 'text', text: 'update' }], - }) - } - expect(lead.session.snapshotEvents().filter(event => event.type.startsWith('team/message/'))).toEqual([]) - await teamFiber.dispose() - lead.cancel({ kind: 'parent' }) - await lead.whenIdle() - }) - - it('steers an idle Lead and cold-resumes a sibling with actual sender attribution', async () => { - const { ctx, lead } = await setup(['hang', textResponse('initial'), textResponse('lead'), textResponse('peer')]) - const alpha = await waitRunning(ctx, (await spawn(ctx, lead, 'alpha')).member.id) - const beta = await spawn(ctx, lead, 'beta') - await waitNoAgent(ctx, beta.member.id) - const leadResult = await ctx.agentTeams.sendMessage(alpha, { target: 'lead', content: content('report'), signal: SIGNAL }) - await lead.whenIdle() - const peerResult = await ctx.agentTeams.sendMessage(alpha, { target: 'beta', content: content('report'), signal: SIGNAL }) - await waitNoAgent(ctx, beta.member.id) - await ctx.sessions.flush(lead.session) - for (const [id, receipt] of [[lead.id, leadResult], [beta.member.id, peerResult]] as const) { - const messages = (await storedEvents(ctx, id)).flatMap(event => event.type === 'user/message' ? [event.data] : []) - expect(messages.find(message => message.id === receipt.messageId)).toMatchObject({ - source: { kind: 'agent-message', senderSessionId: alpha.id }, - }) - } - }) - - it('passes concurrent attempts to the lower delivery owner without a Team queue', async () => { - const { ctx, lead } = await setup(['hang']) - const target = await waitRunning(ctx, (await spawn(ctx, lead, 'target')).member.id) - const entered = Promise.withResolvers() - const release = Promise.withResolvers() - const admitted: string[] = [] - vi.spyOn(hostDeliverer(ctx), deliverSubagentPrompt) - .mockImplementation(async (_parent, _child, blocks, source) => { - const last = blocks.at(-1) - const text = last?.type === 'text' ? last.text : '' - admitted.push(text) - if (text === 'first') { entered.resolve(undefined); await release.promise } - const input = createUserMessage({ content: blocks, source }) - target.steer(input) - return input.id - }) - const first = ctx.agentTeams.sendMessage(lead, { target: 'target', content: content('first'), signal: SIGNAL }) - try { - await entered.promise - const second = await ctx.agentTeams.sendMessage(lead, { target: 'target', content: content('second'), signal: SIGNAL }) - expect(admitted).toEqual(['first', 'second']) - expect(target.inbox.nextStep.some(message => message.id === second.messageId)).toBe(true) - expect(durable(lead).pendingMessages).toEqual([]) - } finally { - release.resolve(undefined) - await first - } - }) - - it('rejects failed attempts without retained intent or activity and supports a later explicit attempt', async () => { - const { ctx, lead } = await setup(['hang']) - const target = await waitRunning(ctx, (await spawn(ctx, lead, 'target')).member.id) - const failure = new Error('delivery unavailable') - const delivery = vi.spyOn(hostDeliverer(ctx), deliverSubagentPrompt).mockRejectedValueOnce(failure) - const waiting = ctx.agentTeams.waitForChange(lead, 10_000, SIGNAL) - let changed = false - void waiting.then(() => { changed = true }) - await expect(ctx.agentTeams.sendMessage(lead, { target: 'target', content: content('first'), signal: SIGNAL })).rejects.toBe(failure) - expect(changed).toBe(false) - expect(durable(lead).pendingMessages).toEqual([]) - delivery.mockRestore() - const receipt = await ctx.agentTeams.sendMessage(lead, { target: 'target', content: content('second'), signal: SIGNAL }) - await expect(waiting).resolves.toEqual({ timedOut: false }) - expect(target.inbox.nextStep.map(message => message.id)).toContain(receipt.messageId) - expect(target.inbox.nextStep.flatMap(message => message.content)).not.toContainEqual({ type: 'text', text: 'first' }) - }) - - it('cancels an ongoing send without retaining intent and leaves the service usable', async () => { - const { ctx, lead } = await setup([textResponse('done')]) - const target = await spawn(ctx, lead, 'target') - await waitNoAgent(ctx, target.member.id) - const entered = Promise.withResolvers() - const cancellation = new Error('caller cancelled') - const delivery = vi.spyOn(hostDeliverer(ctx), deliverSubagentPrompt) - .mockImplementation(async (_parent, _id, _content, _source, signal) => { - entered.resolve(undefined) - return await new Promise((_resolve, reject) => { - signal.addEventListener('abort', () => { - const reason: unknown = signal.reason - reject(reason instanceof Error ? reason : new Error(String(reason))) - }, { once: true }) - }) - }) - const controller = new AbortController() - const sending = ctx.agentTeams.sendMessage(lead, { target: 'target', content: content('cancelled'), signal: controller.signal }) - const rejected = expect(sending).rejects.toBe(cancellation) - await entered.promise - controller.abort(cancellation) - await rejected - delivery.mockRestore() - expect(durable(lead).pendingMessages).toEqual([]) - await ctx.agentTeams.createTask(lead, { subject: 'still usable', description: 'after send cancellation' }) - }) - - it('serializes task commits while direct sends use the independent inbox owner', async () => { - const { ctx, lead } = await setup(['hang']) - const target = await waitRunning(ctx, (await spawn(ctx, lead, 'target')).member.id) - const entered = Promise.withResolvers() - const release = Promise.withResolvers() - const flush = ctx.sessions.flush.bind(ctx.sessions) - vi.spyOn(ctx.sessions, 'flush').mockImplementationOnce(async (session) => { - entered.resolve(undefined) - await release.promise - return await flush(session) - }) - const first = ctx.agentTeams.createTask(lead, { subject: 'first', description: 'first' }) - await entered.promise - const second = ctx.agentTeams.createTask(lead, { subject: 'second', description: 'second' }) - try { - const receipt = await ctx.agentTeams.sendMessage(lead, { target: 'target', content: content('independent'), signal: SIGNAL }) - expect(target.inbox.nextStep.some(message => message.id === receipt.messageId)).toBe(true) - } finally { - release.resolve(undefined) - await Promise.all([first, second]) - } - expect(ctx.agentTeams.listTasks(lead).map(task => task.subject)).toEqual(['first', 'second']) - }) - - it('enforces complete UTF-8 byte limits, exact caller authority, target membership and pre-acceptance cancellation', async () => { - const blocks = [{ type: 'text', text: 'Team message from lead:' }, ...content('中文')] - const { ctx, lead } = await setup(['hang'], { maxMessageBytes: Buffer.byteLength(JSON.stringify(blocks)) }) - const target = await waitRunning(ctx, (await spawn(ctx, lead, 'target')).member.id) - await ctx.agentTeams.sendMessage(lead, { target: 'target', content: content('中文'), signal: SIGNAL }) - await expect(ctx.agentTeams.sendMessage(lead, { target: 'target', content: content('中文x'), signal: SIGNAL })) - .rejects.toMatchObject({ code: 'TEAM_MESSAGE_TOO_LARGE' }) - await expect(ctx.agentTeams.sendMessage({ ...lead }, { target: 'target', content: content('x'), signal: SIGNAL })) - .rejects.toMatchObject({ code: 'TEAM_NOT_MEMBER' }) - await expect(ctx.agentTeams.sendMessage(target, { target: 'target', content: content('x'), signal: SIGNAL })) - .rejects.toMatchObject({ code: 'TEAM_SELF_MESSAGE' }) - await expect(ctx.agentTeams.sendMessage(lead, { target: 'missing', content: content('x'), signal: SIGNAL })) - .rejects.toMatchObject({ code: 'TEAM_MEMBER_NOT_FOUND' }) - const cancellation = new Error('cancel before acceptance') - await expect(ctx.agentTeams.sendMessage(lead, { target: 'target', content: content('x'), signal: AbortSignal.abort(cancellation) })) - .rejects.toBe(cancellation) - }) - - it('interrupts only the current turn and retains an already accepted follow-up', async () => { - const { ctx, lead } = await setup(['hang', textResponse('after interrupt')]) - const started = await spawn(ctx, lead, 'worker') - const worker = await waitRunning(ctx, started.member.id) - const followup = await ctx.agentTeams.sendMessage(lead, { - target: 'worker', content: content('retained follow-up'), signal: SIGNAL, - }) - expect(ctx.agentTeams.interrupt(lead, 'worker')).toEqual({ previousStatus: 'running' }) - await vi.waitFor(() => { expect(worker.status).toBe('idle') }) - expect(worker.inbox.nextStep.some(message => message.id === followup.messageId)).toBe(true) - worker.cancel({ kind: 'parent' }) - await waitNoAgent(ctx, worker.id) - }) +describe('Team waiting', () => { it('waits for one change, supports cancellation, times out, and releases waiters on HMR disposal', async () => { const ctx = new Context() contexts.push(ctx) @@ -1134,335 +642,34 @@ describe('Team direct messages and waiting', () => { expect(ctx.get('agentTeams')).toBeUndefined() }) - it('disposes live teammate Activations and their waits when the Team service unloads', async () => { + it('wakes waiters on member status changes and accepted messages, and reports active peers', async () => { + const { ctx, lead } = await setup(['hang']) + expect(ctx.agentTeams.hasActivePeer(lead)).toBe(false) + const alphaId = await spawn(ctx, lead, 'alpha') + const alpha = await waitRunning(ctx, alphaId) + expect(ctx.agentTeams.hasActivePeer(lead)).toBe(true) + expect(ctx.agentTeams.hasActivePeer(alpha)).toBe(false) + + const messaged = ctx.agentTeams.waitForChange(lead, 10_000, SIGNAL) + alpha.steer(createUserMessage({ content: content('extra context'), source: { kind: 'user' } })) + await expect(messaged).resolves.toEqual({ timedOut: false }) + + const statusChanged = ctx.agentTeams.waitForChange(lead, 10_000, SIGNAL) + ctx.subagents.interrupt(alpha.id, { kind: 'ancestor', agent: lead }) + await expect(statusChanged).resolves.toEqual({ timedOut: false }) + }) + + it('releases waits on unload and leaves teammate Activations to the subagent runtime', async () => { const { ctx, lead, teamFiber } = await setup(['hang']) - const started = await spawn(ctx, lead, 'dispose-worker') - await waitRunning(ctx, started.member.id) + const alpha = await waitRunning(ctx, await spawn(ctx, lead, 'alpha')) const waiting = ctx.agentTeams.waitForChange(lead, 10_000, SIGNAL) await teamFiber.dispose() await expect(waiting).resolves.toEqual({ timedOut: false }) - expect(ctx.agents.get(started.member.id)).toBeUndefined() + expect(ctx.agents.get(alpha.id)).toBe(alpha) expect(ctx.get('agentTeams')).toBeUndefined() - }) - - it('closes creation admission and drains an in-flight spawn before unload completes', async () => { - const { ctx, lead, teamFiber } = await setup(['hang']) - const service = ctx.agentTeams - const start = ctx.subagents.startActivation.bind(ctx.subagents) - const entered = Promise.withResolvers() - const release = Promise.withResolvers() - let childId: SessionId | undefined - vi.spyOn(ctx.subagents, 'startActivation').mockImplementation(async (spec) => { - childId = spec.childId - entered.resolve(undefined) - await release.promise - return start(spec) - }) - const spawning = spawn(ctx, lead, 'disposing-worker') - const rejected = expect(spawning).rejects.toMatchObject({ code: 'TEAM_DISPOSED' }) - await entered.promise - - const disposal = teamFiber.dispose() - await Promise.resolve(undefined) - await expect(service.waitForChange(lead, 3_600_000, SIGNAL)).resolves.toEqual({ timedOut: false }) - await expect(service.spawnTeammate(lead, { - name: 'late-worker', - description: 'must not enter after disposal', - prompt: content('late task'), - context: 'fresh', - provider: 'spawn', - signal: SIGNAL, - })).rejects.toMatchObject({ code: 'TEAM_DISPOSED' }) - release.resolve(undefined) - - await rejected - await disposal - if (childId !== undefined) expect(ctx.agents.get(childId)).toBeUndefined() - expect(ctx.get('agentTeams')).toBeUndefined() - }) - - it('retains an in-flight operation failure during disposal', async () => { - const { ctx } = await setup([]) - const internal = teamInternals(ctx) - const cleanupFailure = new Error('operation failed') - void internal.lifecycle.track(Promise.reject(cleanupFailure)) - - await expect(internal.disposeRuntime()).rejects.toMatchObject({ errors: [cleanupFailure] }) - }) - - it('recognizes wrapped and coded runtime cancellation during disposal settlement', async () => { - const open = new TeamRuntimeLifecycle(100) - const ordinaryFailure = new Error('ordinary failure before disposal') - const openFailures: unknown[] = [] - await open.settle([Promise.reject(ordinaryFailure)], openFailures) - expect(openFailures).toEqual([ordinaryFailure]) - - const lifecycle = new TeamRuntimeLifecycle(100) - lifecycle.close() - const failures: unknown[] = [] - await lifecycle.settle([ - Promise.reject(new Error('wrapped cancellation', { cause: lifecycle.reason })), - Promise.reject(new TeamError('translated cancellation', 'TEAM_DISPOSED')), - ], failures) - expect(failures).toEqual([]) - - const cyclic = new Error('unrelated cyclic failure') - cyclic.cause = cyclic - await lifecycle.settle([Promise.reject(cyclic)], failures) - expect(failures).toEqual([cyclic]) - }) - - it('disposes a live child even after its durable member edge becomes failed', async () => { - const { ctx, lead } = await setup(['hang']) - const childId = SessionId('failed-live-child') - const member = { - id: childId, - name: 'failed-live-worker', - description: 'failed-live-worker responsibility', - provider: 'spawn', - context: 'fresh' as const, - phase: 'provisioning' as const, - } - lead.session.append('team/member', { - version: 2, - teamId: TeamId(lead.id), - member, - }) - await ctx.subagents.startActivation({ - delivery: 'parent', - childId, - provider: 'spawn', - label: member.description, - request: { prompt: content('failed child task'), parent: lead }, - signal: SIGNAL, - }) - await waitRunning(ctx, childId) - lead.session.append('team/member', { - version: 2, - teamId: TeamId(lead.id), - member: { - ...member, - phase: 'failed', - error: 'creation cleanup is pending', - }, - }) - await ctx.sessions.flush(lead.session) - expect(ctx.agentTeams.listMembers(lead)[1]?.status).toBe('failed') - - const internal = ctx.agentTeams as unknown as { disposeRuntime(): Promise } - await internal.disposeRuntime() - expect(ctx.agents.get(childId)).toBeUndefined() - }) - - it('aborts and awaits an admitted cold mailbox dispatch during disposal', async () => { - const { ctx, lead } = await setup([textResponse('worker done')]) - const started = await spawn(ctx, lead, 'mailbox-worker') - await waitNoAgent(ctx, started.member.id) - const entered = Promise.withResolvers() - const aborted = Promise.withResolvers() - const release = Promise.withResolvers() - vi.spyOn(hostDeliverer(ctx), deliverSubagentPrompt) - .mockImplementation(async (_parent, _childId, _content, _source, signal) => { - entered.resolve(undefined) - return await new Promise((_resolve, reject) => { - signal.addEventListener('abort', () => { - aborted.resolve(undefined) - void release.promise.then(() => { - const reason: unknown = signal.reason - reject(reason instanceof Error ? reason : new Error(String(reason))) - }) - }, { once: true }) - }) - }) - - const sending = ctx.agentTeams.sendMessage(lead, { - target: 'mailbox-worker', - content: content('resume during disposal'), - signal: SIGNAL, - }) - await entered.promise - const internal = ctx.agentTeams as unknown as { disposeRuntime(): Promise } - let disposed = false - const disposal = internal.disposeRuntime().then(() => { disposed = true }) - await aborted.promise - await Promise.resolve(undefined) - expect(disposed).toBe(false) - release.resolve(undefined) - - await expect(sending).rejects.toMatchObject({ code: 'TEAM_DISPOSED' }) - await disposal - expect(disposed).toBe(true) - expect(ctx.agents.get(started.member.id)).toBeUndefined() - }) - - it('bounds Team runtime disposal when a continuation drain never settles', { timeout: 30_000 }, async () => { - const { ctx, lead, teamFiber } = await setup(['hang'], { disposalTimeoutMs: 25 }) - const started = await spawn(ctx, lead, 'stuck-worker') - await waitRunning(ctx, started.member.id) - const drain = vi.spyOn(ctx.subagents, 'drainChildren') - .mockImplementation(() => new Promise(() => {})) - - const outcome = await Promise.race([ - teamFiber.dispose().then(() => 'disposed'), - new Promise<'hung'>((resolve) => { setTimeout(() => { resolve('hung') }, 1_000) }), - ]) - expect(outcome).toBe('disposed') - expect(drain).toHaveBeenCalledWith(lead, [started.member.id]) - expect(ctx.get('agentTeams')).toBeUndefined() - }) - - it('shares one settlement deadline across admitted operations', async () => { - const { ctx } = await setup([], { disposalTimeoutMs: 25 }) - const internal = teamInternals(ctx) - const first = Promise.withResolvers() - const second = Promise.withResolvers() - void internal.lifecycle.track(first.promise) - void internal.lifecycle.track(second.promise) - vi.useFakeTimers() - let failure: unknown - const disposal = internal.disposeRuntime().catch((error: unknown) => { failure = error }) - try { - await vi.advanceTimersByTimeAsync(25) - expect(failure).toBeInstanceOf(AggregateError) - await disposal - } finally { - first.resolve(undefined) - second.resolve(undefined) - vi.useRealTimers() - await disposal - } - }) - - it('bounds disposal while an admitted operation ignores cancellation', async () => { - const { ctx, lead } = await setup([], { disposalTimeoutMs: 25 }) - const internal = teamInternals(ctx) - void internal.lifecycle.track(new Promise(() => {})) - - await expect(internal.disposeRuntime()).rejects.toBeInstanceOf(AggregateError) - await expect(ctx.agentTeams.spawnTeammate(lead, { - name: 'after-timeout', - description: 'admission remains closed', - prompt: content('must reject'), - context: 'fresh', - provider: 'spawn', - signal: SIGNAL, - })).rejects.toMatchObject({ code: 'TEAM_DISPOSED' }) - await expect(ctx.agentTeams.sendMessage(lead, { - target: 'nobody', content: content('must reject'), signal: SIGNAL, - })).rejects.toMatchObject({ code: 'TEAM_DISPOSED' }) - - }) - - it('contains recovery callback failures and ignores work scheduled after disposal', async () => { - const { ctx, lead, teamFiber } = await setup([]) - const warnings: string[] = [] - ctx.logger.warn = ((value: unknown) => { warnings.push(String(value)) }) as typeof ctx.logger.warn - const internal = teamInternals(ctx) - internal.roster.recoverFor = async () => { throw new Error('forced recovery failure') } - internal.scheduleRecovery(lead) - await Promise.resolve(undefined) - await Promise.resolve(undefined) - expect(warnings.some(warning => warning.includes('forced recovery failure'))).toBe(true) - - lead.session.append('user/message', createUserMessage({ - content: content('orphan Team source'), - source: { - kind: 'team-message', - teamId: TeamId('absent-team'), - messageId: TeamMessageId('absent-team-message'), - senderId: SessionId('absent-sender'), - senderName: 'absent', - }, - }), { surfaceOp: 'append' }) - await Promise.resolve(undefined) - - const entered = Promise.withResolvers() - const release = Promise.withResolvers() - internal.roster.recoverFor = async () => { - entered.resolve(undefined) - await release.promise - throw new Error('failure after disposal') - } - internal.scheduleRecovery(lead) - await entered.promise - await teamFiber.dispose() - release.resolve(undefined) - await Promise.resolve(undefined) - await Promise.resolve(undefined) - internal.scheduleRecovery(lead) - await Promise.resolve(undefined) - }) - - it('reports contained teardown failures without retaining the Team service', async () => { - const { ctx, lead, teamFiber } = await setup(['hang']) - const started = await spawn(ctx, lead, 'failing-drain') - await waitRunning(ctx, started.member.id) - vi.spyOn(ctx.subagents, 'drainDescendants').mockRejectedValueOnce(new Error('drain failure')) - - await teamFiber.dispose() - expect(ctx.get('agentTeams')).toBeUndefined() - }) - - it('reconciles mismatched persisted children and ignores a concurrently settled member', async () => { - const first = await setup([]) - const liveId = SessionId('live-provisioning-child') - const live = await first.ctx.agents.create({ - sessionId: liveId, - meta: { parentSession: first.lead.id }, - agentOptions: { provider: 'mock', model: 'mock' }, - }) - const provisioning = { - id: liveId, - name: 'mismatched-child', - description: 'mismatched persisted child', - provider: 'spawn', - context: 'fresh' as const, - phase: 'provisioning' as const, - } - first.lead.session.append('team/member', { - version: 2, teamId: TeamId(first.lead.id), member: provisioning, - }) - const reconcileFirst = teamInternals(first.ctx).roster - await reconcileFirst.reconcileProvisioning(first.lead, SIGNAL) - expect(durable(first.lead).members[0]?.phase).toBe('provisioning') - live.agent.session.append('user/message', createUserMessage({ - content: content('persist mismatched child'), source: { kind: 'user' }, - }), { surfaceOp: 'append' }) - await first.ctx.sessions.flush(live.agent.session) - await live.dispose() - await reconcileFirst.reconcileProvisioning(first.lead, SIGNAL) - expect(durable(first.lead).members[0]).toMatchObject({ - phase: 'failed', - error: 'persisted child Session does not match the provisioned continuation', - }) - - const second = await setup([]) - const childId = SessionId('concurrently-settled-child') - const member = { ...provisioning, id: childId, name: 'concurrent-child' } - second.lead.session.append('team/member', { - version: 2, teamId: TeamId(second.lead.id), member, - }) - const entered = Promise.withResolvers() - const release = Promise.withResolvers() - vi.spyOn(second.ctx.sessionPersistence, 'open').mockImplementationOnce(async () => { - entered.resolve(undefined) - await release.promise - throw new Error('late inspection failure') - }) - const reconcileSecond = teamInternals(second.ctx).roster - const reconciling = reconcileSecond.reconcileProvisioning(second.lead, SIGNAL) - await entered.promise - second.lead.session.append('team/member', { - version: 2, - teamId: TeamId(second.lead.id), - member: { ...member, phase: 'failed', error: 'settled elsewhere' }, - }) - release.resolve(undefined) - await reconciling - expect(durable(second.lead).members[0]).toMatchObject({ - phase: 'failed', error: 'settled elsewhere', - }) + ctx.subagents.interrupt(alpha.id, { kind: 'ancestor', agent: lead }) + await waitNoAgent(ctx, alpha.id) }) }) diff --git a/packages/experimental/agent-team/tsconfig.json b/packages/experimental/agent-team/tsconfig.json index e9c2a2f0df..b0562ab6c2 100644 --- a/packages/experimental/agent-team/tsconfig.json +++ b/packages/experimental/agent-team/tsconfig.json @@ -14,7 +14,6 @@ { "path": "../../core/session" }, { "path": "../../core/agent" }, { "path": "../../subagent/subagent" }, - { "path": "../../session/session-persistence" }, { "path": "../../session/session-projection" } ] } diff --git a/packages/experimental/client-ui-agent-team/README.i18n.yaml b/packages/experimental/client-ui-agent-team/README.i18n.yaml index 643f2124cc..9db36471d1 100644 --- a/packages/experimental/client-ui-agent-team/README.i18n.yaml +++ b/packages/experimental/client-ui-agent-team/README.i18n.yaml @@ -18,14 +18,14 @@ en: ba021754d3ae6d9e zh: 18017b40f13e6ffd /deepseek-ai-dsh-experimental-client-ui-agent-team/use-this-package/inspect-and-navigate-the-roster: - en: 62778e911a4ee9e9 - zh: dff1dbbc3d5e4af6 + en: d7c86840f03912b3 + zh: c6aa34d5c7550dd2 /deepseek-ai-dsh-experimental-client-ui-agent-team/use-this-package/inspect-the-task-board: - en: cf09faa436dad9fd - zh: e7423f4111eb88f9 + en: 5c0ec10800cc9fda + zh: b80f1e725dbb894a /deepseek-ai-dsh-experimental-client-ui-agent-team/understand-the-implementation: - en: 3ea723e7ea467be7 - zh: dba02b20bef0fd7f + en: 0bdb38c8f75101b1 + zh: fa31d7a4b1fff12d /deepseek-ai-dsh-experimental-client-ui-agent-team/further-exploration: en: 71ac041029c64d5a zh: fef2e4c5dccb8d1b diff --git a/packages/experimental/client-ui-agent-team/README.md b/packages/experimental/client-ui-agent-team/README.md index 718aa773eb..18e163cbc6 100644 --- a/packages/experimental/client-ui-agent-team/README.md +++ b/packages/experimental/client-ui-agent-team/README.md @@ -31,13 +31,13 @@ Enable this package through [`@deepseek-ai/dsh-experimental-agent-team-profile`] The panel shows the Lead Session's roster and task board from the shared Session store. Task and roster updates appear while the panel stays open. Opening the panel performs no projection requests. The panel shows a loading notice while the conversation or Session list is loading, and an unavailable notice when no Team value is present afterward. -Roster rows show durable names and phases. Provisioning and running members use the shared ongoing loader, inactive members use a person icon, and failed members use error. Live Session status supplies running activity; the shared `modelSelection` projection supplies a model when available. The current conversation carries a Current chat tag and cannot be selected. Selecting the Lead from a teammate conversation opens the Lead Session directly. Selecting an active teammate opens its ordinary continuable child address. The Host validates the parent, child, and mode when history opens; later human prompts use the same addressed-subagent conversation. +Roster rows show descriptions and durable phases. Provisioning and running members use the shared ongoing loader, inactive members use a person icon, and failed members use error. Live Session status supplies running activity; the shared `modelSelection` projection supplies a model when available. The current conversation carries a Current chat tag and cannot be selected. Selecting the Lead from a teammate conversation opens the Lead Session directly. Selecting an active teammate opens its ordinary continuable child address. The Host validates the parent, child, and mode when history opens; later human prompts use the same addressed-subagent conversation. ### Inspect the task board Ready pending tasks use idle, blocked pending tasks use warning, in-progress tasks use ongoing, and completed tasks use done. -The read-only task board shows task identity, owner, blockers, readiness, advisory write scopes, and overlap warnings. Descriptions longer than two lines have an expand toggle. Section headings show member and task counts; an empty board shows a short description, and a lone member with no tasks uses a single-column panel. Team agents create and update tasks through their tools; the panel provides no task mutation controls. When the projection reports a rejected persisted Team record, the panel shows that failure above the last valid roster and tasks. +Member rows show descriptions, which may repeat; the Lead label is localized. The read-only task board shows task identity, owner description (or id when absent from the roster), blockers, readiness, advisory write scopes, and overlap warnings. Descriptions longer than two lines have an expand toggle. Section headings show member and task counts; an empty board shows a short description, and a lone member with no tasks uses a single-column panel. Team agents create and update tasks through their tools; the panel provides no task mutation controls. When the projection reports a rejected persisted Team record, the panel shows that failure above the last valid roster and tasks. ----- @@ -49,7 +49,7 @@ The read-only task board shows task identity, owner, blockers, readiness, adviso The Client export registers its locale dictionaries and one conversation-header slot through Cordis effects; it mounts no Remote namespace. Disposing the plugin fiber removes both registrations. -The panel renders outside the conversation container and stays within the viewport. Member cards use the shared elevation stroke for their outlines in resting, selected, and hover states. Hovering the trigger opens the panel after 150ms; leaving both trigger and panel closes it after a 120ms grace period. Clicking the trigger pins the panel and moves focus into it. Outside clicks and Escape dismiss the panel; Escape returns focus to the trigger only when focus was inside the panel. In a narrow header, the trigger becomes an icon and opens only on click. The component derives every row from the `useSessions`, `useSessionStatus`, and `useSession` seats: the Lead identity comes from the current Session's subagent address, the Team view from `projectionsBySession[lead].values.agentTeam`, member activity from Session status with the list summary as fallback, and the model from `projectionsBySession[member].values.modelSelection.next`. Each roster row selects its own running state. The only injected callback opens a roster Session using the current and target Session ids. Switching conversations closes the panel and clears a navigation failure. +The panel renders outside the conversation container and stays within the viewport. Member cards use the shared elevation stroke for their outlines in resting, selected, and hover states. Hovering the trigger opens the panel after 150ms; leaving both trigger and panel closes it after a 120ms grace period. Clicking the trigger pins the panel and moves focus into it. Outside clicks and Escape dismiss the panel; Escape returns focus to the trigger only when focus was inside the panel. In a narrow header, the trigger becomes an icon and opens only on click. The component derives every row from the `useSessions`, `useSessionStatus`, and `useSession` seats: the Lead id follows the current subagent address and Session list ancestry to the ordinary root; incomplete ancestry leaves the Team unavailable, the Team view from `projectionsBySession[lead].values.agentTeam`, member activity from Session status with the list summary as fallback, and the model from `projectionsBySession[member].values.modelSelection.next`. Each roster row selects its own running state. The only injected callback opens a roster Session using the current and target Session ids. Switching conversations closes the panel and clears a navigation failure. | File | Role | |---|---| diff --git a/packages/experimental/client-ui-agent-team/README.zh.md b/packages/experimental/client-ui-agent-team/README.zh.md index d912200054..e251b1e8e8 100644 --- a/packages/experimental/client-ui-agent-team/README.zh.md +++ b/packages/experimental/client-ui-agent-team/README.zh.md @@ -31,13 +31,13 @@ kind: "package-reference" 面板从共享 Session store 展示 Lead Session 的 roster 与任务板。面板保持打开时,任务和成员更新会直接出现。打开面板不发起投影请求。会话或 Session 列表正在加载时,面板显示加载提示;加载结束后仍无 Team 值时,显示不可用提示。 -Roster 行展示持久名称与阶段。provisioning 和 running 成员使用共享 ongoing loading,inactive 成员使用人物图标,failed 成员使用 error 红点。实时 Session 状态提供运行活动;共享 `modelSelection` 投影在可用时提供模型。当前会话带有“当前会话”标签且不可选择。在 teammate 会话中选择 Lead 会直接打开 Lead Session。选择 active teammate 会打开其普通 continuable 子会话地址。Host 在打开历史时校验 parent、child 与 mode;后续人类提示词使用同一 addressed-subagent 会话。 +Roster 行展示描述与持久阶段。provisioning 和 running 成员使用共享 ongoing loading,inactive 成员使用人物图标,failed 成员使用 error 红点。实时 Session 状态提供运行活动;共享 `modelSelection` 投影在可用时提供模型。当前会话带有“当前会话”标签且不可选择。在 teammate 会话中选择 Lead 会直接打开 Lead Session。选择 active teammate 会打开其普通 continuable 子会话地址。Host 在打开历史时校验 parent、child 与 mode;后续人类提示词使用同一 addressed-subagent 会话。 ### 查看任务板 可开始的 pending 任务使用 idle 灰点,被依赖阻塞的 pending 任务使用 warning 橙点,in-progress 任务使用 ongoing loading,completed 任务使用 done 绿点。 -只读任务板展示任务标识、负责人、依赖、就绪状态、提示性写入范围与重叠警告。超过两行的描述提供展开按钮。分区标题显示成员与任务数量;空任务板显示简短描述,只有一名成员且无任务时采用单列面板。Team agent 通过工具创建和更新任务;面板不提供任务修改控件。当投影报告某条持久 Team 记录被拒绝时,面板在最后有效的 roster 与任务上方显示该失败。 +成员行展示可重复的描述,Lead 标签由语言包提供。只读任务板展示任务标识、负责人描述(成员列表中缺失时显示 id)、依赖、就绪状态、提示性写入范围与重叠警告。超过两行的描述提供展开按钮。分区标题显示成员与任务数量;空任务板显示简短描述,只有一名成员且无任务时采用单列面板。Team agent 通过工具创建和更新任务;面板不提供任务修改控件。当投影报告某条持久 Team 记录被拒绝时,面板在最后有效的 roster 与任务上方显示该失败。 ----- @@ -49,7 +49,7 @@ Roster 行展示持久名称与阶段。provisioning 和 running 成员使用共 Client export 通过 Cordis effect 注册 locale dictionary 与一个 conversation-header slot;它不挂载任何 Remote namespace。Dispose plugin fiber 会移除这两项 registration。 -面板渲染在会话容器外,并保持在视口范围内。成员卡片在静止、选中和悬停状态下均使用共享 elevation 描边绘制轮廓。悬停触发按钮 150ms 后打开面板;指针离开触发按钮和面板后,经过 120ms 宽限关闭。点击触发按钮会固定面板并将焦点移入其中。点击外部或按 Escape 可关闭面板;仅当焦点原本位于面板内时,Escape 才将焦点返回触发按钮。页头较窄时触发按钮折叠为图标,只响应点击打开。组件从 `useSessions`、`useSessionStatus` 与 `useSession` 座位派生每一行:Lead 身份来自当前 Session 的 subagent address,Team 视图来自 `projectionsBySession[lead].values.agentTeam`,成员活动来自 Session 状态并以列表摘要为后备,model 来自 `projectionsBySession[member].values.modelSelection.next`。每个 roster 行只选择自己的运行状态。唯一的注入回调通过当前与目标 Session 的 id 打开 roster Session。切换会话会关闭面板并清除导航失败。 +面板渲染在会话容器外,并保持在视口范围内。成员卡片在静止、选中和悬停状态下均使用共享 elevation 描边绘制轮廓。悬停触发按钮 150ms 后打开面板;指针离开触发按钮和面板后,经过 120ms 宽限关闭。点击触发按钮会固定面板并将焦点移入其中。点击外部或按 Escape 可关闭面板;仅当焦点原本位于面板内时,Escape 才将焦点返回触发按钮。页头较窄时触发按钮折叠为图标,只响应点击打开。组件从 `useSessions`、`useSessionStatus` 与 `useSession` 座位派生每一行:Lead id 从当前 subagent address 沿 Session 列表祖先查找到普通根;祖先信息不完整时 Team 暂不可用,Team 视图来自 `projectionsBySession[lead].values.agentTeam`,成员活动来自 Session 状态并以列表摘要为后备,model 来自 `projectionsBySession[member].values.modelSelection.next`。每个 roster 行只选择自己的运行状态。唯一的注入回调通过当前与目标 Session 的 id 打开 roster Session。切换会话会关闭面板并清除导航失败。 | 文件 | 职责 | |---|---| diff --git a/packages/experimental/client-ui-agent-team/src/client/TeamAction.tsx b/packages/experimental/client-ui-agent-team/src/client/TeamAction.tsx index b418f20336..3716e9d23c 100644 --- a/packages/experimental/client-ui-agent-team/src/client/TeamAction.tsx +++ b/packages/experimental/client-ui-agent-team/src/client/TeamAction.tsx @@ -16,6 +16,7 @@ import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots' import type {} from '@deepseek-ai/dsh-client-ui-conversation/client' import { NS, type TeamKey } from './locales.ts' import css from './TeamAction.module.css' +import { teamRootId } from './team-root.ts' /** Business actions injected by the browser plugin. */ export interface TeamActionInjected { @@ -109,7 +110,7 @@ function TeamMemberRow({ - {member.name} + {member.description ?? (member.role === 'lead' ? t('lead') : member.id)} {isCurrent && {t('current')}} @@ -126,7 +127,7 @@ function TeamMemberRow({ } /** Task card with a two-line description clamp expanded from a toggle in the meta row. */ -function TaskCard({ task, t }: { task: TeamTask; t: TranslateNS }) { +function TaskCard({ task, owner, t }: { task: TeamTask; owner: TeamMemberProjection | undefined; t: TranslateNS }) { const [expanded, setExpanded] = useState(false) const [clamped, setClamped] = useState(false) const textRef = useRef(null) @@ -165,7 +166,7 @@ function TaskCard({ task, t }: { task: TeamTask; t: TranslateNS }) { )} {task.id} - {t('owner')}: {task.ownerName ?? t('unowned')} + {t('owner')}: {owner?.description ?? (owner?.role === 'lead' ? t('lead') : task.ownerId ?? t('unowned'))} {task.status === 'pending' && {task.ready ? t('ready') : t('blocked')}} {task.blockedBy.length > 0 && {t('blockedBy')}: {task.blockedBy.join(', ')}} {task.writeScopes.length > 0 && {t('writeScopes')}: {task.writeScopes.join(', ')}} @@ -189,8 +190,11 @@ export function TeamAction({ open, anchorRef: triggerRef, panelRef, gap: 5, margin: 16, }) const positioned = position !== null - const leadSessionId = useSession(snapshot => snapshot.subagent?.address.parentSessionId) ?? sessionId - const team = useSessions(state => state.projectionsBySession[leadSessionId]?.values.agentTeam) + const parentId = useSession(snapshot => snapshot.subagent?.address.parentSessionId) + const team = useSessions((state) => { + const rootId = teamRootId(sessionId, parentId, state.byId) + return rootId === undefined ? undefined : state.projectionsBySession[rootId]?.values.agentTeam + }) const opening = useSession(snapshot => snapshot.openState === 'loading') const listing = useSessions(state => state.phase === 'pending') const hoverTimer = useRef | undefined>(undefined) @@ -342,7 +346,9 @@ export function TeamAction({ <>

{t('tasks')}{team.tasks.length}

- {team.tasks.map(task => )} + {team.tasks.map(task => ( + member.id === task.ownerId)} t={t} /> + ))}
)} diff --git a/packages/experimental/client-ui-agent-team/src/client/locales.ts b/packages/experimental/client-ui-agent-team/src/client/locales.ts index fe4b853095..8d5780c7fe 100644 --- a/packages/experimental/client-ui-agent-team/src/client/locales.ts +++ b/packages/experimental/client-ui-agent-team/src/client/locales.ts @@ -11,6 +11,7 @@ export const zh = { failure: '团队持久记录无效:{message}', empty: '暂无共享任务,可以通过对话创建', roster: '成员', + lead: '队长', tasks: '共享任务', model: '模型', open: '打开成员会话', @@ -43,6 +44,7 @@ export const en = { failure: 'Invalid persisted Team record: {message}', empty: 'No shared tasks yet. Create them through the conversation.', roster: 'Members', + lead: 'Lead', tasks: 'Shared tasks', model: 'Model', open: 'Open member conversation', diff --git a/packages/experimental/client-ui-agent-team/src/client/mount.ts b/packages/experimental/client-ui-agent-team/src/client/mount.ts index 94a369df04..2ee31d6084 100644 --- a/packages/experimental/client-ui-agent-team/src/client/mount.ts +++ b/packages/experimental/client-ui-agent-team/src/client/mount.ts @@ -9,6 +9,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' import type {} from '@deepseek-ai/dsh-client-ui-workspace/client' import { TeamAction, type TeamActionInjected } from './TeamAction.tsx' import { en, NS, zh, type TeamKey } from './locales.ts' +import { teamRootId } from './team-root.ts' declare module '@deepseek-ai/dsh-client-ui-slots' { interface LocaleNamespaceMap { @@ -29,14 +30,15 @@ export const inject = ['sessions', 'uiWorkspace', 'slots', 'locale'] export function registerAgentTeamUi(ctx: ClientContext): void { ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'client-ui-agent-team: dictionaries') const sessions = ctx.sessions - const leadSessionId = (sessionId: SessionId): SessionId => { + const leadSessionId = (sessionId: SessionId): SessionId | undefined => { const address = sessions.binding(sessionId)?.session.getSnapshot().subagent?.address - return address?.parentSessionId ?? sessionId + return teamRootId(sessionId, address?.parentSessionId, sessions.list.getSnapshot().byId) } const actions: TeamActionInjected = { openTeammate(sessionId: SessionId, childSessionId: SessionId): void { const parentSessionId = leadSessionId(sessionId) + if (parentSessionId === undefined) return if ((sessions.retainInfo(sessionId).getSnapshot().retainedBy.mainView ?? 0) === 0) return if (childSessionId === parentSessionId) { ctx.uiWorkspace.openSession(parentSessionId) diff --git a/packages/experimental/client-ui-agent-team/src/client/team-root.ts b/packages/experimental/client-ui-agent-team/src/client/team-root.ts new file mode 100644 index 0000000000..2d296d0284 --- /dev/null +++ b/packages/experimental/client-ui-agent-team/src/client/team-root.ts @@ -0,0 +1,29 @@ +/** Team root discovery from the current subagent address and retained Session summaries. */ + +import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type { SessionSummary } from '@deepseek-ai/dsh-api-session-controller/client' + +/** + * Follow subagent parents to the ordinary root that owns the shared Team board. + * @param sessionId - current conversation id. + * @param parentId - direct parent from the current conversation's subagent address. + * @param summaries - shared Session list metadata, including unopened ancestors. + * @returns the root id, or undefined while ancestry is missing or cyclic. + */ +export function teamRootId( + sessionId: SessionId, + parentId: SessionId | undefined, + summaries: Readonly>, +): SessionId | undefined { + const seen = new Set([sessionId]) + while (parentId !== undefined) { + if (seen.has(parentId)) return undefined + seen.add(parentId) + const parent = summaries[parentId] + if (parent === undefined) return undefined + if (parent.origin !== 'subagent') return parentId + parentId = parent.parentId + if (parentId === undefined) return undefined + } + return sessionId +} diff --git a/packages/experimental/client-ui-agent-team/tests/browser-plugin.client.spec.ts b/packages/experimental/client-ui-agent-team/tests/browser-plugin.client.spec.ts index 23c5844747..9ff068b0c6 100644 --- a/packages/experimental/client-ui-agent-team/tests/browser-plugin.client.spec.ts +++ b/packages/experimental/client-ui-agent-team/tests/browser-plugin.client.spec.ts @@ -9,18 +9,23 @@ import { apply as nodeApply } from '../src/index.ts' const SESSION = 'team-session' as SessionId const CHILD = 'team-child' as SessionId +const NESTED = 'team-nested' as SessionId -async function bench(options: { addressed?: boolean } = {}) { +async function bench(options: { addressed?: boolean; nested?: boolean; missingParent?: boolean } = {}) { const ctx = new Context() const navigation: unknown[] = [] - let mainSessionId = options.addressed === true ? CHILD : SESSION + let mainSessionId = options.nested === true ? NESTED : options.addressed === true ? CHILD : SESSION ctx.provide('sessions', { - binding: (id: SessionId) => options.addressed === true && id === CHILD + list: { getSnapshot: () => ({ byId: { + [SESSION]: { id: SESSION }, + ...(options.missingParent === true ? {} : { [CHILD]: { id: CHILD, origin: 'subagent', parentId: SESSION } }), + } }) }, + binding: (id: SessionId) => (options.nested === true && id === NESTED) || (options.addressed === true && id === CHILD) ? { session: { getSnapshot: () => ({ subagent: { address: { - parentSessionId: SESSION, - childSessionId: CHILD, + parentSessionId: options.nested === true ? CHILD : SESSION, + childSessionId: id, mode: 'continuable' as const, }, }, @@ -107,6 +112,22 @@ describe('ui-team browser plugin', () => { ]) }) + it('routes a nested teammate back to the root board and its direct roster members', async () => { + const b = await bench({ nested: true }) + b.actions().openTeammate(NESTED, SESSION) + b.actions().openTeammate(NESTED, CHILD) + expect(b.navigation).toEqual([ + ['open', SESSION], + ['open', { parentSessionId: SESSION, childSessionId: CHILD, mode: 'continuable' }], + ]) + }) + + it('withholds navigation until a nested teammate’s ancestry is available', async () => { + const b = await bench({ nested: true, missingParent: true }) + b.actions().openTeammate(NESTED, SESSION) + expect(b.navigation).toEqual([]) + }) + it('opens the Lead from an addressed teammate conversation', async () => { const b = await bench({ addressed: true }) b.actions().openTeammate(CHILD, SESSION) diff --git a/packages/experimental/client-ui-agent-team/tests/team-action.client.spec.tsx b/packages/experimental/client-ui-agent-team/tests/team-action.client.spec.tsx index 11f57e6cc1..3736b48730 100644 --- a/packages/experimental/client-ui-agent-team/tests/team-action.client.spec.tsx +++ b/packages/experimental/client-ui-agent-team/tests/team-action.client.spec.tsx @@ -14,6 +14,7 @@ import { bindSnapshotSelector, makeTranslate } from '@deepseek-ai/dsh-client-tes import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts' import { TeamAction, type TeamActionInjected, type TeamActionProps } from '../src/client/TeamAction.tsx' import { zh } from '../src/client/locales.ts' +import { teamRootId } from '../src/client/team-root.ts' afterEach(() => { cleanup() @@ -32,15 +33,15 @@ const task: TeamTask = { subject: 'Implement runtime', description: 'Build the Team runtime', status: 'in_progress', - ownerName: 'lead', + ownerId: SESSION, blockedBy: [], writeScopes: ['src'], ready: false, writeScopeWarnings: ['write scopes overlap with task-2'], } -const lead: TeamMemberProjection = { id: SESSION, name: 'lead', role: 'lead', phase: 'active' } +const lead: TeamMemberProjection = { id: SESSION, role: 'lead', phase: 'active' } const worker: TeamMemberProjection = { - id: WORKER, name: 'worker', role: 'teammate', phase: 'active', + id: WORKER, description: 'worker', role: 'teammate', phase: 'active', } const team: TeamProjection = { members: [lead, worker], tasks: [task] } @@ -59,7 +60,7 @@ function bench(options: { running?: Record } = {}) { const sessionId = options.sessionId ?? SESSION - const byId: Record = {} + const byId: Record = { [SESSION]: summary(SESSION, false) } for (const [id, running] of Object.entries(options.running ?? {}) as [SessionId, boolean][]) byId[id] = summary(id, running) const sessions = createSnapshotStore({ ids: Object.keys(byId) as SessionId[], byId, phase: 'ready', @@ -133,7 +134,7 @@ describe('TeamAction', () => { expect(screen.queryByRole('button', { name: /刷新|Refresh/u })).toBeNull() setProjection(b.sessions, SESSION, { - members: [lead, worker, { id: 'worker-b' as SessionId, name: 'worker-b', role: 'teammate', phase: 'provisioning' }], + members: [lead, worker, { id: 'worker-b' as SessionId, description: 'worker-b', role: 'teammate', phase: 'provisioning' }], tasks: [task, { ...task, id: TASK_2, subject: 'Pushed task', status: 'pending', ready: true, writeScopeWarnings: [] }], }) expect(screen.getByText('Pushed task')).toBeTruthy() @@ -159,7 +160,7 @@ describe('TeamAction', () => { }) render() openPanel() - expect(screen.getByRole('button', { name: new RegExp(`lead.*${zh['memberStatus.running']}.*lead-model`, 'u') })).toBeTruthy() + expect(screen.getByRole('button', { name: new RegExp(`${zh.lead}.*${zh['memberStatus.running']}.*lead-model`, 'u') })).toBeTruthy() const row = screen.getByRole('button', { name: new RegExp(`worker.*${zh['memberStatus.running']}.*worker-model`, 'u') }) expect(row.querySelector('[data-state="ongoing"]')).not.toBeNull() @@ -179,7 +180,7 @@ describe('TeamAction', () => { openPanel() expect(screen.getByText('Implement runtime')).toBeTruthy() expect(screen.getByRole('button', { name: /^worker/u }).disabled).toBe(true) - fireEvent.click(screen.getByRole('button', { name: /^lead/u })) + fireEvent.click(screen.getByRole('button', { name: new RegExp(`^${zh.lead}`, 'u') })) expect(b.injected.openTeammate).toHaveBeenCalledWith(WORKER, SESSION) }) @@ -202,6 +203,39 @@ describe('TeamAction', () => { expect(screen.getByText(zh.empty)).toBeTruthy() }) + it('withholds roots for missing or cyclic subagent parent metadata', () => { + expect(teamRootId(WORKER, SESSION, { + [SESSION]: { ...summary(SESSION, false), origin: 'subagent' }, + })).toBeUndefined() + expect(teamRootId(WORKER, SESSION, { + [SESSION]: { ...summary(SESSION, false), origin: 'subagent', parentId: WORKER }, + })).toBeUndefined() + }) + + it('shows the member id when no description is available', () => { + const { description: _description, ...member } = worker + const b = bench({ projections: { [SESSION]: { state: 'ready', error: null, values: { + agentTeam: { members: [lead, member], tasks: [] }, + } } } }) + render() + openPanel() + expect(screen.getByRole('button', { name: new RegExp(`^${WORKER}`, 'u') })).toBeTruthy() + }) + + it('reads the root board from a nested teammate and withholds it when ancestry is missing', () => { + const b = bench({ sessionId: 'nested' as SessionId, parentSessionId: WORKER }) + b.sessions.set({ ...b.sessions.getSnapshot(), byId: { + [SESSION]: summary(SESSION, false), + [WORKER]: { ...summary(WORKER, false), origin: 'subagent', parentId: SESSION }, + } }) + render() + openPanel() + expect(screen.getByText(task.subject)).toBeTruthy() + act(() => { b.sessions.set({ ...b.sessions.getSnapshot(), byId: {} }) }) + expect(screen.queryByText(task.subject)).toBeNull() + expect(screen.getByText(zh.unavailable)).toBeTruthy() + }) + it('waits for the shared Session list and accepts cached projections without an explicit read', () => { const b = bench({ projections: {} }) b.sessions.update((draft) => { draft.phase = 'pending' }) @@ -267,7 +301,7 @@ describe('TeamAction', () => { }) it('renders roster/task state variants and reports navigation failures', () => { - const { ownerName: _ownerName, ...unownedTask } = task + const { ownerId: _ownerId, ...unownedTask } = task const b = bench({ projections: { [SESSION]: { @@ -277,13 +311,13 @@ describe('TeamAction', () => { members: [ lead, worker, - { id: 'failed-id' as SessionId, name: 'failed-worker', role: 'teammate', phase: 'failed', error: 'provider failed' }, - { id: 'provisioning-id' as SessionId, name: 'provisioning-worker', role: 'teammate', phase: 'provisioning' }, + { id: 'failed-id' as SessionId, description: 'failed-worker', role: 'teammate', phase: 'failed', error: 'provider failed' }, + { id: 'provisioning-id' as SessionId, description: 'provisioning-worker', role: 'teammate', phase: 'provisioning' }, ], tasks: [ { ...unownedTask, id: 'ready-task' as TeamTaskId, status: 'pending', ready: true }, { ...unownedTask, id: 'blocked-task' as TeamTaskId, status: 'pending', ready: false, blockedBy: [TASK_1] }, - { ...task, id: 'completed-task' as TeamTaskId, status: 'completed', ownerName: 'worker' }, + { ...task, id: 'completed-task' as TeamTaskId, status: 'completed', ownerId: WORKER }, ], }, }, @@ -304,7 +338,7 @@ describe('TeamAction', () => { expect(failedMember.querySelector('[data-state="error"]')).not.toBeNull() expect(provisioningMember.disabled).toBe(true) expect(provisioningMember.querySelector('[data-state="ongoing"]')).not.toBeNull() - expect(screen.getByRole('button', { name: /^lead/u }).disabled).toBe(true) + expect(screen.getByRole('button', { name: new RegExp(`^${zh.lead}`, 'u') }).disabled).toBe(true) const tasks = [...document.querySelectorAll('article')] expect(tasks.map(card => card.querySelector('[data-state]')?.getAttribute('data-state'))) .toEqual(['idle', 'warning', 'done']) diff --git a/packages/experimental/tool-agent-team/README.i18n.yaml b/packages/experimental/tool-agent-team/README.i18n.yaml index f541bc0b5a..5fee8accc6 100644 --- a/packages/experimental/tool-agent-team/README.i18n.yaml +++ b/packages/experimental/tool-agent-team/README.i18n.yaml @@ -3,68 +3,95 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/experimental/tool-agent-team/README.md /: - en: 770d44b09466af9a - zh: b8b84ed9464ce41b + en: 478ab0e5bc6c65b0 + zh: 9d8f14053c34498d /deepseek-ai-dsh-experimental-tool-agent-team: en: 0b01a243291821bc zh: 0dadd38d709ae9bd /deepseek-ai-dsh-experimental-tool-agent-team/summary: - en: cb426055b46b381e - zh: dc29d823d44f0c5e + en: 04709c340cad4542 + zh: ae9e3dc5cf1b9d37 /deepseek-ai-dsh-experimental-tool-agent-team/table-of-contents: en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-experimental-tool-agent-team/use-this-package: - en: 5a56781cb7bf4d3b - zh: 74df21efd6321afd + en: 4230311e6e59e009 + zh: 4efa8f40a9678add /deepseek-ai-dsh-experimental-tool-agent-team/use-this-package/when-to-choose-it: - en: 60ea90ce57cca56f - zh: 181e48b7d4dcf755 + en: c27d5457e1183675 + zh: 3c36d513e6400446 /deepseek-ai-dsh-experimental-tool-agent-team/use-this-package/smallest-working-example: - en: 9af650a79711b3b3 - zh: 2930360011b544e0 + en: 9e03ffbf10e77ce4 + zh: 7362b415e9d97238 /deepseek-ai-dsh-experimental-tool-agent-team/use-this-package/what-the-model-can-do: - en: be42811bbcc3b438 - zh: 014bd055ca694315 + en: de03950e2e41ba1f + zh: eeb52afda9bd2249 /deepseek-ai-dsh-experimental-tool-agent-team/use-this-package/what-success-and-failure-look-like: - en: e49a980372c55892 - zh: d4bcdd36278d040d + en: 8b6cd7c8bd1b8abc + zh: d281898bdd1db783 /deepseek-ai-dsh-experimental-tool-agent-team/understand-the-implementation: en: 3952eb1d9de1f988 zh: 789d51b88f41b710 /deepseek-ai-dsh-experimental-tool-agent-team/understand-the-implementation/design-philosophy: - en: 445806c13f887a09 - zh: 8b2b67044abfd65a + en: b68b30b995449e61 + zh: 984cd44483ef386b /deepseek-ai-dsh-experimental-tool-agent-team/understand-the-implementation/source-map: - en: 29c1bff5023123be - zh: 079c4819ec9ef277 + en: b7032ceddf3452be + zh: 8549087f632e4406 /deepseek-ai-dsh-experimental-tool-agent-team/understand-the-implementation/policy-and-tools: - en: 3e730667f904c7bd - zh: 7f48e6c07c615d77 + en: 7bf6800f9fe88605 + zh: 254355688e4aa6f8 /deepseek-ai-dsh-experimental-tool-agent-team/understand-the-implementation/scoped-registration-and-teardown: en: c2b29f27a74c02c0 zh: 026f2ef93dd604d8 /deepseek-ai-dsh-experimental-tool-agent-team/further-exploration: - en: 4cde96d9b184d1b0 - zh: fedae651acb7301f + en: c0b484e56b3a3990 + zh: becaf4dc193f00d5 /deepseek-ai-dsh-experimental-tool-agent-team/model-experience: en: 215e7ba838619b7b zh: a311da3843709f90 -/deepseek-ai-dsh-experimental-tool-agent-team/model-experience/team-policy-and-tools: - en: 8e1e365ff3b0dfb2 - zh: c6c91fbf1e1df934 -/deepseek-ai-dsh-experimental-tool-agent-team/model-experience/team-policy-and-tools/what-the-model-sees: - en: e8687e69bcdf411d - zh: 84480d4dd33cf339 -/deepseek-ai-dsh-experimental-tool-agent-team/model-experience/team-policy-and-tools/token-effect: - en: 3f49674f937a8ca3 - zh: 79d34d605732abb1 -/deepseek-ai-dsh-experimental-tool-agent-team/model-experience/team-policy-and-tools/kv-cache-effect: - en: 2ba143e89873e768 - zh: 4f09446c0fb13cdb +/deepseek-ai-dsh-experimental-tool-agent-team/model-experience/team-policy-system-prompt: + en: 8757dc3caca9e4e2 + zh: fd0b7535a1b24189 +/deepseek-ai-dsh-experimental-tool-agent-team/model-experience/team-policy-system-prompt/what-the-model-sees: + en: 25079a70c9a3ed87 + zh: 34301e390669a887 +/deepseek-ai-dsh-experimental-tool-agent-team/model-experience/team-policy-system-prompt/what-the-model-sees/team-policy: + en: 824ebdbe434d7b6c + zh: 6a442dbca7cf3625 +/deepseek-ai-dsh-experimental-tool-agent-team/model-experience/team-policy-system-prompt/token-effect: + en: 7e2583f482a60938 + zh: d3e68b204cd3d2b3 +/deepseek-ai-dsh-experimental-tool-agent-team/model-experience/team-policy-system-prompt/kv-cache-effect: + en: 6723f8e3f2497338 + zh: ca68851b279362e1 +/deepseek-ai-dsh-experimental-tool-agent-team/model-experience/team-identity-runtime-context: + en: ed8642015d306d6d + zh: 09d1970fb8e02ff3 +/deepseek-ai-dsh-experimental-tool-agent-team/model-experience/team-identity-runtime-context/what-the-model-sees: + en: 4d17511e1d5fb4f2 + zh: c98734115d753a57 +/deepseek-ai-dsh-experimental-tool-agent-team/model-experience/team-identity-runtime-context/token-effect: + en: f0dc7b201c8b3cf0 + zh: b5f16ef4a133257c +/deepseek-ai-dsh-experimental-tool-agent-team/model-experience/team-identity-runtime-context/kv-cache-effect: + en: 81530c467a42ce68 + zh: 99098262d199cd1a +/deepseek-ai-dsh-experimental-tool-agent-team/model-experience/task-and-wait-tool-schemas: + en: d78dd9222b456c0e + zh: 53ac8c403c7b8afd +/deepseek-ai-dsh-experimental-tool-agent-team/model-experience/task-and-wait-tool-schemas/what-the-model-sees: + en: f66d6638425ca815 + zh: 3d64f4a53c074b72 +/deepseek-ai-dsh-experimental-tool-agent-team/model-experience/task-and-wait-tool-schemas/token-effect: + en: 625799dbf6992c66 + zh: cf32cf61d1200121 +/deepseek-ai-dsh-experimental-tool-agent-team/model-experience/task-and-wait-tool-schemas/kv-cache-effect: + en: 41bfcd6adb8058f5 + zh: 73363ee21021a96a /deepseek-ai-dsh-experimental-tool-agent-team/known-limitations-and-deferred-work: - en: 945184aa77ac76dd - zh: 620b07948615a019 + en: 395ee61922d8c284 + zh: f2e25817b94acf39 /deepseek-ai-dsh-experimental-tool-agent-team/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/experimental/tool-agent-team/README.md b/packages/experimental/tool-agent-team/README.md index 1afed86720..60b1f9aceb 100644 --- a/packages/experimental/tool-agent-team/README.md +++ b/packages/experimental/tool-agent-team/README.md @@ -1,5 +1,5 @@ --- -description: "Nine tools that let the model create, message, and coordinate teammates, for compositions mounting the experimental Team plugins." +description: "Five task-board and wait tools plus coordination guidance for every member of an experimental Team composition." kind: "package-reference" --- @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -This package lets the model create named teammates, send them messages, inspect availability, wait for progress, interrupt stuck work, and coordinate through a shared task board. Every team member receives the same nine tools and guidance for coordinating in a shared workspace. It replaces legacy subagent controls with the same tool names, so compositions that need both must disable the legacy definitions. The package is published under its experimental name and provides no stability guarantee. +This package lets every Team member coordinate shared work: create, claim, and complete tasks on the shared board, and wait for teammates instead of polling. Each member also receives the shared-checkout coordination policy and a statement of its role. Creating, messaging, listing, and interrupting teammates stay on the standard subagent tools, so the package adds only five tool schemas. It is published under its experimental name and provides no stability guarantee. ## Table of Contents @@ -25,47 +25,38 @@ This package lets the model create named teammates, send them messages, inspect ## Use this package -Add this package on top of `@deepseek-ai/dsh-experimental-agent-team` when the model should run a team through tools. Once mounted, every team member — the Lead and each teammate — gets the same nine tools plus the same coordination policy. Each member receives its current role and name through required runtime context; `spawn_teammate` sends the initial task unchanged. +Add this package on top of `@deepseek-ai/dsh-experimental-agent-team` when Team members should coordinate through tools. Once mounted, the Lead and every subagent below it, fresh or fork, get the same five tools, the same policy section, and a required runtime-context statement of their role. ### When to choose it -Choose it when the model should create and coordinate teammates by itself rather than a human driving subagent controls. Avoid it when the legacy global subagent tools with the same names must stay available: the team tools replace them for team members, so a composition that wants both must disable the legacy definitions. +Choose it whenever the Team domain is mounted and models should use the board; without it, the board is reachable only through `ctx.agentTeams`. Teammates are created and reached through the standard `subagent`, `subagent_fork`, `send_message`, `list_agents`, and `interrupt_agent` tools, so the composition also needs `@deepseek-ai/dsh-tool-subagent` and `@deepseek-ai/dsh-tool-subagent-control`. ### Smallest working example -The smallest addition to an existing composition is the two-package fragment from the [agent-team README](../agent-team/README.md#smallest-working-setup): durable session storage, the team domain package, and this package. The plugin itself takes two optional settings: +The plugin takes no configuration; mount it beside the Team domain as in the [agent-team setup](../agent-team/README.md#smallest-working-setup): ```yaml - id: tool-agent-team name: '@deepseek-ai/dsh-experimental-tool-agent-team' - config: - freshProvider: spawn - forkProvider: fork ``` -| Field | Default | Meaning | -|---|---|---| -| `freshProvider` | `spawn` | Provider that starts fresh teammates | -| `forkProvider` | `fork` | Provider that starts fork teammates | +A row that sets `interrupt`, `freshProvider`, or `forkProvider` does not activate, and dsh names the key in its startup warning. The [upgrade guide](../../../docs/upgrade-guide/v0.2.1-alpha.3/agent-team-subagents/guide.md) maps the removed tools and keys to their replacements, and the generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-experimental-tool-agent-team) records the empty configuration. -The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-experimental-tool-agent-team) is the exhaustive source for every accepted field and its JSDoc. - -Try it by asking the Lead model: "create a teammate named reviewer to check the diff, then send reviewer the change summary". The model calls the creation tool and then the messaging tool. +Ask the Lead to start a subagent to review the diff, add a review task, and wait for its result. The `description` explains the subagent’s work; the returned id addresses it. ### What the model can do -The nine tools group into four capabilities: +The five tools cover two capabilities; the standard subagent tools cover the third: -- **Create a teammate** — `spawn_teammate` takes a name, a description, and the initial task; only the Lead can call it. -- **Send messages** — `send_message` steers a running member at its nearest step boundary, starts or resumes an inactive member. -- **See and wait** — `list_agents` returns each member’s `target` and availability; `wait_agent` waits for the next team change; `interrupt_agent` stops a teammate's current turn (Lead only). - **Manage the task board** — `team_task_create`, `team_task_list`, `team_task_get`, and `team_task_update` add, browse, read, and update shared tasks. +- **Wait for teammates** — `wait_agent` waits for the next task, member status, or inbox change after the call starts. +- **Create and reach teammates** — `subagent`, `subagent_fork`, `send_message`, `list_agents`, and `interrupt_agent` come from the subagent tool packages and are unchanged by this package. -Creation and listing results identify members by `target`, with no member Session ID. Use that value in message and interrupt calls or the task tools’ `owner` parameter; task `ownerName` uses the same value. `inactive` means no turn is executing, whether the member is loaded or must be resumed; it does not describe task completion or outcome. `provisioning` and `failed` describe member creation. Any member can message any other member and use the task board; only the Lead creates and interrupts teammates. Task updates keep the domain's owner and revision checks, so an outdated edit is rejected instead of overwriting newer work. +Task `owner` arguments and `ownerId` results use Session ids. Descriptions are display text and may repeat. Any member can use the board; only the Lead reassigns tasks. Updates enforce ownership and revision checks, rejecting stale edits. ### What success and failure look like -`send_message` returns `{ "sent": true }` after inbox acceptance, without exposing the message ID. Success does not confirm recipient processing or a synchronous storage flush; send failures are reported as tool errors. `wait_agent` returns `noProgress` right away when no other member is running or provisioning, telling the caller to wake a teammate first; otherwise it waits for the next change and the caller re-reads state afterward. Task edits based on an outdated revision are rejected rather than overwriting newer work. +Task tools return compact JSON task views or task pages. A stale `expected_revision`, an unauthorized action, or an unready task is reported as a tool error instead of overwriting newer work. `wait_agent` returns `{ "timedOut": false }` after a change and `{ "timedOut": true }` at its timeout; when no other member is running, it returns `noProgress` with reason `no-active-peer` right away, telling the caller to wake a teammate with `send_message` first. ----- @@ -79,23 +70,24 @@ This section explains the design decisions behind the adapter and points at the ### Design philosophy -The adapter is built on three commitments: +The adapter is built on four commitments: -- **Scoped, not global.** Every registration lives on the member Agent's own `ctx`; installation uses the member identity available when the Agent is published. +- **Scoped, not global.** Every registration lives on the member Agent's own `ctx`; installation uses the Team membership available when the Agent is published. +- **One tool family.** Team registers no messaging, listing, or interruption tool; the standard subagent tools serve Leads and teammates alike. - **Declared results, compact JSON.** Every tool declares its complete result schema and renders that value as compact JSON, so the compiler checks `execute` against what the model is promised and no result spends tokens on indentation. - **The domain owns authority.** Tools delegate to `ctx.agentTeams`, which enforces Lead authority and revision checks; the adapter adds no weaker path. -The [Agent Teams Agent Note](../../../.agents/notes/implemented/feature/2026-08-05-agent-teams.md) owns the model-facing and scoping decisions. +The [subagent teammates Agent Note](../../../.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md) owns the model-facing and scoping decisions. ### Source map | File | Role | |---|---| -| [`src/index.ts`](src/index.ts) | Plugin entry: config, the fixed policy text, and the nine scoped tool registrations | +| [`src/index.ts`](src/index.ts) | Plugin entry: removed-key rejection, the fixed policy text, identity context, and the five scoped tool registrations | ### Policy and tools -One `team:policy` section on the member scope states the shared coordination rules; the fixed text and the nine tool registrations are declared in [`src/index.ts`](src/index.ts). The nine tool schemas are registered in scopes recognized as Team members at publication. Scoped registrations with the same names as the legacy global continuable-subagent controls shadow those globals for team members only. +One `team:policy` section and one required `team:identity` runtime-context entry on the member scope carry the shared coordination rules and the member's role; the fixed text and the five tool registrations are declared in [`src/index.ts`](src/index.ts). The five tool schemas are registered in every scope recognized as a Team member at publication, including fork children, so parent and fork requests list identical tools and system prompts. No registration shares a name with a subagent control, so `send_message`, `list_agents`, and `interrupt_agent` resolve to the global subagent tools in every member scope. ### Scoped registration and teardown @@ -112,27 +104,68 @@ Read these pages when the package-level contract is not enough. They move from t - [agent-team package](../agent-team/README.md) — the `ctx.agentTeams` domain service behind these tools. - [Agent Teams subsystem](../../../docs/subsystems/agent-team.md) — durable Team types and service API. +- [Subagent control tools](../../subagent/tool-subagent-control/README.md) — messaging, listing, and interruption by Session id. - [Generated tool catalog](../../../docs/tool-catalog.md#deepseek-aidsh-experimental-tool-agent-team) — every tool schema the model receives. -- [Agent Teams Agent Note](../../../.agents/notes/implemented/feature/2026-08-05-agent-teams.md) — model-facing, scoping, and isolation decisions. +- [Subagent teammates Agent Note](../../../.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md) — model-facing, scoping, and tool-family decisions. ----- ## Model Experience -### Team policy and tools +### Team policy system prompt #### What the model sees -One shared system policy covers Team availability, shared-cwd safety, task coordination, Steer delivery, inbox acceptance, and the Lead’s duty to wait. All nine schemas are identical for Leads and teammates; execution enforces Lead-only operations. Required runtime context states `You are the Team Lead named "lead".` or `You are teammate "".` with `Your Team Lead is named "lead".`, plus roster and messaging instructions. Identity comes from current Team membership, survives optional-context suppression, and is restored when compaction removes its snapshot. An ordinary fork from a teammate receives its current Lead identity. +Every Team member's system prompt carries one `team:policy` section; the Lead and every teammate see the same text: + +##### Team policy + +```markdown +Agent Teams is available in this session. Teammates are subagents. Use agent ids with send_message to reach your direct continuable children or, as a continuable subagent, your parent and continuable siblings. Descriptions explain responsibilities and may repeat. + +The Team Lead and all teammates share the same working directory and filesystem. Edits are immediately visible to every member. Split write work into disjoint scopes, record expected write scopes on shared tasks, and use task dependencies when work must be ordered. Write-scope overlap is advisory, not a lock. + +Prefer read/edit/write for file changes. If a file operation returns FS_STALE_VERSION, read the current file, rebase your intended change onto the new content, and retry. Bash, formatters, code generators, and scripts are not fully protected by the filesystem version guard; coordinate them explicitly and have the Lead review the final diff and run tests. + +Use agent ids as task owners. The Lead can reassign to itself, its direct continuable children, or historical members; other descendants claim tasks themselves. list_agents shows direct contacts by default; descendants scope discovers deeper agents without granting message or assignment authority. inactive means no turn is executing; it does not describe task completion, success, failure, or waiting for other agents. A successful send does not mean the recipient has processed it. Shared-task workflow is list, get, claim with the current revision, perform the work, then complete. Task readiness never starts an owner. Before wait_agent, make sure another required member is running; use send_message first for an inactive direct contact, or coordinate through its parent for a deeper member. wait_agent observes only changes after that call starts, never wakes a member, and returns noProgress immediately when no other member is running. Re-list after wakeup or timeout. The Lead must wait for required teammates before giving the final answer. +``` #### Token effect -Fixed policy and schema cost on every Team member request. Identity is part of the complete runtime-context snapshot. An unchanged retained snapshot adds no message; changed or removed context causes the loop to append a complete snapshot, including identity. Tool calls add compact JSON roster, task, wait, or receipt results. Peer content is retained by the Team domain in the target's history. +The section is a fixed cost on every request of every Team member. #### KV Cache effect -With the same provider/model, shared system policy, and tool schemas, a fork retains the parent request prefix and appends its task and current runtime context. Tool results and peer messages append after the reusable request prefix. Sessions recorded with identity inside the system prompt can change that prefix on their first request under this layout; actual provider cache hits remain best-effort. +The text is identical for the Lead and every descendant and does not change while the plugin stays loaded, so it keeps a stable system-prompt prefix, including between a parent and its `subagent_fork` child. Loading or unloading the plugin changes the system prompt of every member and invalidates their cached prefixes. + +### Team identity runtime context + +#### What the model sees + +Required `team:identity` runtime context states `You are the Team Lead. Your agent id is "".` for a root and `You are a teammate in the Team led by agent "".` for a subagent. It follows current membership, survives optional-context suppression, and is restored after compaction. An ordinary fork of a teammate is a new root and receives the Lead statement. + +#### Token effect + +Identity is part of the complete runtime-context snapshot. An unchanged retained snapshot adds no message; changed or removed context causes the loop to append a complete snapshot, including identity. + +#### KV Cache effect + +Identity arrives in a runtime-context snapshot after the request prefix, so a `subagent_fork` child keeps its parent's system prompt, tools, and inherited messages and appends its teammate statement after them. Actual provider cache hits remain best-effort. + +### Task and wait tool schemas + +#### What the model sees + +Every Team member receives the same five schemas, listed in the [generated tool catalog](../../../docs/tool-catalog.md#deepseek-aidsh-experimental-tool-agent-team). Results are compact JSON task views, task pages, or wait values. When no other member is running, `wait_agent` returns `noProgress` with reason `no-active-peer` and the message `No other Team member is running. wait_agent cannot make progress or wake inactive teammates. Re-list with list_agents and team_task_list, then use send_message to wake a required inactive direct contact before waiting again; coordinate deeper members through their parent.` Lead-only `reassign` is enforced at execution, not by a different schema. + +#### Token effect + +The five schemas are a fixed cost on every Team member request. Tool calls add compact JSON results; `team_task_list` returns at most 100 tasks per page. + +#### KV Cache effect + +The schemas are identical for the Lead and every descendant, including `subagent_fork` children, so a fork's tool list matches its parent's request prefix. Tool results append after the reusable prefix. Loading or unloading the plugin changes every member's tool list and invalidates their cached prefixes. ## Known Limitations and Deferred Work @@ -141,6 +174,7 @@ With the same provider/model, shared system policy, and tool schemas, a fork ret These limits describe what the policy and tools cannot guarantee for a team. They are current package constraints, not a comparison with other collaboration surfaces. - **Prompt policy is coordination, not confinement** — it cannot stop Bash or external processes from writing overlapping files. +- **Installation follows publication** — an Agent that is not a Team member when it is published, such as a subagent whose parent is not loaded, receives no Team tools, policy, or identity, even after its parent loads. - **No Web controls** — browser roster and task-board presentation is outside this runtime package. - **Experimental prototype with no stability promise** — the package is public, but its schemas can change freely while it incubates. diff --git a/packages/experimental/tool-agent-team/README.zh.md b/packages/experimental/tool-agent-team/README.zh.md index 2c449b081e..1554bd9639 100644 --- a/packages/experimental/tool-agent-team/README.zh.md +++ b/packages/experimental/tool-agent-team/README.zh.md @@ -1,5 +1,5 @@ --- -description: "九个让模型创建、发消息与协调 teammate 的工具,供挂载实验性 Team 插件的组合使用。" +description: "五个任务板与等待工具及协作指引,供实验性 Team 组合中的每个成员使用。" kind: "package-reference" --- @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -本包让模型创建具名 teammate、向它们发送消息、查看可用状态、等待进展、中断卡住的工作,并通过共享任务板协调。每个团队成员都会获得相同的九个工具,以及在共享工作区协调的指引。它会取代同名的旧版 subagent 控件,因此同时需要两者的组合必须禁用旧定义。本包以实验性名称公开发布,但不提供稳定性保证。 +本包让每个 Team 成员协调共享工作:在共享任务板上创建、claim 并完成任务,并等待 teammate 而不必轮询。每个成员还会收到共享 checkout 的协作策略以及一条角色声明。创建 teammate、向其发消息、列出与中断 teammate 仍使用标准 subagent 工具,因此本包只增加五个工具 schema。本包以实验性名称公开发布,但不提供稳定性保证。 ## 目录 @@ -25,47 +25,38 @@ kind: "package-reference" ## 使用本包 -当模型应该通过工具运行一支团队时,在 `@deepseek-ai/dsh-experimental-agent-team` 之上挂载本包。挂载后,每个团队成员——Lead 与每个 teammate——都会获得相同的九个工具,以及相同的协作策略。`spawn_teammate` 原样发送初始任务;每个成员通过必需的 runtime context 接收当前角色和名字。 +当 Team 成员应通过工具协调时,在 `@deepseek-ai/dsh-experimental-agent-team` 之上挂载本包。挂载后,Lead 及其下每个 subagent(无论 fresh 或 fork)都会获得相同的五个工具、相同的策略段落,以及一条声明其角色的必需 runtime context。 ### 何时选择 -当模型应该自行创建与协调 teammate、而不是由人来操作 subagent 控件时,选择它。当同名的旧全局 subagent 工具必须继续可用时,请不要选择:团队工具会为团队成员取代它们,因此想同时使用两者的组合必须禁用旧定义。 +只要挂载了 Team 领域包且模型需要使用任务板,就选择本包;没有它时,只能通过 `ctx.agentTeams` 访问任务板。teammate 通过标准 `subagent`、`subagent_fork`、`send_message`、`list_agents` 与 `interrupt_agent` 工具创建和联系,因此组合还需要 `@deepseek-ai/dsh-tool-subagent` 与 `@deepseek-ai/dsh-tool-subagent-control`。 ### 最小工作示例 -对现有组合的最小增量是 [agent-team README](../agent-team/README.zh.md#smallest-working-setup) 中的两包片段:持久会话存储、团队领域包与本包。插件本身只有两个可选设置: +本插件不接受配置;按照 [agent-team 配置](../agent-team/README.zh.md#smallest-working-setup),把它挂载在 Team 领域包旁边: ```yaml - id: tool-agent-team name: '@deepseek-ai/dsh-experimental-tool-agent-team' - config: - freshProvider: spawn - forkProvider: fork ``` -| 字段 | 默认值 | 含义 | -|---|---|---| -| `freshProvider` | `spawn` | 启动 fresh teammate 的提供方 | -| `forkProvider` | `fork` | 启动 fork teammate 的提供方 | +设置 `interrupt`、`freshProvider` 或 `forkProvider` 的配置行不会激活,dsh 会在启动警告中指出该键。[升级指南](../../../docs/upgrade-guide/v0.2.1-alpha.3/agent-team-subagents/guide.zh.md)把已移除的工具与键对应到替代方式,生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-experimental-tool-agent-team)记录空配置。 -生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-experimental-tool-agent-team)是每个受支持字段及其 JSDoc 的穷尽式真源。 - -试试这样要求 Lead 模型:「创建一个名为 reviewer 的 teammate 检查 diff,再把变更摘要发给 reviewer」。模型会调用创建工具,然后调用消息工具。 +让 Lead 启动子代理审查差异、创建审查任务并等待结果。`description` 说明子代理的工作;返回的 id 用于寻址。 ### 模型能做什么 -九个工具分为四类能力: +五个工具覆盖两类能力;第三类由标准 subagent 工具提供: -- **创建 teammate**——`spawn_teammate` 接收名字、描述与初始任务;只有 Lead 可以调用它。 -- **发送消息**——`send_message` 在最近的步骤边界对运行中的成员进行 steering(中途引导)、启动或恢复非活动成员。 -- **查看与等待**——`list_agents` 返回各成员的 `target` 与可用状态;`wait_agent` 等待下一次团队变化;`interrupt_agent` 停止 teammate 的当前轮次(仅限 Lead)。 - **管理任务板**——`team_task_create`、`team_task_list`、`team_task_get` 与 `team_task_update` 添加、浏览、读取与更新共享任务。 +- **等待 teammate**——`wait_agent` 等待调用开始后的下一次任务、成员状态或 inbox 变化。 +- **创建和联系队友**——`subagent`、`subagent_fork`、`send_message`、`list_agents` 和 `interrupt_agent` 来自子代理工具包,本包不修改它们。 -创建和列表结果使用 `target` 标识成员,不包含成员 Session ID。可将该值用于消息和中断调用,或任务工具的 `owner` 参数;任务的 `ownerName` 使用相同值。`inactive` 表示没有轮次在执行,包括已加载和需要恢复的成员;它不表示任务完成或结果。`provisioning` 与 `failed` 描述成员创建状态。任何成员都可以给任何其他成员发消息并使用任务板;只有 Lead 可以创建与中断 teammate。任务更新保留领域的 owner 与 revision 校验,因此过期的编辑会被拒绝,而不是覆盖更新的成果。 +任务 `owner` 参数和 `ownerId` 结果使用 Session id。描述用于展示,可以重复。任何成员均可使用任务板;只有 Lead 能重新分配任务。更新校验所有权和修订,拒绝过期编辑。 ### 成功与失败的表现 -`send_message` 在收件箱接收消息后返回 `{ "sent": true }`,不暴露消息 ID。成功不表示接收方已处理消息,也不保证同步刷新到存储;发送失败会作为工具错误报告。当没有其他成员 running 或 provisioning 时,`wait_agent` 会立即返回 `noProgress`,提示调用方先唤醒 teammate;否则它会等待下一次变化,调用方随后重新读取状态。基于过期 revision 的任务编辑会被拒绝,而不是覆盖更新的成果。 +任务工具返回紧凑 JSON 任务视图或任务分页。过期的 `expected_revision`、未授权操作或未就绪任务会作为工具错误报告,而不是覆盖更新的成果。`wait_agent` 在发生变化后返回 `{ "timedOut": false }`,超时时返回 `{ "timedOut": true }`;当没有其他成员 running 时,它会立即返回 reason 为 `no-active-peer` 的 `noProgress`,提示调用方先用 `send_message` 唤醒 teammate。 ----- @@ -79,23 +70,24 @@ kind: "package-reference" ### 设计理念 -适配器建立在三项承诺之上: +适配器建立在四项承诺之上: -- **按作用域,而非全局。** 每个注册都位于成员 Agent(智能体)自己的 `ctx` 上;安装依据 Agent 发布时可用的成员身份。 +- **按作用域,而非全局。** 每个注册都位于成员 Agent(智能体)自己的 `ctx` 上;安装依据 Agent 发布时可用的 Team 成员关系。 +- **一个工具族。** Team 不注册消息、列表或中断工具;标准 subagent 工具同样服务 Lead 与 teammate。 - **声明式结果,紧凑 JSON。** 每个工具都声明完整结果 schema,并把该值渲染为紧凑 JSON,因此编译器会对照向模型承诺的结果检查 `execute`,任何结果都不会在缩进上消耗 token。 - **领域掌握裁决权。** 工具委托给 `ctx.agentTeams`,后者强制执行 Lead 权限与 revision 校验;适配器不添加更弱的路径。 -[Agent Teams Agent Note](../../../.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md)负责模型侧与 scoping 决策。 +[subagent teammate Agent Note](../../../.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.zh.md)负责模型侧与 scoping 决策。 ### 源码地图 | 文件 | 职责 | |---|---| -| [`src/index.ts`](src/index.ts) | 插件入口:配置、固定策略文本与九个 scoped 工具注册 | +| [`src/index.ts`](src/index.ts) | 插件入口:已移除键的拒绝、固定策略文本、身份 context 与五个 scoped 工具注册 | ### 策略与工具 -member scope 上的一个 `team:policy` 段落说明共享的协作规则;固定文本与九个工具注册都声明在 [`src/index.ts`](src/index.ts)。九个工具 schema 注册在发布时被识别为 Team member 的 scope 中。与旧全局 continuable-subagent 控件同名的 scoped 注册只会为团队成员覆盖这些全局控件。 +member scope 上的一个 `team:policy` 段落和一个必需的 `team:identity` runtime-context 条目承载共享协作规则与成员角色;固定文本与五个工具注册都声明在 [`src/index.ts`](src/index.ts)。五个工具 schema 注册在发布时被识别为 Team 成员的每个 scope 中,包括 fork 子级,因此父请求与 fork 请求列出相同的工具和 system prompt。没有任何注册与 subagent 控件同名,因此在每个成员 scope 中,`send_message`、`list_agents` 与 `interrupt_agent` 都解析为全局 subagent 工具。 ### 按作用域注册与拆除 @@ -112,27 +104,68 @@ member scope 上的一个 `team:policy` 段落说明共享的协作规则;固 - [agent-team 包](../agent-team/README.zh.md)——这些工具背后的 `ctx.agentTeams` 领域服务。 - [Agent Teams 子系统](../../../docs/subsystems/agent-team.zh.md)——持久 Team 类型与服务 API。 +- [子代理控制工具](../../subagent/tool-subagent-control/README.zh.md)——按 Session id 发消息、列举和中断。 - [生成的工具目录](../../../docs/tool-catalog.zh.md#deepseek-aidsh-experimental-tool-agent-team)——模型接收的每个工具 schema。 -- [Agent Teams Agent Note](../../../.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md)——模型侧、scoping 与隔离决策。 +- [subagent teammate Agent Note](../../../.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.zh.md)——模型侧、scoping 与工具族决策。 ----- ## 模型体验 -### Team 策略与工具 +### Team 策略 system prompt #### 模型看到什么 -共享系统 policy 覆盖 Team 可用性、共享 cwd 安全、任务协调、Steer 投递、inbox 接收和 Lead 等待责任。Lead 与 teammate 使用相同的九个 schema;执行时落实 Lead-only 操作。必需的 runtime context 声明 `You are the Team Lead named "lead".`,或 `You are teammate "".` 与 `Your Team Lead is named "lead".`,并提供 roster 和消息操作说明。身份来自当前 Team membership,关闭可选 context 时仍保留,压缩移除快照后会恢复。从 teammate 创建的普通 fork 会收到当前 Lead 身份。 +每个 Team 成员的 system prompt 都包含一个 `team:policy` 段落;Lead 与每个 teammate 看到相同的文本: + +##### Team 策略 + +```markdown +Agent Teams is available in this session. Teammates are subagents. Use agent ids with send_message to reach your direct continuable children or, as a continuable subagent, your parent and continuable siblings. Descriptions explain responsibilities and may repeat. + +The Team Lead and all teammates share the same working directory and filesystem. Edits are immediately visible to every member. Split write work into disjoint scopes, record expected write scopes on shared tasks, and use task dependencies when work must be ordered. Write-scope overlap is advisory, not a lock. + +Prefer read/edit/write for file changes. If a file operation returns FS_STALE_VERSION, read the current file, rebase your intended change onto the new content, and retry. Bash, formatters, code generators, and scripts are not fully protected by the filesystem version guard; coordinate them explicitly and have the Lead review the final diff and run tests. + +Use agent ids as task owners. The Lead can reassign to itself, its direct continuable children, or historical members; other descendants claim tasks themselves. list_agents shows direct contacts by default; descendants scope discovers deeper agents without granting message or assignment authority. inactive means no turn is executing; it does not describe task completion, success, failure, or waiting for other agents. A successful send does not mean the recipient has processed it. Shared-task workflow is list, get, claim with the current revision, perform the work, then complete. Task readiness never starts an owner. Before wait_agent, make sure another required member is running; use send_message first for an inactive direct contact, or coordinate through its parent for a deeper member. wait_agent observes only changes after that call starts, never wakes a member, and returns noProgress immediately when no other member is running. Re-list after wakeup or timeout. The Lead must wait for required teammates before giving the final answer. +``` #### Token 影响 -每次 Team member 请求都有固定策略与 schema 成本。身份是完整 runtime-context 快照的一部分。保留的快照未变化时不追加消息;context 变化或被移除时,loop 追加包含身份的完整快照。工具调用会增加紧凑 JSON roster、task、wait 或 receipt 结果。Peer 内容由 Team 领域保留在 target 历史中。 +该段落是每个 Team 成员每次请求的固定成本。 #### KV Cache 影响 -provider/model、共享 system 策略和工具 schema 相同时,fork 保留父请求前缀并追加任务与当前 runtime context。工具结果与 peer 消息追加在可复用请求前缀之后。原先在 system prompt 中记录身份的 Session,首次使用此布局请求时可能改变该前缀;提供方实际缓存命中仍为尽力而为。 +该文本对 Lead 与每个后代都相同,并且在插件保持加载期间不会变化,因此它保持稳定的 system prompt 前缀,包括父级与其 `subagent_fork` 子级之间。加载或卸载插件会改变每个成员的 system prompt,并使其已缓存前缀失效。 + +### Team 身份 runtime context + +#### 模型看到什么 + +必需的 `team:identity` 运行时上下文对根显示 `You are the Team Lead. Your agent id is "".`,对子代理显示 `You are a teammate in the Team led by agent "".`。它跟随当前成员身份,不受可选上下文关闭影响,并在压缩后恢复。队友的普通分叉是新的根,接收 Lead 声明。 + +#### Token 影响 + +身份是完整 runtime-context 快照的一部分。保留的快照未变化时不追加消息;context 变化或被移除时,loop 追加包含身份的完整快照。 + +#### KV Cache 影响 + +身份通过请求前缀之后的 runtime-context 快照到达,因此 `subagent_fork` 子级保留父级的 system prompt、工具与继承消息,并在其后追加 teammate 声明。提供方实际缓存命中仍为尽力而为。 + +### 任务与等待工具 schema + +#### 模型看到什么 + +每个 Team 成员都收到相同的五个 schema,列在[生成的工具目录](../../../docs/tool-catalog.zh.md#deepseek-aidsh-experimental-tool-agent-team)中。结果是紧凑 JSON 任务视图、任务分页或等待值。当没有其他成员 running 时,`wait_agent` 返回 reason 为 `no-active-peer` 的 `noProgress` 以及消息 `No other Team member is running. wait_agent cannot make progress or wake inactive teammates. Re-list with list_agents and team_task_list, then use send_message to wake a required inactive direct contact before waiting again; coordinate deeper members through their parent.`。仅限 Lead 的 `reassign` 在执行时落实,而不是通过不同的 schema。 + +#### Token 影响 + +五个 schema 是每个 Team 成员请求的固定成本。工具调用会增加紧凑 JSON 结果;`team_task_list` 每页最多返回 100 个任务。 + +#### KV Cache 影响 + +这些 schema 对 Lead 与每个后代(包括 `subagent_fork` 子级)都相同,因此 fork 的工具列表与父级请求前缀一致。工具结果追加在可复用前缀之后。加载或卸载插件会改变每个成员的工具列表,并使其已缓存前缀失效。 ## 已知限制与延期工作 @@ -141,6 +174,7 @@ provider/model、共享 system 策略和工具 schema 相同时,fork 保留 这些限制说明策略与工具无法为一支团队保证什么。它们是当前包约束,不是与其他协作方式的对比。 - **提示词策略只负责协调,不负责 confinement**——它无法阻止 Bash 或外部进程写入重叠文件。 +- **安装取决于发布时刻**——发布时不是 Team 成员的 Agent(例如 parent 未加载的 subagent)不会获得 Team 工具、策略或身份,即使其 parent 之后加载也是如此。 - **没有 Web 控制功能**——浏览器 roster 与任务板呈现不属于该运行时包。 - **实验原型,无稳定性承诺**——本包公开发布,但孵化期间 schema 仍可自由变更。 diff --git a/packages/experimental/tool-agent-team/locale/en.json b/packages/experimental/tool-agent-team/locale/en.json index f517900c98..180857c53d 100644 --- a/packages/experimental/tool-agent-team/locale/en.json +++ b/packages/experimental/tool-agent-team/locale/en.json @@ -1,6 +1,6 @@ { "meta": { "title": "Team Tools", - "description": "Give agents tools to coordinate members, exchange messages, and manage shared tasks." + "description": "Give team members tools to manage shared tasks and wait for teammates." } } diff --git a/packages/experimental/tool-agent-team/locale/zh.json b/packages/experimental/tool-agent-team/locale/zh.json index fee0b1da5e..be005dfad4 100644 --- a/packages/experimental/tool-agent-team/locale/zh.json +++ b/packages/experimental/tool-agent-team/locale/zh.json @@ -1,6 +1,6 @@ { "meta": { "title": "团队工具", - "description": "为智能体提供成员协调、消息通信和共享任务管理工具。" + "description": "为团队成员提供共享任务管理和等待队友的工具。" } } diff --git a/packages/experimental/tool-agent-team/src/index.ts b/packages/experimental/tool-agent-team/src/index.ts index bc827c5f8c..74b153e147 100644 --- a/packages/experimental/tool-agent-team/src/index.ts +++ b/packages/experimental/tool-agent-team/src/index.ts @@ -1,10 +1,10 @@ -/** Scoped model-facing tools for the opt-in Agent Teams runtime. */ +/** Scoped model-facing task-board and wait tools for the opt-in Agent Teams runtime. */ import type { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import type { Agent } from '@deepseek-ai/dsh-agent' +import { SessionId } from '@deepseek-ai/dsh-session' import { TeamTaskId } from '@deepseek-ai/dsh-experimental-agent-team' -import type { TeamMemberView } from '@deepseek-ai/dsh-experimental-agent-team' import { defineTool } from '@deepseek-ai/dsh-tools' import type { InferValue, ValueSchemaSpec } from '@deepseek-ai/dsh-tools' @@ -13,62 +13,25 @@ export const name = 'tool-agent-team' /** Services required by the Team tool plugin. */ export const inject = ['agents', 'agentTeams', 'tools', 'systemPrompt'] -/** Tool routing configuration. */ -export interface Config { - /** Register `interrupt_agent` and its policy guidance. Defaults to true; false leaves the domain API available. */ - readonly interrupt?: boolean - /** Continuable-subagent provider used for fresh teammates. */ - readonly freshProvider?: string - /** Continuable-subagent provider used for completed-prefix fork teammates. */ - readonly forkProvider?: string -} +/** The Team tool plugin takes no configuration. */ +export type Config = Record /** Loader schema for the opt-in Team tool plugin. */ -export const Config: z = z.object({ - interrupt: z.boolean().default(true), - freshProvider: z.string().default('spawn'), - forkProvider: z.string().default('fork'), -}) +export const Config: z = z.object({}) + +/** Configuration keys removed when Team reused subagent creation and messaging. */ +const REMOVED_CONFIG_KEYS = ['interrupt', 'freshProvider', 'forkProvider'] as const /** Model-facing collaboration guidance shared by Lead and teammates. */ -function teamPolicy(interrupt: boolean): string { - return `Agent Teams is available in this session +const TEAM_POLICY = `Agent Teams is available in this session. Teammates are subagents. Use agent ids with send_message to reach your direct continuable children or, as a continuable subagent, your parent and continuable siblings. Descriptions explain responsibilities and may repeat. The Team Lead and all teammates share the same working directory and filesystem. Edits are immediately visible to every member. Split write work into disjoint scopes, record expected write scopes on shared tasks, and use task dependencies when work must be ordered. Write-scope overlap is advisory, not a lock. Prefer read/edit/write for file changes. If a file operation returns FS_STALE_VERSION, read the current file, rebase your intended change onto the new content, and retry. Bash, formatters, code generators, and scripts are not fully protected by the filesystem version guard; coordinate them explicitly and have the Lead review the final diff and run tests. -Use the target returned by spawn_teammate or list_agents for send_message${interrupt ? ' and interrupt_agent' : ''}, or as owner when assigning or filtering shared tasks. send_message steers a running target at its nearest step boundary and starts or resumes an inactive target. inactive means no turn is executing; it does not describe task completion, success, failure, or waiting for other agents. provisioning means member creation is in progress; failed means member creation failed. A peer message starts with its sender name. A successful send does not mean the recipient has processed it. Shared-task workflow is list, get, claim with the current revision, perform the work, then complete. Task readiness never starts an owner. Before wait_agent, use list_agents and make sure another required member is running or provisioning; use send_message first when the required member is inactive. wait_agent observes only changes after that call starts, never wakes a member, and returns noProgress immediately when no other member can produce a change. Re-list after wakeup or timeout. The Lead must wait for required teammates before giving the final answer.` -} +Use agent ids as task owners. The Lead can reassign to itself, its direct continuable children, or historical members; other descendants claim tasks themselves. list_agents shows direct contacts by default; descendants scope discovers deeper agents without granting message or assignment authority. inactive means no turn is executing; it does not describe task completion, success, failure, or waiting for other agents. A successful send does not mean the recipient has processed it. Shared-task workflow is list, get, claim with the current revision, perform the work, then complete. Task readiness never starts an owner. Before wait_agent, make sure another required member is running; use send_message first for an inactive direct contact, or coordinate through its parent for a deeper member. wait_agent observes only changes after that call starts, never wakes a member, and returns noProgress immediately when no other member is running. Re-list after wakeup or timeout. The Lead must wait for required teammates before giving the final answer.` -const ACTIVE_WAIT_STATUSES: ReadonlySet = new Set(['running', 'provisioning']) -const NO_ACTIVE_PEER_MESSAGE = 'No other Team member is running or provisioning. wait_agent cannot make progress or wake inactive teammates. Re-list with list_agents and team_task_list, then use send_message to wake each required inactive teammate before waiting again.' - -/** - * One model-facing roster row. The Lead pseudo-row omits the - * teammate-only provisioning fields, so only identity, role, status, and - * diagnostics are required. - */ -const MEMBER_VIEW_SCHEMA = { - type: 'object', - additionalProperties: false, - properties: { - target: { type: 'string', required: true }, - role: { type: 'string', required: true, enum: ['lead', 'teammate'] }, - status: { type: 'string', required: true, enum: ['running', 'inactive', 'provisioning', 'failed'] }, - description: { type: 'string' }, - provider: { type: 'string' }, - context: { type: 'string', enum: ['fresh', 'fork'] }, - model: { type: 'string' }, - diagnostics: { type: 'array', required: true, items: { type: 'string' } }, - }, -} as const - -/** Expose the member name as its model-facing target. */ -function modelMember(member: TeamMemberView): InferValue { - const { id: _id, name, ...details } = member - return { target: name, ...details } -} +const NO_ACTIVE_PEER_MESSAGE = 'No other Team member is running. wait_agent cannot make progress or wake inactive teammates. Re-list with list_agents and team_task_list, then use send_message to wake a required inactive direct contact before waiting again; coordinate deeper members through their parent.' /** One shared task, matching the public `TeamTaskView`. */ const TASK_VIEW_SCHEMA = { @@ -80,7 +43,7 @@ const TASK_VIEW_SCHEMA = { subject: { type: 'string', required: true }, description: { type: 'string', required: true }, status: { type: 'string', required: true, enum: ['pending', 'in_progress', 'completed', 'deleted'] }, - ownerName: { type: 'string' }, + ownerId: { type: 'string' }, blockedBy: { type: 'array', required: true, items: { type: 'string' } }, writeScopes: { type: 'array', required: true, items: { type: 'string' } }, ready: { type: 'boolean', required: true }, @@ -88,24 +51,6 @@ const TASK_VIEW_SCHEMA = { }, } as const -const SPAWN_VALUE_SCHEMA = { - type: 'object', - additionalProperties: false, - properties: { - member: { ...MEMBER_VIEW_SCHEMA, required: true }, - }, -} as const - -const MEMBER_LIST_VALUE_SCHEMA = { type: 'array', items: MEMBER_VIEW_SCHEMA } as const - -const SEND_VALUE_SCHEMA = { - type: 'object', - additionalProperties: false, - properties: { - sent: { type: 'boolean', required: true, const: true }, - }, -} as const - /** `noProgress` is present only on the model-only shortcut that skips the wait. */ const WAIT_VALUE_SCHEMA = { type: 'object', @@ -123,14 +68,6 @@ const WAIT_VALUE_SCHEMA = { }, } as const -const INTERRUPT_VALUE_SCHEMA = { - type: 'object', - additionalProperties: false, - properties: { - previousStatus: { type: 'string', required: true, enum: ['running', 'inactive'] }, - }, -} as const - const TASK_LIST_VALUE_SCHEMA = { type: 'object', additionalProperties: false, @@ -165,7 +102,7 @@ function callingAgent(agent: Agent | undefined, toolName: string): Agent { } /** Register the complete Team tool set in one exact Agent scope. */ -function install(agent: Agent, ctx: Context, config: Required): () => void { +function install(agent: Agent, ctx: Context): () => void { const scoped = agent.ctx const disposers: Array<() => unknown> = [] const register = (disposer: () => unknown): void => { disposers.push(disposer) } @@ -173,82 +110,24 @@ function install(agent: Agent, ctx: Context, config: Required): () => vo register(scoped.systemPrompt.section({ name: 'team:policy', order: scoped.systemPrompt.getSectionOrder('TEAM_POLICY'), - text: teamPolicy(config.interrupt), + text: TEAM_POLICY, })) register(scoped.systemPrompt.context({ name: 'team:identity', order: scoped.systemPrompt.getContextOrder('TEAM_IDENTITY'), required: true, + interpolate: false, text: () => { - const { role, name } = ctx.agentTeams.membership(agent) - return `${role === 'lead' - ? 'You are the Team Lead named "lead".' - : `You are teammate "${name}".\nYour Team Lead is named "lead".\nTo message your Team Lead, use send_message({ target: "lead", message: "..." }).`} -Use list_agents({}) to find your teammates and their names. -To message another teammate, use send_message({ target: "", message: "..." }).` + const { role, root } = ctx.agentTeams.membership(agent) + return role === 'lead' + ? `You are the Team Lead. Your agent id is ${JSON.stringify(agent.id)}.` + : `You are a teammate in the Team led by agent ${JSON.stringify(root.id)}.` }, })) - register(scoped.tools.register(defineTool({ - name: 'spawn_teammate', - description: 'Create one named, durable teammate. Only the Team Lead may call this tool.', - parameters: { - name: { type: 'string', required: true, description: 'Unique lower-kebab-case teammate name.' }, - description: { type: 'string', required: true, description: 'Short description of the delegated responsibility.' }, - prompt: { type: 'string', required: true, description: 'Complete initial task for the teammate.' }, - context: { - type: 'string', - enum: ['fresh', 'fork'], - description: 'fresh starts without Lead history; fork inherits completed Lead turns. Defaults to fresh.', - }, - }, - output: jsonOutput(SPAWN_VALUE_SCHEMA), - async execute(args, exec) { - const agent = callingAgent(exec.agent, 'spawn_teammate') - const context = args.context ?? 'fresh' - const result = await ctx.agentTeams.spawnTeammate(agent, { - name: args.name, - description: args.description, - prompt: [{ type: 'text', text: args.prompt }], - context, - provider: context === 'fork' ? config.forkProvider : config.freshProvider, - signal: exec.signal, - }) - return { member: modelMember(result.member) } - }, - }))) - - register(scoped.tools.register(defineTool({ - name: 'send_message', - description: 'Send one message to another Team member. A running target receives it at the nearest step boundary; an inactive target starts or resumes a turn.', - parameters: { - target: { type: 'string', required: true, description: 'Member target returned by spawn_teammate or list_agents, including lead.' }, - message: { type: 'string', required: true, description: 'Self-contained message for the target.' }, - }, - output: jsonOutput(SEND_VALUE_SCHEMA), - async execute(args, exec) { - await ctx.agentTeams.sendMessage(callingAgent(exec.agent, 'send_message'), { - target: args.target, - content: [{ type: 'text', text: args.message }], - signal: exec.signal, - }) - return { sent: true } - }, - }))) - - register(scoped.tools.register(defineTool({ - name: 'list_agents', - description: 'List the Lead and every durable teammate with an addressable target and current availability. inactive means no turn is executing, not a task result. provisioning and failed describe member creation.', - parameters: {}, - output: jsonOutput(MEMBER_LIST_VALUE_SCHEMA), - execute(_args, exec) { - return Promise.resolve(ctx.agentTeams.listMembers(callingAgent(exec.agent, 'list_agents')).map(modelMember)) - }, - }))) - register(scoped.tools.register(defineTool({ name: 'wait_agent', - description: 'Wait for the next teammate status, message, or shared-task change after this call starts. This never wakes inactive members and returns noProgress immediately when no other member is running or provisioning. Re-list after wakeup or timeout instead of polling.', + description: 'Wait for the next teammate status, message, or shared-task change after this call starts. This never wakes inactive members and returns noProgress immediately when no other member is running. Re-list after wakeup or timeout instead of polling.', parameters: { timeout_ms: { type: 'integer', @@ -266,9 +145,7 @@ To message another teammate, use send_message({ target: "", messa } // The active-peer read and waiter registration must remain one synchronous // span; awaiting between them can lose the only peer-status edge. - const hasActivePeer = ctx.agentTeams.listMembers(caller).some(member => - member.id !== caller.id && ACTIVE_WAIT_STATUSES.has(member.status)) - if (!hasActivePeer) { + if (!ctx.agentTeams.hasActivePeer(caller)) { return { timedOut: false, noProgress: { @@ -281,20 +158,6 @@ To message another teammate, use send_message({ target: "", messa }, }))) - if (config.interrupt) { - register(scoped.tools.register(defineTool({ - name: 'interrupt_agent', - description: 'Interrupt one teammate\'s current turn while preserving its pending inbox. Team Lead only.', - parameters: { - target: { type: 'string', required: true, description: 'Teammate target returned by spawn_teammate or list_agents.' }, - }, - output: jsonOutput(INTERRUPT_VALUE_SCHEMA), - execute(args, exec) { - return Promise.resolve(ctx.agentTeams.interrupt(callingAgent(exec.agent, 'interrupt_agent'), args.target)) - }, - }))) - } - register(scoped.tools.register(defineTool({ name: 'team_task_create', description: 'Create one unowned pending task on the shared Team task board.', @@ -328,7 +191,7 @@ To message another teammate, use send_message({ target: "", messa enum: ['pending', 'in_progress', 'completed'], description: 'Optional exact status filter.', }, - owner: { type: 'string', description: 'Optional member target from spawn_teammate or list_agents, matching ownerName; use unowned for tasks without an owner.' }, + owner: { type: 'string', description: 'Optional exact ownerId filter; use unowned for tasks without an owner.' }, ready: { type: 'boolean', description: 'Optional readiness filter.' }, cursor: { type: 'integer', description: 'Zero-based result offset. Defaults to 0.' }, limit: { type: 'integer', description: 'Number of rows, 1 through 100. Defaults to 50.' }, @@ -338,7 +201,7 @@ To message another teammate, use send_message({ target: "", messa const status = args.status const filtered = ctx.agentTeams.listTasks(callingAgent(exec.agent, 'team_task_list')).filter(task => (status === undefined || task.status === status) - && (args.owner === undefined || (args.owner === 'unowned' ? task.ownerName === undefined : task.ownerName === args.owner)) + && (args.owner === undefined || (args.owner === 'unowned' ? task.ownerId === undefined : task.ownerId === args.owner)) && (args.ready === undefined || task.ready === args.ready)) const cursor = args.cursor ?? 0 const limit = args.limit ?? 50 @@ -382,7 +245,7 @@ To message another teammate, use send_message({ target: "", messa description: { type: 'string', description: 'Replacement details for edit.' }, blocked_by: { type: 'array', items: { type: 'string' }, description: 'Complete blocker list for set_dependencies.' }, write_scopes: { type: 'array', items: { type: 'string' }, description: 'Replacement advisory write scopes for edit.' }, - owner: { type: 'string', description: 'Member target from spawn_teammate or list_agents for Lead-only reassign; omit to unassign.' }, + owner: { type: 'string', description: 'Agent id of the Lead or one of its direct continuable children for Lead-only reassign; omit to unassign.' }, }, output: jsonOutput(TASK_VIEW_SCHEMA), async execute(args, exec) { @@ -394,7 +257,7 @@ To message another teammate, use send_message({ target: "", messa ...args.description === undefined ? {} : { description: args.description }, ...args.blocked_by === undefined ? {} : { blockedBy: args.blocked_by.map(TeamTaskId) }, ...args.write_scopes === undefined ? {} : { writeScopes: args.write_scopes }, - ...args.owner === undefined ? {} : { owner: args.owner }, + ...args.owner === undefined ? {} : { owner: SessionId(args.owner) }, }) }, }))) @@ -407,17 +270,21 @@ To message another teammate, use send_message({ target: "", messa } } -/** Install Team tools in every live or subsequently published Team member scope. */ +/** + * Install Team tools in every live or subsequently published Team member scope. + * @param ctx - context carrying the Agent registry, Team service, tools, and system prompt. + * @param config - the empty plugin configuration; removed keys fail at load. + */ export function apply(ctx: Context, config: Config = {}): void { - const resolved: Required = { - interrupt: config.interrupt ?? true, - freshProvider: config.freshProvider ?? 'spawn', - forkProvider: config.forkProvider ?? 'fork', + for (const key of REMOVED_CONFIG_KEYS) { + if (Object.hasOwn(config, key)) { + throw new Error(`tool-agent-team: ${key} is no longer supported; create teammates with a subagent tool`) + } } const installed = new Map void>() const maybeInstall = (agent: Agent): void => { if (installed.has(agent) || ctx.agentTeams.tryMembership(agent) === undefined) return - installed.set(agent, install(agent, ctx, resolved)) + installed.set(agent, install(agent, ctx)) } for (const agent of ctx.agents.list()) maybeInstall(agent) ctx.on('agent/created', ({ agent }) => { maybeInstall(agent) }) diff --git a/packages/experimental/tool-agent-team/tests/tool-team.spec.ts b/packages/experimental/tool-agent-team/tests/tool-team.spec.ts index 3715748b7b..7f0b879066 100644 --- a/packages/experimental/tool-agent-team/tests/tool-team.spec.ts +++ b/packages/experimental/tool-agent-team/tests/tool-team.spec.ts @@ -16,6 +16,7 @@ import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork-in-process' import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process' import { renderPrompt, renderContextSnapshot } from '@deepseek-ai/dsh-system-prompt' import * as ToolSubagentControl from '@deepseek-ai/dsh-tool-subagent-control' +import * as ToolSubagentListAgents from '@deepseek-ai/dsh-tool-subagent-control/list-agents' import { defineContentToolFixture } from '@deepseek-ai/dsh-tools' import { resolveAdapterOptions } from '@deepseek-ai/dsh-llm-deepseek' import { serialize } from '@deepseek-ai/dsh-llm-deepseek/src/serialize.ts' @@ -36,17 +37,14 @@ function runtimeContextText(request: GenerateOptions) { } const SIGNAL = new AbortController().signal -const TOOL_NAMES = [ - 'spawn_teammate', - 'send_message', - 'list_agents', - 'wait_agent', - 'interrupt_agent', +const TEAM_TOOLS = [ 'team_task_create', - 'team_task_list', 'team_task_get', + 'team_task_list', 'team_task_update', -].sort() + 'wait_agent', +] +const leadIdentity = (agent: Agent): string => `You are the Team Lead. Your agent id is ${JSON.stringify(agent.id)}.` const roots: string[] = [] const contexts = new Set() @@ -69,7 +67,7 @@ afterEach(async () => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) }) -async function setup(script: ConstructorParameters[0], legacyControl = false) { +async function setup(script: ConstructorParameters[0]) { const ctx = new Context() contexts.add(ctx) await mountAgentLoopTestDependencies(ctx) @@ -80,7 +78,8 @@ async function setup(script: ConstructorParameters[0], legac await ctx.plugin(TestSessionQuery) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(SubagentService) - if (legacyControl) await ctx.plugin(ToolSubagentControl) + await ctx.plugin(ToolSubagentControl) + await ctx.plugin(ToolSubagentListAgents) await ctx.plugin(SubagentSpawn, { providerName: 'spawn' }) await ctx.plugin(SubagentFork, { providerName: 'fork' }) await ctx.plugin(TeamService) @@ -111,11 +110,16 @@ function text(result: Awaited>): string { return result.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('') } -function spawnedChildId(ctx: Context, lead: Agent, result: Awaited>): SessionId { - const parsed = JSON.parse(text(result)) as { member: { target: string } } - const member = ctx.agentTeams.listMembers(lead).find(member => member.name === parsed.member.target) - if (member === undefined) throw new Error('spawn_teammate target has no roster member') - return member.id +/** Start a continuable child of the Lead with a task description. */ +async function spawn(ctx: Context, lead: Agent, description: string | undefined, prompt: string, provider = 'spawn'): Promise { + const started = await ctx.subagents.startActivation({ + provider, + label: `${description ?? 'shared'} task`, + request: { prompt: [{ type: 'text', text: prompt }], parent: lead }, + signal: SIGNAL, + delivery: 'parent', + }) + return started.childId } async function assembly(ctx: Context, agent: Agent) { @@ -124,6 +128,14 @@ async function assembly(ctx: Context, agent: Agent) { return ctx.systemPrompt.assemble({ scope }) } +function teamTools(assembled: Awaited>): string[] { + return assembled.tools.map(schema => schema.name).filter(name => TEAM_TOOLS.includes(name)).sort() +} + +function teammateIdentity(lead: Agent): string { + return `You are a teammate in the Team led by agent ${JSON.stringify(lead.id)}.` +} + async function runTurn(agent: Agent, text: string): Promise { agent.followup(createUserMessage({ content: [{ type: 'text', text }], source: { kind: 'user' } })) await agent.whenIdle() @@ -141,192 +153,70 @@ async function waitNoAgent(ctx: Context, id: SessionId): Promise { await vi.waitFor(() => { expect(ctx.agents.get(id)).toBeUndefined() }, { timeout: 5_000 }) } +function interrupt(ctx: Context, lead: Agent, target: SessionId): void { + ctx.subagents.interrupt(target, { kind: 'ancestor', agent: lead }) +} + describe('dsh-tool-team', () => { - it.each(['running', 'inactive', 'provisioning', 'failed'] as const)( - 'projects %s members consistently in creation, listing, and schemas', async (status) => { - const { ctx, lead } = await setup([]) - const member = { - id: SessionId('private-member-session'), name: 'reviewer', role: 'teammate' as const, - status, description: 'review changes', diagnostics: [], - } - vi.spyOn(ctx.agentTeams, 'spawnTeammate').mockResolvedValue({ member }) - vi.spyOn(ctx.agentTeams, 'listMembers').mockReturnValue([member]) - const expected = { - target: 'reviewer', role: 'teammate', status, - description: 'review changes', diagnostics: [], - } - const spawned = await execute(ctx, lead, 'spawn_teammate', { - name: 'reviewer', description: 'review changes', prompt: 'review', - }) - const listed = await execute(ctx, lead, 'list_agents', {}) - expect(spawned.isError).toBe(false) - expect(listed.isError).toBe(false) - expect(JSON.parse(text(spawned))).toEqual({ member: expected }) - expect(JSON.parse(text(listed))).toEqual([expected]) - const scope = scopeOf(lead.ctx) - const spawnSchema = ctx.tools.get('spawn_teammate', scope)?.output.schema.properties?.member - const listSchema = ctx.tools.get('list_agents', scope)?.output.schema.items - for (const schema of [spawnSchema, listSchema]) { - expect(schema?.properties).toHaveProperty('target') - expect(schema?.properties).not.toHaveProperty('id') - expect(schema?.properties).not.toHaveProperty('name') - expect(schema?.properties?.status?.enum).toEqual(['running', 'inactive', 'provisioning', 'failed']) - } - expect(ctx.agentTeams.listMembers(lead)).toEqual([member]) - }, - ) - - it.each(['running', 'inactive'] as const)('returns interrupted %s status', async (previousStatus) => { - const { ctx, lead } = await setup([]) - const interrupt = vi.spyOn(ctx.agentTeams, 'interrupt').mockReturnValue({ previousStatus }) - const result = await execute(ctx, lead, 'interrupt_agent', { target: 'reviewer' }) - expect(JSON.parse(text(result))).toEqual({ previousStatus }) - expect(interrupt).toHaveBeenCalledWith(lead, 'reviewer') - }) - - it('omits interrupt tools and guidance for Leads and new teammates when disabled', async () => { - const { ctx, lead, fiber } = await setup(['hang']) - await fiber.dispose() - await ctx.plugin(toolTeam, { interrupt: false }) - const spawned = await execute(ctx, lead, 'spawn_teammate', { - name: 'worker', description: 'stay active', prompt: 'wait', - }) - expect(spawned.isError, text(spawned)).toBe(false) - const childId = spawnedChildId(ctx, lead, spawned) - const child = await waitRunning(ctx, childId) - for (const agent of [lead, child]) { - const assembled = await assembly(ctx, agent) - expect(assembled.tools.map(schema => schema.name).filter(name => TOOL_NAMES.includes(name)).sort()) - .toEqual(TOOL_NAMES.filter(name => name !== 'interrupt_agent')) - expect(renderPrompt(assembled)).not.toContain('interrupt_agent') - const result = await execute(ctx, agent, 'interrupt_agent', { target: 'worker' }) - expect(result.isError).toBe(true) - expect(text(result)).toContain('unknown tool "interrupt_agent"') - } - expect(child.status).toBe('running') - expect((await execute(ctx, lead, 'list_agents', {})).isError).toBe(false) - expect(ctx.agentTeams.interrupt(lead, 'worker')).toEqual({ previousStatus: 'running' }) - await waitNoAgent(ctx, childId) - }) - - it('uses returned targets for messages, interruption, and task assignment', async () => { + it('installs task and wait tools with the shared policy for the Lead and every subagent below it', async () => { const { ctx, lead } = await setup(['hang', 'hang']) - const spawned = await execute(ctx, lead, 'spawn_teammate', { - name: 'reviewer', description: 'review changes', prompt: 'wait for work', - }) - const { member } = JSON.parse(text(spawned)) as { member: { target: string } } - const childId = spawnedChildId(ctx, lead, spawned) - const child = await waitRunning(ctx, childId) - expect(member).not.toHaveProperty('id') - expect(member).not.toHaveProperty('name') - const listed = JSON.parse(text(await execute(ctx, lead, 'list_agents', {}))) as Array<{ target: string }> - expect(listed.map(row => row.target)).toEqual(['lead', member.target]) - const created = await execute(ctx, lead, 'team_task_create', { subject: 'review', description: 'review changes' }) - const task = JSON.parse(text(created)) as { id: string; revision: number } - const assigned = await execute(ctx, lead, 'team_task_update', { - task_id: task.id, expected_revision: task.revision, action: 'reassign', owner: member.target, - }) - expect(assigned.isError).toBe(false) - expect(JSON.parse(text(assigned))).toMatchObject({ ownerName: member.target }) - const tasks = await execute(ctx, lead, 'team_task_list', { owner: listed[1]!.target }) - expect(JSON.parse(text(tasks))).toMatchObject({ tasks: [{ id: task.id, ownerName: member.target }] }) - const sent = await execute(ctx, lead, 'send_message', { target: member.target, message: 'review the diff' }) - expect(sent.isError).toBe(false) - expect(JSON.parse(text(sent))).toEqual({ sent: true }) - const interrupted = await execute(ctx, lead, 'interrupt_agent', { target: listed[1]!.target }) - expect(interrupted.isError).toBe(false) - await child.whenIdle() - expect(child.status).toBe('idle') - expect(ctx.agentTeams.interrupt(lead, member.target)).toEqual({ previousStatus: 'inactive' }) - const stored = await execute(ctx, lead, 'list_agents', {}) - expect(JSON.parse(text(stored))).toContainEqual(expect.objectContaining({ target: member.target, status: 'inactive' })) - expect(ctx.agentTeams.listMembers(lead)[1]).toMatchObject({ id: childId, name: member.target, status: 'inactive' }) - }) - - it('installs the complete scoped schema and shared-checkout policy for roots and teammates', async () => { - const { ctx, lead } = await setup(['hang']) const leadAssembly = await assembly(ctx, lead) - expect(leadAssembly.tools.map(schema => schema.name).filter(name => TOOL_NAMES.includes(name)).sort()) - .toEqual(TOOL_NAMES) + expect(teamTools(leadAssembly)).toEqual(TEAM_TOOLS) const leadPrompt = renderPrompt(leadAssembly) - expect(leadPrompt).toContain('Agent Teams is available in this session\n\n') + expect(leadPrompt).toContain('Teammates are subagents.') expect(leadPrompt).toContain('FS_STALE_VERSION') - expect(leadPrompt).toContain('Bash, formatters, code generators, and scripts are not fully protected') expect(leadPrompt).toContain('Task readiness never starts an owner') expect(leadPrompt).toContain('returns noProgress immediately') - expect(leadPrompt).not.toContain('Your Team role') - expect(renderContextSnapshot(leadAssembly)).toContain('You are the Team Lead named "lead".') + expect(renderContextSnapshot(leadAssembly)).toContain(leadIdentity(lead)) + expect(leadAssembly.tools.map(schema => schema.name)).not.toContain('spawn_teammate') + // Messaging, listing, and interruption stay the global subagent controls. + for (const name of ['send_message', 'list_agents', 'interrupt_agent']) { + expect(ctx.tools.get(name, scopeOf(lead.ctx))).toBe(ctx.tools.get(name)) + } - const spawned = await execute(ctx, lead, 'spawn_teammate', { - name: 'tool-worker', - description: 'exercise scoped tools', - prompt: 'stay available', - }) - expect(spawned.isError, text(spawned)).toBe(false) - const childId = spawnedChildId(ctx, lead, spawned) - const child = await waitRunning(ctx, childId) - const childAssembly = await assembly(ctx, child) - expect(childAssembly.tools.map(schema => schema.name).filter(name => TOOL_NAMES.includes(name)).sort()) - .toEqual(TOOL_NAMES) - expect(renderPrompt(childAssembly)).toBe(leadPrompt) - expect(renderContextSnapshot(childAssembly)).toContain('You are teammate "tool-worker".') - expect(childAssembly.contexts).toContainEqual(expect.objectContaining({ name: 'team:identity' })) - const initialPrompt = child.session.snapshotEvents().find(event => event.type === 'user/message' - && event.data.source.kind === 'user') - expect(initialPrompt?.type === 'user/message' - ? initialPrompt.data.content.flatMap(block => block.type === 'text' ? [block.text] : []) - : []).toEqual(['stay available']) - - const denied = await execute(ctx, child, 'spawn_teammate', { - name: 'nested', description: 'not allowed', prompt: 'no', - }) - expect(denied.isError).toBe(true) - expect(text(denied)).toContain('only the Team Lead') - await execute(ctx, lead, 'interrupt_agent', { target: 'tool-worker' }) - await vi.waitFor(() => { expect(ctx.agents.get(childId)).toBeUndefined() }, { timeout: 5_000 }) + const namedId = await spawn(ctx, lead, 'reviewer', 'stay available') + const unnamedId = await spawn(ctx, lead, undefined, 'stay available too') + for (const id of [namedId, unnamedId]) { + const child = await waitRunning(ctx, id) + const childAssembly = await assembly(ctx, child) + expect(teamTools(childAssembly)).toEqual(TEAM_TOOLS) + expect(renderPrompt(childAssembly)).toBe(leadPrompt) + expect(renderContextSnapshot(childAssembly)).toContain(teammateIdentity(lead)) + expect(ctx.tools.get('send_message', scopeOf(child.ctx))).toBe(ctx.tools.get('send_message')) + } + interrupt(ctx, lead, namedId) + interrupt(ctx, lead, unnamedId) + await Promise.all([waitNoAgent(ctx, namedId), waitNoAgent(ctx, unnamedId)]) }) it.each([ - ['fresh', undefined], ['fork', undefined], ['fork', 'in-history'], - ] as const)('records %s teammate identity with update mode %s and keeps the wire prefix', async (mode, systemPromptUpdate) => { + ['spawn', undefined], ['fork', undefined], ['fork', 'in-history'], + ] as const)('records %s teammate identity with update mode %s and keeps the wire prefix', async (provider, systemPromptUpdate) => { const { ctx, lead, adapter } = await setup([textResponse('parent answer'), textResponse('child answer')]) if (systemPromptUpdate !== undefined) adapter.systemPromptUpdate = systemPromptUpdate await runTurn(lead, 'Parent task') const parentRequest = serializeRequest(adapter.requests[0]!) const parentHistory = structuredClone(lead.session.deriveMessages()) - const spawned = await execute(ctx, lead, 'spawn_teammate', { - name: 'reviewer', description: 'review', prompt: 'Review the work', context: mode, - }) - expect(spawned.isError, text(spawned)).toBe(false) - const childId = spawnedChildId(ctx, lead, spawned) + const childId = await spawn(ctx, lead, 'reviewer', 'Review the work', provider) await waitNoAgent(ctx, childId) const childRequest = serializeRequest(adapter.requests[1]!) expect(childRequest.tools).toEqual(parentRequest.tools) expect(childRequest.system).toEqual(parentRequest.system) - expect(childRequest.system).not.toContain('Your Team role') - if (mode === 'fork') { + if (provider === 'fork') { expect(childRequest.messages.slice(0, parentRequest.messages.length)).toEqual(parentRequest.messages) expect(adapter.requests[1]!.messages.slice(0, parentHistory.length)).toEqual(parentHistory) } else { expect(JSON.stringify(childRequest.messages)).not.toContain('Parent task') } - expect(runtimeContextText(adapter.requests[1]!)).toContain('You are teammate "reviewer".') - expect(childRequest.messages.at(-1)?.content).toContainEqual({ type: 'text', text: 'Review the work' }) - await using persisted = await ctx.sessionPersistence.open(childId, 'read') - const { events } = await persisted.read() - const initial = events.findLast(event => event.type === 'user/message' && event.data.source.kind === 'user') - expect(initial?.type === 'user/message' ? initial.data.content : []).toEqual([ - { type: 'text', text: 'Review the work' }, - ]) + expect(runtimeContextText(adapter.requests[1]!)).toContain(teammateIdentity(lead)) }) - it('reuses unchanged Lead identity in an ordinary Lead fork', async () => { + it('appends the new Lead id after inherited history in an ordinary fork', async () => { const { ctx, lead, adapter } = await setup([textResponse('parent answer'), textResponse('fork answer')]) await runTurn(lead, 'Parent task') const seed = lead.session.snapshotEvents() - const childId = SessionId('ordinary-team-fork') const handle = await ctx.agents.create({ - sessionId: childId, + sessionId: SessionId('ordinary-team-fork'), seed, inheritedEventCount: SessionLogOffset(seed.length), meta: { parentSession: lead.id, isSeeded: true }, @@ -339,19 +229,17 @@ describe('dsh-tool-team', () => { expect(childRequest.tools).toEqual(parentRequest.tools) expect(childRequest.messages.slice(0, parentRequest.messages.length)).toEqual(parentRequest.messages) expect(childRequest.messages.at(-1)?.content).toContainEqual({ type: 'text', text: 'Continue independently' }) + expect(runtimeContextText(adapter.requests[1]!)).toContain(leadIdentity(handle.agent)) expect(JSON.stringify(childRequest.messages)).not.toContain('system-reminder') expect(handle.agent.session.snapshotEvents().slice(0, seed.length)).toEqual(seed) - expect(ctx.agentTeams.listMembers(handle.agent).map(member => member.name)).toEqual(['lead']) + expect(ctx.agentTeams.membership(handle.agent)).toMatchObject({ role: 'lead' }) await handle.dispose() }) it.each([undefined, 'in-history'] as const)('appends current Lead identity to a teammate fork with update mode %s', async (systemPromptUpdate) => { const { ctx, lead, adapter } = await setup([textResponse('review done'), textResponse('lead notified'), textResponse('fork answer')]) if (systemPromptUpdate !== undefined) adapter.systemPromptUpdate = systemPromptUpdate - const spawned = await execute(ctx, lead, 'spawn_teammate', { - name: 'reviewer', description: 'review', prompt: 'Review the work', - }) - const teammateId = spawnedChildId(ctx, lead, spawned) + const teammateId = await spawn(ctx, lead, 'reviewer', 'Review the work') await waitNoAgent(ctx, teammateId) await using persisted = await ctx.sessionPersistence.open(teammateId, 'read') const { events: seed } = await persisted.read() @@ -372,8 +260,7 @@ describe('dsh-tool-team', () => { expect(fork.tools).toEqual(original.tools) expect(fork.messages.slice(0, original.messages.length)).toEqual(original.messages) expect(forkRequest.messages.slice(0, inheritedHistory.length)).toEqual(inheritedHistory) - expect(fork.messages.at(-1)?.content).toContainEqual({ type: 'text', text: 'Continue independently' }) - expect(runtimeContextText(forkRequest)).toContain('You are the Team Lead named "lead".') + expect(runtimeContextText(forkRequest)).toContain(leadIdentity(handle.agent)) expect(handle.agent.session.snapshotEvents().slice(0, seed.length)).toEqual(seed) } finally { await handle.dispose() @@ -382,8 +269,8 @@ describe('dsh-tool-team', () => { it('restores compacted identity and does not repeat an unchanged retained snapshot', async () => { const { ctx, lead, adapter } = await setup([ - toolCallResponse('first-list', 'list_agents', {}), - toolCallResponse('second-list', 'list_agents', {}), + toolCallResponse('first-list', 'team_task_list', {}), + toolCallResponse('second-list', 'team_task_list', {}), textResponse('done'), ]) ctx.on('agent/pre-step', async ({ agent, step }, next) => { @@ -401,15 +288,12 @@ describe('dsh-tool-team', () => { } return next() }) - const spawned = await execute(ctx, lead, 'spawn_teammate', { - name: 'reviewer', description: 'review', prompt: 'Review the work', - }) - const childId = spawnedChildId(ctx, lead, spawned) + const childId = await spawn(ctx, lead, 'reviewer', 'Review the work') await waitNoAgent(ctx, childId) for (const request of adapter.requests.filter(request => request.sessionId === childId)) { const snapshots = request.messages.filter(message => message.source?.kind === 'runtime-context') expect(snapshots).toHaveLength(1) - expect(runtimeContextText(request)).toContain('You are teammate "reviewer".') + expect(runtimeContextText(request)).toContain(teammateIdentity(lead)) } await using persisted = await ctx.sessionPersistence.open(childId, 'read') const { events } = await persisted.read() @@ -417,45 +301,18 @@ describe('dsh-tool-team', () => { && event.data.source.kind === 'runtime-context')).toHaveLength(2) }) - it.each(['reject', 'empty', 'abort'] as const)('does not revive a teammate step after %s', async (mode) => { - const { ctx, lead, adapter } = await setup([]) - ctx.on('agent/pre-step', async ({ agent }, next) => { - const decision = await next() - if (agent === lead) return decision - if (mode === 'reject') return { kind: 'reject' } - if (mode === 'abort') agent.cancel({ kind: 'user' }) - return { kind: 'enter', messages: [] } - }) - await execute(ctx, lead, 'spawn_teammate', { - name: 'reviewer', description: 'review', prompt: 'Review the work', - }) - const childId = SessionId(ctx.agentTeams.listMembers(lead).find(member => member.name === 'reviewer')!.id) - await waitNoAgent(ctx, childId) - expect(adapter.requests.filter(request => request.sessionId === childId)).toEqual([]) - await using persisted = await ctx.sessionPersistence.open(childId, 'read') - const { events } = await persisted.read() - expect(events.filter(event => event.type === 'user/message' - && event.data.source.kind === 'user')).toEqual([]) - }) - it('keeps required Team identities when optional runtime context is suppressed', async () => { const { ctx, lead, adapter } = await setup([textResponse('worker done')]) ctx.systemPrompt.suppressRuntimeContext() - const spawned = await execute(ctx, lead, 'spawn_teammate', { - name: 'reviewer', description: 'review', prompt: 'Review the work', - }) - expect(spawned.isError, text(spawned)).toBe(false) - await waitNoAgent(ctx, spawnedChildId(ctx, lead, spawned)) - expect(renderContextSnapshot(await assembly(ctx, lead))).toContain('You are the Team Lead named "lead".') - expect(runtimeContextText(adapter.requests[0]!)).toContain('You are teammate "reviewer".') + const childId = await spawn(ctx, lead, 'reviewer', 'Review the work') + await waitNoAgent(ctx, childId) + expect(renderContextSnapshot(await assembly(ctx, lead))).toContain(leadIdentity(lead)) + expect(runtimeContextText(adapter.requests[0]!)).toContain(teammateIdentity(lead)) }) it('returns actionable no-progress output and renders structured wait cancellation', async () => { const inactiveSetup = await setup([textResponse('worker done')]) - const inactiveSpawn = await execute(inactiveSetup.ctx, inactiveSetup.lead, 'spawn_teammate', { - name: 'inactive-worker', description: 'finish immediately', prompt: 'finish', - }) - const inactiveId = spawnedChildId(inactiveSetup.ctx, inactiveSetup.lead, inactiveSpawn) + const inactiveId = await spawn(inactiveSetup.ctx, inactiveSetup.lead, 'inactive-worker', 'finish') await waitNoAgent(inactiveSetup.ctx, inactiveId) const noProgress = await execute(inactiveSetup.ctx, inactiveSetup.lead, 'wait_agent', { timeout_ms: 3_600_000 }) expect(noProgress.isError).toBe(false) @@ -463,7 +320,7 @@ describe('dsh-tool-team', () => { timedOut: false, noProgress: { reason: 'no-active-peer', - message: 'No other Team member is running or provisioning. wait_agent cannot make progress or wake inactive teammates. Re-list with list_agents and team_task_list, then use send_message to wake each required inactive teammate before waiting again.', + message: 'No other Team member is running. wait_agent cannot make progress or wake inactive teammates. Re-list with list_agents and team_task_list, then use send_message to wake a required inactive direct contact before waiting again; coordinate deeper members through their parent.', }, }) for (const timeout_ms of [9_999, 3_600_001, Number.MAX_SAFE_INTEGER + 1]) { @@ -473,10 +330,7 @@ describe('dsh-tool-team', () => { } const activeSetup = await setup(['hang']) - const activeSpawn = await execute(activeSetup.ctx, activeSetup.lead, 'spawn_teammate', { - name: 'active-worker', description: 'stay active', prompt: 'wait', - }) - const activeId = spawnedChildId(activeSetup.ctx, activeSetup.lead, activeSpawn) + const activeId = await spawn(activeSetup.ctx, activeSetup.lead, 'active-worker', 'wait') await waitRunning(activeSetup.ctx, activeId) const controller = new AbortController() const waiting = execute(activeSetup.ctx, activeSetup.lead, 'wait_agent', { timeout_ms: 10_000 }, controller.signal) @@ -485,40 +339,24 @@ describe('dsh-tool-team', () => { const aborted = await waiting expect(aborted.isError).toBe(true) expect(text(aborted)).toBe("Error: wait_agent aborted: { kind: 'user' }") - await execute(activeSetup.ctx, activeSetup.lead, 'interrupt_agent', { target: 'active-worker' }) + interrupt(activeSetup.ctx, activeSetup.lead, activeId) await waitNoAgent(activeSetup.ctx, activeId) }) - it('adapts roster, mailbox, wait, and task CAS operations to canonical JSON', async () => { - const { ctx, lead } = await setup(['hang', textResponse('lead received wakeup')]) - const spawned = await execute(ctx, lead, 'spawn_teammate', { - name: 'json-worker', description: 'json worker', prompt: 'wait', context: 'fresh', - }) - const childId = spawnedChildId(ctx, lead, spawned) + it('adapts task CAS operations and waits to canonical JSON', async () => { + const { ctx, lead } = await setup(['hang']) + const childId = await spawn(ctx, lead, 'json-worker', 'wait') const child = await waitRunning(ctx, childId) - const roster = await execute(ctx, child, 'list_agents', {}) - expect(JSON.parse(text(roster))).toMatchObject([ - { target: 'lead', role: 'lead' }, - { target: 'json-worker', role: 'teammate' }, - ]) - // Every Team result reaches the model as compact JSON: indentation would - // spend tokens on every roster, task, and receipt without adding meaning. - expect(text(roster)).toBe(JSON.stringify(JSON.parse(text(roster)))) - const peer = await execute(ctx, child, 'send_message', { target: 'lead', message: 'progress report' }) - expect(peer.isError).toBe(false) - expect(JSON.parse(text(peer))).toEqual({ sent: true }) - const followup = await execute(ctx, child, 'send_message', { target: 'lead', message: 'review the report' }) - expect(followup.isError).toBe(false) - expect(JSON.parse(text(followup))).toEqual({ sent: true }) - await lead.whenIdle() - const created = await execute(ctx, lead, 'team_task_create', { subject: 'tool task', description: 'created through tool', blocked_by: [], write_scopes: ['src/team'], }) + // Every Team result reaches the model as compact JSON: indentation would + // spend tokens on every task and wait result without adding meaning. + expect(text(created)).toBe(JSON.stringify(JSON.parse(text(created)))) const task = JSON.parse(text(created)) as { id: string; revision: number } const listed = await execute(ctx, child, 'team_task_list', { ready: true, limit: 1 }) expect(JSON.parse(text(listed))).toMatchObject({ tasks: [{ id: task.id, ready: true }] }) @@ -529,7 +367,7 @@ describe('dsh-tool-team', () => { expected_revision: task.revision, action: 'claim', }) - expect(JSON.parse(text(claimed))).toMatchObject({ status: 'in_progress', ownerName: 'json-worker' }) + expect(JSON.parse(text(claimed))).toMatchObject({ status: 'in_progress', ownerId: childId }) const stale = await execute(ctx, lead, 'team_task_update', { task_id: task.id, expected_revision: task.revision, @@ -551,18 +389,13 @@ describe('dsh-tool-team', () => { await expect(wait).resolves.toMatchObject({ isError: false }) expect((await completedCall).isError).toBe(false) - const childInterrupt = await execute(ctx, child, 'interrupt_agent', { target: 'json-worker' }) - expect(childInterrupt.isError).toBe(true) - await execute(ctx, lead, 'interrupt_agent', { target: 'json-worker' }) - await vi.waitFor(() => { expect(ctx.agents.get(childId)).toBeUndefined() }, { timeout: 5_000 }) + interrupt(ctx, lead, childId) + await waitNoAgent(ctx, childId) }) it('adapts optional task filters, mutations, pagination, and default waiting', async () => { const { ctx, lead } = await setup(['hang']) - const spawned = await execute(ctx, lead, 'spawn_teammate', { - name: 'fork-worker', description: 'fork worker', prompt: 'stay active', context: 'fork', - }) - const childId = spawnedChildId(ctx, lead, spawned) + const childId = await spawn(ctx, lead, 'fork-worker', 'stay active', 'fork') await waitRunning(ctx, childId) const firstResult = await execute(ctx, lead, 'team_task_create', { @@ -579,7 +412,7 @@ describe('dsh-tool-team', () => { const claim = JSON.parse(text(claimed)) as { revision: number } expect(JSON.parse(text(await execute(ctx, lead, 'team_task_list', { - status: 'in_progress', owner: 'lead', cursor: 0, limit: 1, + status: 'in_progress', owner: lead.id, cursor: 0, limit: 1, })))).toMatchObject({ tasks: [{ id: first.id }] }) expect(JSON.parse(text(await execute(ctx, lead, 'team_task_list', { owner: 'unowned', limit: 1, @@ -614,7 +447,7 @@ describe('dsh-tool-team', () => { task_id: first.id, expected_revision: dependency.revision, action: 'reassign', - owner: 'fork-worker', + owner: childId, })).isError).toBe(true) const wait = execute(ctx, lead, 'wait_agent', {}) @@ -628,62 +461,36 @@ describe('dsh-tool-team', () => { expect((await wait).isError).toBe(false) expect((await wake).isError).toBe(false) - await execute(ctx, lead, 'interrupt_agent', { target: 'fork-worker' }) - await vi.waitFor(() => { expect(ctx.agents.get(childId)).toBeUndefined() }, { timeout: 5_000 }) + interrupt(ctx, lead, childId) + await waitNoAgent(ctx, childId) }) it('removes and reinstalls every scoped registration across plugin HMR without stopping the child', async () => { const { ctx, lead, fiber } = await setup(['hang']) - const spawned = await execute(ctx, lead, 'spawn_teammate', { - name: 'hmr-worker', description: 'hmr worker', prompt: 'wait', - }) - const childId = spawnedChildId(ctx, lead, spawned) + const childId = await spawn(ctx, lead, 'hmr-worker', 'wait') const child = await waitRunning(ctx, childId) await fiber.dispose() - expect((await assembly(ctx, lead)).tools.map(schema => schema.name).some(name => TOOL_NAMES.includes(name))).toBe(false) - expect((await assembly(ctx, child)).tools.map(schema => schema.name).some(name => TOOL_NAMES.includes(name))).toBe(false) - expect(ctx.agents.get(childId)).toBe(child) - for (const agent of [lead, child]) { - expect((await assembly(ctx, agent)).contexts.some(context => context.name === 'team:identity')).toBe(false) - } - - const disabled = await ctx.plugin(toolTeam, { interrupt: false }) for (const agent of [lead, child]) { const assembled = await assembly(ctx, agent) - expect(assembled.tools.map(schema => schema.name).filter(name => TOOL_NAMES.includes(name)).sort()) - .toEqual(TOOL_NAMES.filter(name => name !== 'interrupt_agent')) - expect(renderPrompt(assembled)).not.toContain('interrupt_agent') + expect(teamTools(assembled)).toEqual([]) + expect(assembled.contexts.some(context => context.name === 'team:identity')).toBe(false) } - expect(child.status).toBe('running') - await disabled.dispose() + expect(ctx.agents.get(childId)).toBe(child) const replacement = await ctx.plugin(toolTeam) - expect((await assembly(ctx, lead)).tools.map(schema => schema.name).filter(name => TOOL_NAMES.includes(name)).sort()) - .toEqual(TOOL_NAMES) - expect((await assembly(ctx, child)).tools.map(schema => schema.name).filter(name => TOOL_NAMES.includes(name)).sort()) - .toEqual(TOOL_NAMES) - await execute(ctx, lead, 'interrupt_agent', { target: 'hmr-worker' }) - await vi.waitFor(() => { expect(ctx.agents.get(childId)).toBeUndefined() }, { timeout: 5_000 }) + for (const agent of [lead, child]) expect(teamTools(await assembly(ctx, agent))).toEqual(TEAM_TOOLS) + expect(child.status).toBe('running') + interrupt(ctx, lead, childId) + await waitNoAgent(ctx, childId) await replacement.dispose() }) - it('shadows legacy global control names only inside Team member scopes', async () => { - const { ctx, lead, fiber } = await setup([], true) - const teamSchema = (await assembly(ctx, lead)).tools.find(schema => schema.name === 'send_message') - expect(JSON.stringify(teamSchema)).toContain('target') - expect(JSON.stringify(teamSchema)).not.toContain('subagent_id') - - await fiber.dispose() - const legacySchema = (await assembly(ctx, lead)).tools.find(schema => schema.name === 'send_message') - expect(JSON.stringify(legacySchema)).toContain('agent_id') - }) - it('rolls back partial scoped installation after a same-scope collision', async () => { const { ctx, lead, fiber } = await setup([]) await fiber.dispose() lead.ctx.tools.register(defineContentToolFixture({ - name: 'spawn_teammate', + name: 'team_task_create', description: 'intentional collision', parameters: {}, async execute() { return [{ type: 'text', text: 'collision' }] }, @@ -691,91 +498,65 @@ describe('dsh-tool-team', () => { await expect(ctx.plugin(toolTeam)).rejects.toThrow(/already registered/u) const assembled = await assembly(ctx, lead) - expect(assembled.tools.filter(schema => TOOL_NAMES.includes(schema.name)).map(schema => schema.name)) - .toEqual(['spawn_teammate']) + expect(teamTools(assembled)).toEqual(['team_task_create']) expect(assembled.contexts.some(context => context.name === 'team:identity')).toBe(false) }) - it('resolves direct-apply defaults without Loader schema normalization', async () => { - const { ctx, lead, fiber, adapter } = await setup([textResponse('ordinary child')]) + it('applies directly without configuration and rejects removed configuration keys', async () => { + const { ctx, lead, fiber } = await setup([]) await fiber.dispose() - toolTeam.apply(ctx, {}) - expect((await assembly(ctx, lead)).tools.map(schema => schema.name).filter(name => TOOL_NAMES.includes(name)).sort()) - .toEqual(TOOL_NAMES) - const ordinary = await ctx.subagents.startActivation({ - delivery: 'parent', - provider: 'spawn', - label: 'ordinary child', - request: { prompt: [{ type: 'text', text: 'finish' }], parent: lead }, - signal: SIGNAL, - }) - await vi.waitFor(() => { expect(ctx.agents.get(ordinary.childId)).toBeUndefined() }, { timeout: 5_000 }) - const request = adapter.requests.find(request => request.sessionId === ordinary.childId) - expect(request).toBeDefined() - expect(JSON.stringify(request?.messages)).not.toContain('You are the Team Lead') + for (const key of ['interrupt', 'freshProvider', 'forkProvider']) { + expect(() => { toolTeam.apply(ctx, { [key]: 'x' }) }).toThrow(`${key} is no longer supported`) + } + toolTeam.apply(ctx) + expect(teamTools(await assembly(ctx, lead))).toEqual(TEAM_TOOLS) }) - it.each([true, false])('reinstalls Team scope with interrupt=%s before a cold-resumed request', async (interrupt) => { - const { ctx, lead, adapter, fiber } = await setup([textResponse('first'), textResponse('lead received settlement'), 'hang']) - await fiber.dispose() - await ctx.plugin(toolTeam, { interrupt }) - const spawned = await execute(ctx, lead, 'spawn_teammate', { - name: 'cold-worker', description: 'cold worker', prompt: 'finish once', - }) - const childId = spawnedChildId(ctx, lead, spawned) - await vi.waitFor(() => { expect(ctx.agents.get(childId)).toBeUndefined() }, { timeout: 5_000 }) - expect(await ctx.subagents.listChildren(lead.id)).toContainEqual(expect.objectContaining({ - id: childId, - mode: 'continuable', - })) + it('installs Team scope before a cold-resumed teammate request', async () => { + const { ctx, lead, adapter } = await setup([textResponse('first'), textResponse('lead received settlement'), 'hang']) + const childId = await spawn(ctx, lead, 'cold-worker', 'finish once') + await waitNoAgent(ctx, childId) await vi.waitFor(() => { expect(adapter.requests.filter(request => request.sessionId === lead.id)).toHaveLength(1) }) await lead.whenIdle() - const receipt = await ctx.agentTeams.sendMessage(lead, { - target: 'cold-worker', - content: [{ type: 'text', text: 'resume with Team scope' }], - signal: SIGNAL, - }) - expect(receipt.messageId).toEqual(expect.any(String)) + await ctx.subagents.sendMessage(lead, childId, [{ type: 'text', text: 'resume with Team scope' }], { signal: SIGNAL }) const resumed = await waitRunning(ctx, childId) - expect((await assembly(ctx, resumed)).tools.map(schema => schema.name) - .filter(name => TOOL_NAMES.includes(name)).sort()).toEqual(TOOL_NAMES.filter(name => interrupt || name !== 'interrupt_agent')) - expect(renderPrompt(await assembly(ctx, resumed)).includes('interrupt_agent')).toBe(interrupt) - expect(renderContextSnapshot(await assembly(ctx, resumed))).toContain('You are teammate "cold-worker".') - const childRequests = () => adapter.requests.filter(request => request.messages.some(message => message.role === 'user' - && message.content.some(block => block.type === 'text' && block.text.includes('You are teammate "cold-worker".')))) + expect(teamTools(await assembly(ctx, resumed))).toEqual(TEAM_TOOLS) + expect(renderContextSnapshot(await assembly(ctx, resumed))).toContain(teammateIdentity(lead)) + const childRequests = () => adapter.requests.filter(request => request.sessionId === childId) await vi.waitFor(() => { expect(childRequests()).toHaveLength(2) }) const firstMessages = childRequests()[0]!.messages expect(childRequests()[1]!.messages.slice(0, firstMessages.length)).toEqual(firstMessages) - expect(resumed.session.snapshotEvents().filter(event => event.type === 'user/message' - && event.data.source.kind === 'user')).toHaveLength(1) - ctx.agentTeams.interrupt(lead, 'cold-worker') - await vi.waitFor(() => { expect(ctx.agents.get(childId)).toBeUndefined() }, { timeout: 5_000 }) + interrupt(ctx, lead, childId) + await waitNoAgent(ctx, childId) + }) + + it('installs nothing for a subagent whose parent left the registry', async () => { + const { ctx } = await setup([textResponse('child done')]) + const parent = await ctx.agents.create({ + sessionId: SessionId('temporary-parent'), + agentOptions: { provider: 'mock', model: 'mock' }, + }) + const childId = await spawn(ctx, parent.agent, 'orphan', 'finish') + await waitNoAgent(ctx, childId) + await parent.dispose() + const orphan = await ctx.agents.resume({ + resumeSessionId: childId, + agentOptions: { provider: 'mock', model: 'mock' }, + }) + expect(teamTools(await assembly(ctx, orphan.agent))).toEqual([]) + await orphan.dispose() }) it('fails safely without a calling Agent and has the function-plugin export shape', async () => { const { ctx } = await setup([]) - const result = await execute(ctx, undefined, 'list_agents', {}) + const result = await execute(ctx, undefined, 'wait_agent', {}) expect(result.isError).toBe(true) - expect(text(result)).toContain('unknown tool "list_agents"') + expect(text(result)).toContain('unknown tool "wait_agent"') expect('default' in toolTeam).toBe(false) expect(toolTeam.name).toBe('tool-agent-team') expect(toolTeam.inject).toEqual(['agents', 'agentTeams', 'tools', 'systemPrompt']) }) - - it('uses configured fresh and fork provider names', async () => { - const { ctx, lead, fiber } = await setup([textResponse('custom')]) - await fiber.dispose() - await ctx.plugin(SubagentSpawn, { providerName: 'team-fresh' }) - await ctx.plugin(toolTeam, { freshProvider: 'team-fresh', forkProvider: 'fork' }) - const result = await execute(ctx, lead, 'spawn_teammate', { - name: 'custom-provider', description: 'custom provider', prompt: 'go', - }) - expect(result.isError).toBe(false) - const childId = spawnedChildId(ctx, lead, result) - await vi.waitFor(() => { expect(ctx.agents.get(childId)).toBeUndefined() }, { timeout: 5_000 }) - expect(ctx.agentTeams.listMembers(lead)[1]).toMatchObject({ provider: 'team-fresh' }) - }) }) diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 97da088137..f506223049 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -326,25 +326,19 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ signature: 'membership(agent: Agent): TeamMembership', description: 'Resolve one exact live Agent\'s Team role.', parameters: [{ name: 'agent', description: 'exact live Agent used as the authority credential.' }], - returns: 'its root, Team identity, role, and model-facing name.', + returns: 'its Lead, Team identity, and role.', }, { - signature: 'listMembers(agent: Agent): TeamMemberView[]', - description: 'List the runtime-enriched roster visible to one Team member.', - parameters: [{ name: 'agent', description: 'exact live Team member.' }], - returns: 'Lead and teammate rows in creation order.', + signature: 'tryMembership(agent: Agent): TeamMembership | undefined', + description: 'Resolve a caller without throwing, used by scoped-tool installation and observers.', + parameters: [{ name: 'agent', description: 'candidate exact live Agent.' }], + returns: 'Team membership, or undefined for stale identities and children whose parent left the registry.', }, { - signature: 'async spawnTeammate(caller: Agent, request: SpawnTeammateRequest): Promise', - description: 'Create one named, continuable direct child of the Team Lead.', - parameters: [{ name: 'caller', description: 'exact live Lead Agent.' }, { name: 'request', description: 'immutable name, description, prompt, context mode, provider, and cancellation.' }], - returns: 'the active roster row.', - }, - { - signature: 'async sendMessage(caller: Agent, request: SendTeamMessageRequest): Promise', - description: 'Steer one peer message into the target inbox or reject the attempt.', - parameters: [{ name: 'caller', description: 'exact live sending Team member.' }, { name: 'request', description: 'target name, content, and cancellation before acceptance.' }], - returns: 'accepted inbox identity; acceptance follows normal Agent persistence and does not await model processing.', + signature: 'hasActivePeer(caller: Agent): boolean', + description: 'Whether another live member of the caller\'s Team is currently running a turn.', + parameters: [{ name: 'caller', description: 'exact live Team member.' }], + returns: 'whether a peer can still produce a Team change.', }, { signature: 'async createTask(caller: Agent, request: CreateTeamTaskRequest): Promise', @@ -372,22 +366,10 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ }, { signature: 'async waitForChange(caller: Agent, timeoutMs: number, signal: AbortSignal): Promise', - description: 'Wait for the next Team-domain or member-status change.', + description: 'Wait for the next task change, member status change, or message a member\'s inbox accepts.', parameters: [{ name: 'caller', description: 'exact live Team member waiting for activity.' }, { name: 'timeoutMs', description: 'bounded wait duration from ten seconds through one hour.' }, { name: 'signal', description: 'caller cancellation for the wait only.' }], returns: 'one observed change or a timeout result.', }, - { - signature: 'interrupt(caller: Agent, targetName: string): { previousStatus: \'running\' | \'inactive\' }', - description: 'Interrupt one live teammate turn without clearing its pending inbox.', - parameters: [{ name: 'caller', description: 'exact live Lead Agent.' }, { name: 'targetName', description: 'durable teammate name.' }], - returns: 'the target status sampled before cancellation.', - }, - { - signature: 'tryMembership(agent: Agent): TeamMembership | undefined', - description: 'Resolve a caller without throwing, used by scoped-tool installation and observers.', - parameters: [{ name: 'agent', description: 'candidate exact live Agent.' }], - returns: 'Team membership, or undefined for non-Team subagents and stale identities.', - }, ], }, { @@ -3043,8 +3025,8 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ }, { signature: 'async sendMessage( sender: Agent, targetId: SessionId, content: ContentBlock[], options: SubagentSendMessageOptions, ): Promise', - description: 'Steer one model-authored message to the sender\'s direct parent or direct continuable child. A running target admits it at the nearest step boundary; an idle target starts a turn, and an absent direct child cold-resumes from persistence. The service derives durable sender attribution from the exact live sender. Caller cancellation stops only pre-acceptance work.', - parameters: [{ name: 'sender', description: 'exact live Agent authorizing and originating the message.' }, { name: 'targetId', description: 'durable direct-parent or direct-child session id.' }, { name: 'content', description: 'model-authored content to deliver.' }, { name: 'options', description: 'caller cancellation before inbox acceptance.' }], + description: 'Steer one model-authored message to the sender\'s direct parent, direct continuable child, or continuable sibling under their shared live parent. A running target admits it at the nearest step boundary; an idle target starts a turn, and an absent direct child cold-resumes from persistence. The service derives durable sender attribution from the exact live sender. Caller cancellation stops only pre-acceptance work.', + parameters: [{ name: 'sender', description: 'exact live Agent authorizing and originating the message.' }, { name: 'targetId', description: 'durable direct-parent, direct-child, or sibling session id.' }, { name: 'content', description: 'model-authored content to deliver.' }, { name: 'options', description: 'caller cancellation before inbox acceptance.' }], returns: 'the accepted message\'s inbox id.', throws: ['when continuation services are unavailable, adjacency is rejected, or the message was not admitted.'], }, @@ -6973,14 +6955,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'SearchResultView', declaration: 'export type SearchResultView = SearchMatchesResultView | SearchPathsResultView;', }, - { - name: 'SendTeamMessageRequest', - declaration: 'export interface SendTeamMessageRequest {\n readonly target: string;\n readonly content: ContentBlock[];\n readonly signal: AbortSignal;\n}', - }, - { - name: 'SendTeamMessageResult', - declaration: 'export interface SendTeamMessageResult {\n readonly messageId: MessageId;\n}', - }, { name: 'SerializedElement', declaration: 'export interface SerializedElement {\n readonly type: \'Box\' | \'Text\' | \'Button\';\n readonly props: Readonly>;\n readonly children: readonly SerializedNode[];\n readonly actionId?: string;\n}', @@ -7701,14 +7675,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'SkillViewOptions', declaration: 'export interface SkillViewOptions extends SkillLookupOptions {\n readonly scope?: ScopeKey | undefined;\n}', }, - { - name: 'SpawnTeammateRequest', - declaration: 'export interface SpawnTeammateRequest {\n readonly name: string;\n readonly description: string;\n readonly prompt: ContentBlock[];\n readonly context: \'fresh\' | \'fork\';\n readonly provider: string;\n readonly signal: AbortSignal;\n}', - }, - { - name: 'SpawnTeammateResult', - declaration: 'export interface SpawnTeammateResult {\n readonly member: TeamMemberView;\n}', - }, { name: 'SpeechCatalog', declaration: 'export interface SpeechCatalog extends SpeechSnapshot {\n readonly maxAudioBytes: number;\n readonly maxDurationSeconds: number;\n}', @@ -8027,11 +7993,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'TeamMembership', - declaration: 'export interface TeamMembership {\n readonly root: Agent;\n readonly id: TeamId;\n readonly role: \'lead\' | \'teammate\';\n readonly name: string;\n}', - }, - { - name: 'TeamMemberView', - declaration: 'export interface TeamMemberView {\n readonly id: SessionId;\n readonly name: string;\n readonly role: \'lead\' | \'teammate\';\n readonly status: \'running\' | \'inactive\' | \'provisioning\' | \'failed\';\n readonly description?: string;\n readonly provider?: string;\n readonly context?: \'fresh\' | \'fork\';\n readonly model?: string;\n readonly diagnostics: string[];\n}', + declaration: 'export interface TeamMembership {\n readonly root: Agent;\n readonly id: TeamId;\n readonly role: \'lead\' | \'teammate\';\n}', }, { name: 'TeamTaskAction', @@ -8047,7 +8009,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'TeamTaskView', - declaration: 'export interface TeamTaskView {\n readonly id: TeamTaskId;\n readonly revision: number;\n readonly subject: string;\n readonly description: string;\n readonly status: TeamTaskStatus;\n readonly blockedBy: TeamTaskId[];\n readonly writeScopes: string[];\n readonly ownerName?: string;\n readonly ready: boolean;\n readonly writeScopeWarnings: string[];\n}', + declaration: 'export interface TeamTaskView {\n readonly id: TeamTaskId;\n readonly revision: number;\n readonly subject: string;\n readonly description: string;\n readonly status: TeamTaskStatus;\n readonly blockedBy: TeamTaskId[];\n readonly writeScopes: string[];\n readonly ownerId?: SessionId;\n readonly ready: boolean;\n readonly writeScopeWarnings: string[];\n}', }, { name: 'TeamWaitResult', @@ -8543,7 +8505,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'UpdateTeamTaskRequest', - declaration: 'export interface UpdateTeamTaskRequest {\n readonly taskId: TeamTaskId;\n readonly expectedRevision: number;\n readonly action: TeamTaskAction;\n readonly subject?: string;\n readonly description?: string;\n readonly blockedBy?: readonly TeamTaskId[];\n readonly writeScopes?: readonly string[];\n readonly owner?: string;\n}', + declaration: 'export interface UpdateTeamTaskRequest {\n readonly taskId: TeamTaskId;\n readonly expectedRevision: number;\n readonly action: TeamTaskAction;\n readonly subject?: string;\n readonly description?: string;\n readonly blockedBy?: readonly TeamTaskId[];\n readonly writeScopes?: readonly string[];\n readonly owner?: SessionId;\n}', }, { name: 'UserMessage', diff --git a/packages/preset/agent-preset/skills/cordis-composition-reference/references/packages.md b/packages/preset/agent-preset/skills/cordis-composition-reference/references/packages.md index 87b03eba27..7c29410ca4 100644 --- a/packages/preset/agent-preset/skills/cordis-composition-reference/references/packages.md +++ b/packages/preset/agent-preset/skills/cordis-composition-reference/references/packages.md @@ -183,7 +183,7 @@ Every package below exports a Cordis plugin that a bundle patch can name in a Lo | Package | Config | Description | |---|---|---| -| `@deepseek-ai/dsh-experimental-agent-team` | yes | Implicit-root Agent Teams roster, direct peer messaging, and shared task DAG | +| `@deepseek-ai/dsh-experimental-agent-team` | yes | Implicit-root Agent Teams roster of subagents, shared task DAG, and change waits | | `@deepseek-ai/dsh-experimental-api-speech-to-text` | yes | Authenticated experimental speech transcription for browser clients | | `@deepseek-ai/dsh-experimental-auto-review` | no | Per-tool LLM authorization review for the DeepSeek Harness Auto permission preset | | `@deepseek-ai/dsh-experimental-browser-use-chrome-devtools-mcp` | yes | Experimental per-Session Chromium browser tools through chrome-devtools-mcp | diff --git a/packages/subagent/subagent/README.i18n.yaml b/packages/subagent/subagent/README.i18n.yaml index 5e077a955e..2e2a850a13 100644 --- a/packages/subagent/subagent/README.i18n.yaml +++ b/packages/subagent/subagent/README.i18n.yaml @@ -33,8 +33,8 @@ en: a740e5e9bdd661d9 zh: 712a5458fc8b3615 /deepseek-ai-dsh-subagent/use-this-package/messaging-interrupting-and-discovering: - en: 050177fbbaade432 - zh: 99f2b9f98088314d + en: 4fe78b59da2b20cf + zh: 34558b3d6b194215 /deepseek-ai-dsh-subagent/use-this-package/failure-and-recovery: en: 245fb0d5c3df5ccf zh: e69eed9e5070ce25 @@ -57,8 +57,8 @@ en: d338e68edd438f57 zh: 0496090cc567b19d /deepseek-ai-dsh-subagent/understand-the-implementation/ownership-and-invariants: - en: b74886a091a327d4 - zh: 2124546878ba1aa2 + en: 4e22346c9e9197d1 + zh: 9afe58cf57eedcc1 /deepseek-ai-dsh-subagent/further-exploration: en: 5b0f408dd901d90a zh: e11866d5fd878327 @@ -93,8 +93,8 @@ en: 59e848fca82a46fb zh: fcd6a8a2bb58609f /deepseek-ai-dsh-subagent/known-limitations-and-deferred-work: - en: b663a2e94185901c - zh: 8d2937aa203bda59 + en: fae8f84638614022 + zh: dc6fbc8ac3e2284b /deepseek-ai-dsh-subagent/known-limitations-and-deferred-work/dev-note: en: 25cc0909348666bb zh: 95517f12de100049 diff --git a/packages/subagent/subagent/README.md b/packages/subagent/subagent/README.md index 93c21b5b88..a1d9fcd93f 100644 --- a/packages/subagent/subagent/README.md +++ b/packages/subagent/subagent/README.md @@ -66,7 +66,7 @@ In-process spawn accepts an independent `agentPreset`; fork and external backend ### Messaging, interrupting, and discovering -Every exact live Agent can use `sendMessage()` with a direct continuable child; a resident continuable child can also use it with its direct parent. A working target receives the Agent message through Steer at its nearest step; an idle target starts a turn, and only a direct child can be cold-resumed. The parent can also interrupt a running descendant or list its children at any time. A browser continuation prompt independently selects Queue or Steer and may carry image parts: the Host admits and persists each image batch through the attachment store before the child inbox accepts the message, and refuses delivery when the child's declared model does not accept image input. Direct-child discovery reads the parent-owned `subagentCatalog` projection. `listChildren(parentSessionId, signal?)` owns a live-preferred Session observation and returns the catalog asynchronously without reading child logs. It forwards cancellation and releases the observation after materialization. Materialization preserves parent event order in O(D) time for D facts. External entries are leaves and require no child Session observation. Descendant discovery recursively reads local child catalogs in parent event order, observing each reachable Session once. It skips branches whose catalogs cannot be read and returns diagnostics for them; neither path loads or resumes a child Agent. +Every exact live Agent can use `sendMessage()` with a direct continuable child; a resident continuable child can also message its direct parent or another continuable child of that parent by Session id. Sibling delivery uses the shared live parent’s authority and preserves the actual sender’s id. Descriptions are display text and may repeat. A working target receives the Agent message through Steer at its nearest step; an idle target starts a turn, and direct children and siblings can be cold-resumed. The parent can also interrupt a running descendant or list its children at any time. A browser continuation prompt independently selects Queue or Steer and may carry image parts: the Host admits and persists each image batch through the attachment store before the child inbox accepts the message, and refuses delivery when the child's declared model does not accept image input. Direct-child discovery reads the parent-owned `subagentCatalog` projection. `listChildren(parentSessionId, signal?)` owns a live-preferred Session observation and returns the catalog asynchronously without reading child logs. It forwards cancellation and releases the observation after materialization. Materialization preserves parent event order in O(D) time for D facts. External entries are leaves and require no child Session observation. Descendant discovery recursively reads local child catalogs in parent event order, observing each reachable Session once. It skips branches whose catalogs cannot be read and returns diagnostics for them; neither path loads or resumes a child Agent. ### Failure and recovery @@ -133,7 +133,7 @@ Logs without that selection before the first own descriptor, including historica - **The manager owns accepted work** — unpublished failures roll back; published handles cancel their exact activation and await child-first disposal. - **Registration is effect-scoped** — removing a provider blocks new starts but never revokes accepted runs. -- **Agent-message authority is exact adjacency** — `sendMessage()` requires the exact live sender; every sender may target a direct continuable child, while only a sender with a resident continuable Activation may target its direct parent. +- **Agent-message authority follows live lineage** — `sendMessage()` requires the exact live sender; every sender may target a direct continuable child. A resident continuable sender may also target its direct parent or a continuable sibling under the shared live parent’s authority. - **The descriptor is log-only** — a session event absent from model history and retained across compaction; the writer records the resolved child provider, model, and reasoning effort for cold resume. Historical one-shot descriptors remain readable. - **This runtime answers archive admission for children** ([seam](../../workspace/workspace/README.md)) — `workspace/session-activity` reports the live subagent descendants inside a turn as the `subagent` family, found by the durable lineage this package records (`parentSession` with the subagent origin, any depth, never a fork) and labelled from each child's descriptor through a live Session observation when the Session query service is composed, otherwise by id; `workspace/session-stop` cancels each of them with the parent cause, one at a time, so one child refusing its cancel is logged while its siblings still stop. The parent's own turn, its jobs, and the archived-lineage step gate belong to the API Session Controller. @@ -203,7 +203,7 @@ These limits define when the seam is a poor fit or needs special operational car - **Descendant reads are sequential** — each reachable catalog, including one-shot children, takes one observation. A cold Session without a valid prepared observation requires a full-log read; large cold trees can accumulate storage latency. - **External children execute once** — ACP, DSH SDK, Codex, and Claude Code have no local child Session and accept no follow-up input. Their parent-owned records remain discoverable after disposal. -- **Adjacent model messaging only** — `sendMessage()` requires an exact live sender; every sender may target a direct continuable child, while only a sender with a resident continuable Activation may target its direct parent. Browser prompts use a separate human Queue-or-Steer control path. +- **Messaging is limited to parents, children, and siblings** — targets use Session ids. A resident continuable child can reach its direct parent and continuable siblings while the parent is live; deeper descendants and unrelated trees are not message targets. Browser prompts use a separate human Queue-or-Steer control path. - **A direct parent must remain live for child-to-parent delivery** — the service has no durable parent mailbox; a missing parent rejects the message instead of accepting work it cannot wake. - **Wake gap during cancellation convergence** — a follow-up accepted after an interrupt signal but before the Agent becomes idle stays queued until another waking send. - **Pending injected context retains an Activation** — settlement conservatively treats every Inbox occurrence as unfinished. Context parked after the Agent becomes idle keeps the child and its live ancestors resident until a waking delivery claims it, a queue mutation removes it, or manager teardown discards it. Host completion joins active work, pending child creation, and resource cleanup, but does not wait for a subtree containing only idle parked inboxes; it neither completes those activation results nor discards their messages. diff --git a/packages/subagent/subagent/README.zh.md b/packages/subagent/subagent/README.zh.md index 5e8553bac9..624591227b 100644 --- a/packages/subagent/subagent/README.zh.md +++ b/packages/subagent/subagent/README.zh.md @@ -66,7 +66,7 @@ kind: "package-reference" ### 消息、中断与发现 -每个确切在线 Agent 都可以对直接可继续 child 使用 `sendMessage()`;驻留的可继续 child 还可以对自己的直接 parent 使用它。正在工作的目标通过 Steer 在最近 step 接收 Agent 消息;空闲目标启动轮次,且只有直接 child 可以冷恢复。parent 也可以随时中断正在运行的后代或列举自己的子级。浏览器发出的继续执行 prompt 会独立选择 Queue 或 Steer,并且可以携带图片部分:Host 先通过附件存储完成整批图片的准入与持久化,子级 inbox 才接受这条消息;当子级声明的模型不接受图片输入时拒绝投递。 直接子级发现读取 parent 自有的 `subagentCatalog` projection。`listChildren(parentSessionId, signal?)` 持有一次优先实时来源的 Session 观察,异步返回目录,不读取子级日志。它转发取消信号,并在物化后释放观察。物化以 O(D) 时间保留 D 条事实的父日志事件顺序。外部条目是叶节点,不需要观察子 Session。后代发现按父事件顺序递归读取子级目录,每个可达 Session 观察一次。无法读取目录的分支会被跳过,并返回诊断;两条路径都不加载或恢复子级 Agent。 +每个确切在线 Agent 都可以对直接可继续 child 使用 `sendMessage()`;驻留的可继续 child 还可以按 Session id 向直接 parent 或该 parent 的其他可继续 child 发消息。同级投递使用共同在线 parent 的权限,并保留实际发送方的 id。描述用于展示,可以重复。正在工作的目标通过 Steer 在最近 step 接收 Agent 消息;空闲目标启动轮次,且直接 child 和同级均可冷恢复。parent 也可以随时中断正在运行的后代或列举自己的子级。浏览器发出的继续执行 prompt 会独立选择 Queue 或 Steer,并且可以携带图片部分:Host 先通过附件存储完成整批图片的准入与持久化,子级 inbox 才接受这条消息;当子级声明的模型不接受图片输入时拒绝投递。 直接子级发现读取 parent 自有的 `subagentCatalog` projection。`listChildren(parentSessionId, signal?)` 持有一次优先实时来源的 Session 观察,异步返回目录,不读取子级日志。它转发取消信号,并在物化后释放观察。物化以 O(D) 时间保留 D 条事实的父日志事件顺序。外部条目是叶节点,不需要观察子 Session。后代发现按父事件顺序递归读取子级目录,每个可达 Session 观察一次。无法读取目录的分支会被跳过,并返回诊断;两条路径都不加载或恢复子级 Agent。 ### 失败与恢复 @@ -133,7 +133,7 @@ kind: "package-reference" - **管理器拥有已接受工作**——未发布的失败会回滚;已发布句柄取消对应 activation,并等待子级优先释放。 - **注册受 effect 作用域约束**——移除提供方会阻止新启动,但绝不撤销已接受的运行。 -- **Agent 消息权限基于确切相邻关系**——`sendMessage()` 要求确切在线 sender;每个 sender 都可以指定直接可继续 child,只有具备驻留可继续 Activation 的 sender 可以指定自己的直接 parent。 +- **Agent 消息权限来自在线谱系**——`sendMessage()` 要求确切在线 sender;每个 sender 都可以指定直接可继续 child。驻留的可继续 sender 还可以向直接 parent 发消息,或使用共同在线 parent 的权限向可继续同级发消息。 - **描述符仅进日志**——它是会话事件,不进入模型历史,并跨压缩(compaction)保留;写入器记录解析后的子级提供方、模型与推理强度,用于冷恢复。历史 one-shot 描述符仍可读取。 - **本 runtime 为子代理回答归档准入**([接缝](../../workspace/workspace/README.zh.md))——`workspace/session-activity` 把回合中的在线子代理子孙作为 `subagent` 族报告:按本包记录的持久化血缘查找(带 subagent 来源的 `parentSession`,任意深度,从不包括 fork),组合了 Session query 服务时经一次活会话 observation 从各 child 的描述符取名称,否则只报 id;`workspace/session-stop` 以父级原因逐个取消它们,一个拒绝取消的 child 只记日志,其兄弟仍会停止。父级自身的回合、它的任务以及已归档血缘的步骤门禁归 API Session Controller。 @@ -203,7 +203,7 @@ You are a delegated subagent: your permission scope was fixed when you were star - **后代读取串行执行**——每个可达目录(包括一次性子级)都需要一次观察。冷 Session 缺少有效的 prepared 观察时需要读取完整日志;大型冷会话树可能累积存储延迟。 - **外部子级执行一次**——ACP、DSH SDK、Codex 与 Claude Code 没有本地子 Session,也不接受后续输入。释放后仍可发现父级拥有的执行记录。 -- **仅允许相邻模型消息**——`sendMessage()` 要求确切在线 sender;每个 sender 都可以指定直接可继续 child,只有具备驻留可继续 Activation 的 sender 可以指定自己的直接 parent。浏览器提示使用独立的人类 Queue 或 Steer 控制路径。 +- **消息限于父级、子级和同级**——目标使用 Session id。驻留的可继续 child 在 parent 在线时可以联系直接 parent 和可继续同级;更深的后代与无关树不是消息目标。浏览器提示使用独立的人类 Queue 或 Steer 控制路径。 - **child 到 parent 的投递要求直接 parent 保持在线**——服务没有持久 parent mailbox;parent 缺失时会拒绝消息,而非接受无法唤醒的工作。 - **取消收敛期间存在唤醒缺口**——中断信号发出后、Agent 进入 idle 前被接受的后续消息会保持排队,直到另一条唤醒发送到达。 - **待处理的注入上下文会保留 Activation**——settlement 会保守地把每个 Inbox occurrence 都视为未完成。Agent 进入 idle 后停放的上下文会让 child 及其在线祖先继续驻留,直到唤醒投递将其 claim、queue 变更将其移除,或 manager teardown 将其丢弃。宿主完成检查等待活动工作、待创建子级与资源清理,但不会等待仅包含空闲停放 Inbox 的子树;它既不结算这些 activation 的结果,也不丢弃其消息。 diff --git a/packages/subagent/subagent/src/catalog.ts b/packages/subagent/subagent/src/catalog.ts index d640c8f6b8..2e89bcf7dd 100644 --- a/packages/subagent/subagent/src/catalog.ts +++ b/packages/subagent/subagent/src/catalog.ts @@ -14,8 +14,9 @@ import type { SessionId, SessionLogOffset, } from '@deepseek-ai/dsh-session' -import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection' +import type { ProjectionDefinition, SessionProjectionRegistry } from '@deepseek-ai/dsh-session-projection' import type { SubagentCatalogEntry } from './projection-types.ts' +import { SubagentError } from './error.ts' /** Catalog payload version emitted by local child creation. */ export const SUBAGENT_CATALOG_VERSION = 0 @@ -168,3 +169,21 @@ export function establishExternalCatalogChild(parent: Session, childId: SessionI label, }) } + +/** + * Read the current catalog rows of one live parent Session. + * @param projections - Session projection registry owning the catalog unit, when composed. + * @param parent - live parent Session whose own catalog facts are read. + * @returns direct-child rows in catalog event order. + * @throws {SubagentError} `CONTINUATION_UNAVAILABLE` when the catalog projection is not registered. + */ +export function liveCatalogEntries( + projections: SessionProjectionRegistry | undefined, + parent: Session, +): SubagentCatalogEntry[] { + const state = projections?.stateOf(parent, 'subagentCatalog') + if (state === undefined) { + throw new SubagentError('subagent sibling messaging requires the subagentCatalog projection', 'CONTINUATION_UNAVAILABLE') + } + return subagentCatalogEntries(state) +} diff --git a/packages/subagent/subagent/src/index.ts b/packages/subagent/subagent/src/index.ts index 119cc26a6a..b9fcd36922 100644 --- a/packages/subagent/subagent/src/index.ts +++ b/packages/subagent/subagent/src/index.ts @@ -78,6 +78,7 @@ import { subagentCatalogProjectionDefinition } from './catalog.ts' import { deliverSubagentPrompt } from './internal.ts' export type {} from './catalog.ts' +export type {} from './projection.ts' export * from './out-of-process.ts' export { AssistantOutputFold, finalAssistantOutput } from './assistant-output.ts' export { SubagentRunId } from './types.ts' @@ -294,13 +295,13 @@ export class SubagentRuntime extends TypertRemoteService { } /** - * Steer one model-authored message to the sender's direct parent or direct - * continuable child. A running target admits it at the nearest step boundary; + * Steer one model-authored message to the sender's direct parent, direct + * continuable child, or continuable sibling under their shared live parent. A running target admits it at the nearest step boundary; * an idle target starts a turn, and an absent direct child cold-resumes from * persistence. The service derives durable sender attribution from the exact * live sender. Caller cancellation stops only pre-acceptance work. * @param sender - exact live Agent authorizing and originating the message. - * @param targetId - durable direct-parent or direct-child session id. + * @param targetId - durable direct-parent, direct-child, or sibling session id. * @param content - model-authored content to deliver. * @param options - caller cancellation before inbox acceptance. * @returns the accepted message's inbox id. diff --git a/packages/subagent/subagent/src/internal.ts b/packages/subagent/subagent/src/internal.ts index 4d19c55435..6221e8354c 100644 --- a/packages/subagent/subagent/src/internal.ts +++ b/packages/subagent/subagent/src/internal.ts @@ -80,31 +80,3 @@ export function queueHostSubagentPrompt( 'queue', ) } - -/** - * Steer one host-protocol message without exposing another Service operation. - * @param runtime - subagent runtime owning continuation residency. - * @param parent - exact live direct parent authorizing delivery. - * @param childId - durable direct-child session id. - * @param content - host-authored content to deliver. - * @param source - durable host-protocol source descriptor. - * @param signal - caller cancellation before inbox acceptance. - * @returns the accepted message's inbox id. - */ -export function steerHostSubagentPrompt( - runtime: SubagentRuntime, - parent: Agent, - childId: SessionId, - content: ContentBlock[], - source: MessageSource, - signal: AbortSignal, -): Promise { - return (runtime as unknown as HostPromptDeliverer)[deliverSubagentPrompt]( - parent, - childId, - content, - source, - signal, - 'steer', - ) -} diff --git a/packages/subagent/subagent/src/manager.ts b/packages/subagent/subagent/src/manager.ts index b89732c29e..998c7e7913 100644 --- a/packages/subagent/subagent/src/manager.ts +++ b/packages/subagent/subagent/src/manager.ts @@ -15,7 +15,7 @@ import { brandString } from '@deepseek-ai/dsh-brand' import { ReasoningEffortId, contentHasImage, createUserMessage, errorChain } from '@deepseek-ai/dsh-llm' import type { ContentBlock, MessageId, MessageSource } from '@deepseek-ai/dsh-llm' import { SessionLogOffset } from '@deepseek-ai/dsh-session' -import type { SessionId, SessionEvent, UserMessage } from '@deepseek-ai/dsh-session' +import type { Session, SessionId, SessionEvent, UserMessage } from '@deepseek-ai/dsh-session' import type { SessionPersistence } from '@deepseek-ai/dsh-session-persistence' import type { SessionObservation, SessionQueryEngine } from '@deepseek-ai/dsh-session-query' import type { ObjectJsonSchema } from '@deepseek-ai/dsh-tools' @@ -24,11 +24,14 @@ import { childSessionMeta, resolveChildAgentOptions, resolveChildDepth, } from './child-agent.ts' import type { ChildComposition, DelegatedPolicyOverrides } from './child-agent.ts' -import { createAgentMessage, withContinuableReturnGuidance, createSettlementMessage } from './continuation-messages.ts' +import { + createAgentMessage, createSettlementMessage, withContinuableReturnGuidance, +} from './continuation-messages.ts' import { assertSubagentMaxDepth } from './depth.ts' import { foldSubagentDescriptor, snapshotSubagentDescriptor } from './descriptor.ts' import type { SubagentDescriptorData } from './descriptor.ts' -import { establishCatalogChild, establishExternalCatalogChild } from './catalog.ts' +import { establishCatalogChild, establishExternalCatalogChild, liveCatalogEntries } from './catalog.ts' +import type { SubagentCatalogEntry } from './projection-types.ts' import { SubagentError } from './error.ts' import { isAdjacentAgentSendMessageTool } from './internal.ts' import type { ActivationObserver } from './lifecycle.ts' @@ -475,6 +478,15 @@ export class SubagentManager { } } + /** + * Read one live Session's own catalog rows. + * @param session - live Session whose children are read. + * @returns catalog rows in event order. + */ + private catalogOf(session: Session): SubagentCatalogEntry[] { + return liveCatalogEntries(this.ctx.get('sessionProjections'), session) + } + /** * Start an external backend under activation ownership. * @param spec - task, provider, cancellation, and delivery policy. @@ -531,11 +543,11 @@ export class SubagentManager { } /** - * Deliver one model-authored message to a direct continuable child or to the - * sender's direct parent. A missing direct child cold-resumes through the - * ordinary continuation lifecycle. + * Deliver a message to the sender's direct parent, direct continuable child, + * or continuable sibling. Sibling delivery uses the shared live parent's + * authority and preserves the sender's identity; an absent child cold-resumes. * @param sender - exact live Agent authorizing and originating the message. - * @param targetId - durable direct-parent or direct-child session id. + * @param targetId - durable direct-parent, direct-child, or sibling Session id. * @param content - model-authored content to deliver. * @param options - caller cancellation before acceptance. * @returns the accepted message's inbox id. @@ -546,20 +558,15 @@ export class SubagentManager { content: ContentBlock[], options: SubagentSendMessageOptions, ): Promise { + options.signal.throwIfAborted() if (this.ctx.agents.get(sender.id) !== sender) { - throw new SubagentError( - 'message delivery requires the exact live sender agent', - 'UNAUTHORIZED', - ) + throw new SubagentError('message delivery requires the exact live sender agent', 'UNAUTHORIZED') } this.assertAdmitting(sender) - const senderActivation = this.resident.get(sender.id) - if (senderActivation !== undefined - && senderActivation.kind === 'local' - && senderActivation.handle.agent === sender - && senderActivation.parent.id === targetId) { - options.signal.throwIfAborted() - return this.sendToParent(senderActivation, sender, content) + const activation = this.resident.get(sender.id) + const resident = activation?.kind === 'local' && activation.handle.agent === sender ? activation : undefined + if (resident !== undefined && resident.parent.id === targetId) { + return this.sendToParent(resident, sender, content) } if (sender.session.header.parentSession === targetId) { throw new SubagentError( @@ -567,6 +574,15 @@ export class SubagentManager { 'UNAUTHORIZED', ) } + if (resident !== undefined && targetId !== sender.id + && this.catalogOf(resident.parent.session).some(child => child.id === targetId && child.mode === 'continuable')) { + const message = createAgentMessage(sender, content) + return this.deliverToChild(resident.parent, targetId, [...message.content], { + signal: options.signal, + delivery: 'steer', + source: message.source, + }) + } return this.deliverToChild(sender, targetId, content, { signal: options.signal, delivery: 'steer', @@ -1372,7 +1388,11 @@ export class SubagentManager { ? { reasoningEffort: ReasoningEffortId(descriptor.agentReasoningEffort) } : {}, }, - composition: { agentPreset: agentPreset ?? undefined, persona: descriptor.persona, toolFilter: descriptor.toolFilter }, + composition: { + agentPreset: agentPreset ?? undefined, + persona: descriptor.persona, + toolFilter: descriptor.toolFilter, + }, signal: options.signal, }) } catch (error: unknown) { diff --git a/packages/subagent/subagent/tests/catalog.spec.ts b/packages/subagent/subagent/tests/catalog.spec.ts index c242f1c423..d14f35d4f7 100644 --- a/packages/subagent/subagent/tests/catalog.spec.ts +++ b/packages/subagent/subagent/tests/catalog.spec.ts @@ -12,6 +12,7 @@ import { import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session' import { establishExternalCatalogChild, + liveCatalogEntries, subagentCatalogProjectionDefinition, } from '../src/catalog.ts' import type { SubagentCatalogState } from '../src/catalog.ts' @@ -52,6 +53,19 @@ function fact( } describe('subagent catalog projection', () => { + it('rejects live sibling discovery without its authoritative catalog projection', async () => { + const ctx = new Context() + try { + await ctx.plugin(SessionStore) + const parent = ctx.sessions.create(header.id) + expect(() => liveCatalogEntries(undefined, parent)).toThrow('requires the subagentCatalog projection') + await ctx.plugin(SessionProjectionRegistry) + expect(() => liveCatalogEntries(ctx.sessionProjections, parent)).toThrow('requires the subagentCatalog projection') + } finally { + await ctx.fiber.dispose() + } + }) + it('records an external child once with its label and creation time', async () => { const ctx = new Context() try { diff --git a/packages/subagent/subagent/tests/sibling-messages.spec.ts b/packages/subagent/subagent/tests/sibling-messages.spec.ts new file mode 100644 index 0000000000..a06f654cab --- /dev/null +++ b/packages/subagent/subagent/tests/sibling-messages.spec.ts @@ -0,0 +1,150 @@ +import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { Context } from '@deepseek-ai/cordis' +import type { Agent } from '@deepseek-ai/dsh-agent' +import AgentLoop from '@deepseek-ai/dsh-agent-loop' +import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' +import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' +import { SessionId } from '@deepseek-ai/dsh-session' +import type { SessionEvent } from '@deepseek-ai/dsh-session' +import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' +import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process' +import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts' +import SubagentRuntime from '../src/index.ts' +import { loadStoredSession } from './persistence-helpers.ts' +import { TestSessionQuery } from './test-session-query.ts' + +/** A model reply, or a turn that stays open until its Agent is cancelled. */ +type Reply = StreamChunk[] | 'hang' + +/** Routes each request to the reply queue of the task marker found in its history. */ +class RoutedAdapter extends MockAdapter { + readonly routed: GenerateOptions[] = [] + + constructor(private readonly replies: Map) { + super([]) + } + + override async * stream(options: GenerateOptions): AsyncIterable { + this.routed.push(options) + const text = JSON.stringify(options.messages) + const marker = [...this.replies.keys()].find(key => text.includes(`TASK:${key}`)) + const reply = marker === undefined ? undefined : this.replies.get(marker)?.shift() + if (reply === undefined) throw new Error(`RoutedAdapter: no reply for ${marker ?? 'unknown agent'}`) + if (reply !== 'hang') { + yield* reply + return + } + yield { type: 'block-start', index: 0, blockType: 'text' } + yield { type: 'text-delta', index: 0, text: 'working' } + await new Promise((_resolve, reject) => { + if (options.signal?.aborted) { reject(new Error('aborted')); return } + options.signal?.addEventListener('abort', () => { reject(new Error('aborted')) }, { once: true }) + }) + } +} + +const roots: string[] = [] +const disposers: Array<() => Promise> = [] + +afterEach(async () => { + for (const dispose of disposers.splice(0)) await dispose() + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }) +}) + +/** Boot persistence, the Agent loop, one spawn provider, and the routed model. */ +async function setup(replies: Record) { + const ctx = new Context() + await mountAgentLoopTestDependencies(ctx) + const root = mkdtempSync(join(tmpdir(), 'dsh-subagent-siblings-')) + roots.push(root) + await ctx.plugin(JsonlSessionPersistence, { root }) + await ctx.plugin(AgentLoop, { agents: [] }) + await ctx.plugin(TestSessionQuery) + await mountWorkingDirectoryFixture(ctx) + await ctx.plugin(SubagentRuntime) + await ctx.plugin(SubagentSpawn, { providerName: 'spawn' }) + const adapter = new RoutedAdapter(new Map(Object.entries(replies))) + ctx.llm.registerAdapter(['mock'], adapter) + const parent = await ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' }) + disposers.push(() => ctx.fiber.dispose()) + return { ctx, parent, adapter } +} + +const signal = new AbortController().signal + +/** Start one caller-delivery child whose history carries `TASK:`. */ +async function start(ctx: Context, parent: Agent, marker: string, description = 'shared responsibility'): Promise { + const started = await ctx.subagents.startActivation({ + provider: 'spawn', + label: description, + request: { prompt: [{ type: 'text', text: `TASK:${marker}` }], parent }, + signal, + delivery: 'caller', + }) + return started.childId +} + +/** Wait until a child's first model request is open. */ +async function running(ctx: Context, id: SessionId): Promise { + await vi.waitFor(() => { expect(ctx.agents.get(id)?.status).toBe('running') }) + return ctx.agents.get(id)! +} + +/** Wait until a child released its Activation. */ +async function settled(ctx: Context, id: SessionId): Promise { + await vi.waitFor(() => { expect(ctx.agents.get(id)).toBeUndefined() }, { timeout: 5_000 }) +} + +/** Read a child's stored user messages after its Activation settles. */ +async function storedUserMessages(ctx: Context, id: SessionId): Promise[]> { + await settled(ctx, id) + const stored = await loadStoredSession(ctx.sessionPersistence, id) + return stored.events.filter((event): event is SessionEvent<'user/message'> => event.type === 'user/message') +} + +/** Concatenate the text blocks of one message. */ +function textOf(event: SessionEvent<'user/message'>): string { + return event.data.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('') +} + +describe('subagent sibling messages', () => { + it('keeps duplicate descriptions as display text and addresses siblings by Session id', async () => { + const { ctx, parent } = await setup({ a: ['hang'], b: [textResponse('done'), textResponse('resumed')] }) + const a = await start(ctx, parent, 'a') + const sender = await running(ctx, a) + const b = await start(ctx, parent, 'b') + await settled(ctx, b) + expect(a).not.toBe(b) + expect(await ctx.subagents.listChildren(parent.id)).toEqual([ + expect.objectContaining({ id: a, mode: 'continuable', label: 'shared responsibility' }), + expect.objectContaining({ id: b, mode: 'continuable', label: 'shared responsibility' }), + ]) + expect(parent.session.snapshotEvents().filter(event => event.type === 'subagent/catalog').map(event => event.data.version)) + .toEqual([0, 0]) + + await ctx.subagents.sendMessage(sender, b, [{ type: 'text', text: 'hello' }], { signal }) + const delivered = (await storedUserMessages(ctx, b)).find(event => event.data.source.kind === 'agent-message')! + expect(textOf(delivered)).toBe(`Agent ${a} sent a message: hello`) + expect(delivered.data.source).toEqual({ kind: 'agent-message', form: 'relay', senderSessionId: a }) + ctx.subagents.interrupt(a, { kind: 'ancestor', agent: parent }) + await settled(ctx, a) + }) + + it('keeps unrelated parents outside sibling delivery even when descriptions match', async () => { + const { ctx, parent } = await setup({ a: ['hang'], b: ['hang'] }) + const other = await ctx.agentLoop.create(SessionId('other-parent'), { provider: 'mock', model: 'mock' }) + const a = await start(ctx, parent, 'a') + const sender = await running(ctx, a) + const b = await start(ctx, other, 'b') + await running(ctx, b) + await expect(ctx.subagents.sendMessage(sender, b, [{ type: 'text', text: 'denied' }], { signal })) + .rejects.toMatchObject({ code: 'UNAUTHORIZED' }) + ctx.subagents.interrupt(a, { kind: 'ancestor', agent: parent }) + ctx.subagents.interrupt(b, { kind: 'ancestor', agent: other }) + await Promise.all([settled(ctx, a), settled(ctx, b)]) + }) +}) diff --git a/packages/subagent/tool-subagent-control/README.i18n.yaml b/packages/subagent/tool-subagent-control/README.i18n.yaml index 56654bd595..73439c4bc2 100644 --- a/packages/subagent/tool-subagent-control/README.i18n.yaml +++ b/packages/subagent/tool-subagent-control/README.i18n.yaml @@ -9,8 +9,8 @@ en: 465d36925c792167 zh: 269cac314f638827 /deepseek-ai-dsh-tool-subagent-control/summary: - en: 7464eb99b6f618eb - zh: 8c7628c8e9f163ca + en: b397bc87605b3dcc + zh: a97e97e30be53faa /deepseek-ai-dsh-tool-subagent-control/table-of-contents: en: d152484eb41ac6b4 zh: 09388d293f9be9cb @@ -21,29 +21,29 @@ en: 4a52c81317ec9001 zh: ebd9f1ba920da50a /deepseek-ai-dsh-tool-subagent-control/use-this-package/send-message: - en: d80bb1df2eb740b7 - zh: e3203d9868a82910 + en: b160d6a41a73d64f + zh: 902a8e74db0c17a0 /deepseek-ai-dsh-tool-subagent-control/use-this-package/interrupt-agent: - en: af01696cc8deaf83 - zh: 86e40bc28d5362b1 + en: 4ccd504f8886d8b0 + zh: 480ab8cba905ac02 /deepseek-ai-dsh-tool-subagent-control/use-this-package/list-agents: - en: 631c0a34dadcb8f8 - zh: dbc536512cae070f + en: a5cfa30c936ca353 + zh: d8a83a6190cbd7c8 /deepseek-ai-dsh-tool-subagent-control/understand-the-implementation: en: c5cdf01a78c990f2 zh: 53c180d726ce4502 /deepseek-ai-dsh-tool-subagent-control/understand-the-implementation/design-concept: - en: 0af7238c690ee8d6 - zh: 5049eea23770ffed + en: 18102f0d2d227c32 + zh: 0fa0350be29f9353 /deepseek-ai-dsh-tool-subagent-control/understand-the-implementation/delivery-and-signal-ownership: - en: 4c7698ccea49a60b - zh: fbf129ff583e6ab9 + en: b596c53c18b3e7f5 + zh: bd17f4216a3a6301 /deepseek-ai-dsh-tool-subagent-control/understand-the-implementation/listing-projection: - en: 70e344022e6e535e - zh: 355488590648a82e + en: 25dd569dbb350eb7 + zh: bd7ae865d93a6c4b /deepseek-ai-dsh-tool-subagent-control/understand-the-implementation/source-map: - en: 95f73146f8735ab1 - zh: 9c67e064c4000613 + en: c86b31cecfd505a0 + zh: 14250cb3bf3e614f /deepseek-ai-dsh-tool-subagent-control/further-exploration: en: df2bf39d0ffffd43 zh: 4cd02551f7433c90 @@ -54,14 +54,14 @@ en: e4c7376d4f29dbd5 zh: 6b7a9eaf69c9ea53 /deepseek-ai-dsh-tool-subagent-control/model-experience/tool-schema/what-the-model-sees: - en: b32bc9bcb45a8c77 - zh: 4bd3eba1e8612da7 + en: d0c7a044fe6b9bf4 + zh: 94e7b4fc4615f356 /deepseek-ai-dsh-tool-subagent-control/model-experience/tool-schema/token-effect: - en: db207dc8d5860684 - zh: ad2663b4967fbca9 + en: 282c65ac0eebfbdd + zh: e9610d49f72918b0 /deepseek-ai-dsh-tool-subagent-control/model-experience/tool-schema/kv-cache-effect: - en: 4b77d42b96b3519d - zh: dd393d8f0f6536c1 + en: 89619e31f3ea7585 + zh: 5681bc8096cd8e32 /deepseek-ai-dsh-tool-subagent-control/model-experience/interrupt-result: en: d64e936e83e89a2b zh: b9905c33af6cca45 @@ -78,11 +78,11 @@ en: ab964e307bd37dca zh: 03f975252f838a42 /deepseek-ai-dsh-tool-subagent-control/model-experience/delivery-result/what-the-model-sees: - en: 4a0a4955589012ae - zh: 2bbbb5196a8aa4fd + en: c562b973653a1465 + zh: 543e0877d9620114 /deepseek-ai-dsh-tool-subagent-control/model-experience/delivery-result/token-effect: - en: 0f134e37e4bd9965 - zh: ceb858d01266e976 + en: fba7eb5d4c53013a + zh: 4e481bbb133c8263 /deepseek-ai-dsh-tool-subagent-control/model-experience/delivery-result/kv-cache-effect: en: f5d5f97a153f7c70 zh: 3d6bc05b9eadfbc2 @@ -90,17 +90,17 @@ en: 094e55d258f2ec2e zh: f364bc9046530bff /deepseek-ai-dsh-tool-subagent-control/model-experience/listing-result/what-the-model-sees: - en: efbbd1e36c7b2edd - zh: a8a95475666ed1dc + en: 5d64b578e3301189 + zh: 68b04cf88e83bde7 /deepseek-ai-dsh-tool-subagent-control/model-experience/listing-result/token-effect: - en: 4688e849b3143dfc - zh: f22b5520c454f9e7 + en: 55161776c603f199 + zh: 8e5625243f42dbaa /deepseek-ai-dsh-tool-subagent-control/model-experience/listing-result/kv-cache-effect: en: 4a629edb36a17e32 zh: 5a7539cfbf404c54 /deepseek-ai-dsh-tool-subagent-control/known-limitations-and-deferred-work: - en: 31cd8a1934cf0093 - zh: 2fd07e05c8e58e9a + en: 053847e4b5f93871 + zh: f0b5ae067bc02c28 /deepseek-ai-dsh-tool-subagent-control/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/subagent/tool-subagent-control/README.md b/packages/subagent/tool-subagent-control/README.md index 27507df343..aa835c08af 100644 --- a/packages/subagent/tool-subagent-control/README.md +++ b/packages/subagent/tool-subagent-control/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -`dsh-tool-subagent-control` adds the global control tools for continuable children: `send_message` steers between a direct parent and child, `interrupt_agent` stops a child's current turn while keeping its inbox and descendants intact, and `list_agents` (from the separately loadable `list-agents` plugin) lists continuable children by durable id and label. Parents and continuable children inherit the same `send_message` definition and ordering, so model communication adds no child-only tool schema. No tool's presence decides whether a delegation tool starts continuable work. +`dsh-tool-subagent-control` adds the global control tools for continuable children: `send_message` steers between a parent and child or between siblings, `interrupt_agent` stops a descendant’s current turn while keeping its inbox and descendants intact, and the separately loadable `list-agents` plugin discovers agents by id and description. Parents and continuable children inherit the same tool definitions and ordering. ## Table of Contents @@ -45,15 +45,15 @@ This package takes no configuration: the root plugin provides `send_message` and ### send_message -Sends a message to an Agent named by `agent_id`: any exact live Agent may target its direct continuable child, while a resident continuable child may also target its direct parent. A working target receives the message at its nearest step boundary through Steer; an inactive target starts or resumes a turn through the continuation lifecycle. The call returns only acceptance (the accepted message's stable `messageId`), never a reply. A failure — an unsupported target, unavailable parent, unknown child, descriptor-less child that cannot be resumed, or rejected admission — states the message was not delivered. +Sends a message to the Session id in `agent_id`. Any exact live Agent may target its direct continuable child; a resident continuable child may also target its direct parent or continuable siblings. Sibling delivery uses the shared live parent’s authority and preserves the actual sender. A working target receives the message at its nearest step boundary through Steer; an inactive child or sibling starts or resumes a turn. Success returns inbox acceptance (`messageId`), never a reply. An unsupported target, unavailable parent, unknown child, missing descriptor, or rejected admission returns an error. ### interrupt_agent -External executions stop permanently and cannot receive follow-ups. Local targets stop only their current turn: queued messages stay parked until a later `send_message`, descendants keep running, and the child stays available for follow-ups. The call returns when the stop request is accepted, not when the target is quiet; interrupting an already-finished agent is an accepted no-op, and self, sibling, stale, and non-ancestor callers get errored results. +`agent_id` identifies a descendant by Session id. External executions stop permanently and cannot receive follow-ups. Local targets stop only their current turn: queued messages stay parked until a later `send_message`, descendants keep running, and the child stays available for follow-ups. The call returns when the stop request is accepted, not when the target is quiet; interrupting an already-finished agent is an accepted no-op, and self, sibling, stale, and non-ancestor callers get errored results. ### list_agents -Lists the continuable children below the calling agent: `children` (default) reads direct children from the parent catalog without opening child logs; `descendants` recursively reads child catalogs in stable pre-order, annotating each entry with its durable direct-parent session id and depth. Status comes from the live Agent registry — `running` or `inactive`. External entries are omitted without reading a child Session. Readable one-shot children are omitted from output but their catalogs remain traversal nodes. Unknown modes and unreadable child catalogs, including one-shot children, appear as diagnostics only in `descendants` scope. Ordinary Session forks are not catalog entries, so neither those forks nor their descendants are listed from the source Session. +Lists agents by id and description. `children` (default) lists direct continuable children from the parent catalog and, for a continuable subagent with a live parent, first lists that parent and its continuable siblings. `descendants` lists only reachable descendants in stable pre-order, with their direct parent id and depth. Live status is `running` or `inactive`. External entries are omitted without reading child Sessions; readable one-shot children are traversal nodes but not output rows. Unknown modes and unreadable child catalogs produce diagnostics only in `descendants` scope. Ordinary Session forks are not catalog entries. ----- @@ -67,22 +67,22 @@ This section explains what the tools delegate to the subagent service; the obser ### Design concept -Thin adapters over `ctx.subagents.sendMessage()`, `interrupt()`, and the list projections; the tools perform no lifecycle routing. Residency, cold resume, and authorization belong to the service, and the tools pass the exact live calling agent (`exec.agent`) as both sender and authority. +Thin adapters over `ctx.subagents.sendMessage()`, `interrupt()`, and the list projections; residency, cold resume, and authorization belong to the service. Tools pass the exact live calling Agent (`exec.agent`) as sender and authority, and convert `agent_id` to the branded Session id. ### Delivery and signal ownership -The tool forwards its execution signal, which owns admission only until inbox acceptance. Once the target accepts a message, it cannot be cancelled through this tool. Every message is framed as `Agent sent a message:` and recorded with `{ kind: 'agent-message', form: 'relay', senderSessionId: sender.id }`; the service derives that attribution and never treats it as authority. +The tool forwards its execution signal, which owns admission only until inbox acceptance. Accepted messages cannot be cancelled through this tool. Every message is framed as `Agent sent a message:` and recorded with `{ kind: 'agent-message', form: 'relay', senderSessionId: sender.id }`, including sibling delivery under the parent’s authority. Attribution never grants authority. ### Listing projection -`list_agents` derives the root id from the calling agent, reads the service catalog without a cursor, refines each candidate's status through the live Agent registry, and omits one-shot children because they cannot accept `send_message`. Descendant traversal preserves each parent catalog's event order. Unknown modes produce diagnostics while their catalogs remain traversable; unreadable catalogs produce diagnostics and stop that branch. Descendants absent from reachable catalogs cannot be discovered. +`list_agents` reads the caller’s catalog without a cursor and refines status through the live Agent registry. In `children` scope, a continuable caller’s live parent catalog supplies the parent and sibling rows. Descendant traversal preserves catalog order; unknown modes remain traversable with diagnostics, while unreadable catalogs stop that branch. Descendants absent from reachable catalogs cannot be discovered. ### Source map | File | Role | |---|---| | [`src/index.ts`](src/index.ts) | `send_message` and `interrupt_agent` registration | -| [`src/list-agents.ts`](src/list-agents.ts) | `list_agents` registration: direct-child discovery and status refinement | +| [`src/list-agents.ts`](src/list-agents.ts) | `list_agents` registration: parent, sibling, and child discovery and status refinement | @@ -106,15 +106,15 @@ Read these pages when the package-level contract is not enough; they move from t #### What the model sees -The generated [schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent-control): `send_message` takes `agent_id` and `message`; `interrupt_agent` takes `agent_id`; `list_agents` takes no parameters. +The generated [schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent-control): `send_message` takes an `agent_id` and `message`; `interrupt_agent` takes an `agent_id`; `list_agents` takes an optional `scope`. All addresses are Session ids; descriptions are display text and may repeat. #### Token effect -Fixed schema cost per parent request. +Fixed schema cost per request, independent of the number or descriptions of children. #### KV Cache effect -Prefix-stable; the schema does not change at runtime. +Prefix-stable; the schemas do not change at runtime, and a parent and its continuable children, including fork children, receive identical definitions. ### Interrupt result @@ -134,11 +134,11 @@ Append-only; each result follows the reusable request prefix. #### What the model sees -`message delivered to agent ` on acceptance; the canonical output carries the accepted `messageId`. A failure — a non-adjacent target, unavailable parent, unknown child, descriptor-less child that cannot be resumed, or admission rejected — is an errored result whose message states the message was not delivered. +`message delivered to agent ` on acceptance; canonical output carries the accepted `messageId`. A target outside the caller’s supported parent, child, and sibling routes, or rejected admission, produces an errored result. #### Token effect -One short acknowledgement per call; the target's response never returns through this call. A child uses the same tool with its initial task's parent id to append selected content to parent history. +One short acknowledgement per call; the target’s response never returns through this call. Children use parent and sibling ids from `list_agents` for follow-up messages. #### KV Cache effect @@ -148,11 +148,11 @@ Append-only; newly visible content follows the reusable request prefix and does #### What the model sees -One line per continuable child in stable catalog order: ` [] —