mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-09 04:02:35 +00:00
Merge remote-tracking branch 'origin/master' into worktree/llm-pi-ai-config-exposure-05f455
# Conflicts: # docs/config-catalog.i18n.yaml # docs/config-catalog.md # docs/config-catalog.zh.md
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# 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/implemented/architecture/2026-07-30-web-config-plane.md
|
||||
2026-07-30-web-config-plane.md: 1326f50a792b6c6f791c9515ea03cc362d16fc7e
|
||||
2026-07-30-web-config-plane.zh.md: bea4bbc237f44864a5fa9bf677267ca0055c13ed
|
||||
2026-07-30-web-config-plane.md: ac989cb100190e9a41ebf04b5b2d80125d49e0cb
|
||||
2026-07-30-web-config-plane.zh.md: 26f11352b4362d3cbded3d1dc9205e289ee9b18c
|
||||
|
||||
@@ -22,7 +22,7 @@ The request-level configuration seam made LLM adapter configuration restart-free
|
||||
|
||||
**A hand-written editor over a schema model layer.** `ctx.settingsSchema`, provided by `dsh-client-ui-settings`, rehydrates the wire's `toJSON()` envelope into live schemastery nodes for validation, path resolution, and immutable draft editing — but no generic rendering: the first cut shipped a full schema-driven form renderer, and the resulting page was an unstyled schema dump (every advanced field flattened onto the card, raw field names as labels, the `retryPolicy` unsupported-fallback in the main flow). The hand-written direction won over adding a hint/grouping system, and a further simplification removed the reference input entirely: the card's primary field is one **API key** input, a whole-section provider without a configured key opens as its setup card, and the collapsed 自定义设置 fold carries the curated per-family extras (`baseURL` for both families, `reasoningEffort` for deepseek / `reasoning` for pi-ai, plus direct DeepSeek model rows with `id`, `name`, and `contextWindow`). Existing model fields outside that visible set survive array edits; retry policy, timeouts, and other fields remain owned by `settings.yaml`. Validation still runs the rehydrated schema before writing, while adapter-specific checks reject catalog invariants that the serialized schema cannot express. The card's colors resolve through the `--dsw-alias-*` design tokens; it had named `--border`/`--surface`/`--text-*`, which nothing in this app defines, so it rendered their light-mode fallbacks and stayed light under the dark theme. The model catalog takes the row shape the pi-ai provider form introduces: one bordered entry per model, id and display name on the row, and the capacities behind the row's own disclosure, so the two editors read as one design rather than diverging. Every field keeps the indexed `aria-label` that names it. Both capacities are text fields reading a decimal `K`/`M` suffix (`1M` is 1000K, matching how capacities are quoted) and storing the plain count: a field holds the typed text while it has focus, because re-deriving it from the parsed count on every keystroke would rewrite `1000` to `1K` mid-word, and text that does not parse stays on screen so the save-time rejection names a row the user can still see. The shared class names carry only declared token spellings: `--dsw-alias-border-subtle`, `--dsw-alias-text-tertiary`, and `--dsw-alias-text-primary` are undeclared, so naming them resolves to the light-mode literals in their fallback slots. A styles test now rejects any `--dsw-*` name the token sheet does not declare, so the next editor to name one fails rather than shipping a light-only surface.
|
||||
|
||||
**The Models page is a three-domain join with service-shaped apply semantics.** Rows are configured providers; the add card's select is the dormant directory remainder. Route liveness still gates readiness and invalidates the join, but the page does not render it as provider status because configuration presence and runtime availability are distinct. The key path stays reference-shaped without ever showing a reference: a typed key stores **write-only** through `credentials.set` under the profile's `apiKeyEnv`, deriving `<ROUTE>_API_KEY` when none exists (the pi-ai profile records the derivation only when a key is entered), so `settings.yaml` never carries a key value; a blank pi-ai key materializes a reference-free profile and preserves provider-native authentication. Profile edits and removals land as minimal path-addressed `settings.mutate` operations against the redacted user section, which never names a secret the page did not receive. Removing a user-layer provider first opens a localized confirmation dialog whose row actions, title, description, and final action identify the same provider; confirmation removes an exact configured+writable derived credential before the profile, while custom, environment, and unidentified targets remain untouched. Both stages are idempotent and a partial failure stays in the dialog for retry. DeepSeek's model list is array-replace configuration: inherited effective rows remain visible until the first edit materializes the complete list in the user layer, and reset unsets the list override. The partial-commit and credential-ownership rationale lives in the [provider credential lifecycle note](../bug-fix/2026-08-06-provider-credential-lifecycle.md).
|
||||
**The Models page is a three-domain join with service-shaped apply semantics.** Rows are configured providers; the add card's select is the dormant directory remainder. Route liveness still gates readiness and invalidates the join, but the page does not render it as provider status because configuration presence and runtime availability are distinct. The key path stays reference-shaped without ever showing a reference: a typed key stores **write-only** through `credentials.set` under the profile's `apiKeyEnv`, deriving `<ROUTE>_API_KEY` when none exists (the pi-ai profile records the derivation only when a key is entered), so `settings.yaml` never carries a key value; a blank pi-ai key materializes a reference-free profile and preserves provider-native authentication. Profile edits and removals land as minimal path-addressed `settings.mutate` operations against the redacted user section, which never names a secret the page did not receive. Removing a user-layer provider first opens a localized confirmation dialog whose row actions, title, description, and final action identify the same provider; confirmation removes an exact configured+writable derived credential before the profile, while custom, environment, and unidentified targets remain untouched. Both stages are idempotent and a partial failure stays in the dialog for retry. DeepSeek's model list is array-replace configuration: inherited effective rows remain visible until the first edit materializes the complete list in the user layer, and reset unsets the list override. `llm.discoverModels` results stay in picker-local state until **Add selected**; configured ids start unchecked, while **Select all** / **Deselect all** changes only that local set, so bulk selection preserves the same capacity-protection rule. The partial-commit and credential-ownership rationale lives in the [provider credential lifecycle note](../bug-fix/2026-08-06-provider-credential-lifecycle.md).
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -36,4 +36,4 @@ The request-level configuration seam made LLM adapter configuration restart-free
|
||||
|
||||
## Consequences
|
||||
|
||||
The whole loop is pinned keyless in the browser lane (`apps/web/tests/models-settings.e2e.ts`): the add card offers the dormant pi-ai catalog, adding `minimax-cn` with a typed key writes the reference-only profile into `settings.yaml`, stores the value into the harness home's `.env` under the derived `MINIMAX_CN_API_KEY`, registers the route live on the topology frame, and the customized fold merges `reasoning` beside the reference — zero model calls, ARIA goldens for the add-card, configured, and identified delete-confirmation states, plus a scaffold `harnessHome` so tests never touch a real `~/.dsh` (the provider under test is one whose derived reference cannot collide with a developer's exported keys). The settings-shell scenario intercepts the pathless native intent; Service Definition, provider, wire, React, and native-opener tests separately pin provider absence, custom-path resolution, absent-file materialization, owner-only permissions, hidden remote/unavailable states, duplicate-click collapse, localized failure, macOS text-editor dispatch, and Linux/Windows desktop dispatch. The removal scenario proves cancellation leaves both profile and key intact, then confirmation removes both the profile and its identified managed credential. The DeepSeek onboarding fixture edits the default catalog into a user-owned list, persists an arbitrary model id/name/context window, removes the active row, and observes the model selector's empty-selection fallback. The rename touched 239 files (fixtures, goldens, docs, python) with no compatibility alias. The renderer replacement needed no wire change: apply semantics, redaction, and the directory join were renderer-agnostic all along. Deferred: a per-row models preview (the picker already lists models) and a page address for live routes that never declared configurability.
|
||||
The whole loop is pinned keyless in the browser lane (`apps/web/tests/models-settings.e2e.ts`): the add card offers the dormant pi-ai catalog, adding `minimax-cn` with a typed key writes the reference-only profile into `settings.yaml`, stores the value into the harness home's `.env` under the derived `MINIMAX_CN_API_KEY`, registers the route live on the topology frame, and the customized fold merges `reasoning` beside the reference — zero model calls, ARIA goldens for the add-card, configured, model-picker, and identified delete-confirmation states, plus a scaffold `harnessHome` so tests never touch a real `~/.dsh` (the provider under test is one whose derived reference cannot collide with a developer's exported keys). The component suite pins configured-id exclusions and both directions of the bulk toggle. The settings-shell scenario intercepts the pathless native intent; Service Definition, provider, wire, React, and native-opener tests separately pin provider absence, custom-path resolution, absent-file materialization, owner-only permissions, hidden remote/unavailable states, duplicate-click collapse, localized failure, macOS text-editor dispatch, and Linux/Windows desktop dispatch. The removal scenario proves cancellation leaves both profile and key intact, then confirmation removes both the profile and its identified managed credential. The DeepSeek onboarding fixture edits the default catalog into a user-owned list, persists an arbitrary model id/name/context window, removes the active row, and observes the model selector's empty-selection fallback. The rename touched 239 files (fixtures, goldens, docs, python) with no compatibility alias. The renderer replacement needed no wire change: apply semantics, redaction, and the directory join were renderer-agnostic all along. Deferred: a per-row models preview (the picker already lists models) and a page address for live routes that never declared configurability.
|
||||
|
||||
@@ -22,7 +22,7 @@ Status: implemented
|
||||
|
||||
**架在 schema 模型层之上的手写编辑器。**`dsh-client-ui-settings` 提供的 `ctx.settingsSchema` 把 wire 的 `toJSON()` 信封还原(rehydrate)为活的 schemastery 节点,用于校验、路径解析与不可变草稿编辑——但不做通用渲染:第一版交付了完整的 schema 驱动表单渲染器,得到的却是一个未加样式、把 schema 原样倾倒出来的页面(每个进阶字段都平铺到卡片上、原始字段名直接充当标签、`retryPolicy` 的「不支持」回退落在主流程里)。手写方向胜过了再加一套提示/分组系统,进一步的简化又把引用输入框整个移除:卡片的主字段是一个 **API 密钥** 输入框,未配置密钥的整分节提供方会以其设置卡片的形式打开,收起的「自定义设置」折叠区承载按家族精选的额外字段(两个家族都有 `baseURL`,deepseek 有 `reasoningEffort`/pi-ai 有 `reasoning`,另有直接 DeepSeek 模型行的 `id`、`name` 和 `contextWindow`)。现有模型字段中不在可见集合内的部分会在数组编辑后保留;重试策略、超时及其他字段仍归 `settings.yaml` 所有。校验仍会在写入前运行还原出的 schema,适配器特有的检查则会拒绝序列化 schema 无法表达的目录不变量。卡片的颜色经 `--dsw-alias-*` 设计 token 解析;它此前引用的 `--border`/`--surface`/`--text-*` 在本应用中无人定义,于是渲染出的是它们的亮色模式回退值,在暗色主题下依旧保持亮色。模型目录采用 pi-ai 提供方表单引入的行形态:每个模型一个带边框的条目,ID 与显示名称落在行上,容量则收在该行自己的折叠区里,使两个编辑器呈现为同一套设计,而不是各自分岔。每个字段都保留那个为其命名的带序号 `aria-label`。两项容量都是文本输入框,读取十进制的 `K`/`M` 后缀(`1M` 即 1000K,与容量的通行标注方式一致)并存储纯数值:字段持有焦点期间保留键入的文本,因为若每次按键都从解析出的数值重新推导该文本,`1000` 会在尚未输完时就被改写成 `1K`;无法解析的文本也会留在屏幕上,因此保存时的拒绝点名的是用户仍能看见的那一行。共用的类名只承载已声明的 token 写法:`--dsw-alias-border-subtle`、`--dsw-alias-text-tertiary` 和 `--dsw-alias-text-primary` 均未声明,写出它们就会解析为各自回退槽位中的亮色模式字面值。现在有一个样式测试会拒绝 token 表未声明的任何 `--dsw-*` 名称,因此下一个写出这类名称的编辑者会当场失败,而不是交付一个只有亮色的界面。
|
||||
|
||||
**Models 页是一次三领域联接,应用语义与服务同形。**每一行是一个已配置的提供方;「新增」卡片的选择框是可配置提供方目录中剩余的休眠条目。路由存活状态仍用于就绪判定,并会使该联接失效,但页面不将其渲染为提供方状态,因为配置存在与运行时可用性是两个不同概念。密钥通道保持引用形态,却从不展示任何引用:键入的密钥经 `credentials.set` **只写**存入 profile 的 `apiKeyEnv` 之下,引用不存在时便派生 `<ROUTE>_API_KEY`(仅在输入密钥时,pi-ai profile 才会记录该派生),因此 `settings.yaml` 从不携带密钥值;留空 pi-ai 密钥会具化一个不带引用的 profile,并保留提供方原生认证。profile 的编辑和删除会针对脱敏后的用户分节,以按路径寻址的最小 `settings.mutate` 操作落地,绝不会点名页面未收到的机密。删除用户层提供方时,会先打开本地化确认对话框,其行操作、标题、说明和最终操作都会点名同一个提供方;确认后会先清除与派生目标精确匹配且已配置、可写的凭据,再删除 profile,自定义目标、环境目标和无法识别的目标则保持不变。两个阶段都具备幂等性,部分失败会留在对话框中供重试。DeepSeek 的模型列表是数组替换配置:继承而来的生效模型行会一直显示,直到第一次编辑将完整列表具化到用户层;重置则会取消设置该列表覆盖。部分提交与凭据所有权的理由记录在[提供方凭据生命周期 note](../bug-fix/2026-08-06-provider-credential-lifecycle.md)中。
|
||||
**Models 页是一次三领域联接,应用语义与服务同形。**每一行是一个已配置的提供方;「新增」卡片的选择框是可配置提供方目录中剩余的休眠条目。路由存活状态仍用于就绪判定,并会使该联接失效,但页面不将其渲染为提供方状态,因为配置存在与运行时可用性是两个不同概念。密钥通道保持引用形态,却从不展示任何引用:键入的密钥经 `credentials.set` **只写**存入 profile 的 `apiKeyEnv` 之下,引用不存在时便派生 `<ROUTE>_API_KEY`(仅在输入密钥时,pi-ai profile 才会记录该派生),因此 `settings.yaml` 从不携带密钥值;留空 pi-ai 密钥会具化一个不带引用的 profile,并保留提供方原生认证。profile 的编辑和删除会针对脱敏后的用户分节,以按路径寻址的最小 `settings.mutate` 操作落地,绝不会点名页面未收到的机密。删除用户层提供方时,会先打开本地化确认对话框,其行操作、标题、说明和最终操作都会点名同一个提供方;确认后会先清除与派生目标精确匹配且已配置、可写的凭据,再删除 profile,自定义目标、环境目标和无法识别的目标则保持不变。两个阶段都具备幂等性,部分失败会留在对话框中供重试。DeepSeek 的模型列表是数组替换配置:继承而来的生效模型行会一直显示,直到第一次编辑将完整列表具化到用户层;重置则会取消设置该列表覆盖。`llm.discoverModels` 的结果会留在选择框的本地状态中,直至点击**添加所选**;已配置的 ID 默认不勾选,**全选**/**取消全选**也只会改变这一本地集合,因此批量选择仍遵守同一条容量保护规则。部分提交与凭据所有权的理由记录在[提供方凭据生命周期 note](../bug-fix/2026-08-06-provider-credential-lifecycle.md)中。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
@@ -36,4 +36,4 @@ Status: implemented
|
||||
|
||||
## 后果
|
||||
|
||||
整条闭环以无密钥方式固定在浏览器测试通道(`apps/web/tests/models-settings.e2e.ts`):「新增」卡片提供休眠的 pi-ai catalog,携键入的密钥添加 `minimax-cn` 会把只含引用的 profile 写入 `settings.yaml`、把密钥值存入 harness 家目录 `.env` 中派生的 `MINIMAX_CN_API_KEY` 之下、路由随拓扑帧注册为存活,「自定义设置」折叠区则把 `reasoning` 合并到引用旁边——全程零模型调用,「新增」卡片态、已配置态与已点名目标的删除确认态各有 ARIA golden,另有脚手架式的 `harnessHome`,测试绝不触碰真实的 `~/.dsh`(受测提供方是派生引用不可能与开发者已导出密钥相撞的那一个)。设置外壳场景会截获无路径参数的原生意图;Service Definition、提供方、wire、React 与原生打开器测试分别固定了提供方缺失、自定义路径解析、缺失文件创建、仅属主权限、远程/不可用时隐藏、重复点击合并、本地化失败、macOS 文本编辑器分发,以及 Linux/Windows 桌面分发。删除场景证明,取消会保留 profile 和密钥,随后的确认会同时删除 profile 及其已识别的受管凭据。DeepSeek 首次使用 fixture 会把默认目录编辑为用户自有列表、持久化任意模型的 ID/名称/上下文窗口、移除活动模型行,并观察模型选择器的空选择回退。这次重命名触及 239 个文件(fixture(测试前置数据)、golden、文档、python),未保留兼容别名。替换渲染器不需要任何 wire 变更:应用语义、脱敏与目录联接从一开始就与渲染器无关。延后事项:每行的模型预览(选择器已能列出模型)和为从未声明可配置性的存活路由提供页面地址。
|
||||
整条闭环以无密钥方式固定在浏览器测试通道(`apps/web/tests/models-settings.e2e.ts`):「新增」卡片提供休眠的 pi-ai catalog,携键入的密钥添加 `minimax-cn` 会把只含引用的 profile 写入 `settings.yaml`、把密钥值存入 harness 家目录 `.env` 中派生的 `MINIMAX_CN_API_KEY` 之下、路由随拓扑帧注册为存活,「自定义设置」折叠区则把 `reasoning` 合并到引用旁边——全程零模型调用,「新增」卡片态、已配置态、模型选择框态与已点名目标的删除确认态各有 ARIA golden,另有脚手架式的 `harnessHome`,测试绝不触碰真实的 `~/.dsh`(受测提供方是派生引用不可能与开发者已导出密钥相撞的那一个)。组件测试固定了已配置 ID 的排除与批量切换的两个方向。设置外壳场景会截获无路径参数的原生意图;Service Definition、提供方、wire、React 与原生打开器测试分别固定了提供方缺失、自定义路径解析、缺失文件创建、仅属主权限、远程/不可用时隐藏、重复点击合并、本地化失败、macOS 文本编辑器分发,以及 Linux/Windows 桌面分发。删除场景证明,取消会保留 profile 和密钥,随后的确认会同时删除 profile 及其已识别的受管凭据。DeepSeek 首次使用 fixture 会把默认目录编辑为用户自有列表、持久化任意模型的 ID/名称/上下文窗口、移除活动模型行,并观察模型选择器的空选择回退。这次重命名触及 239 个文件(fixture(测试前置数据)、golden、文档、python),未保留兼容别名。替换渲染器不需要任何 wire 变更:应用语义、脱敏与目录联接从一开始就与渲染器无关。延后事项:每行的模型预览(选择器已能列出模型)和为从未声明可配置性的存活路由提供页面地址。
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# 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/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md
|
||||
2026-08-10-fork-children-stay-one-shot.md: 10e2607e55b67671519fd18c26d0ae8fc0ca7268
|
||||
2026-08-10-fork-children-stay-one-shot.zh.md: d9a5561ed6fb8a32ca6c47553a49e2948eb59256
|
||||
2026-08-10-fork-children-stay-one-shot.md: 44b947a3e0580263f1973aaf24534b7b2f01c0b6
|
||||
2026-08-10-fork-children-stay-one-shot.zh.md: 4a9e5ab10ba1f437e4a8caf6c08f08412113c23d
|
||||
|
||||
@@ -42,7 +42,7 @@ The reintroduction condition is recorded as a `TODO(fork-continuable-prefix-reus
|
||||
- A forked child's request prefix stays byte-identical to its parent's unless the deployment configures `persona` or `toolFilter` on the fork delegation tool, so the token cost of seeding buys provider-side reuse again.
|
||||
- The fork provider's continuable path has no production caller and no assembled-composition coverage. It keeps its package-level tests, and the seam still accepts it, so a bundle or `--patch` overlay can reintroduce it with no code change and no warning.
|
||||
- `subagent_fork`'s model-visible schema changes: the continuable background wording is replaced by the one-shot task wording in the base bundle, and disappears entirely from the two examples. The affected keyless snapshot tool-schema sidecars are re-recorded in the same change.
|
||||
- The report obligation's reach narrows to spawned children in shipped deployments. Its default `wakeup` scheduling, authority model, and coverage are unchanged.
|
||||
- The report obligation's reach narrows to spawned children in shipped deployments. Its default `next-step` scheduling, authority model, and coverage remain independent of fork composition.
|
||||
|
||||
### Accepted risks
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ one-shot child——前台与后台皆然——经由 `SubagentRuntime.start()`
|
||||
- 除非部署在 fork 委派工具上配置了 `persona` 或 `toolFilter`,fork child 的请求前缀与其 parent 逐字节相同,因此初始内容的 token 成本重新换来了提供方侧的复用。
|
||||
- fork 提供方的可继续路径没有生产调用方,也没有整体组装层面的覆盖。它保留自己的包内测试,seam 也仍然接受它,因此某个组合包或 `--patch` 覆盖层可以无需改动代码、也不会有任何警告地把它重新引入。
|
||||
- `subagent_fork` 面向模型的 schema 发生变化:base 组合包中可继续的后台措辞被 one-shot 的 task 措辞取代,在两个示例中则完全消失。受影响的无密钥快照工具 schema 伴随文件在同一次改动中重新记录。
|
||||
- 在随附部署中,report 义务的覆盖范围收窄到 spawn 出的 child。它的 `wakeup` 默认调度、权限模型与覆盖均保持不变。
|
||||
- 在随附部署中,report 义务的覆盖范围收窄到 spawn 出的 child。它的 `next-step` 默认调度、权限模型与覆盖仍独立于 fork 组合。
|
||||
|
||||
### 已接受的风险
|
||||
|
||||
|
||||
+3
-3
@@ -1,6 +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/implemented/simplification/2026-08-11-remove-empty-experimental-package-group.md
|
||||
2026-08-11-remove-empty-experimental-package-group.md: e5e81e3e3763f216921b3f3b74709b64be3dee37
|
||||
2026-08-11-remove-empty-experimental-package-group.zh.md: d44d0daaf346a5fea317f8c8c6a23f26eaa3cec0
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md
|
||||
2026-08-18-experimental-agent-teams-packages.md: cec73be48a5cde25fae570587d2ecd17a4a87d01
|
||||
2026-08-18-experimental-agent-teams-packages.zh.md: 71b702daa8694fddec6e2a0972704f19955cb509
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# Agent Note: Incubate Agent Teams as private experimental packages
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-18-experimental-agent-teams-packages.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Agent Teams needs the real Session log, subagent lifecycle, tools, examples, snapshots, and repository checks while its service and tool contracts continue to change. Placing those packages in a product-role group makes them members of the dsh release family and gives them the same publication expectation as stable packages.
|
||||
|
||||
An experimental directory without a current package previously imposed placement, dependency, promotion, and release rules on no consumer. Agent Teams supplies the concrete consumer, but the directory needs mechanical release exclusion and dependency isolation rather than a documentation-only status.
|
||||
|
||||
## Decision
|
||||
|
||||
`packages/experimental/team` and `packages/experimental/tool-team` are private workspace packages. Their npm names remain `@deepseek-ai/dsh-team` and `@deepseek-ai/dsh-tool-team`, so promotion changes paths and release metadata without renaming imports.
|
||||
|
||||
The dsh release family and local baseline publisher exclude every manifest below `packages/experimental/`. Workspace constraints require each experimental package to set `private: true` and omit `publishConfig`. The same top-level check rejects `dependencies`, `optionalDependencies`, and `peerDependencies` from release packages, release apps, or the Python runtime to an experimental package. Experimental packages may depend on release packages and each other; tests may use them through `devDependencies`, and examples may load them explicitly.
|
||||
|
||||
The generic caller-reserved continuable child identity and selective direct-child drain remain in the stable Subagent service. They own Subagent identity and Activation lifecycle without importing or naming Agent Teams; the experimental Team service consumes them in the permitted direction.
|
||||
|
||||
Experimental status changes publication and compatibility expectations only. The packages retain the repository's ordinary documentation, invariant, lifecycle, security, unit, real-composition, and snapshot requirements. Promotion requires review of the public contracts, limitations, test evidence, release payload, runtime dependents, and a named owner accepting stable-package obligations.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep Agent Teams in a product-role group and describe it as opt-in.** Opt-in composition controls model behavior but does not exclude packages from publication or prevent stable packages from taking runtime dependencies on them.
|
||||
|
||||
**Reserve an empty experimental group.** A directory without a current package has no owner or release mechanism to test. The group exists only while concrete packages need its enforced treatment.
|
||||
|
||||
**Move the Subagent prerequisites into the experimental directory.** Child identity allocation and Activation teardown belong to the Subagent owner and contain no Team-specific contract. Moving or duplicating them would invert the dependency or split one lifecycle across packages.
|
||||
|
||||
**Publish experimental packages under different npm names.** Experimental suffixes would make promotion rename every import and configuration row without strengthening release exclusion; private manifests and release-family filtering enforce the actual requirement.
|
||||
|
||||
## Consequences
|
||||
|
||||
Agent Teams can use the full repository graph and quality checks without entering official tarballs or becoming a supported runtime dependency. A release package cannot expose Team until the Team packages are promoted, so CLI and Web experiments use explicit example or experimental compositions instead of the shipped base bundles.
|
||||
|
||||
The product-role grouping is less direct while the packages incubate, and promotion creates path churn. Keeping npm names stable limits that churn to repository paths, metadata, documentation, and composition.
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# Agent Note: 将 Agent Teams 作为私有实验性包孵化
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-18-experimental-agent-teams-packages.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Agent Teams 的服务与工具约定仍在变化,但它需要使用真实 Session 日志、subagent 生命周期、工具、示例、快照和仓库检查。把这些包放在产品职责组会使其成为 dsh 发布系列成员,并获得与稳定包相同的发布预期。
|
||||
|
||||
没有实际包的 experimental 目录曾经让没有消费方的放置、依赖、promotion 和发布规则长期存在。Agent Teams 提供了具体消费方,但该目录需要机械强制的发布排除与依赖隔离,不能只用文档标记状态。
|
||||
|
||||
## 决策
|
||||
|
||||
`packages/experimental/team` 与 `packages/experimental/tool-team` 是私有 workspace 包。其 npm 名仍为 `@deepseek-ai/dsh-team` 与 `@deepseek-ai/dsh-tool-team`,因此 promotion 只需修改路径与发布元数据,无需重命名 import。
|
||||
|
||||
dsh 发布系列与本地 baseline 发布器均排除 `packages/experimental/` 下的所有 manifest。workspace 约束要求每个实验性包设置 `private: true` 并省略 `publishConfig`。同一个顶层检查会拒绝发布包、发布 app 或 Python runtime 通过 `dependencies`、`optionalDependencies` 或 `peerDependencies` 依赖实验性包。实验性包可以依赖发布包和其他实验性包;测试可以通过 `devDependencies` 使用它们,示例可以显式加载它们。
|
||||
|
||||
通用的调用方预留 continuable child 身份和精确 direct-child drain 仍属于稳定 Subagent 服务。它们负责 Subagent 身份与 Activation 生命周期,不 import 或命名 Agent Teams;实验性 Team 服务沿允许的方向消费这些能力。
|
||||
|
||||
实验性状态只改变发布与兼容性预期。这些包仍须满足仓库的一般文档、不变式、生命周期、安全、单元测试、真实组合测试和快照要求。promotion 前必须评审公开约定、限制、测试证据、发布 payload、运行时依赖方,并由一名具名 owner 接受稳定包义务。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**把 Agent Teams 留在产品职责组,并标为显式启用。** 显式启用的组合可以控制模型行为,但不会阻止包发布,也不能阻止稳定包对其建立运行时依赖。
|
||||
|
||||
**预留空的 experimental 组。** 没有实际包的目录没有 owner,也没有可供测试的发布机制。只有具体包需要这套强制处理时,该组才存在。
|
||||
|
||||
**把 Subagent 前置能力移入 experimental 目录。** child 身份分配与 Activation teardown 属于 Subagent owner,且不包含 Team 专用约定。移动或复制这些能力会反转依赖方向,或把同一个生命周期拆到多个包中。
|
||||
|
||||
**使用不同的 npm 名发布实验性包。** experimental 后缀会让 promotion 重命名所有 import 与配置项,却不能加强发布排除;私有 manifest 与发布系列过滤已经强制执行实际要求。
|
||||
|
||||
## 后果
|
||||
|
||||
Agent Teams 可以使用完整仓库依赖图与质量检查,而不进入正式 tarball,也不会成为受支持的运行时依赖。在 Team 包 promotion 前,发布包不能暴露 Team,因此 CLI 和 Web 实验使用显式示例或实验性组合,而不是已发布的基础组合包。
|
||||
|
||||
孵化期间的产品职责分组不够直接,promotion 也会产生路径改动。保持 npm 名稳定后,改动范围仅包括仓库路径、元数据、文档和组合。
|
||||
@@ -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/implemented/bug-fix/2026-08-17-image-dimension-admission-limit.md
|
||||
2026-08-17-image-dimension-admission-limit.md: 027259c0949d142ce8d8af27e7daa2abd54769ab
|
||||
2026-08-17-image-dimension-admission-limit.zh.md: 3b66fe9a474f965653f94dacc7e0b8b0d0b9229a
|
||||
@@ -0,0 +1,30 @@
|
||||
# Agent Note: Per-side image dimension admission limit
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-17-image-dimension-admission-limit.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
`read_image` durably committed an image and appended its block to session history before any dimension check beyond byte count and total pixels. Deployed model routes reject a request with HTTP 400 when it carries many images and any of them has a side above 2000px. An admitted image rides every later request of its session, so one oversized read poisoned the durable history: the next model request failed, and so did every retry, permanently killing the session. The same gap applied to every other image producer (host uploads, MCP tool images) because admission had no per-side bound at all.
|
||||
|
||||
## Decision
|
||||
|
||||
`ImageAttachmentLimits` carries `maxImageDimension`, enforced during the admission full decode (`detectImage`) as `IMAGE_DIMENSION_TOO_LARGE`, so every producer that commits through the attachment service refuses an oversized image before anything reaches durable history. `LocalAttachmentStore` exposes it as the `maxImageDimension` config field with default `DEFAULT_MAX_IMAGE_DIMENSION = 2000`, the strictest per-side bound deployed routes enforce; deployments with laxer routes raise it from cordis.yml. `read_image` maps `IMAGE_DIMENSION_TOO_LARGE` and `IMAGE_TOO_MANY_PIXELS` to model-facing errors that name the resolved path and the limit and tell the model to downscale and retry — the turn continues as a recoverable tool error. The Web composer surfaces `IMAGE_DIMENSION_TOO_LARGE` with dedicated copy naming the limit. The `read-image-dimension` snapshot scenario replays the refusal keylessly through the assembled app: a 2001x1 workspace fixture, a recoverable tool error, and a completed turn.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Downscale at admission instead of refusing.** Resampling changes the stored bytes away from what the caller supplied, adds a resampling-quality policy, and hides the limit from the model. Refusal keeps admission a pure gate; the model or user can downscale with full knowledge. Worth revisiting only if refusals prove frequent in practice.
|
||||
- **Enforce at the provider adapter per route.** Too late: by the time a request is assembled the image is already durable history, so every route and every retry re-fails. Admission is the last point where a provider-rejected image can be kept out.
|
||||
- **Repair already-poisoned sessions** (drop or replace the oversized block on later requests). Out of scope for this fix; admission prevents new poisonings, and history rewriting needs its own design against the model-visible ⟺ logged invariant.
|
||||
|
||||
## Related
|
||||
|
||||
- [Minimal read_image tool](../feature/2026-08-10-minimal-read-image-tool.md) — the tool whose admission gap this closes.
|
||||
- [Web image intake and limits alignment](../feature/2026-08-12-web-image-intake-and-limits-alignment.md) — the composer-side surfacing of the same `ImageAttachmentLimits`.
|
||||
|
||||
## Consequences
|
||||
|
||||
- One oversized `read_image` can no longer break a session; the model sees an actionable error and the turn completes.
|
||||
- Images with a side above 2000px are refused even in compositions whose routes would accept them on small requests; such deployments must raise `maxImageDimension` explicitly.
|
||||
- Sessions that already carry an oversized image remain broken; this change does not repair existing history.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Agent Note: 图片单边尺寸准入上限
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-17-image-dimension-admission-limit.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
`read_image` 在字节数与总像素之外没有任何尺寸检查,就把图片持久提交并追加进会话历史。已部署的模型路由在请求携带多张图片且其中任何一张单边超过 2000px 时会以 HTTP 400 拒绝整个请求。已接纳的图片会随该会话之后的每次请求发送,因此一次超限读取就毒化了持久历史:下一次模型请求失败,之后的每次重试同样失败,会话被永久杀死。其他图片来源(宿主上传、MCP 工具图片)存在同样的缺口,因为准入完全没有单边上限。
|
||||
|
||||
## Decision
|
||||
|
||||
`ImageAttachmentLimits` 增加 `maxImageDimension`,在准入完整解码(`detectImage`)中以 `IMAGE_DIMENSION_TOO_LARGE` 强制执行,因此所有经附件服务提交的来源都会在任何内容进入持久历史之前拒绝超限图片。`LocalAttachmentStore` 将其暴露为 `maxImageDimension` 配置项,默认值 `DEFAULT_MAX_IMAGE_DIMENSION = 2000`,即已部署路由强制执行的最严格单边上限;路由更宽松的部署可在 cordis.yml 中调高。`read_image` 把 `IMAGE_DIMENSION_TOO_LARGE` 与 `IMAGE_TOO_MANY_PIXELS` 映射为面向模型的错误,指明解析后的路径与上限并提示缩图重试,本轮以可恢复的工具错误继续。Web 输入框对 `IMAGE_DIMENSION_TOO_LARGE` 给出指明上限的专用文案。`read-image-dimension` 快照场景通过组装后的应用无 key 回放这次拒绝:2001x1 的工作区 fixture、一条可恢复的工具错误、一个正常完成的轮次。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **准入时缩图而非拒绝。** 重采样会让存储字节偏离调用方提供的内容,引入重采样质量策略,还会对模型隐藏上限。拒绝让准入保持为纯粹的门禁;模型或用户可以在知情的前提下自行缩图。只有当拒绝在实践中频繁出现时才值得重新考虑。
|
||||
- **在 provider 适配器按路由强制执行。** 为时已晚:组装请求时图片已是持久历史,每条路由、每次重试都会再次失败。准入是把必然被上游拒绝的图片挡在外面的最后一道关口。
|
||||
- **修复已被毒化的会话**(在之后的请求中丢弃或替换超限图片块)。不在本次修复范围内;准入阻止新的毒化,而重写历史需要针对「模型可见 ⟺ 已记录」不变量单独设计。
|
||||
|
||||
## Related
|
||||
|
||||
- [最小 read_image 工具](../feature/2026-08-10-minimal-read-image-tool.md),本次修复补上的正是该工具的准入缺口。
|
||||
- [Web 图片摄入与限制对齐](../feature/2026-08-12-web-image-intake-and-limits-alignment.md),同一组 `ImageAttachmentLimits` 在输入框侧的呈现。
|
||||
|
||||
## Consequences
|
||||
|
||||
- 一次超限的 `read_image` 不再能弄坏会话;模型看到可操作的错误,轮次正常完成。
|
||||
- 单边超过 2000px 的图片即使在其路由本可接受(小请求)的组合中也会被拒绝;这类部署必须显式调高 `maxImageDimension`。
|
||||
- 已经携带超限图片的会话仍然是坏的;本次改动不修复既有历史。
|
||||
+6
@@ -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/implemented/bug-fix/2026-08-17-subagent-report-settlement-ordering.md
|
||||
2026-08-17-subagent-report-settlement-ordering.md: 30dfab5e96a7cea2ef6d4f03f480d17a86c5e775
|
||||
2026-08-17-subagent-report-settlement-ordering.zh.md: 658eb18e3a8cb40734136af32c6c62faef066a6e
|
||||
@@ -0,0 +1,44 @@
|
||||
# Agent Note: Subagent reports precede their settlement notices
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-17-subagent-report-settlement-ordering.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A continuable child can explicitly report selected content and later produce an unconditional manager-authored settlement notice. Report delivery used `Agent.followup()` and entered the parent's `next-turn` queue, while settlement delivery to a running parent used `Agent.steer()` and entered `next-step`. The first step of a turn claims the complete `next-step` batch before one `next-turn` message, so the later settlement notice could reach the model before the earlier report. The assembled report scenario required `reportDelivery: quiet` to avoid that nondeterministic interleaving. [Issue #2600](https://github.com/deepseek-harness/deepseek-harness/issues/2600) records the defect.
|
||||
|
||||
The report tool tells a child to report whenever a finding changes what its parent should do next. Deferring that message to a later turn contradicted the tool's scheduling meaning and separated causally ordered messages across queues with different claim priority.
|
||||
|
||||
## Decision
|
||||
|
||||
`SubagentReportDelivery` is `'quiet' | 'next-step'`, and `next-step` is the default. Next-step delivery calls `parent.steer()`, so a running parent reads the report at its nearest safe step boundary and an idle parent starts a turn. Quiet delivery continues to call `parent.inject()` and enters the same queue without waking an idle parent.
|
||||
|
||||
The continuation manager retains `sendWaking()` and `admitWaking()` around next-step reports delivered to resident continuable parents. Their purpose is waking-send admission accounting, independent of whether the message targets a step or a turn: the receiving Activation remains live between synchronous inbox insertion and the microtask that observes the wake.
|
||||
|
||||
### Ordering across parent states
|
||||
|
||||
A running parent receives an accepted report and the child's later settlement notice in the same `next-step` FIFO. If the parent becomes idle before settlement arrives, it has already claimed the report; settlement may then open a later turn without reversing the observed order.
|
||||
|
||||
During parent maintenance, the report occupies `next-step` and latches a wake, while settlement may occupy `next-turn` because maintenance reports idle status. The initial claim still takes next-step input before the queued turn. Waking input submitted after cancellation is redirected by `Agent.send()` to `next-turn`, so report and settlement follow the core agent's cancellation convergence rather than bypassing it.
|
||||
|
||||
### Verification
|
||||
|
||||
The report package holds a parent inside an active model request, submits a child report, settles that child, and asserts the pending parent batch is ordered `subagent-report`, then `subagent-settled`, with no queued later turn. Separate coverage pins repeated reports as one FIFO next-step batch, idle-parent wakeup, and waking admission accounting for a continuable parent.
|
||||
|
||||
The assembled ACP report scenario uses the shipped default. Its scheduling fence keeps the child behind the parent's delegation turn and holds the parent in maintenance until settlement follows the report. The report latches the wake while the settlement notice queues a turn; when maintenance ends, the parent claims next-step input before next-turn input and observes both notices in causal order without a quiet-delivery overlay.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep the `wakeup` name but change its implementation to `steer()`.** The existing public description defined `wakeup` as one later parent turn. Reusing the value for a different inbox target would leave configuration unable to state the behavior it selects. The pre-release configuration instead names `next-step` directly.
|
||||
|
||||
**Expose `quiet | next-step | next-turn`.** A next-turn report still permits a later next-step settlement notice to overtake it. Preserving report-before-settlement would require a cross-queue ordering barrier, and no current deployment requires next-turn isolation strongly enough to own that mechanism.
|
||||
|
||||
**Move settlement notices to `next-turn`.** Settlement batching deliberately uses the next-step queue so several children finishing together cost one parent step instead of one turn each. Moving settlement would increase latency and model work to retain a report scheduling mode with no current consumer.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A report may extend an open parent turn. It never interrupts the active model request or tool execution; the agent loop admits it only at a step boundary.
|
||||
- Reports accepted together share one next-step batch, preserving FIFO order and reducing the turn amplification of the former one-turn-per-report behavior.
|
||||
- The `wakeup` configuration value is rejected rather than retained as an alias. This repository has no external pre-release compatibility promise for Cordis configuration.
|
||||
- `quiet` remains the deployment escape for reports that must not wake a parked parent, with the existing risk that no model reads them until another waking input arrives.
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
# Agent Note: Subagent report 先于其结算通知
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-17-subagent-report-settlement-ordering.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
可继续 child 可以显式上报选中内容,之后还会产生一条由管理器撰写且无条件投递的结算通知。报告投递曾使用 `Agent.followup()` 并进入 parent 的 `next-turn` 队列,而面向运行中 parent 的结算投递使用 `Agent.steer()` 并进入 `next-step`。一个轮次的第一个 step 会先领取完整 `next-step` 批次,再领取一条 `next-turn` 消息,因此较晚的结算通知可能先于较早的报告到达模型。整体组装的报告场景必须使用 `reportDelivery: quiet`,才能避开这种不确定交错。[Issue #2600](https://github.com/deepseek-harness/deepseek-harness/issues/2600)记录了该缺陷。
|
||||
|
||||
report 工具要求 child 在发现会改变 parent 下一步动作的信息时上报。把这条消息推迟到后续轮次,既违背了工具的调度含义,也让具有因果顺序的消息分散到领取优先级不同的队列中。
|
||||
|
||||
## 决策
|
||||
|
||||
`SubagentReportDelivery` 为 `'quiet' | 'next-step'`,默认值为 `next-step`。Next-step 投递调用 `parent.steer()`,因此运行中的 parent 会在最近的安全 step 边界读取报告,空闲 parent 则会启动一个轮次。静默投递继续调用 `parent.inject()`,进入同一队列但不唤醒空闲 parent。
|
||||
|
||||
对于投递到驻留可继续 parent 的 next-step 报告,继续执行管理器会保留外围的 `sendWaking()` 与 `admitWaking()`。它们负责唤醒发送的准入记账,与消息面向 step 还是 turn 无关:接收方 Activation 在同步插入 inbox 与观察该唤醒的微任务之间保持在线。
|
||||
|
||||
### 不同 parent 状态下的顺序
|
||||
|
||||
运行中的 parent 会在同一个 `next-step` FIFO 中接收已接受的报告和该 child 稍后的结算通知。若 parent 在结算到达前变为空闲,它已经领取了报告;结算随后可以开启一个更晚的轮次,而不会反转观察顺序。
|
||||
|
||||
parent 处于 maintenance 时,报告占据 `next-step` 并锁存一次唤醒,而结算可能因为 maintenance 呈现空闲状态而占据 `next-turn`。首次领取仍会先取 next-step 输入,再取排队轮次。取消后提交的唤醒输入会由 `Agent.send()` 重定向到 `next-turn`,因此报告和结算会遵循核心 agent 的取消收敛,而不会绕过它。
|
||||
|
||||
### 验证
|
||||
|
||||
report 包把 parent 保持在一个活动模型请求中,提交 child 报告,再让该 child 结算,并断言等待中的 parent 批次按 `subagent-report`、`subagent-settled` 排序,且没有排队的后续轮次。独立覆盖还会固定重复报告形成一个 FIFO next-step 批次、空闲 parent 唤醒,以及可继续 parent 的唤醒准入记账。
|
||||
|
||||
整体组装的 ACP 报告场景使用随附默认值。调度围栏让 child 等到 parent 的委派轮次之后,并让 parent 保持 maintenance,直至结算跟在报告之后到达。报告会锁存唤醒,结算通知则排入后续轮次;maintenance 结束时,parent 先领取 next-step 输入、再领取 next-turn 输入,因此无需静默投递 overlay 也能按因果顺序观察两条通知。
|
||||
|
||||
## 备选方案
|
||||
|
||||
**保留 `wakeup` 名称,但把其实现改为 `steer()`。** 既有公开描述把 `wakeup` 定义为一个后续 parent 轮次。让该值复用于不同的 inbox 目标,会使配置无法准确说明自己选择的行为。预发布配置因此直接使用 `next-step` 名称。
|
||||
|
||||
**暴露 `quiet | next-step | next-turn`。** Next-turn 报告仍可能被稍后的 next-step 结算通知超越。要保住报告先于结算,需要跨队列顺序屏障;当前没有任何部署对 next-turn 隔离的需求强到足以承担该机制。
|
||||
|
||||
**把结算通知移到 `next-turn`。** 结算批处理刻意使用 next-step 队列,使多个一起结束的 child 只花费 parent 的一个 step,而不是各自一个轮次。移动结算会增加延迟和模型工作量,只为保留一个没有当前消费方的报告调度模式。
|
||||
|
||||
## 后果
|
||||
|
||||
- 报告可能延长已打开的 parent 轮次。它绝不会打断活动模型请求或工具执行;agent loop 只会在 step 边界准入它。
|
||||
- 一起接受的报告会共享一个 next-step 批次,保持 FIFO 顺序,并减少原先每份报告各占一个轮次所造成的轮次放大。
|
||||
- `wakeup` 配置值会被拒绝,而不是保留为别名。本仓库对预发布 Cordis 配置不作外部兼容承诺。
|
||||
- 对于不得唤醒停驻 parent 的报告,`quiet` 仍是部署退路,同时保留既有风险:在另一条唤醒输入到达之前,没有模型会读取这些报告。
|
||||
@@ -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/implemented/bug-fix/2026-08-18-request-image-payload-bound.md
|
||||
2026-08-18-request-image-payload-bound.md: df10df39d18c7da4660b566e8f0b6a5a60ff8dc1
|
||||
2026-08-18-request-image-payload-bound.zh.md: 070f2d194f1459f3f2728fdf9d5d2db2c3a24385
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: Request-level image payload bound
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-18-request-image-payload-bound.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Every image in session history is base64-inlined into every model request by the pi-ai adapter, so a long session's request body grows monotonically with each admitted image. Gateways cap request-body size; once the accumulated payload crossed such a cap the request was rejected with 413 (`Failed to buffer the request body: length limit exceeded`), and because nothing bounds or trims the assembled request, every retry resent the same oversized body. The session was permanently unusable, and the failure text matched no `classifyPiAiError` rule, so it surfaced as the generic `PI_AI_ERROR`. Admission bounds (per image, per message) cannot prevent this: each image is individually admissible, and the sum still grows without bound. Two screenshots were enough to trigger it in production.
|
||||
|
||||
## Decision
|
||||
|
||||
The pi-ai provider profile carries `maxRequestImageBytes` (default `DEFAULT_MAX_REQUEST_IMAGE_BYTES = 20MiB`, a positive integer, per route, changeable from cordis.yml and the `llm-pi-ai` settings section). At request conversion, `toPiContext` sums the base64 length of every image in history (derived from `ImageAttachmentRef.bytes` without reading data) and, while the sum exceeds the bound, replaces the oldest images with a fixed model-facing placeholder. The placeholder tells the model to read the file again when a path is available or ask the user to attach the image again. The most recent images are omitted last; an image larger than the bound is itself omitted. Offload locations use message and nested block indexes rather than object identity, so replaying the same JSON log produces the same request. Offloaded images are never read from the attachment store. `classifyPiAiError` classifies 413 and specific request-body-cap wording as `INVALID_REQUEST` (resending the same body cannot succeed). Four images admitted at the attachment store's 3.5MiB raw-image default occupy at most 18.67MiB after base64 expansion. The 20MiB request-image default therefore retains four such images and reserves the rest of a 32MiB request for system prompts, history, tools, and JSON. Deployments behind stricter gateways lower the value per route.
|
||||
|
||||
## Offload is conversion, not history
|
||||
|
||||
The placeholder is model-visible but not logged as a session event. It stays within the model-visible ⟺ logged invariant the same way the adapter's other serialization does (`(no output)` fallbacks, text-only folding): the offload locations are a pure function of the logged history and the route configuration, so the exact request remains reconstructable from the session log plus the composition. A logged elision event becomes necessary only when offload decisions gain non-deterministic inputs (for example live gateway feedback), which belongs to the deferred capability-metadata design.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Fail the request with a clear error instead of offloading.** Keeps the model informed but leaves the session wedged: the user cannot remove images from durable history, so a hard failure at the bound is permanent. Offload keeps the session serviceable, which is the point of the fix.
|
||||
- **Upload images once and reference them by URL / file id.** Removes the linear body growth entirely and is the right medium-term shape (providers and the internal gateway both document a Files path), but it introduces upload lifecycle management across providers and is far beyond a P0 hotfix.
|
||||
- **Count the full request body, not only images.** Text and tools contribute little and their sizes are only known after full serialization per protocol; bounding the dominant term with explicit headroom is accurate enough for the failure being fixed and much simpler. Revisit inside the route-capability design.
|
||||
- **Trim at admission instead.** Admission cannot see future accumulation; only the assembled request knows its total. Admission-side bounds (per-side dimension, bytes) remain as the first layer and are owned by [the dimension-limit note](2026-08-17-image-dimension-admission-limit.md).
|
||||
|
||||
## Related
|
||||
|
||||
- [Per-side image dimension admission limit](2026-08-17-image-dimension-admission-limit.md) — the admission-layer companion fix; together they close the two observed session-poisoning failures (400 dimension, 413 body size).
|
||||
|
||||
## Consequences
|
||||
|
||||
- An image-heavy long session keeps completing requests. The oldest images are omitted first; the most recent image is omitted only when it cannot fit within the bound.
|
||||
- Crossing the bound rewrites an early message, so the provider prompt-cache prefix ends at the newly offloaded image until the offloaded prefix stabilizes.
|
||||
- The bound counts base64 image payload only; deployments must keep it below their gateway's request-body cap with headroom, and the shipped default cannot know a private gateway's cap.
|
||||
- Route capability metadata driving admission and assembly together (image count, per-image size, request size, provider token formulas) remains deferred design work tracked outside this fix.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: 请求级图片载荷上限
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-18-request-image-payload-bound.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
pi-ai 适配器把会话历史中的每张图片 base64 内联进每一个模型请求,长会话的请求体随每张入库图片单调增长。网关对请求体大小设有上限;累积载荷一旦越线,请求被以 413 拒绝(`Failed to buffer the request body: length limit exceeded`),而组装层没有任何约束或裁剪,每次重试都会原样重发同一个超限请求体,会话永久不可用。该报错文本不匹配 `classifyPiAiError` 的任何规则,只能落进笼统的 `PI_AI_ERROR`。准入上限(单图、单消息)无法阻止这一点:每张图片单独看都合规,总和仍然无界增长。线上两张截图即可触发。
|
||||
|
||||
## Decision
|
||||
|
||||
pi-ai provider profile 增加 `maxRequestImageBytes`(默认 `DEFAULT_MAX_REQUEST_IMAGE_BYTES = 20MiB`,正整数,按路由生效,可从 cordis.yml 与 `llm-pi-ai` settings 段修改)。请求转换时,`toPiContext` 由 `ImageAttachmentRef.bytes` 推算每张历史图片的 base64 长度(无需读取数据)求和,总和超过上限时从最老的图片开始替换为一段固定的模型可见占位文本。占位文本要求模型在有路径时重新读取文件,否则请用户重新附上图片。越新的图片越晚被省略;单张图片本身超过上限时也会被省略。offload 位置用消息与嵌套块的索引表示,不依赖对象身份,因此重放同一份 JSON 日志会产生相同请求。被 offload 的图片不会从附件存储读取。`classifyPiAiError` 把 413 与明确的请求体上限措辞归类为 `INVALID_REQUEST`(原样重发不可能成功)。四张按附件存储默认上限准入的 3.5MiB 原始图片,经 base64 膨胀后最多占 18.67MiB。20MiB 请求图片默认上限因此可保留四张这样的图片,并在 32MiB 请求内为系统提示词、历史、工具与 JSON 保留其余容量。网关更严格的部署按路由调低该值。
|
||||
|
||||
## offload 是转换而非历史
|
||||
|
||||
占位文本模型可见,但不记录为会话事件。它与适配器的其他序列化(`(no output)` 回退、纯文本折叠)以同样的方式满足「模型可见 ⟺ 已记录」不变量:offload 位置是已记录历史与路由配置的纯函数,确切请求仍可由会话日志加组合配置重建。只有当 offload 决策引入非确定性输入(例如网关的实时反馈)时才需要记录省略事件,那属于暂缓的能力元数据设计。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **在上限处直接报错而不 offload。** 模型知情,但会话仍然卡死:用户无法从持久历史中删除图片,越线即永久失败。offload 让会话保持可用,这正是本修复的目标。
|
||||
- **图片上传一次、按 URL / file id 引用。** 从结构上消除请求体线性增长,是正确的中期形态(各提供方与内部网关都有 Files 路径),但要跨提供方管理上传生命周期,远超 P0 热修复范围。
|
||||
- **统计完整请求体而非只统计图片。** 文本与工具占比很小,且其大小要到按协议完整序列化后才可知;对主导项设上限并留出显式余量,对所修故障足够精确且简单得多。留到路由能力设计中再议。
|
||||
- **改在准入侧裁剪。** 准入看不到未来的累积,只有组装后的请求知道自己的总量。准入侧上限(单边尺寸、字节)作为第一层保留,归[尺寸上限笔记](2026-08-17-image-dimension-admission-limit.md)所有。
|
||||
|
||||
## Related
|
||||
|
||||
- [图片单边尺寸准入上限](2026-08-17-image-dimension-admission-limit.md),准入层的配套修复;两者合起来封住已观测到的两类会话毒化故障(400 尺寸、413 请求体)。
|
||||
|
||||
## Consequences
|
||||
|
||||
- 图片较多的长会话持续可用。最老的图片优先省略;仅当最新图片本身无法装进上限时才会省略它。
|
||||
- 越过上限会改写较早的一条消息,提供方 prompt cache 前缀在新被 offload 的图片处截止,直到被 offload 的前缀稳定。
|
||||
- 上限只统计 base64 图片载荷;部署必须让它低于自家网关的请求体上限并留出余量,发行默认值无法预知私有网关的上限。
|
||||
- 由路由能力元数据同时驱动准入与组装(图片数量、单图大小、请求大小、提供方 token 公式)的设计仍为暂缓工作,在本修复之外跟踪。
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# 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/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.md
|
||||
2026-07-22-web-multimodal-image-input-and-durable-attachments.md: 5162e0d6f63715ffd83cff1a622e07c110a7938e
|
||||
2026-07-22-web-multimodal-image-input-and-durable-attachments.zh.md: d3a5cf18a7d308753d5e46741cea2b2993536f70
|
||||
2026-07-22-web-multimodal-image-input-and-durable-attachments.md: 6a12380992c3205d6bb3f5701f093b3335fae2f9
|
||||
2026-07-22-web-multimodal-image-input-and-durable-attachments.zh.md: a8e94154e435a5359e7928d3b11eba7d1aee92f7
|
||||
|
||||
+2
-2
@@ -140,9 +140,9 @@ Composer thumbnails and each `MessageImage` own ephemeral original-preview state
|
||||
|
||||
### Limits and trust boundaries
|
||||
|
||||
Version one accepts PNG, JPEG, WebP, and GIF only. SVG and remote URLs are excluded. Default limits are 5 MiB per image, 20 images and 100 MiB aggregate image bytes per message, and 40 million intrinsic pixels per image. These deployment-varying limits are validated backend configuration and enforced by the host before persistence. The client connection carrier has an independent configurable `maxRequestBodyBytes` cap (160 MiB by default) for every API request and fails load if it cannot hold the attachment service's aggregate image limit after base64 and envelope expansion; lowering image policy therefore never silently lowers the carrier limit for valid text or other RPCs. A body without a declared length is rejected the moment it crosses the cap rather than drained to its end.
|
||||
Version one accepts PNG, JPEG, WebP, and GIF only. SVG and remote URLs are excluded. Default limits are 3.5 MiB per image, 20 images and 100 MiB aggregate image bytes per message, 40 million intrinsic pixels per image, and 2000 pixels on either side. These deployment-varying limits are validated backend configuration and enforced by the host before persistence. The client connection carrier has an independent configurable `maxRequestBodyBytes` cap (160 MiB by default) for every API request and fails load if it cannot hold the attachment service's aggregate image limit after base64 and envelope expansion; lowering image policy therefore never silently lowers the carrier limit for valid text or other RPCs. A body without a declared length is rejected the moment it crosses the cap rather than drained to its end.
|
||||
|
||||
Malformed base64, unsupported or mismatched media, truncated image payloads, excess bytes, excess image count, excess pixels, missing objects, and integrity mismatches return stable structured failures. Original filenames are reduced to a display basename, control characters are removed, and no local path is logged or returned to the browser.
|
||||
Malformed base64, unsupported or mismatched media, truncated image payloads, excess bytes, excess image count, excess pixels, excess per-side dimensions, missing objects, and integrity mismatches return stable structured failures. Original filenames are reduced to a display basename, control characters are removed, and no local path is logged or returned to the browser.
|
||||
|
||||
### Package and surface changes
|
||||
|
||||
|
||||
+2
-2
@@ -140,9 +140,9 @@ Pi-AI 适配器是首条视觉输入路径:它在请求时解析 `ctx.attachme
|
||||
|
||||
### 限制与信任边界
|
||||
|
||||
第一版仅接受 PNG、JPEG、WebP 和 GIF。不接受 SVG 和远程 URL。默认限制为每张图片 5 MiB、每条消息 20 张图片和 100 MiB 图片总字节数,以及每张图片 4,000 万个固有像素。这些随部署变化的限制属于经过校验的后端配置,并由宿主在持久化前强制执行。客户端连接载体为每个 API 请求设置独立且可配置的 `maxRequestBodyBytes` 上限(默认 160 MiB);如果该上限无法容纳附件服务的图片总量限制经 base64 和请求封装膨胀后的大小,加载就会失败。因此,降低图片策略绝不会静默降低有效文本或其他 RPC 的载体上限。未声明长度的请求体在越过上限的瞬间即被拒绝,而不是先读完再拒。
|
||||
第一版仅接受 PNG、JPEG、WebP 和 GIF。不接受 SVG 和远程 URL。默认限制为每张图片 3.5 MiB、每条消息 20 张图片和 100 MiB 图片总字节数、每张图片 4,000 万个固有像素,以及任一边 2,000 像素。这些随部署变化的限制属于经过校验的后端配置,并由宿主在持久化前强制执行。客户端连接载体为每个 API 请求设置独立且可配置的 `maxRequestBodyBytes` 上限(默认 160 MiB);如果该上限无法容纳附件服务的图片总量限制经 base64 和请求封装膨胀后的大小,加载就会失败。因此,降低图片策略绝不会静默降低有效文本或其他 RPC 的载体上限。未声明长度的请求体在越过上限的瞬间即被拒绝,而不是先读完再拒。
|
||||
|
||||
格式错误的 base64、不支持或不匹配的媒体、截断的图片数据、超出字节限制、超出图片数量、超出像素限制、对象缺失和完整性不匹配都会返回稳定的结构化错误。原始文件名只保留用于显示的末段,控制字符会被移除,并且任何本地路径都不会写入日志或返回浏览器。
|
||||
格式错误的 base64、不支持或不匹配的媒体、截断的图片数据、超出字节限制、超出图片数量、超出像素限制、超出单边尺寸限制、对象缺失和完整性不匹配都会返回稳定的结构化错误。原始文件名只保留用于显示的末段,控制字符会被移除,并且任何本地路径都不会写入日志或返回浏览器。
|
||||
|
||||
### 包与接口变更
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# 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/implemented/feature/2026-07-28-continuable-subagent-conversations.md
|
||||
2026-07-28-continuable-subagent-conversations.md: f00b3c8bf4da08363ca0b46ddda581811fbda214
|
||||
2026-07-28-continuable-subagent-conversations.zh.md: 6e1d279db8fec6cfb08371275a4a1fe97e9d7da0
|
||||
2026-07-28-continuable-subagent-conversations.md: f456bacbf775bf914b47051e19639811e2385f65
|
||||
2026-07-28-continuable-subagent-conversations.zh.md: b7f2080b157285e5928022b4ef9b9bf411c70191
|
||||
|
||||
@@ -157,7 +157,7 @@ It adds no host-user continuation, subagent steering operation, durable mailbox,
|
||||
|
||||
**Let the provider create, resume, or deliver through an Agent handle.** Initial providers own only `prepareContinuable()` and its detached creation-spec distinction: whether a child begins fresh or with a parent prefix. The manager must call `ctx.agents.create()` through its private activation-owner scope so that scope is a structural owner of every handle. A persisted in-process Session already contains the initial prefix and generic reconstruction descriptor, while delivery belongs to the Agent inbox. Giving providers any later handle, `SubagentRun`, or message ownership would retain provider ownership with no shipped behavior to justify it.
|
||||
|
||||
**Make report delivery part of the base lifecycle.** Repeatable child-to-parent reporting is compatible with this lifecycle, but quiet versus waking delivery, acknowledgement, durability, and retry behavior are independent product choices. The later report package remains optional and consumes an explicit child-setup hook, so continuable residency does not silently grant a return channel.
|
||||
**Make report delivery part of the base lifecycle.** Repeatable child-to-parent reporting is compatible with this lifecycle, but quiet versus next-step delivery, acknowledgement, durability, and retry behavior are independent product choices. The later report package remains optional and consumes an explicit child-setup hook, so continuable residency does not silently grant a return channel.
|
||||
|
||||
**Treat `SessionHeader.parentSession` as live ownership.** Durable lineage does not prove that the recorded parent currently owns the child. Membership in the live parent's `ownedChildren` records the process-local relationship without changing the durable parent id.
|
||||
|
||||
@@ -209,7 +209,7 @@ Retaining an Activation while descendants run consumes Agent resources proportio
|
||||
|
||||
The process-local inbox and ownership graph do not coordinate two harness processes. Deployments allowing concurrent access to one persistence store still require a durable lease and mailbox protocol.
|
||||
|
||||
Without the optional report package, completing a child turn neither sends its content to nor wakes the historical parent. With the package, only an explicit `report` call sends selected content; quiet delivery does not wake the parent, while waking delivery enqueues one later turn. In every case the detailed child output remains in its durable Session.
|
||||
Without the optional report package, completing a child turn neither sends its content to nor wakes the historical parent. With the package, only an explicit `report` call sends selected content; quiet delivery does not wake the parent, while next-step delivery wakes it and joins its nearest step boundary. In every case the detailed child output remains in its durable Session.
|
||||
|
||||
Queueing every continuation message means a parent cannot correct an in-progress child turn immediately; the correction runs as the next turn. A later UI steering action may reduce that latency without changing follow-up ordering.
|
||||
|
||||
|
||||
+2
-2
@@ -157,7 +157,7 @@ activation-owner 作用域之所以存在,是因为普通 Cordis owner effect
|
||||
|
||||
**让提供方通过 Agent handle 创建、恢复 child 或投递消息。** 初始提供方只持有 `prepareContinuable()` 及其分离式创建规格这一项差异:child 是全新启动,还是带有 parent 前缀。管理器必须通过私有 activation-owner 作用域自行调用 `ctx.agents.create()`,使该作用域成为每个 handle 的结构化所有者。持久化的进程内会话已经包含初始前缀及通用重建描述符,消息投递则属于 Agent inbox。让提供方持有任何后续 handle、`SubagentRun` 或消息所有权,会让提供方保留所有权,却没有已发布行为需要它。
|
||||
|
||||
**将报告投递纳入基础生命周期。** 可重复的 child 到 parent 报告与该生命周期兼容,但静默投递还是唤醒投递、确认、持久性和重试行为都是独立的产品决策。后续的 report 包保持可选,并消费一个显式的 child 设置钩子,因此可继续驻留不会默认授予返回通道。
|
||||
**将报告投递纳入基础生命周期。** 可重复的 child 到 parent 报告与该生命周期兼容,但静默投递还是 next-step 投递、确认、持久性和重试行为都是独立的产品决策。后续的 report 包保持可选,并消费一个显式的 child 设置钩子,因此可继续驻留不会默认授予返回通道。
|
||||
|
||||
**将 `SessionHeader.parentSession` 视为在线所有权。** 持久化谱系不能证明已记录的 parent 当前持有 child。在线 parent 的 `ownedChildren` 成员关系会记录进程内关系,而不改变持久化 parent id。
|
||||
|
||||
@@ -209,7 +209,7 @@ activation-owner 作用域之所以存在,是因为普通 Cordis owner effect
|
||||
|
||||
进程内 inbox 和所有权图无法协调两个 harness 进程。允许多个进程并发访问同一持久化存储的部署,仍需要持久化 lease 和邮箱协议。
|
||||
|
||||
未安装可选 report 包时,完成 child 轮次既不会把内容发送给历史 parent,也不会唤醒它。安装后,只有显式调用 `report` 才会发送选中内容;静默投递不唤醒 parent,唤醒投递则会排入一个后续轮次。无论如何,child 的详细输出都会保留在其持久化会话中。
|
||||
未安装可选 report 包时,完成 child 轮次既不会把内容发送给历史 parent,也不会唤醒它。安装后,只有显式调用 `report` 才会发送选中内容;静默投递不唤醒 parent,next-step 投递则会唤醒它并加入最近的 step 边界。无论如何,child 的详细输出都会保留在其持久化会话中。
|
||||
|
||||
将每条继续执行消息排队,意味着 parent 无法立即纠正正在进行的 child 轮次;纠正操作会在下一个轮次执行。后续 UI steering 操作可以缩短该延迟,而不改变 follow-up 排序。
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# 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/implemented/feature/2026-07-30-continuable-subagent-report-tool.md
|
||||
2026-07-30-continuable-subagent-report-tool.md: f120e65facc9c2549afd8aed589c6fc54921ef99
|
||||
2026-07-30-continuable-subagent-report-tool.zh.md: f35764cc82896f23ff50b07790078f125763f5c0
|
||||
2026-07-30-continuable-subagent-report-tool.md: 07d17f18f318a86070d9b8612512fa3c2a3815e2
|
||||
2026-07-30-continuable-subagent-report-tool.zh.md: 3d9947f2c12c6d2e34633113b67051894cac9f75
|
||||
|
||||
@@ -8,7 +8,7 @@ English | [中文](2026-07-30-continuable-subagent-report-tool.zh.md)
|
||||
|
||||
Continuable in-process subagents can receive later parent messages, retain descendants, settle, and cold-resume, but the base lifecycle gives them no way to send selected content back to their direct parent. Their complete output already remains reconstructable from the durable child Session, so the missing capability is explicit delivery rather than result storage.
|
||||
|
||||
Treating every final assistant message as an implicit result would conflate turn completion with reporting. A long-lived child may have nothing useful to report in one turn, may report progress several times in another, and must remain available after reporting. Recipient authority, quiet versus waking delivery, acknowledgement, durability, and retry behavior therefore need one explicit contract.
|
||||
Treating every final assistant message as an implicit result would conflate turn completion with reporting. A long-lived child may have nothing useful to report in one turn, may report progress several times in another, and must remain available after reporting. Recipient authority, quiet versus next-step delivery, acknowledgement, durability, and retry behavior therefore need one explicit contract.
|
||||
|
||||
## Decision
|
||||
|
||||
@@ -20,7 +20,7 @@ The feature is a collaboration control, not a result-bearing execution wrapper.
|
||||
|
||||
`report` accepts exactly `{ output: string }` and returns exactly `{ messageId: string }`. It accepts no child id, recipient id, or delivery mode. `exec.agent` binds the tool call to the reporting child, the service derives the sole recipient from durable `parentSession`, and deployment config owns scheduling.
|
||||
|
||||
`messageId` is the stable `MessageId` of the user-role message accepted by the parent. It is not an `InboxItemId`: quiet delivery creates no inbox occurrence, while waking delivery creates one occurrence for the same stable message. It is also not a read receipt, parent-log acknowledgement, turn-completion receipt, or persistence flush.
|
||||
`messageId` is the stable `MessageId` of the user-role message accepted into the parent's inbox. It is not a read receipt, parent-log acknowledgement, turn-completion receipt, or persistence flush.
|
||||
|
||||
The description states that reporting is required before finishing, repeatable, direct-parent-only, and non-terminal. It warns that a failed tool result may still follow an accepted send because a later `tools/post-execute` failure can replace the result. Without an idempotency key, stronger wording would encourage duplicate retries after ambiguous failure.
|
||||
|
||||
@@ -36,17 +36,17 @@ Nested reporting crosses exactly one edge. A grandchild reports to its direct ch
|
||||
|
||||
### Delivery policy
|
||||
|
||||
The package validates `reportDelivery: 'quiet' | 'wakeup'`; the default is `wakeup` ([why the default reversed](2026-08-06-continuable-child-report-obligation.md)).
|
||||
The package validates `reportDelivery: 'quiet' | 'next-step'`; the default is `next-step` ([ordering decision](../bug-fix/2026-08-17-subagent-report-settlement-ordering.md)).
|
||||
|
||||
Quiet delivery calls `parent.inject()`. It adds model-visible context without starting a parent model request: an idle parent appends before the call returns, while an admitting or running parent stages the report for the next safe log position. It creates no inbox occurrence and therefore no synthetic continuation-manager acceptance record.
|
||||
Quiet delivery calls `parent.inject()`. It adds model-visible next-step context without waking an idle parent; a running parent stages the report for the next safe log position.
|
||||
|
||||
Waking delivery calls `parent.followup()`. It creates one ordinary FIFO parent turn, wakes a parked parent driver, and never steers an open turn. When that parent is itself a continuable Activation, the send uses the manager's existing admission accounting so the parent cannot settle between synchronous enqueue and the admission microtask.
|
||||
Next-step delivery calls `parent.steer()`. It wakes a parked parent and joins a running parent's nearest step boundary. When that parent is itself a continuable Activation, the send uses the manager's existing admission accounting so the parent cannot settle between synchronous inbox insertion and the admission microtask. Reports share the next-step FIFO with a later settlement notice, preserving their accepted causal order.
|
||||
|
||||
Both modes frame one user-role message as `Background subagent <child-id> reported:` followed by the exact `output`. The durable message source is `{ kind: 'subagent-report', senderSessionId: child.id }`. Normal Agent ordering governs concurrent sends; the subagent layer creates no second queue.
|
||||
|
||||
### Acknowledgement and recovery
|
||||
|
||||
Success means the exact live parent synchronously accepted the message. An idle quiet injection is already appended at that boundary, while staged quiet context becomes reconstructable only when it reaches its normal log boundary. Waking delivery has an inbox occurrence whose id remains separate from the returned stable message id.
|
||||
Success means the exact live parent synchronously accepted the message. The context becomes reconstructable only when it reaches its normal log boundary; a next-step delivery has woken the parent, while quiet delivery may remain pending. The inbox message id remains separate from the returned stable message id.
|
||||
|
||||
The first version provides no durable mailbox, idempotency key, delivery receipt, retry protocol, or exactly-once claim. A process failure can leave the caller uncertain, and retry after an unknown outcome may duplicate a report. The durable child transcript remains the recovery source when the parent is unavailable.
|
||||
|
||||
@@ -62,7 +62,7 @@ This seam keeps the continuation manager unaware of tool names. The report packa
|
||||
|
||||
The ACP snapshot harness adds `waitForSubagentTurnEnd`, selecting the Nth harvested child by the same order as `session.N.jsonl`. It waits for a closed child turn containing a request header so a continuable child's earlier descriptor-seed turn cannot satisfy the boundary. This lets the assembled scenario wait for the child-side report without inventing a parent-visible signal.
|
||||
|
||||
The authored snapshot starts a continuable child, executes the real scope-local `report` tool, observes the one ordinary parent turn the default waking delivery creates, and then submits a later parent prompt that consumes the framed report. It declares child pins `1`, so the otherwise non-global `report` schema and the child's own prompt are checked against `tool-schemas.1.expected.json` and `system-prompt.1.expected.md` while the root keeps the class pins. The generated tool catalog separately mints a child scope to include the same scope-local schema.
|
||||
The authored snapshot starts a continuable child, executes the real scope-local `report` tool, and observes default next-step delivery before the manager's later settlement notice. A snapshot-only maintenance fence holds the parent until both messages are pending, proving next-step input is claimed before queued next-turn input when the parent resumes. It declares child pins `1`, so the otherwise non-global `report` schema and the child's own prompt are checked against `tool-schemas.1.expected.json` and `system-prompt.1.expected.md` while the root keeps the class pins. The generated tool catalog separately mints a child scope to include the same scope-local schema.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -76,7 +76,7 @@ Waking on every report creates unsolicited turns and can cascade through nested
|
||||
|
||||
### Let the child choose the delivery mode
|
||||
|
||||
Giving the model a mode argument grants it control over scheduler pressure and makes behavior deployment-dependent. The child chooses content and timing; deployment config chooses whether that content starts another Agent turn.
|
||||
Giving the model a mode argument grants it control over scheduler pressure and makes behavior deployment-dependent. The child chooses content and timing; deployment config chooses whether that content wakes the parent.
|
||||
|
||||
### Register a global tool
|
||||
|
||||
@@ -101,18 +101,18 @@ A post-creation revocation check can reject the Activation only after the Agent
|
||||
## Consequences
|
||||
|
||||
- A continuable in-process child exposes exactly one scope-local `report` schema only while the report package's contribution is installed; unrelated Agents never expose it.
|
||||
- The tool returns the parent message's stable `MessageId`. Quiet delivery has no `InboxItemId`; waking delivery has a separate inbox occurrence.
|
||||
- The tool returns the parent message's stable `MessageId`; its inbox occurrence is not a separate public identity.
|
||||
- Only the exact resident child may report, and only to the exact live direct parent derived from durable lineage. The service has no recipient parameter or offline fallback.
|
||||
- Waking delivery is the validated default: it creates exactly one later FIFO turn and never steers an open turn. Quiet delivery never starts a parent request.
|
||||
- Next-step delivery is the validated default: it wakes an idle parent or extends a running parent's turn at the nearest step boundary. Quiet delivery never wakes an idle parent.
|
||||
- Child cancellation or disposal after parent acceptance does not retract the report. Before acceptance, child disposal, drain, parent loss, or caller cancellation rejects the operation.
|
||||
- Fresh and resumed Activations compose current setup contributions before publication. Grants wait for the next Activation; revocation is immediate for resident children.
|
||||
- Unit coverage pins visibility, allow-list behavior, both delivery modes, stable message and sender identities, nested routing, invalid senders, absent parents, cancellation, drain, revocation races, and the absence of Jobs or implicit final reporting.
|
||||
- The keyless assembled snapshot proves the real child tool, the one waking parent turn, durable parent framing, and later parent consumption.
|
||||
- The keyless assembled snapshot proves the real child tool, default next-step ordering before settlement, and durable parent framing.
|
||||
|
||||
### Accepted risks
|
||||
|
||||
The acceptance boundary is weaker than durable end-to-end delivery. A crash can leave the result ambiguous, and retries may duplicate reports.
|
||||
|
||||
Waking delivery can amplify model work when nested children report frequently. Deployment ownership through `reportDelivery` bounds but does not remove that risk.
|
||||
Next-step delivery can amplify model work when nested children report frequently. Reports waiting together share one step, and deployment ownership through `reportDelivery` bounds but does not remove that risk.
|
||||
|
||||
Registry presence is the parent liveness signal. A host-owned parent whose `AgentHandle.dispose()` has started but has not yet unwound its scope can still accept and append a report that it will not act on in this process. Closing that gap requires an Agent-level disposal-start signal rather than subagent-layer inference.
|
||||
|
||||
+12
-12
@@ -8,7 +8,7 @@ Status: implemented
|
||||
|
||||
可继续的进程内 subagent 能够接收 parent 后续发来的消息、保留后代、结算并冷恢复,但基础生命周期无法让它们将选中内容发送给直接 parent。child 的完整输出已可从持久化会话中重建,因此缺失的能力是显式投递,而非结果存储。
|
||||
|
||||
如果将每条 assistant 最终消息都视为隐式结果,就会混淆轮次完成与报告。长期运行的 child 可能在某个轮次中无内容可报告,也可能在另一个轮次多次报告进展,而且报告后必须仍可继续工作。因此,接收方权限、静默投递与唤醒投递、确认、持久性和重试行为都需要一份显式约定。
|
||||
如果将每条 assistant 最终消息都视为隐式结果,就会混淆轮次完成与报告。长期运行的 child 可能在某个轮次中无内容可报告,也可能在另一个轮次多次报告进展,而且报告后必须仍可继续工作。因此,接收方权限、静默投递与 next-step 投递、确认、持久性和重试行为都需要一份显式约定。
|
||||
|
||||
## 决策
|
||||
|
||||
@@ -20,7 +20,7 @@ Status: implemented
|
||||
|
||||
`report` 只接受 `{ output: string }`,也只返回 `{ messageId: string }`。它不接受 child id、接收方 id 或投递模式。`exec.agent` 将工具调用绑定到发送报告的 child;服务从持久化 `parentSession` 中推导唯一接收方,调度则由部署配置决定。
|
||||
|
||||
`messageId` 是 parent 接受的用户角色消息所对应的稳定 `MessageId`。它不是 `InboxItemId`:静默投递不创建 inbox 条目实例,唤醒投递则会为同一条稳定消息创建一个条目实例。它也不是已读回执、parent 日志确认、轮次完成回执或持久化 flush。
|
||||
`messageId` 是已接受进入 parent inbox 的用户角色消息所对应的稳定 `MessageId`。它不是已读回执、parent 日志确认、轮次完成回执或持久化 flush。
|
||||
|
||||
工具描述会明确报告操作在结束前必须执行、可重复、仅限直接 parent 且不会结束轮次。它还会警告:发送被接受后,后续 `tools/post-execute` 失败可能替换工具结果,因此工具结果失败时内容仍可能已经送达。没有幂等键时,更强的表述会诱导调用方在结果不明确的失败后重复重试。
|
||||
|
||||
@@ -36,17 +36,17 @@ root、one-shot child、伪造对象、陈旧 Agent 和同 id 替换对象都以
|
||||
|
||||
### 投递策略
|
||||
|
||||
该包会校验 `reportDelivery: 'quiet' | 'wakeup'`,默认值为 `wakeup`(见[默认值反转的理由](2026-08-06-continuable-child-report-obligation.md))。
|
||||
该包会校验 `reportDelivery: 'quiet' | 'next-step'`,默认值为 `next-step`(见[顺序决策](../bug-fix/2026-08-17-subagent-report-settlement-ordering.md))。
|
||||
|
||||
静默投递调用 `parent.inject()`。它会添加模型可见上下文,但不启动 parent 模型请求:若 parent 空闲,则在调用返回前追加消息;若 parent 正在准入或运行,则暂存报告,留到下一个安全日志位置。该模式不创建 inbox 条目实例,因此也不会产生虚构的继续执行管理器接受记录。
|
||||
静默投递调用 `parent.inject()`。它会添加模型可见的 next-step 上下文,但不唤醒空闲 parent;运行中的 parent 会把报告暂存到下一个安全日志位置。
|
||||
|
||||
唤醒投递调用 `parent.followup()`。它会创建一个普通的 FIFO parent 轮次,唤醒已停驻的 parent driver,且绝不 steering(中途引导)已开始的轮次。当该 parent 本身也是可继续 Activation 时,发送会使用管理器现有的准入计数,防止 parent 在同步入队与准入微任务之间结算。
|
||||
Next-step 投递调用 `parent.steer()`。它会唤醒停驻的 parent,并加入运行中 parent 最近的 step 边界。当该 parent 本身也是可继续 Activation 时,发送会使用管理器现有的准入记账,防止 parent 在同步插入 inbox 与准入微任务之间结算。报告与稍后的结算通知共享 next-step FIFO,从而保持其被接受时的因果顺序。
|
||||
|
||||
两种模式都会将一条用户角色消息封装为 `Background subagent <child-id> reported:`,后面跟随完全原样的 `output`。持久化消息来源为 `{ kind: 'subagent-report', senderSessionId: child.id }`。并发发送的顺序由 Agent 的常规规则决定;subagent 层不会创建第二条队列。
|
||||
|
||||
### 确认与恢复
|
||||
|
||||
成功表示确切的在线 parent 已同步接受该消息。空闲 parent 在接受静默注入时已经完成追加,而暂存的静默上下文只有到达正常日志边界后才可重建。唤醒投递包含一个 inbox 条目实例,其 id 与返回的稳定消息 id 保持分离。
|
||||
成功表示确切的在线 parent 已同步接受该消息。上下文只有到达正常日志边界后才可重建;next-step 投递已经唤醒 parent,而静默投递可能继续等待。inbox 消息 id 不会成为另一个公开身份。
|
||||
|
||||
首个版本不提供持久化邮箱、幂等键、投递回执、重试协议或恰好一次保证。进程故障可能让调用方无法确定结果,在结果未知时重试则可能重复报告。parent 不可用时,持久化 child transcript(文本记录)仍是恢复来源。
|
||||
|
||||
@@ -62,7 +62,7 @@ subagent seam 新增 `registerContinuableSetup(contribution): () => void`,由
|
||||
|
||||
ACP(Agent Client Protocol)快照 harness 新增 `waitForSubagentTurnEnd`,按与 `session.N.jsonl` 相同的顺序选择第 N 个已收集 child。它会等待一个包含请求 header 的已闭合 child 轮次,以防可继续 child 早期播种描述符的轮次错误满足该边界。这样,整体组装的场景无需伪造 parent 可见信号,就能等待 child 侧报告。
|
||||
|
||||
手写快照会启动一个可继续 child,执行真实的作用域局部 `report` 工具,观察默认唤醒投递所产生的那一个普通 parent 轮次,然后提交一条后续 parent 提示词,使其消费封装后的报告。它声明 child pin `1`,因此本不属于全局的 `report` schema 与该 child 自身的提示词会分别与 `tool-schemas.1.expected.json` 和 `system-prompt.1.expected.md` 比对,root 则继续使用类别 pin。生成的工具目录会另外铸造一个 child 作用域,以收录同一个作用域局部 schema。
|
||||
手写快照会启动一个可继续 child,执行真实的作用域局部 `report` 工具,并观察默认 next-step 投递先于管理器稍后的结算通知。一个仅用于快照的 maintenance 围栏会保持 parent,直至两条消息都处于待领取状态,从而证明 parent 恢复时先领取 next-step 输入、再领取排队的 next-turn 输入。它声明 child pin `1`,因此本不属于全局的 `report` schema 与该 child 自身的提示词会分别与 `tool-schemas.1.expected.json` 和 `system-prompt.1.expected.md` 比对,root 则继续使用类别 pin。生成的工具目录会另外铸造一个 child 作用域,以收录同一个作用域局部 schema。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
@@ -76,7 +76,7 @@ ACP(Agent Client Protocol)快照 harness 新增 `waitForSubagentTurnEnd`,
|
||||
|
||||
### 允许 child 选择投递模式
|
||||
|
||||
向模型提供 mode 参数会赋予其控制调度器压力的能力,并使行为依赖部署。child 只决定内容和时机;该内容是否启动另一个 Agent 轮次,由部署配置决定。
|
||||
向模型提供 mode 参数会赋予其控制调度器压力的能力,并使行为依赖部署。child 只决定内容和时机;该内容是否唤醒 parent,由部署配置决定。
|
||||
|
||||
### 注册全局工具
|
||||
|
||||
@@ -101,18 +101,18 @@ ACP(Agent Client Protocol)快照 harness 新增 `waitForSubagentTurnEnd`,
|
||||
## 影响
|
||||
|
||||
- 只有安装 report 包贡献时,可继续进程内 child 才会恰好暴露一个作用域局部 `report` schema;无关 Agent 永远不会暴露该 schema。
|
||||
- 工具返回 parent 消息的稳定 `MessageId`。静默投递没有 `InboxItemId`;唤醒投递会产生一个单独的 inbox 条目实例。
|
||||
- 工具返回 parent 消息的稳定 `MessageId`;其 inbox 中的出现不会成为另一个公开身份。
|
||||
- 只有确切的驻留 child 才能报告,且只能报告给根据持久化谱系推导的确切在线直接 parent。服务不接受接收方参数,也不提供离线 fallback。
|
||||
- 唤醒投递是校验后的默认模式:它会恰好创建一个后续 FIFO 轮次,绝不 steering 已开始的轮次。静默投递则绝不会启动 parent 请求。
|
||||
- Next-step 投递是校验后的默认模式:它会唤醒空闲 parent,或在最近的 step 边界延长运行中 parent 的轮次。静默投递绝不会唤醒空闲 parent。
|
||||
- parent 接受后取消或 dispose child 不会撤回报告。接受前,child dispose、drain、parent 丢失或调用方取消都会拒绝操作。
|
||||
- 新建和恢复的 Activation 都会在发布前组合当前设置贡献。新授权等待下一个 Activation 才生效,而已驻留 child 的授权撤销立即生效。
|
||||
- 单元覆盖固定可见性、allow-list 行为、两种投递模式、稳定的消息与发送方身份、嵌套路由、无效发送方、缺失的 parent、取消、drain、撤销竞争,以及不存在 Task 或隐式最终报告。
|
||||
- 无密钥整体组装快照证明真实 child 工具、那一个被唤醒的 parent 轮次、持久化 parent 封装,以及 parent 后续消费。
|
||||
- 无密钥整体组装快照证明真实 child 工具、默认 next-step 顺序先于结算,以及持久化 parent 封装。
|
||||
|
||||
### 已接受的风险
|
||||
|
||||
该接受边界弱于持久化端到端投递。崩溃可能导致结果不明,重试则可能重复报告。
|
||||
|
||||
唤醒投递可能在嵌套 child 频繁报告时放大模型工作量。通过 `reportDelivery` 交由部署所有者控制,可以限制该风险,但无法完全消除。
|
||||
嵌套 child 频繁报告时,next-step 投递可能放大模型工作量。一起等待的报告会共享一个 step,通过 `reportDelivery` 交由部署所有者控制也会限制该风险,但无法完全消除。
|
||||
|
||||
注册表中的存在性就是 parent 在线信号。宿主拥有的 parent 如果已开始 `AgentHandle.dispose()` 但尚未完成其作用域清理,仍可能接受并追加一条本进程不会再处理的报告。要弥合这个缺口,需要 Agent 层面的 dispose 开始信号,不能由 subagent 层推断。
|
||||
|
||||
@@ -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/implemented/feature/2026-08-05-agent-teams.md
|
||||
2026-08-05-agent-teams.md: 43e92d4a9807e6a178436d96722b141b1bac45bb
|
||||
2026-08-05-agent-teams.zh.md: 72989623bb49334beae3b49ce2f6973781558c69
|
||||
@@ -0,0 +1,73 @@
|
||||
# Agent Note: Durable Agent Teams over continuable children
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-05-agent-teams.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The subagent seam supplies fresh/fork providers, durable child Sessions, FIFO follow-ups, and cold-resumable Activations. Its direct-parent controls do not provide peer communication, a stable named roster, or shared task ownership. A coordinator can create several workers, but workers cannot address one another, durable follow-up intent lives only in target inboxes, and no common compare-and-set board prevents stale assignment updates.
|
||||
|
||||
All same-process Agents also share one checkout. Filesystem edit tools can reject an observed stale version, but Bash, formatters, generators, and external writers bypass that fence. Treating a teammate name or task owner as a file lock would hide rather than solve this concurrency boundary.
|
||||
|
||||
The model-visible Team tools remain opt-in so the default tool catalog and simple-task behavior do not change. An explicitly requested Team must survive child Activation settlement and mailbox delivery races long enough for the Lead to aggregate the result before process teardown.
|
||||
|
||||
## 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`.
|
||||
|
||||
The implementation is split into `@deepseek-ai/dsh-team`, which owns `ctx.teams` and durable semantics, and `@deepseek-ai/dsh-tool-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 explicit delegation policy permits Team creation only when the user asks for Agent Teams or teammates. Both packages are private members of `packages/experimental/`; the [experimental package decision](../architecture/2026-08-18-experimental-agent-teams-packages.md) owns release exclusion, 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.
|
||||
|
||||
## 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 mailbox-dispatch transactions, 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.
|
||||
|
||||
## Mailbox and task transactions
|
||||
|
||||
Peer communication is a Lead-log mailbox. `team/message/queued` is appended and flushed before delivery. The target message carries the stable message id and sender identity in both durable source metadata and a short model-visible prefix. A target receipt is acknowledged with `team/message/delivered` only after its pending inbox item or recorded user message is flushed. Immediate admission is serialized per target in queued-log order, recovery retries queued-minus-delivered in the same order, and delivery folds live or persisted target inbox/history state before cold resume. Every current-version Team payload is runtime-validated before entering replay state. The Team runtime tracks dispatch and asynchronous acknowledgement work from synchronous admission until settlement; disposal closes admission and awaits both before removing the service. Current waiters wake only after the owning Team event flush succeeds.
|
||||
|
||||
Quiet `send_message` injects, flushes, and acknowledges immediately for a live target without waking it; an inactive target remains queued until another event materializes that teammate. Waking `followup_task` becomes the target's next FIFO turn and may cold-resume it. Success means the message is already durable even when immediate delivery is deferred. The mechanism provides process-local retry and target-Session de-duplication, not a cross-process exactly-once claim.
|
||||
|
||||
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.
|
||||
|
||||
## Shared checkout boundary
|
||||
|
||||
All members use the same cwd and observe writes immediately. The policy tells members to partition tasks, record advisory write scopes, order dependent work, and let the Lead inspect the final diff and run tests. A filesystem stale-version rejection requires rereading and rebasing the intended change. No equivalent guarantee is claimed for Bash, formatters, code generation, or direct external writes.
|
||||
|
||||
Worktree isolation is not a harness runtime behavior. A deployment or prompt may arrange separate worktrees, but the Team domain does not infer branches, merge changes, or silently change cwd. This preserves the existing same-world subagent and sandbox contracts.
|
||||
|
||||
## 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.
|
||||
|
||||
**Store mail in each target Session before delivery.** Rejected because an inactive target is intentionally not materialized for quiet mail. The always-live Lead Session is the transaction home; target recording is the acknowledgement and de-duplication boundary.
|
||||
|
||||
**Treat task ownership or write scopes as locks.** Rejected because external writers bypass them, crashed owners remain durable, and path-prefix overlap cannot prove semantic independence. False mutual exclusion is more dangerous than an explicit warning.
|
||||
|
||||
**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. Explicit composition keeps model-visible ownership unambiguous without changing shipped requests.
|
||||
|
||||
**Use an in-memory board and mailbox.** Rejected because child settlement, HMR, and process interruption would lose accepted coordination state and make retries ambiguous.
|
||||
|
||||
**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.
|
||||
|
||||
## Testing
|
||||
|
||||
Package tests cover identity, name and authority checks, provider selection, reserved-id persistence collisions, child-before-Lead flush ordering, durable provisioning failure and pending-inbox JSONL/SQLite reconciliation, concurrent target-local ordering, pending/history de-duplication, mailbox limits, post-flush notification, bounded disposal with in-flight creation and dispatch cancellation, failed-member cleanup, task CAS and DAG validation, write-scope warnings, wait cancellation/timeout, inbox-preserving interruption, ordinary-fork isolation, legacy-control shadowing, compact declared-schema result rendering, and scoped registration HMR at per-file 100% coverage. A keyless headless Loader snapshot assembles the real Team plugins and records teammate creation, peer mail, dependent tasks, waiting, and Lead aggregation.
|
||||
|
||||
## Consequences
|
||||
|
||||
The Lead Session grows with whole task/member snapshots and mailbox acknowledgements. This favors independently inspectable recovery over compact deltas; configured task and pending-mail bounds cap active state, while 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 a wakeup can incur cold-resume latency. A quiet message for an inactive target can remain pending indefinitely until the target is otherwise materialized. A failed member permanently consumes its name and member slot, making provisioning failures visible instead of silently recycling identity.
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,73 @@
|
||||
# Agent Note: Durable Agent Teams over continuable children
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-05-agent-teams.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
subagent seam 已提供 fresh/fork provider、持久 child Session、FIFO follow-up 与可冷恢复 Activation。它的直接 parent control 不提供 peer 通讯、稳定具名 roster 或共享任务 owner。coordinator 可以创建多个 worker,但 worker 无法互相寻址;持久 follow-up 意图只存在于 target inbox;也没有公共 compare-and-set 任务板来阻止陈旧 assignment 更新。
|
||||
|
||||
同进程 Agent 还共享一个 checkout。文件系统 edit 工具可以拒绝已观察到的陈旧版本,但 Bash、formatter、generator 与外部 writer 会绕过该屏障。把 teammate name 或 task owner 当作文件锁只会掩盖而不是解决该并发边界。
|
||||
|
||||
面向模型的 Team 工具保持显式启用,使默认工具目录与简单任务行为不变。显式请求的 Team 必须能跨越 child Activation settlement 与 mailbox 投递竞争,使 Lead 在进程 teardown 前汇总结果。
|
||||
|
||||
## 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` 被排除。
|
||||
|
||||
实现拆分为 `@deepseek-ai/dsh-team` 与 `@deepseek-ai/dsh-tool-team`:前者负责 `ctx.teams` 和持久语义,后者负责 scoped schema 与模型指引。每个 Team 工具都声明完整的结果 schema,并把该值渲染为紧凑 JSON,因此编译器会检查每个 `execute` 是否符合对模型的承诺,也没有结果把 token 花在缩进上。部署显式挂载两个插件,并可禁用具有相同模型可见名称的旧 continuable control。显式 delegation 策略只允许在用户要求 Agent Teams 或 teammate 时创建 Team。 两个包都是 `packages/experimental/` 的私有成员;[实验性包决策](../architecture/2026-08-18-experimental-agent-teams-packages.md)负责发布排除、依赖隔离与 promotion。
|
||||
|
||||
Lead 必须等待所需工作后才能给出最终答案。进程 teardown 仍是最终生命周期 owner,并会 drain continuation Activation;Team task owner 是持久状态,不会因 idle、interrupt 或进程退出自动释放。
|
||||
|
||||
## Provisioning and recovery
|
||||
|
||||
创建操作先在 Lead Session 中追加并 flush `team/member` provisioning 快照,再通过选定 fresh 或 fork provider 启动预留的 continuable child。初始 inbox 获准前的失败会追加 failed 快照;成功会先 flush child 中已接受的 inbox 条目,再追加 active。恢复会在初始消息仍处于 pending 或已进入用户消息历史时识别它。名字由第一条 provisioning 记录永久保留,包括失败后也不能复用。dispose 会关闭准入,中止并等待已获准的创建与 mailbox dispatch 事务,再停止 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 契约一致。
|
||||
|
||||
## Mailbox and task transactions
|
||||
|
||||
Peer 通讯使用 Lead 日志 mailbox。投递前先追加并 flush `team/message/queued`。target message 会在持久 source metadata 与短模型可见前缀中同时携带稳定 message id 和 sender identity。只有 pending inbox 条目或已记录用户消息完成 flush,Lead 日志才写入 `team/message/delivered` acknowledgement。即时准入按 target 和 queued 日志顺序串行化,恢复按同一顺序重试 queued-minus-delivered,并在冷恢复前折叠 live 或 persisted target 的 inbox/历史状态。每个当前版本 Team payload 都会经过运行时验证后才进入 replay state。Team runtime 从同步准入到 settlement 全程跟踪 dispatch 与异步 acknowledgement 工作;dispose 会关闭准入,并在移除服务前等待两者。当前 waiter 只在所属 Team event flush 成功后被唤醒。
|
||||
|
||||
对于 live target,quiet `send_message` 会立即注入、flush 并确认,但不会唤醒它;inactive target 会保持 queued,直到其他事件 materialize 该 teammate。waking `followup_task` 成为 target 的下一个 FIFO turn,并可冷恢复。即使即时投递被推迟,成功也表示消息已经持久化。该机制提供进程内重试与 target Session 去重,不宣称跨进程 exactly-once。
|
||||
|
||||
共享 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。
|
||||
|
||||
## Shared checkout boundary
|
||||
|
||||
所有 member 使用相同 cwd,并立即观察写入。策略要求 member 切分任务、记录提示性 write scope、为有序工作添加依赖,并由 Lead 检查最终 diff 和运行测试。文件系统 stale-version 拒绝后必须重新读取并 rebase 修改意图。Bash、formatter、codegen 与直接外部写入不具备等价保证。
|
||||
|
||||
Worktree isolation 不是 harness runtime 行为。deployment 或 prompt 可以安排独立 worktree,但 Team 领域不会推断 branch、merge 变更或静默改变 cwd。这样保留既有 same-world subagent 与 sandbox 契约。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**用 peer id 扩展 direct-child subagent tool。** 拒绝,因为 parent/child 权限与 Team peer membership 是不同领域。向 continuation seam 增加 peer access 会削弱 exact-parent authorization,仍无法为 roster 与 task 提供持久 owner。
|
||||
|
||||
**投递前把 mail 存入每个 target Session。** 拒绝,因为 quiet mail 不会 materialize inactive target。始终 live 的 Lead Session 是事务 owner;target recording 是 acknowledgement 与去重边界。
|
||||
|
||||
**把 task ownership 或 write scope 当作锁。** 拒绝,因为外部 writer 会绕过它们,崩溃 owner 会持久保留,而路径前缀重叠不能证明语义独立。虚假的互斥保证比明确 warning 更危险。
|
||||
|
||||
**自动创建隔离 worktree。** 拒绝,因为 worktree 创建、branch 命名、merge 策略、ignored file、构建产物与 cleanup 都是 deployment 选择;它也会改变既有 subagent 与 sandbox 暴露的 same-world 行为。
|
||||
|
||||
**在默认工具目录中启用 Team。** 拒绝,因为 scoped Team control 会覆盖同名旧全局工具,主动 delegation 也会给简单任务增加延迟和 token 成本。显式组合可以保持面向模型的归属明确,同时不改变默认 request。
|
||||
|
||||
**使用内存 task board 与 mailbox。** 拒绝,因为 child settlement、HMR 与进程中断会丢失已接受协调状态,并让重试变得含糊。
|
||||
|
||||
**让 Team 工具返回未类型化 JSON。** 拒绝,因为未声明的结果类型会让 `execute` 在没有编译错误的情况下偏离对模型的承诺,也会引入在每份 roster、task 与回执上都消耗 token 的缩进。因此每个 Team 工具都声明完整的结果 schema,并由一个共享 helper 紧凑渲染。
|
||||
|
||||
## Testing
|
||||
|
||||
Package test 以逐文件 100% coverage 覆盖身份、名字与权限检查、provider 选择、预留 id 持久化冲突、child-before-Lead flush 顺序、持久 provisioning 失败与 pending-inbox JSONL/SQLite 对账、target-local 并发顺序、pending/history 去重、mailbox 限额、flush 后 notification、取消在途创建与 dispatch 的有界 dispose、failed member cleanup、task CAS 与 DAG 校验、write-scope warning、wait cancel/timeout、保留 inbox 的 interrupt、普通 fork 隔离、旧 control shadowing、声明 schema 的紧凑结果渲染与 scoped registration HMR。一条 keyless headless Loader 快照会组合真实 Team 插件,并记录 teammate 创建、peer mail、依赖任务、等待与 Lead 汇总。
|
||||
|
||||
## Consequences
|
||||
|
||||
Lead Session 会随着完整 task/member 快照与 mailbox acknowledgement 增长。该设计用可独立检查的恢复能力换取更紧凑的 delta;配置的 task 与 pending-mail 限额限制 active state,而 deleted 与 delivered 历史会保持 append-only,直到更广泛的 Session retention 生效。
|
||||
|
||||
active roster member 可以不驻留,因此 `inactive` 不表示失败,wakeup 可能产生 cold-resume 延迟。发往 inactive target 的 quiet message 可能无限等待,直到 target 因其他原因 materialize。failed member 会永久占用名字与 member slot,使 provisioning failure 保持可见而不是静默回收身份。
|
||||
|
||||
协调可以降低 checkout 冲突概率,但无法消除文件系统 CAS 工具之外的写入。最终 diff 与测试仍是 Lead 的集成边界。
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# 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/implemented/feature/2026-08-06-continuable-child-report-obligation.md
|
||||
2026-08-06-continuable-child-report-obligation.md: f152ec1b8c353f094f2ba70785112eb1e165c510
|
||||
2026-08-06-continuable-child-report-obligation.zh.md: 4ec17e4642ffac385e6ce5464f41a3f4b3bebdbf
|
||||
2026-08-06-continuable-child-report-obligation.md: e771e81831147dd02a6c32a543c8d8944c2ec2f4
|
||||
2026-08-06-continuable-child-report-obligation.zh.md: 7b4f8e7f5dead9f6b5803236e3593551c66da12d
|
||||
|
||||
@@ -17,7 +17,7 @@ The return channel is an instruction the child receives, not a capability it may
|
||||
- the `report` tool, whose description now states that the child calls it once before finishing with a self-contained final result, and earlier for progress that changes what the parent should do next;
|
||||
- a `tool:report` system-prompt section at order 117 carrying the same obligation in the child's own voice, so a child that never reads tool descriptions closely still receives it.
|
||||
|
||||
`reportDelivery` now defaults to `wakeup`. An accepted report creates exactly one ordinary later parent turn and wakes a parked parent driver; it still never steers an open turn. `quiet` remains available for deployments that prefer unread reports over turn amplification.
|
||||
`reportDelivery` defaults to `next-step`. An accepted report wakes a parked parent driver or joins a running parent's nearest step boundary, matching the instruction to report findings that change the parent's next action. `quiet` remains available for deployments that prefer unread reports over model-work amplification. The [report/settlement ordering decision](../bug-fix/2026-08-17-subagent-report-settlement-ordering.md) owns the scheduling rationale.
|
||||
|
||||
### Why the section and the description both exist
|
||||
|
||||
@@ -33,7 +33,7 @@ That boundary is deliberate: prompt text can only reach a child that is still ru
|
||||
|
||||
### Snapshot coverage
|
||||
|
||||
The assembled ACP `subagent-report` scenario now exercises the shipped default: the child reports, the parked parent takes one ordinary turn on that report, and a later prompt still reads the report back out of the durable log. Because the child's scope now composes a prompt the class pin cannot describe, the snapshot harness gained `pinsChildSystemPrompts`, the exact counterpart of the existing `pinsChildToolSchemas`: it moves one child fixture's prompt into `system-prompt.<n>.expected.md`, leaves every other request-header field to the class pin, requires the sidecar exactly when declared, and rejects a sidecar identical to that class pin so a redundant copy cannot drift.
|
||||
The assembled ACP `subagent-report` scenario exercises the shipped default: the child reports while the parent is in maintenance, the later settlement notice queues behind it, and the resumed parent claims the next-step report before next-turn settlement. Because the child's scope composes a prompt the class pin cannot describe, the snapshot harness has `pinsChildSystemPrompts`, the exact counterpart of `pinsChildToolSchemas`: it moves one child fixture's prompt into `system-prompt.<n>.expected.md`, leaves every other request-header field to the class pin, requires the sidecar exactly when declared, and rejects a sidecar identical to that class pin so a redundant copy cannot drift.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -48,13 +48,13 @@ The assembled ACP `subagent-report` scenario now exercises the shipped default:
|
||||
## Consequences
|
||||
|
||||
- Every continuable in-process child with this package loaded carries one extra prompt section and a longer `report` description in every request; no other Agent's request changes.
|
||||
- The default deployment wakes the parent once per accepted report. A nested tree that reports frequently consumes extra parent turns; `quiet` is the documented escape.
|
||||
- The default deployment wakes the parent once per accepted report. A nested tree that reports frequently consumes extra parent requests, while reports waiting together share one step; `quiet` is the documented escape.
|
||||
- `installReportTool` requires `ctx.systemPrompt` in the child scope, so the package declares `systemPrompt` in `inject` and fails at load rather than at the next child materialization.
|
||||
- Unit coverage pins the new default, two load-bearing instruction phrases, the section's child-only scope against both the parent and a sibling, and rollback or revocation of both registrations.
|
||||
- Three assembled ACP scenarios with continuable children pin the complete instruction text through the new sidecar; a future change to any child-scoped section fails those scenarios instead of passing silently.
|
||||
|
||||
### Accepted risks
|
||||
|
||||
Waking by default amplifies model work in deep trees. The deployment owns that through `reportDelivery`, and the amplification is bounded by one turn per accepted report.
|
||||
Next-step delivery by default amplifies model work in deep trees. The deployment owns that through `reportDelivery`; reports waiting together share one step, and one accepted report causes at most one wake.
|
||||
|
||||
A child can still finish without reporting, and this change cannot detect it. Only the runtime's own [settlement account](2026-08-06-manager-owned-subagent-settlement-delivery.md) closes that case.
|
||||
|
||||
+4
-4
@@ -17,7 +17,7 @@ Status: implemented
|
||||
- `report` 工具,其描述现在说明 child 要在结束前调用一次并给出自足的最终结果,并在部分进展会改变 parent 下一步动作时提前调用;
|
||||
- 一个 order 为 117 的 `tool:report` 系统提示词 section,用 child 自己的语气承载同一条义务,使从不细读工具描述的 child 仍能收到它。
|
||||
|
||||
`reportDelivery` 的默认值现在是 `wakeup`。一条被接受的报告恰好创建一个普通的后续 parent 轮次并唤醒停驻的 parent 驱动;它仍然绝不 steering(中途引导)已开始的轮次。对于宁可让报告无人阅读也要避免轮次放大的部署,`quiet` 依旧可用。
|
||||
`reportDelivery` 的默认值为 `next-step`。一条被接受的报告会唤醒停驻的 parent driver,或加入运行中 parent 最近的 step 边界,与发现会改变 parent 下一步动作时上报的指令一致。对于宁可让报告无人阅读也要避免模型工作量放大的部署,`quiet` 依旧可用。[报告与结算顺序决策](../bug-fix/2026-08-17-subagent-report-settlement-ordering.md)负责调度理由。
|
||||
|
||||
### 为什么 section 与描述同时存在
|
||||
|
||||
@@ -33,7 +33,7 @@ Status: implemented
|
||||
|
||||
### 快照覆盖
|
||||
|
||||
整体组装的 ACP `subagent-report` 场景现在演练随附的默认行为:child 上报,停驻的 parent 就该报告执行一个普通轮次,随后的提示词仍能从持久化日志中把报告读回来。由于该 child 的作用域现在组合出类别 pin 无法描述的提示词,快照 harness 新增了 `pinsChildSystemPrompts`,它与既有 `pinsChildToolSchemas` 完全对称:把一个 child fixture 的提示词移入 `system-prompt.<n>.expected.md`,其余请求 header 字段仍归类别 pin 所有,要求 sidecar 恰好在声明时存在,并拒绝与该类别 pin 完全相同的 sidecar,使冗余副本无法悄悄漂移。
|
||||
整体组装的 ACP `subagent-report` 场景演练随附的默认行为:child 在 parent 处于 maintenance 时上报,稍后的结算通知排在其后,而恢复的 parent 会先领取 next-step 报告、再领取 next-turn 结算。由于该 child 的作用域组合出类别 pin 无法描述的提示词,快照 harness 提供 `pinsChildSystemPrompts`,它与 `pinsChildToolSchemas` 完全对称:把一个 child fixture 的提示词移入 `system-prompt.<n>.expected.md`,其余请求 header 字段仍归类别 pin 所有,要求 sidecar 恰好在声明时存在,并拒绝与该类别 pin 完全相同的 sidecar,使冗余副本无法悄悄漂移。
|
||||
|
||||
## 备选方案
|
||||
|
||||
@@ -48,13 +48,13 @@ Status: implemented
|
||||
## 后果
|
||||
|
||||
- 加载本包后,每个可继续进程内 child 的每次请求都会多出一个提示词 section 和一段更长的 `report` 描述;其他任何 Agent 的请求都不变。
|
||||
- 默认部署会为每条被接受的报告唤醒 parent 一次。频繁上报的嵌套树会消耗额外的 parent 轮次;`quiet` 是有文档记载的退路。
|
||||
- 默认部署会为每条被接受的报告唤醒 parent 一次。频繁上报的嵌套树会消耗额外的 parent 请求,而一起等待的报告会共享一个 step;`quiet` 是有文档记载的退路。
|
||||
- `installReportTool` 需要 child 作用域中的 `ctx.systemPrompt`,因此本包在 `inject` 中声明 `systemPrompt`,从而在加载时失败,而不是等到下一次 child 物化时。
|
||||
- 单元覆盖固定了新默认值、两处关键指令措辞、该 section 相对 parent 与同级均仅限 child 的作用域,以及两项注册在安装回滚或撤销时的清理。
|
||||
- 三个带可继续 child 的整体组装 ACP 场景通过新的 sidecar 逐字固定完整的 child 提示词;今后任何对 child 作用域 section 的改动都会让这些场景失败,而不是悄悄通过。
|
||||
|
||||
### 已接受的风险
|
||||
|
||||
默认唤醒会在深层树中放大模型工作量。部署通过 `reportDelivery` 掌握该取舍,且放大幅度以每条被接受报告一个轮次为界。
|
||||
默认 next-step 投递会在深层树中放大模型工作量。部署通过 `reportDelivery` 掌握该取舍;一起等待的报告会共享一个 step,且每条被接受的报告至多产生一次唤醒。
|
||||
|
||||
child 仍可能不上报就结束,本次改动无法检测这一点。只有运行时自己的[结算记账](2026-08-06-manager-owned-subagent-settlement-delivery.md)才能补上这一情形。
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# 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/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.md
|
||||
2026-08-06-manager-owned-subagent-settlement-delivery.md: b3e7d3794cd075d1ef7d20568b99457520bc1fde
|
||||
2026-08-06-manager-owned-subagent-settlement-delivery.zh.md: b4f1e8e120cf88ceb5d9a696fbb58d61ecff09d8
|
||||
2026-08-06-manager-owned-subagent-settlement-delivery.md: 27daa6d5150950efb50bf23dea945498651d2c09
|
||||
2026-08-06-manager-owned-subagent-settlement-delivery.zh.md: 19abce4fc9c151872d1e02e4ca2efa3953be065b
|
||||
|
||||
+3
-3
@@ -62,7 +62,7 @@ Three assembled ACP scenarios cover the notice: a child that never reports, a ch
|
||||
|
||||
A keyless headless Loader snapshot covers the user-visible path end to end. Its replay parent omits `run_in_background` to exercise the continuable background default, never calls `list_agents`, `send_message`, or Task tools, consumes the manager-authored `subagent-settled` notice, and produces its final answer. The child never calls `report`, so the transcript cannot pass through the cooperative report path. A test-only Loader fence holds the parent's post-spawn request until the real manager notice enters its inbox, removing platform scheduling from the transcript without synthesizing the notice.
|
||||
|
||||
`subagent-report` needed one more concession. With the shipped waking report default, that scenario has two independent parent wakes — the report and the settlement — and whether the second extends the first's turn or opens its own is a genuine coin flip that measured 50/50 across runs. No authored transcript can hold both orders. Its overlay therefore pins `reportDelivery: quiet`, leaving settlement as the only wake, and a snapshot-only pre-step fence holds the child until the parent's spawn turn ends so that wake opens one deterministic turn claiming both messages. The waking report default keeps its coverage in the report package's own tests.
|
||||
The `subagent-report` scenario uses the default next-step report delivery. A snapshot-only fence holds the child until the parent's spawn turn ends, then holds the parent in maintenance until settlement follows the report. The resumed parent claims the next-step report before the queued next-turn settlement. The [report/settlement ordering decision](../bug-fix/2026-08-17-subagent-report-settlement-ordering.md) owns this cross-state ordering.
|
||||
|
||||
The refusal and interruption wordings are pinned verbatim in unit tests rather than in a replayed transcript: producing them needs a rejecting policy plugin or a cancellation fenced at a step boundary, which the keyless assemblies do not otherwise carry, and the assembled scenarios already pin the notice pathway itself end to end.
|
||||
|
||||
@@ -87,7 +87,7 @@ The refusal and interruption wordings are pinned verbatim in unit tests rather t
|
||||
- `Activation` carries `parentSession` and `announced`. The first exists because the child handle is disposed before delivery; the second is what keeps a rolled-back materialization silent.
|
||||
- `foldConsumedWork()` replaces `dsh-session`'s `findLastMessageTurnEnd()` and moves to `dsh-agent`, which owns the inbox marker it reads; the one-shot in-process path folds the same answer and does not classify a cut-short one-shot child as `completed`.
|
||||
- Unit coverage pins the unconditional contract, each terminal reason, idle and busy scheduling, the batch, the maintenance regression, the pre-release ordering, a parent that is gone, and a rejected send that must not fail teardown.
|
||||
- Three ACP scenarios use an explicit settlement fence, and `subagent-report` has a config overlay that pins quiet report delivery.
|
||||
- Three ACP scenarios use an explicit settlement fence, and `subagent-report` pins the default report-before-settlement next-step order.
|
||||
- A keyless headless Loader snapshot pins background start → manager-authored settlement notice → final parent answer with no polling or child `report` call.
|
||||
|
||||
### Accepted risks
|
||||
@@ -100,4 +100,4 @@ Stop-reason attribution is a best effort over the log's existing splice vocabula
|
||||
|
||||
Turn amplification is real for deep or wide trees, and it is not configurable by design. The step-boundary batch bounds it for simultaneous settlement but not for children that settle apart.
|
||||
|
||||
Two independent waking sources cannot be ordered in an authored transcript. The assembled coverage pins each separately rather than their interleaving.
|
||||
Reports and their later settlement notices are ordered through the parent's next-step FIFO. Independent settlements from sibling children retain their actual delivery order rather than a synthetic sibling ordering.
|
||||
|
||||
+3
-3
@@ -62,7 +62,7 @@ Status: implemented
|
||||
|
||||
另有一个无密钥的 headless Loader 快照端到端覆盖用户可见路径。其重放父级省略 `run_in_background` 以覆盖可继续后台默认路径,从不调用 `list_agents`、`send_message` 或 Task 工具,消费管理器写入的 `subagent-settled` 通知,并给出最终答案。child 从不调用 `report`,因此该 transcript 不可能经由协作式上报路径通过。一个仅用于测试的 Loader 栅栏会把父级启动后的请求保持到真实管理器通知进入其 inbox 为止,从 transcript 中排除平台调度差异,但不会伪造该通知。
|
||||
|
||||
`subagent-report` 还需要多做一步让步。在随附的唤醒上报默认值下,该场景有两个互相独立的父级唤醒——上报与结算——而第二个究竟是延长第一个的轮次还是另开一个轮次,是一枚真正的硬币,多次运行实测约为五五开。任何手写 transcript 都无法同时容纳两种顺序。因此它的 overlay 固定 `reportDelivery: quiet`,使结算成为唯一唤醒;另一个仅用于快照的 pre-step 栅栏会把 child 保持到父级启动轮次结束,使这次唤醒开启一个确定轮次并同时认领两条消息。唤醒上报默认值的覆盖则保留在 report 包自身的测试中。
|
||||
`subagent-report` 场景使用默认 next-step 报告投递。一个仅用于快照的围栏会让 child 等到 parent 的派生轮次结束,随后让 parent 保持 maintenance,直至结算跟在报告之后到达。恢复的 parent 会先领取 next-step 报告、再领取排队的 next-turn 结算。[报告与结算顺序决策](../bug-fix/2026-08-17-subagent-report-settlement-ordering.md)负责说明这种跨状态顺序。
|
||||
|
||||
拒绝与中断两种措辞在单元测试中逐字钉死,而不进入重放 transcript:触发它们需要一个会拒绝的策略插件、或一次在 step 边界被栅栏卡住的取消,而无密钥组装本身并不携带这些;通知通路本身已由整体组装场景端到端钉住。
|
||||
|
||||
@@ -87,7 +87,7 @@ Status: implemented
|
||||
- `Activation` 携带 `parentSession` 与 `announced`。前者存在是因为 child handle 在投递前已被 dispose;后者让被回滚的物化保持静默。
|
||||
- `foldConsumedWork()` 取代 `dsh-session` 的 `findLastMessageTurnEnd()`,并迁移到 `dsh-agent`——它拥有该 fold 所读取的 inbox 标记;一次性 in-process 路径折叠同一个答案,不会把被中途切断的一次性 child 归类为 `completed`。
|
||||
- 单元覆盖固定了无条件约定、每种终止原因、空闲与繁忙两种调度、批量语义、维护期回归、释放前顺序、父级已消失,以及一次不得让拆卸失败的发送被拒。
|
||||
- 三个 ACP 场景使用显式的结算栅栏,`subagent-report` 带有固定静默上报投递的配置 overlay。
|
||||
- 三个 ACP 场景使用显式的结算围栏,`subagent-report` 固定默认的报告先于结算的 next-step 顺序。
|
||||
- 一个无密钥的 headless Loader 快照固定了「后台启动 → 管理器写入的结算通知 → 父级最终答案」路径,其中没有轮询,也没有 child `report` 调用。
|
||||
|
||||
### 已接受的风险
|
||||
@@ -100,4 +100,4 @@ Status: implemented
|
||||
|
||||
对于深或宽的树,轮次放大是真实存在的,而且按设计不可配置。step 边界的批量语义只能约束同时结算的情形,无法约束分散结算的 child。
|
||||
|
||||
两个互相独立的唤醒源无法在手写 transcript 中排序。整体组装覆盖分别固定它们,而不固定它们的交错。
|
||||
报告与其稍后的结算通知通过 parent 的 next-step FIFO 排序。来自同级 child 的独立结算保留其实际投递顺序,不会虚构同级间的顺序。
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# 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/implemented/feature/2026-08-11-background-first-continuable-delegation.md
|
||||
2026-08-11-background-first-continuable-delegation.md: 3eab4ba36d8e6b2a1984450149021450741f3b89
|
||||
2026-08-11-background-first-continuable-delegation.zh.md: b8f14e0c40c341fc91b2fd0dc8c3f00fc2e14b66
|
||||
2026-08-11-background-first-continuable-delegation.md: 59232ae8821ef4a093fd610ecbbb39690316ce6c
|
||||
2026-08-11-background-first-continuable-delegation.zh.md: 928d1aef31babe853cb48f6df114c79a188da345
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ The model-facing text divides responsibility by location:
|
||||
- the `run_in_background` parameter states the lifecycle-specific default and when to override it;
|
||||
- a `tool:<toolName>` system-prompt section tells the model to start independent delegations together, continue useful work while they run, and choose foreground only when the next action depends on the result. The section renders only when that tool remains visible in the assembly scope, so a child tool restriction removes the schema and its guidance together.
|
||||
|
||||
The [continuable child report obligation](2026-08-06-continuable-child-report-obligation.md) remains unchanged: the child prompt requires one self-contained final report and earlier reports for findings that change the parent's next action. Manager-owned settlement remains unconditional and does not inspect whether a report arrived. The two messages may repeat final content, but they retain distinct authors and purposes: `report` is the child's explicit handoff, while settlement records how the run ended and preserves terminal output when the child cannot cooperate. `reportDelivery` remains deployment scheduling policy with `wakeup` as its default.
|
||||
The [continuable child report obligation](2026-08-06-continuable-child-report-obligation.md) remains unchanged: the child prompt requires one self-contained final report and earlier reports for findings that change the parent's next action. Manager-owned settlement remains unconditional and does not inspect whether a report arrived. The two messages may repeat final content, but they retain distinct authors and purposes: `report` is the child's explicit handoff, while settlement records how the run ended and preserves terminal output when the child cannot cooperate. `reportDelivery` remains deployment scheduling policy with `next-step` as its default, preserving report-before-settlement order through the parent inbox.
|
||||
|
||||
The keyless headless `subagent-settlement` scenario omits `run_in_background`, receives the immediate child id, and reaches the final parent answer through the manager-authored settlement notice even though its fixture deliberately does not call `report`. Package tests separately pin explicit `false` as foreground, the parent scheduling text, and the child's mandatory-report prompt.
|
||||
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ child 作用域的 `report` 提示词要求发送自包含的最终报告,而[
|
||||
- `run_in_background` 参数说明具体生命周期的默认值以及何时覆盖;
|
||||
- `tool:<toolName>` 系统提示词 section 会告诉模型同时启动相互独立的委派、在它们运行时继续有用工作,并且仅当下一步动作依赖结果时选择前台。只有当该工具在组装作用域中仍可见时才会渲染这个 section,因此子级工具限制会同时移除 schema 与对应指引。
|
||||
|
||||
[可继续 child 上报义务](2026-08-06-continuable-child-report-obligation.md)保持不变:child 提示词要求发送一份自包含的最终报告,并在发现会改变 parent 下一步动作的信息时提前报告。由管理器负责的结算仍然无条件执行,不检查报告是否已经到达。这两条消息可能重复最终内容,但作者和用途不同:`report` 是 child 的显式交接,结算则记录本次运行如何结束,并在 child 无法配合时保留终止输出。`reportDelivery` 仍是部署调度策略,默认值仍为 `wakeup`。
|
||||
[可继续 child 上报义务](2026-08-06-continuable-child-report-obligation.md)保持不变:child 提示词要求发送一份自包含的最终报告,并在发现会改变 parent 下一步动作的信息时提前报告。由管理器负责的结算仍然无条件执行,不检查报告是否已经到达。这两条消息可能重复最终内容,但作者和用途不同:`report` 是 child 的显式交接,结算则记录本次运行如何结束,并在 child 无法配合时保留终止输出。`reportDelivery` 仍是部署调度策略,默认值为 `next-step`,通过 parent inbox 保持报告先于结算的顺序。
|
||||
|
||||
无密钥 headless `subagent-settlement` 场景省略 `run_in_background`,收到立即返回的 child id;尽管 fixture(测试前置数据)有意不调用 `report`,它仍通过管理器生成的结算通知到达 parent 最终答案。包测试另行固定了显式 `false` 的前台语义、parent 调度文本以及 child 的强制报告提示词。
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# 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/implemented/feature/2026-08-11-background-job-completion-wakes-an-idle-owner.md
|
||||
2026-08-11-background-job-completion-wakes-an-idle-owner.md: 5193fda633dac78f06e6eb5e7b97be6aaa0ea94c
|
||||
2026-08-11-background-job-completion-wakes-an-idle-owner.zh.md: fa338f06b2950ad9b1d88450ddac9f13a643cd15
|
||||
2026-08-11-background-job-completion-wakes-an-idle-owner.md: 15ff0fbdc173f6cadaa2f75e265effe61e512f15
|
||||
2026-08-11-background-job-completion-wakes-an-idle-owner.zh.md: b7b528ce9294fde39ca82c293edf099b8b9fddef
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ Injection is correct there. A cancelled turn is a user pressing stop, and reopen
|
||||
|
||||
The bound exists because this chain is self-exciting in a way subagent settlement is not. Settlement is bounded by how many children the model spawned; a woken turn can start the background job whose completion wakes it again, with nobody watching. `dsh run` needs no separate policy: its one user message is claimed in the first turn and never repeats, so the budget is spent monotonically and the process terminates.
|
||||
|
||||
`completionDelivery: quiet` restores the old lane for idle owners. It exists for deterministic transcripts, and mirrors the `reportDelivery` switch on `tool-subagent-report` in name, values, and default.
|
||||
`completionDelivery: quiet` restores the old lane for idle owners. It exists for deterministic transcripts; job completion independently retains `quiet | wakeup` because its bounded owner-turn policy differs from next-step subagent reports.
|
||||
|
||||
### Teardown claims the report
|
||||
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ Status: implemented
|
||||
|
||||
设界是因为这条链会自激,而 subagent 结算不会。结算受限于模型派生了多少子 agent;被唤醒的一轮却可能启动某个后台任务,而它的完成又会唤醒同一个所有者,且无人旁观。`dsh run` 不需要单独策略:它唯一的用户消息在第一轮就被领取且不会重复,因此预算单调消耗,进程必然终止。
|
||||
|
||||
`completionDelivery: quiet` 为空闲所有者恢复旧通道。它的存在是为了确定性 transcript,并在名称、取值与默认值上都对齐 `tool-subagent-report` 的 `reportDelivery` 开关。
|
||||
`completionDelivery: quiet` 为空闲所有者恢复旧通道。它的存在是为了确定性 transcript;后台任务完成会独立保留 `quiet | wakeup`,因为其有界的所有者轮次策略不同于 next-step subagent 报告。
|
||||
|
||||
### 销毁自行认领报告
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# 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/implemented/feature/2026-08-12-web-image-intake-and-limits-alignment.md
|
||||
2026-08-12-web-image-intake-and-limits-alignment.md: 2f8b99bb4850d9875dcba0a03ae8ad9f340d1506
|
||||
2026-08-12-web-image-intake-and-limits-alignment.zh.md: 62d5ebd54275ae8de0e0b9ba701ba34042dfcde7
|
||||
2026-08-12-web-image-intake-and-limits-alignment.md: 00cf7ea99d63e848c4b5839da1d97d94c9fb8464
|
||||
2026-08-12-web-image-intake-and-limits-alignment.zh.md: d88ba25a3f3e3be4a3655080a45123ada6cdebcd
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ The second alignment step for issue #2248, after the [attachment display note](2
|
||||
|
||||
**History thumbnails (DeepSeek Chat rules).** A message's lone image renders at 240px on its long edge with the displayed ratio clamped to [0.25, 4], cropped by `cover` with the anchor at the top of very tall images and the left of very wide ones, never upscaled; several images render as fixed 64px square tiles in one wrapping row (10px gap, user messages right-aligned). Consecutive assistant `image` blocks merge into one gallery so they tile instead of each opening a one-image row.
|
||||
|
||||
**Limits aligned and projected.** Defaults are 20 images / 5 MiB per image / 100 MiB aggregate (`attachment-local`), with the HTTP carrier cap raised to one shared `DEFAULT_MAX_REQUEST_BODY_BYTES = 160 MiB` (http-bridge, previously two independent 32 MiB literals) to satisfy the load-time capacity assertion (aggregate × 4/3 + headroom ≈ 134.3 MiB). Consumer products cluster at 10–20 attachments (ChatGPT 10, Gemini 10, Claude 20; DeepSeek Chat's 50 is the outlier), and a vision-model image costs roughly 1300–4800 tokens, so 50 images can fill a 200k context in one message. The 5 MiB per-image default admits images across Anthropic routes that impose either a 5 MiB or 10 MiB maximum; deployments using only routes with the larger limit can override it. A 512 MiB aggregate cannot pass this transport because base64-in-JSON would need a single JSON string past V8's ~512 MiB string ceiling. The limits reach clients as the `imageLimits` session projection — a constant-per-boot unit (`apply` returns the same state reference, so baselines alone carry it and no change frames exist) registered by **apiproxy**, not the attachment Service Definition: `dsh-llm` depends on `dsh-attachment` (`ImageBlock` → `ImageAttachmentRef`), so the seam package referencing `dsh-session-projection` (whose graph reaches `dsh-llm` through `dsh-session`) closes a project-reference cycle, and the per-message count/aggregate rules the value describes are the proxy's own admission checks anyway. The `SessionProjectionMap` merge rides the proxy's sessions wire-contract file, which every client program already includes through the carrier's type re-exports.
|
||||
**Limits aligned and projected.** Defaults are 20 images / 3.5 MiB per image / 100 MiB aggregate (`attachment-local`), with the HTTP carrier cap raised to one shared `DEFAULT_MAX_REQUEST_BODY_BYTES = 160 MiB` (http-bridge, previously two independent 32 MiB literals) to satisfy the load-time capacity assertion (aggregate × 4/3 + headroom ≈ 134.3 MiB). Consumer products cluster at 10–20 attachments (ChatGPT 10, Gemini 10, Claude 20; DeepSeek Chat's 50 is the outlier), and a vision-model image costs roughly 1300–4800 tokens, so 50 images can fill a 200k context in one message. Including base64 padding, a 3.5 MiB encoded file occupies at most 4.67 MiB and leaves 0.33 MiB below a 5 MiB route check. Deployments using only routes with larger limits can override it. A 512 MiB aggregate cannot pass this transport because base64-in-JSON would need a single JSON string past V8's ~512 MiB string ceiling. The limits reach clients as the `imageLimits` session projection — a constant-per-boot unit (`apply` returns the same state reference, so baselines alone carry it and no change frames exist) registered by **apiproxy**, not the attachment Service Definition: `dsh-llm` depends on `dsh-attachment` (`ImageBlock` → `ImageAttachmentRef`), so the seam package referencing `dsh-session-projection` (whose graph reaches `dsh-llm` through `dsh-session`) closes a project-reference cycle, and the per-message count/aggregate rules the value describes are the proxy's own admission checks anyway. The `SessionProjectionMap` merge rides the proxy's sessions wire-contract file, which every client program already includes through the carrier's type re-exports.
|
||||
|
||||
**Intake pre-check and error copy.** Both intake gestures converge on one `intakeImages` wrapper in InputBar that checks count, per-image bytes, and aggregate bytes against the projection before `addImages`: a violating batch is refused whole (DeepSeek Chat semantics) with an immediate banner naming the limit — no submit-time rollback theater. The host checks stay as the backstop for callers that bypass the composer. Banner copy follows one principle the user set: reasons a user can act on (model without vision, count, size, resolution, format — now a positive list of supported formats instead of echoing the rejected MIME type) get product sentences naming the way out; reasons they cannot act on (corrupt base64, lost references, read failures) fold into one send-failed sentence that keeps the reason code, because the product currently faces developers and a reportable code beats a dead end. Non-attachment error codes keep the raw message + code presentation.
|
||||
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ issue #2248 的第二步对齐,接在[附件展示 note](2026-08-11-web-attach
|
||||
|
||||
**历史缩略图(DeepSeek Chat 规则)。** 一条消息仅有的一张图长边 240px、展示比例钳制在 [0.25, 4],`cover` 裁切,特别高的图锚定顶部、特别宽的锚定左侧,从不放大;多张图渲染为固定 64px 方块,单个可换行的横排(10px 间距,用户消息右对齐)。assistant 连续的 `image` 块合并进同一个画廊,平铺而不是各占一行。
|
||||
|
||||
**上限对齐并投影。** 默认值为每条消息 20 张、单图 5 MiB、总量 100 MiB(`attachment-local`),HTTP 载体上限提为唯一共享的 `DEFAULT_MAX_REQUEST_BODY_BYTES = 160 MiB`(http-bridge,原先是两个独立的 32 MiB 字面量),以满足加载时的容量断言(总量 × 4/3 加余量 ≈ 134.3 MiB)。消费级产品集中在 10 到 20 个附件(ChatGPT 10、Gemini 10、Claude 20;DeepSeek Chat 的 50 是例外),且视觉模型一张图约 1300 到 4800 token,因此 50 张图可在一条消息中填满 200k 上下文。默认单图上限采用 5 MiB,可适用于分别采用 5 MiB 或 10 MiB 上限的 Anthropic 路由;仅使用较大上限路由的部署可以覆盖该值。512 MiB 总量无法通过当前传输,因为 base64 进 JSON 需要一个超过 V8 约 512 MiB 字符串上限的单个 JSON 字符串。限额以 `imageLimits` 会话投影到达客户端。它是每次启动恒定的单元(`apply` 返回同一状态引用,因此只靠基线携带、不存在变更帧),由 **apiproxy** 而非 attachment Service Definition 注册:`dsh-llm` 依赖 `dsh-attachment`(`ImageBlock` → `ImageAttachmentRef`),seam 包引用 `dsh-session-projection`(其图谱经 `dsh-session` 到达 `dsh-llm`)会闭合 project-reference 环,而该值描述的每消息数量与总量规则本来就是 proxy 自己的准入检查。`SessionProjectionMap` 合并放在 proxy 的 sessions 协议文件里,每个客户端程序都经载体的类型再导出包含它。
|
||||
**上限对齐并投影。** 默认值为每条消息 20 张、单图 3.5 MiB、总量 100 MiB(`attachment-local`),HTTP 载体上限提为唯一共享的 `DEFAULT_MAX_REQUEST_BODY_BYTES = 160 MiB`(http-bridge,原先是两个独立的 32 MiB 字面量),以满足加载时的容量断言(总量 × 4/3 加余量 ≈ 134.3 MiB)。消费级产品集中在 10 到 20 个附件(ChatGPT 10、Gemini 10、Claude 20;DeepSeek Chat 的 50 是例外),且视觉模型一张图约 1300 到 4800 token,因此 50 张图可在一条消息中填满 200k 上下文。3.5 MiB 编码文件包括 base64 填充在内最多占 4.67 MiB,在 5 MiB 路由检查下保留 0.33 MiB 余量。仅使用较大上限路由的部署可以覆盖该值。512 MiB 总量无法通过当前传输,因为 base64 进 JSON 需要一个超过 V8 约 512 MiB 字符串上限的单个 JSON 字符串。限额以 `imageLimits` 会话投影到达客户端。它是每次启动恒定的单元(`apply` 返回同一状态引用,因此只靠基线携带、不存在变更帧),由 **apiproxy** 而非 attachment Service Definition 注册:`dsh-llm` 依赖 `dsh-attachment`(`ImageBlock` → `ImageAttachmentRef`),seam 包引用 `dsh-session-projection`(其图谱经 `dsh-session` 到达 `dsh-llm`)会闭合 project-reference 环,而该值描述的每消息数量与总量规则本来就是 proxy 自己的准入检查。`SessionProjectionMap` 合并放在 proxy 的 sessions 协议文件里,每个客户端程序都经载体的类型再导出包含它。
|
||||
|
||||
**加入预检与错误文案。** 两种加入手势汇合到 InputBar 的一个 `intakeImages` 包装:在 `addImages` 之前按投影检查数量、单图字节与总字节,违规的一批整体拒收(DeepSeek Chat 语义)并立刻弹出点名上限的横幅——不再有提交时的回滚戏码。宿主检查保留,兜底绕过 composer 的调用方。横幅文案遵循用户定下的一条原则:用户能解决的原因(模型不支持视觉、数量、大小、分辨率、格式——格式改为正面列出支持列表而不是回显被拒的 MIME 类型)用点明出路的产品句子;用户无法解决的原因(base64 损坏、引用丢失、读取失败)折叠为一条保留原因码的发送失败句子,因为产品当前面向开发者,可上报的码好过死胡同。非附件错误码保留原文加错误码的展示。
|
||||
|
||||
|
||||
@@ -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/implemented/process/2026-08-17-readme-assets-on-cdn.md
|
||||
2026-08-17-readme-assets-on-cdn.md: 8918e2118123382553e1ea295351482030e0e640
|
||||
2026-08-17-readme-assets-on-cdn.zh.md: a1314068ff0bbebfef5bb0053f04aebe650f6101
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: README assets publish from a dedicated repository
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-17-readme-assets-on-cdn.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The public Chinese README embeds three community QR codes. Repository-relative images make each replacement depend on a source change and the separate public-repository publication flow, even though the image bytes do not change product code or documentation text.
|
||||
|
||||
The images need stable public URLs while their source bytes, publication credentials, cache behavior, and update history remain explicit and reviewable.
|
||||
|
||||
## Decision
|
||||
|
||||
The README references fixed URLs under `https://cdn.deepseek.com/harness/readme/`. The private [`deepseek-harness/readme-cdn-assets`](https://github.com/deepseek-harness/readme-cdn-assets) repository owns the three allowlisted PNG files, their tests, and their publication code. A push to its `master` branch runs `publish.yml`, which installs the pinned Huawei OBS SDK, tests `scripts/upload.mjs`, and publishes the images.
|
||||
|
||||
The uploader accepts only the three README filenames, verifies each source is a PNG file, and uploads it to `dp-cdn-deepseek/harness/readme/` with `Content-Type: image/png` and `Cache-Control: no-store`. It checks the OBS response status, reports the resulting public URL, and closes the client on both success and failure. Repository Actions Secrets supply `OBS_DSH_README_ACCESS_KEY_ID` and `OBS_DSH_README_SECRET_ACCESS_KEY`; the OBS identity needs write access only to that object prefix.
|
||||
|
||||
The assets repository provides the update history and rollback source. The public README keeps the same URLs across image replacements, so ordinary image updates do not require a product-repository change or a public-repository synchronization.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep repository-relative images on `master`.** This preserves GitHub as the only image host, but every operational QR-code replacement remains coupled to the code review and public-repository publication path.
|
||||
|
||||
**Keep a long-lived assets branch in the product repository.** A branch avoids product `master` changes, but it leaves image ownership, OBS credentials, and publication workflow attached to the product repository and its repository-wide automation. A dedicated repository gives that operational source one default branch and one narrow responsibility.
|
||||
|
||||
**Use content-addressed CDN object names.** Immutable objects avoid stale caches, but each image replacement must also change the README URL, which removes the independent update path this workflow exists to provide.
|
||||
|
||||
**Allow the uploader to publish arbitrary paths.** A generic uploader could serve future assets without code changes, but the same credentials could then overwrite unrelated CDN objects. The fixed allowlist keeps this publication job limited to the README images it owns.
|
||||
|
||||
## Consequences
|
||||
|
||||
Community QR codes can change through one assets-repository push while the public README remains unchanged. The product repository carries no OBS dependency or credential, uploads retain an auditable git source, and CDN responses carry `Cache-Control: no-store`.
|
||||
|
||||
The README depends on the public CDN and GitHub's image proxy, while publication depends on a second private repository and its two Actions Secrets. `no-store` gives up edge and browser caching for these small files.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: README 资产通过专用仓库发布
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-17-readme-assets-on-cdn.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
公开中文 README 嵌入了 3 张社区二维码。使用仓库相对路径时,每次替换都依赖源码变更以及独立的公开仓库发布流程,即使图片字节并未改变产品代码或文档文字。
|
||||
|
||||
这些图片需要稳定的公开 URL,同时必须明确并可评审地保存源文件字节、发布凭证、缓存行为和更新历史。
|
||||
|
||||
## 决策
|
||||
|
||||
README 引用 `https://cdn.deepseek.com/harness/readme/` 下的固定 URL。私有仓库 [`deepseek-harness/readme-cdn-assets`](https://github.com/deepseek-harness/readme-cdn-assets) 负责管理 3 张允许发布的 PNG 文件、相应测试和发布代码。向该仓库的 `master` 分支 push 会运行 `publish.yml`,安装固定版本的华为云 OBS SDK、测试 `scripts/upload.mjs` 并发布图片。
|
||||
|
||||
上传脚本只接受 3 个 README 图片文件名,验证每个源文件均为 PNG,并以 `Content-Type: image/png` 和 `Cache-Control: no-store` 上传到 `dp-cdn-deepseek/harness/readme/`。脚本检查 OBS 响应状态、报告对应公开 URL,并在成功或失败后关闭客户端。仓库级 GitHub Actions Secret 提供 `OBS_DSH_README_ACCESS_KEY_ID` 和 `OBS_DSH_README_SECRET_ACCESS_KEY`;OBS 身份只需拥有该对象前缀的写权限。
|
||||
|
||||
资产仓库提供更新记录和回滚真源。图片替换后,公开 README 继续使用相同 URL,因此常规图片更新无需修改产品仓库或同步公开仓库。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**继续在 `master` 上使用仓库相对图片。**这种做法只使用 GitHub 托管图片,但每次运营二维码替换仍与代码评审和公开仓库发布流程耦合。
|
||||
|
||||
**在产品仓库中保留长期资产分支。**资产分支可以避免修改产品 `master`,但图片所有权、OBS 凭证和发布工作流仍依附于产品仓库及其全仓自动化。专用仓库为这项运营资源提供单一默认分支和单一职责。
|
||||
|
||||
**使用内容寻址的 CDN 对象名。**不可变对象不会产生陈旧缓存,但每次替换图片还必须修改 README URL,无法提供此工作流所需的独立更新路径。
|
||||
|
||||
**允许上传脚本发布任意路径。**通用上传脚本可以在不改代码的情况下支持未来资产,但同一组凭证也能覆盖无关 CDN 对象。固定允许列表将发布任务限制在它负责的 README 图片内。
|
||||
|
||||
## 后果
|
||||
|
||||
社区二维码可以通过一次资产仓库 push 更新,公开 README 无需改变。产品仓库不携带 OBS 依赖或凭证;上传内容保留可审计的 git 真源;CDN 响应携带 `Cache-Control: no-store`。
|
||||
|
||||
README 依赖公开 CDN 和 GitHub 图片代理,发布流程则依赖另一个私有仓库及其 2 个 GitHub Actions Secret。`no-store` 为这些小文件放弃边缘节点和浏览器缓存。
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
# Agent Note: Remove the empty experimental package group
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-11-remove-empty-experimental-package-group.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The package hierarchy reserves `packages/experimental/` for prototypes and internal-only plugins, but no package has used the group. The empty group adds placement, dependency, promotion, and release rules without a current package or release mechanism that needs them.
|
||||
|
||||
The original group aimed to let the team share prototypes against the real plugin graph without implying product support. That need remains possible, but it does not justify a permanent repository category before a concrete package exists.
|
||||
|
||||
## Decision
|
||||
|
||||
The package hierarchy has no reserved experimental or internal-only group. Packages continue to live in groups selected for their current product role.
|
||||
|
||||
A concrete package that needs different release, stability, or dependency treatment requires a decision based on its actual consumers and release mechanism. That decision may reintroduce a dedicated group when it can also define and enforce the exclusion rules.
|
||||
|
||||
This note consolidates and supersedes the experimental-package-group decision, whose active triplet is removed with the empty directory.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep the empty group.** It provides an obvious future incubation location, but it also keeps repository rules with no current owner, package, or enforcement mechanism.
|
||||
|
||||
**Move the experimental rules into the general package instructions.** This preserves the policy without an empty directory, but makes every package change carry rules for a hypothetical package class.
|
||||
|
||||
**Put concrete experimental packages in product-role groups with README labels.** This preserves product-role colocation, but labels alone cannot enforce release and runtime-dependency rules. A future package can evaluate this option against its actual release mechanism.
|
||||
|
||||
**Treat every package as experimental until the first tagged release.** This applies a broad temporary status without providing durable treatment for packages that remain experimental after releases begin.
|
||||
|
||||
**Require prototypes to stay outside the repository.** This would lose access to the real plugin graph, examples, snapshots, and lifecycle checks. Removing the reserved group does not impose that restriction; a concrete prototype can establish the placement it needs.
|
||||
|
||||
## Consequences
|
||||
|
||||
The hierarchy loses an unused group and its special release and dependency policy. It also gives up a predeclared location for team discovery and a ready-made promotion path.
|
||||
|
||||
The first package that needs experimental or internal-only treatment must define where it lives, how releases exclude it, which runtime dependencies are allowed, and what condition promotes or removes it. A dedicated group can return when those rules have a current consumer and enforceable mechanism.
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
# Agent Note: 移除空的实验性包分组
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-11-remove-empty-experimental-package-group.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
包层级结构预留 `packages/experimental/` 用于原型和内部专用插件,但从未有包使用该分组。这个空分组添加了放置、依赖、提升和发布规则,却没有需要这些规则的现有包或发布机制。
|
||||
|
||||
原分组旨在让团队基于真实插件图共享原型,同时不暗示产品会提供支持。这项需求将来可能出现,但在具体包出现前,不足以支持一个永久的仓库类别。
|
||||
|
||||
## 决策
|
||||
|
||||
包层级结构不再预留实验性或内部专用分组。包继续按照当前产品职责放入对应分组。
|
||||
|
||||
如果具体包需要不同的发布、稳定性或依赖处理,必须根据其实际消费方和发布机制做出决策。只要该决策同时定义并强制执行排除规则,就可以重新引入专用分组。
|
||||
|
||||
本 Agent Note 整合并取代实验性包分组决策;该旧决策的活跃三文件组随空目录一并移除。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**保留空分组。** 它为未来孵化工作提供明确位置,但也会保留没有当前负责人、包或强制执行机制的仓库规则。
|
||||
|
||||
**将实验性规则移入通用包指令。** 这可以在不保留空目录的情况下延续政策,但会让每次包变更都携带针对假设包类别的规则。
|
||||
|
||||
**将具体实验性包放入产品职责分组,并用 README 标注。** 这会保持产品职责共置,但仅靠标注无法强制执行发布和运行时依赖规则。未来的包可以根据实际发布机制评估此选项。
|
||||
|
||||
**在首个带标签的版本发布前,将每个包都视为实验性。** 这会施加宽泛的临时状态,却无法为发布开始后仍处于实验状态的包提供持久处理方式。
|
||||
|
||||
**要求原型留在仓库外。** 这会失去真实插件图、示例、快照和生命周期检查。移除预留分组并不施加这项限制;具体原型可以建立自身所需的放置规则。
|
||||
|
||||
## 后果
|
||||
|
||||
包层级结构移除了未使用的分组及其特殊发布和依赖政策,同时也放弃了预先声明的团队发现位置和现成的提升路径。
|
||||
|
||||
第一个需要实验性或内部专用处理的包必须定义其存放位置、发布版本如何排除它、允许哪些运行时依赖,以及包在何种条件下获得提升或被移除。当这些规则具有当前消费方和可强制执行的机制时,可以恢复专用分组。
|
||||
+6
@@ -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/implemented/simplification/2026-08-12-trim-agent-teams-read-and-lifecycle-surface.md
|
||||
2026-08-12-trim-agent-teams-read-and-lifecycle-surface.md: 736ed2587c8b2ffd9d824efa14dd0d249c43f669
|
||||
2026-08-12-trim-agent-teams-read-and-lifecycle-surface.zh.md: be30d9239d4dc0c44adda63d9488e7a25d217a9e
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
# Agent Note: Trim the Agent Teams read and lifecycle surface
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-12-trim-agent-teams-read-and-lifecycle-surface.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Agent Teams correctly owns durable roster, peer-mailbox, and shared-task policy, while the subagent continuation manager owns continuable child Activations. The first implementation nevertheless duplicated data and lifecycle mechanics across those roles.
|
||||
|
||||
The read surface published `TeamSnapshot`, including pending mail that no production caller read. Web `team.get` called that snapshot for Team identity and a global revision, discarded its collections, then called `listMembers()` and `listTasks()`; the browser used neither the Team id it had already addressed nor the global revision. Public member and task views also repeated internal fields: member `error` duplicated `diagnostics`, task `ownerId` duplicated the model/UI owner name, and task timestamps had no reader. `SpawnTeammateResult.initialMessageId`, `TeamDeliverySource`, and the public resolved config type likewise had no consumer.
|
||||
|
||||
Durable member, task, message, and acknowledgement payloads copied timestamps already owned by the Session event envelope. Message `targetName` repeated the immutable roster lookup. The fold validated these values only against their previous copies, so the extra fields added format and validation code without deciding behavior.
|
||||
|
||||
`waitForChange()` returned a zero-or-one `changes` array with a domain kind and Lead-log revision even though every caller immediately re-listed authoritative state. The accompanying `team/changed` event had no production listener. Team interruption bypassed subagent authorization and cancellation semantics by calling `Agent.cancel()` directly. Team teardown separately combined cancellation, descendant drain, `whenIdle()`, and Agent-registry polling even though the continuation manager alone owns Activation release.
|
||||
|
||||
## Decision
|
||||
|
||||
The Team service keeps the distinct product responsibilities: durable named roster, Lead-log mailbox, and task DAG. It does not merge with the generic subagent catalog or task service.
|
||||
|
||||
Inside `@deepseek-ai/dsh-team`, `TeamService` is the Cordis-facing façade and disposal coordinator. `TeamJournal` owns per-Lead transaction order and append-plus-flush publication; `TeamRoster` owns membership and provisioning; `TeamMailbox` owns target-local dispatch, acknowledgement, and retry state; `TeamTaskBoard` owns task authorization, DAG transitions, and derived views; `TeamActivity` owns current waiters; and `TeamRuntimeLifecycle` owns the single admission cutoff and bounded settlement. These package-private collaborators share the existing service capability without publishing additional Cordis services.
|
||||
|
||||
The unused snapshot API and global Team revision are removed. Host reads return only roster and task views; they do not repeat the already-addressed Team id. Member failures appear once in `diagnostics`. Task views expose `ownerName` but keep `ownerId` inside the durable service implementation. Spawn returns the member view only, and validated config is private.
|
||||
|
||||
Durable Team values retain only fields needed to replay Team behavior. Session event `seq` and `time` own ordering and timing; roster membership owns immutable names. Member/task/message timestamps, message `targetName`, and acknowledgement `deliveredAt` are removed. Task CAS retains its task-local `revision`, which is behavioral rather than observational metadata.
|
||||
|
||||
`waitForChange()` now returns `{ timedOut }`. A committed Team append or live member-status edge wakes current waiters after the owning flush, and callers re-list. The unused `team/changed` event, change kind, change revision, and disposal sentinel are removed.
|
||||
|
||||
Team `interrupt()` resolves the durable roster name, then delegates to `SubagentService.interrupt()` with exact ancestor authority. Team teardown selects the roster's exact live direct-child ids and calls the new `drainContinuableChildren(parent, childIds)` continuation operation. That operation authorizes exact direct ownership, opens selected Activation disposal synchronously, recursively releases descendants child-first, leaves siblings and parent-wide admission alone, and treats absent targets as no-ops. Full teardown clears pending inbox work; only interrupt promises `keepInbox`.
|
||||
|
||||
Creation and dispatch remain separate in-flight sets because disposal must await creation before dispatches that creation recovery can register. The mailbox's durable enqueue/acknowledgement, target-side de-duplication, FIFO dispatch repair, provisioning reconciliation, and Host fold fallback remain unchanged.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Merge Team messaging into subagent follow-up.** Rejected. Subagent follow-up addresses a child by Session id and owns Activation delivery; Team messaging adds immutable names, peer authorization, durable enqueue-before-delivery, quiet inactive behavior, acknowledgement, retry, and sender framing.
|
||||
|
||||
**Replace Team tasks with the generic task service.** Rejected. The Team board is a Lead-log DAG with CAS revisions, member ownership, dependencies, tombstones, and advisory write scopes. Those are product semantics, not duplicate storage plumbing.
|
||||
|
||||
**Keep the public fields for future consumers.** Rejected before the first tagged release. Every removed field lacked a production reader, and pending mail or timing can be projected from the authoritative log if a concrete product later needs them.
|
||||
|
||||
**Subscribe to `agent/disposed` from Team teardown.** Rejected. The Team fiber is already unwinding when teardown runs, so new event registration is invalid. More importantly, an observer would still duplicate the continuation manager's ownership instead of asking that owner to release exact children.
|
||||
|
||||
**Use `drainContinuableDescendants()` on the Lead.** Rejected because it would stop non-Team continuable children and close admission for the whole Lead lineage. Draining descendants of each teammate stops only grandchildren and leaves the teammate Activation itself to Team polling. The exact-child operation expresses the required set directly.
|
||||
|
||||
**Preserve teammate inboxes during full teardown.** Rejected after testing the real handle lifecycle. `AgentHandle.dispose()` is a full release and clears unclaimed inbox work. Describing it as resumable parking would be false; interruption remains the non-disposing operation that preserves pending input.
|
||||
|
||||
**Keep all runtime responsibilities in one `TeamService` class.** Rejected because the class would own unrelated task policy, roster provisioning, mailbox delivery queues, waiters, and shutdown settlement. Package-private state owners retain one public service while making each asynchronous set and lifecycle controller belong to the operation family that settles it.
|
||||
|
||||
## Testing
|
||||
|
||||
Subagent tests cover exact-child selection, duplicate ids, sibling isolation, recursive descendant release, wrong-parent authorization, and a manager-less no-op. Team tests cover delegated interrupt, bounded exact-child teardown, provisioning cleanup, mailbox recovery, wait wake/timeout/disposal, and the reduced views and durable records; white-box failure injection addresses the package-private roster, mailbox, and journal owners instead of widening `TeamService`. Host, tool, and client tests cover the reduced wire and model-visible results. Type checking covers the public deletion across host and browser faces.
|
||||
|
||||
## Consequences
|
||||
|
||||
Team and subagent remain separate capability seams with one lifecycle owner. Team chooses which roster children belong to its runtime; subagent performs interruption and Activation teardown. The Team surface is smaller, persisted records no longer mirror their Session envelope, and wait consumers cannot mistake an advisory change kind or revision for a coherent snapshot. Package-private state ownership keeps `TeamService` focused on the public operations, Cordis event wiring, recovery order, and disposal order; the split adds internal modules but no public API or durable-format change.
|
||||
|
||||
Web `team.get` still folds once for members and once for tasks. A coherent combined snapshot was not a consumer requirement, and adding an incremental cache would introduce a separate consistency mechanism rather than simplify this seam.
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
# Agent Note: Trim the Agent Teams read and lifecycle surface
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-12-trim-agent-teams-read-and-lifecycle-surface.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
Agent Teams 合理地拥有持久 roster、peer mailbox 与共享任务策略,subagent continuation manager 则拥有 continuable child Activation。但第一版仍在这两类角色之间重复了数据和生命周期机制。
|
||||
|
||||
读取表面发布了 `TeamSnapshot`,其中包含没有生产调用方读取的 pending mail。Web `team.get` 为 Team identity 与全局 revision 调用该 snapshot,丢弃其中的集合,再调用 `listMembers()` 和 `listTasks()`;浏览器既不用已经寻址过的 Team id,也不用全局 revision。公开 member 与 task view 还重复内部字段:member `error` 与 `diagnostics` 重复,task `ownerId` 与模型/UI 使用的 owner name 重复,task 时间戳没有读取方。`SpawnTeammateResult.initialMessageId`、`TeamDeliverySource` 和公开的 resolved config type 同样没有消费者。
|
||||
|
||||
持久 member、task、message 与 acknowledgement payload 复制了 Session event envelope 已拥有的时间戳。message `targetName` 重复不可变 roster lookup。fold 只拿这些值与其旧副本互相校验,因此额外字段增加格式与校验代码,却不决定行为。
|
||||
|
||||
`waitForChange()` 返回长度为零或一的 `changes` 数组,携带领域 kind 与 Lead-log revision,但所有调用方都会立即重新列出权威状态。配套 `team/changed` event 没有生产 listener。Team interrupt 绕过 subagent 授权与取消语义,直接调用 `Agent.cancel()`。Team teardown 又自行组合 cancel、descendant drain、`whenIdle()` 与 Agent registry 轮询,尽管只有 continuation manager 拥有 Activation release。
|
||||
|
||||
## Decision
|
||||
|
||||
Team 服务保留独立的产品职责:持久具名 roster、Lead-log mailbox 与 task DAG。它不会与通用 subagent catalog 或 task service 合并。
|
||||
|
||||
在 `@deepseek-ai/dsh-team` 内,`TeamService` 是面向 Cordis 的 façade 与 disposal 协调者。`TeamJournal` 负责每个 Lead 的 transaction 顺序以及 append-plus-flush 发布;`TeamRoster` 负责 membership 与 provisioning;`TeamMailbox` 负责 target-local dispatch、acknowledgement 与 retry 状态;`TeamTaskBoard` 负责 task 授权、DAG transition 与派生 view;`TeamActivity` 负责当前 waiter;`TeamRuntimeLifecycle` 负责唯一的准入截止与有界 settlement。这些包内 collaborator 共享现有 service capability,不发布额外 Cordis service。
|
||||
|
||||
删除未使用的 snapshot API 与全局 Team revision。Host 读取只返回 roster 与 task view,不重复已经寻址的 Team id。member failure 只在 `diagnostics` 出现一次。task view 暴露 `ownerName`,把 `ownerId` 留在持久服务实现内部。spawn 只返回 member view,已校验 config 改为私有。
|
||||
|
||||
持久 Team value 只保留回放 Team 行为所需字段。Session event 的 `seq` 与 `time` 负责顺序和时间;roster membership 负责不可变名字。member/task/message 时间戳、message `targetName` 与 acknowledgement `deliveredAt` 均删除。task CAS 保留 task-local `revision`,因为它是行为字段,而非观测元数据。
|
||||
|
||||
`waitForChange()` 现在返回 `{ timedOut }`。已提交 Team append 或 live member-status edge 会在所属 flush 后唤醒当前 waiter,调用方随后重新列出状态。未使用的 `team/changed` event、change kind、change revision 与 disposal sentinel 一并删除。
|
||||
|
||||
Team `interrupt()` 先解析持久 roster name,再以确切 ancestor authority 委托 `SubagentService.interrupt()`。Team teardown 选择 roster 中确切的 live direct-child id,并调用新的 continuation 操作 `drainContinuableChildren(parent, childIds)`。该操作验证确切直接所有权,同步开启所选 Activation 的 disposal,递归以 child-first 释放后代,不影响 sibling 与 parent-wide admission,缺席目标视为 no-op。完整 teardown 会清空 pending inbox;只有 interrupt 承诺 `keepInbox`。
|
||||
|
||||
创建与 dispatch 继续使用分离的 in-flight set,因为 dispose 必须先等待创建,再等待创建 recovery 可能注册的 dispatch。mailbox 的持久 enqueue/acknowledgement、target-side 去重、FIFO dispatch 修复、provisioning 对账与 Host fold fallback 保持不变。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**把 Team messaging 合并进 subagent follow-up。** 否决。subagent follow-up 按 Session id 寻址 child 并负责 Activation 投递;Team messaging 额外提供不可变名字、peer 授权、先持久 enqueue 再投递、quiet inactive 行为、acknowledgement、retry 与 sender framing。
|
||||
|
||||
**用通用 task service 替换 Team task。** 否决。Team board 是带 CAS revision、member owner、dependency、tombstone 与 advisory write scope 的 Lead-log DAG。这些是产品语义,不是重复存储 plumbing。
|
||||
|
||||
**为未来消费者保留公开字段。** 在首次 tag 发布前否决。每个删除字段都没有生产读取方;若未来有具体产品需要 pending mail 或时间,可从权威日志投影。
|
||||
|
||||
**在 Team teardown 中订阅 `agent/disposed`。** 否决。teardown 运行时 Team fiber 已在解绑,新 event registration 无效。更重要的是,observer 仍会重复 continuation manager 的所有权,而不是要求 owner 释放确切 child。
|
||||
|
||||
**对 Lead 使用 `drainContinuableDescendants()`。** 否决,因为它会停止非 Team continuable child,并关闭整个 Lead 谱系的准入。对每个 teammate drain descendants 又只会停止孙级,把 teammate Activation 本身留给 Team 轮询。exact-child 操作直接表达所需集合。
|
||||
|
||||
**在完整 teardown 时保留 teammate inbox。** 在真实 handle lifecycle 测试后否决。`AgentHandle.dispose()` 是完整 release,会清空未 claim 的 inbox 工作。把它描述成可恢复 parking 是错误的;interrupt 仍是保留 pending input 的非 disposing 操作。
|
||||
|
||||
**把全部 runtime 职责保留在一个 `TeamService` class 中。** 否决,因为该 class 会同时拥有互不相关的 task policy、roster provisioning、mailbox delivery queue、waiter 与 shutdown settlement。包内 state owner 在保留单一公开 service 的同时,让每组异步状态与 lifecycle controller 归属于负责结算它们的 operation family。
|
||||
|
||||
## Testing
|
||||
|
||||
Subagent 测试覆盖 exact-child selection、重复 id、sibling 隔离、递归 descendant release、错误 parent 授权与 manager 缺席时的 no-op。Team 测试覆盖委托 interrupt、有界 exact-child teardown、provisioning cleanup、mailbox recovery、wait 的 wake/timeout/dispose,以及缩减后的 view 与持久记录;白盒 failure injection 直接访问包内 roster、mailbox 与 journal owner,不扩宽 `TeamService`。Host、tool 和 client 测试覆盖缩减后的 wire 与模型可见结果。typecheck 覆盖 Host 与浏览器表面的公开删除。
|
||||
|
||||
## Consequences
|
||||
|
||||
Team 与 subagent 仍是独立 capability seam,但只有一个生命周期 owner。Team 选择哪些 roster child 属于其运行时;subagent 执行 interrupt 与 Activation teardown。Team 表面更小,持久记录不再镜像 Session envelope,wait 消费方也不会把提示性的 change kind 或 revision 误当作一致 snapshot。包内 state ownership 让 `TeamService` 专注于公开 operation、Cordis event wiring、recovery 顺序与 disposal 顺序;该拆分增加内部 module,但不改变公开 API 或持久格式。
|
||||
|
||||
Web `team.get` 仍为 member 与 task 各折叠一次。消费方不要求一致的组合 snapshot,而增加增量 cache 会引入独立一致性机制,不是对该 seam 的简化。
|
||||
@@ -43,6 +43,7 @@ packages/ @deepseek-ai/dsh-<pkg> workspaces at packages/<group>/<pkg>/
|
||||
boot/ shared app-bin glue
|
||||
sdk/ JSON-RPC protocol, server, and TypeScript client
|
||||
examples/ demo bundles (agent-spine + CLI/ACP/JSON-RPC bins)
|
||||
experimental/ private prototypes excluded from official releases
|
||||
support/ dev/test infrastructure
|
||||
util/ zero-dependency utilities
|
||||
python/ Python SDK and bundled runtime (see python/README.md)
|
||||
|
||||
+1
-1
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write README.md
|
||||
README.md: 8a4bd01332a23ce4144c661784bc549e0ba72d21
|
||||
README.zh.md: b7bc214bfb1fd8a76a47de3f0aa242122aeb7603
|
||||
README.zh.md: c507bf884bd426feead6a96adbdb5c136456e3b5
|
||||
|
||||
+3
-3
@@ -50,9 +50,9 @@ pnpm dsh web
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center"><img src="assets/community-wecom-assistant.png" alt="DeepSeek Harness 企微小助手二维码" width="180" height="180"></td>
|
||||
<td align="center"><a href="https://trtgsjkv6r.feishu.cn/share/base/form/shrcnIt5twSVdLGD52KJBckGCgg"><img src="assets/community-wecom-survey.png" alt="DeepSeek Harness 入群问卷二维码" width="180" height="180"></a></td>
|
||||
<td align="center"><img src="assets/community-wechat-official-account.png" alt="DeepSeek Harness 团队微信公众号二维码" width="180" height="180"></td>
|
||||
<td align="center"><img src="https://cdn.deepseek.com/harness/readme/community-wecom-assistant.png" alt="DeepSeek Harness 企微小助手二维码" width="180" height="180"></td>
|
||||
<td align="center"><a href="https://trtgsjkv6r.feishu.cn/share/base/form/shrcnIt5twSVdLGD52KJBckGCgg"><img src="https://cdn.deepseek.com/harness/readme/community-wecom-survey.png" alt="DeepSeek Harness 入群问卷二维码" width="180" height="180"></a></td>
|
||||
<td align="center"><img src="https://cdn.deepseek.com/harness/readme/community-wechat-official-account.png" alt="DeepSeek Harness 团队微信公众号二维码" width="180" height="180"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -186,9 +186,10 @@ export function installAssembledBootEnv(): void {
|
||||
/**
|
||||
* Mount the assembled application on the fixture transport; the teardown
|
||||
* registered by installAssembledBootEnv disposes it.
|
||||
* @param search - fixture query string used to select deterministic host behavior.
|
||||
*/
|
||||
export function mountAssembledApp(): void {
|
||||
history.replaceState(null, '', '/?fixture')
|
||||
export function mountAssembledApp(search = '?fixture'): void {
|
||||
history.replaceState(null, '', `/${search}`)
|
||||
const root = document.createElement('div')
|
||||
root.id = 'root'
|
||||
document.body.appendChild(root)
|
||||
|
||||
@@ -108,12 +108,16 @@ describe('web e2e: Cordis tools use their owned cards', () => {
|
||||
// NOT the plugin running. Until a person answers, the browser half has not
|
||||
// been fetched, evaluated, or mounted anywhere on this page.
|
||||
expect(await page.locator('[data-snapshot-probe]').count()).toBe(0)
|
||||
const sessionId = await runTurnSettled
|
||||
// Approving from idle makes the run-outcome steer a distinct continuation
|
||||
// turn, matching the recorded replay and keeping turn grouping deterministic.
|
||||
const approvalTurnSettled = scaffold.whenTurnSettled()
|
||||
await approve.click()
|
||||
await expect.poll(() => page.locator('[data-snapshot-probe]').count(), { timeout: 30_000 }).toBe(1)
|
||||
await approvalTurnSettled
|
||||
|
||||
const sessionId = await runTurnSettled
|
||||
await expect.poll(() => page.getByText('The Cordis Plugin is running.', { exact: true }).count(), { timeout: 15_000 })
|
||||
.toBeGreaterThanOrEqual(1)
|
||||
await expect.poll(() => input.isEnabled(), { timeout: 15_000 }).toBe(true)
|
||||
const stopTurnSettled = scaffold.whenTurnSettled()
|
||||
await input.fill(STOP_PROMPT)
|
||||
await input.press('Enter')
|
||||
|
||||
@@ -197,3 +197,35 @@ it('accepts a whole-page drop under the limits-labeled overlay and refuses an ov
|
||||
const rail = document.querySelector('[role="group"][aria-label="Pending images"]')
|
||||
expect([...(rail?.querySelectorAll('img') ?? [])]).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('renders a host dimension rejection with the projected 2000px limit', async () => {
|
||||
mountAssembledApp('?fixture&fixturePrompt=reject')
|
||||
|
||||
const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
|
||||
const start = tree.querySelector<HTMLButtonElement>('button[aria-label="New session in fixture"]')
|
||||
if (start === null) throw new Error('fixture Workspace new-session action missing')
|
||||
fireEvent.click(start)
|
||||
|
||||
const textarea = await screen.findByPlaceholderText('Describe what you want to build', {}, { timeout: 10_000 })
|
||||
const image = new File([new Uint8Array([137, 80, 78, 71])], 'too-wide.png', { type: 'image/png' })
|
||||
fireEvent.paste(textarea, {
|
||||
clipboardData: {
|
||||
items: [{ kind: 'file', type: 'image/png', getAsFile: () => image }],
|
||||
getData: () => '',
|
||||
},
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('[role="group"][aria-label="Pending images"]')).not.toBeNull()
|
||||
})
|
||||
fireEvent.keyDown(textarea, { key: 'Enter' })
|
||||
|
||||
const message = 'Image sides must be at most 2000px; downscale it and try again'
|
||||
const toast = await screen.findByText(message)
|
||||
expect({ role: toast.closest('[role="alert"]')?.getAttribute('role'), text: toast.textContent }).toMatchInlineSnapshot(`
|
||||
{
|
||||
"role": "alert",
|
||||
"text": "Image sides must be at most 2000px; downscale it and try again",
|
||||
}
|
||||
`)
|
||||
expect(document.querySelector('[role="group"][aria-label="Pending images"]')).not.toBeNull()
|
||||
})
|
||||
|
||||
@@ -30,6 +30,7 @@ const EMPTY_EXPECTED = join(SNAPSHOT_DIR, 'empty.expected.md')
|
||||
const CONFIGURED_EXPECTED = join(SNAPSHOT_DIR, 'configured.expected.md')
|
||||
const DECLARED_EXPECTED = join(SNAPSHOT_DIR, 'declared.expected.md')
|
||||
const DECLARED_EDIT_EXPECTED = join(SNAPSHOT_DIR, 'declared-edit.expected.md')
|
||||
const MODEL_PICKER_EXPECTED = join(SNAPSHOT_DIR, 'model-picker.expected.md')
|
||||
const NATIVE_DELETE_EXPECTED = join(SNAPSHOT_DIR, 'native-delete.expected.md')
|
||||
const DELETE_EXPECTED = join(SNAPSHOT_DIR, 'delete.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
@@ -178,6 +179,42 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it('selects and clears the discovered model catalog in one action', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-models-picker'))
|
||||
const settingsDialog = page.getByRole('dialog', { name: '设置' })
|
||||
await settingsDialog.getByRole('button', { name: '编辑 minimax-cn' }).click()
|
||||
await settingsDialog.getByText('自定义设置').click()
|
||||
await settingsDialog.getByRole('button', { name: '获取可用模型' }).click()
|
||||
|
||||
const picker = page.getByRole('dialog', { name: '选择要添加的模型' })
|
||||
await picker.waitFor({ timeout: 10_000 })
|
||||
const boxes = picker.getByRole('checkbox')
|
||||
const count = await boxes.count()
|
||||
expect(count).toBeGreaterThan(0)
|
||||
expect(await boxes.evaluateAll(nodes => nodes.map(node => (node as HTMLInputElement).checked))).toEqual(
|
||||
Array.from({ length: count }, () => true),
|
||||
)
|
||||
|
||||
await picker.getByRole('button', { name: '取消全选' }).click()
|
||||
expect(await boxes.evaluateAll(nodes => nodes.map(node => (node as HTMLInputElement).checked))).toEqual(
|
||||
Array.from({ length: count }, () => false),
|
||||
)
|
||||
await picker.getByRole('button', { name: '全选' }).waitFor()
|
||||
const snapshot = await captureStableAria(
|
||||
page,
|
||||
'[role="dialog"][aria-label="选择要添加的模型"]',
|
||||
scaffold.workspaceCwd,
|
||||
)
|
||||
await compareOrRefreshGolden(MODEL_PICKER_EXPECTED, snapshot, MODE)
|
||||
|
||||
await picker.getByRole('button', { name: '全选' }).click()
|
||||
expect(await boxes.evaluateAll(nodes => nodes.map(node => (node as HTMLInputElement).checked))).toEqual(
|
||||
Array.from({ length: count }, () => true),
|
||||
)
|
||||
await picker.getByRole('button', { name: '取消', exact: true }).click()
|
||||
await settingsDialog.getByRole('button', { name: '取消', exact: true }).click()
|
||||
}, 60_000)
|
||||
|
||||
it('declares a route the adapter does not ship', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-models-declare'))
|
||||
const dialog = page.getByRole('dialog', { name: '设置' })
|
||||
@@ -280,7 +317,8 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
|
||||
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, [
|
||||
'configured.expected.md', 'declared-edit.expected.md', 'declared.expected.md',
|
||||
'delete.expected.md', 'empty.expected.md', 'native-delete.expected.md',
|
||||
'delete.expected.md', 'empty.expected.md', 'model-picker.expected.md',
|
||||
'native-delete.expected.md',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
- dialog "选择要添加的模型":
|
||||
- heading "选择要添加的模型" [level=2]
|
||||
- button "关闭":
|
||||
- img
|
||||
- paragraph: 以下是模型提供方的可用模型,勾选要添加的模型。
|
||||
- button "全选"
|
||||
- list:
|
||||
- listitem:
|
||||
- checkbox "MiniMax-M2.7"
|
||||
- text: MiniMax-M2.7
|
||||
- listitem:
|
||||
- checkbox "MiniMax-M2.7-highspeed"
|
||||
- text: MiniMax-M2.7-highspeed
|
||||
- listitem:
|
||||
- checkbox "MiniMax-M3"
|
||||
- text: MiniMax-M3
|
||||
- button "取消"
|
||||
- button "添加所选"
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 35 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 59 KiB |
@@ -2,5 +2,5 @@
|
||||
# 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 docs/architecture.md
|
||||
architecture.md: a1507fa5e54f6703e89f09a5d387e6c9afc81ade
|
||||
architecture.zh.md: 4642a1e7691bccf4d52d9a84c92c8237c3c6658b
|
||||
architecture.md: 2a51992a4f99d1827259c0a2ca2daa309f0ddd95
|
||||
architecture.zh.md: d1629a088ed8d2fe61a7e20ab2ab1fd3709abd50
|
||||
|
||||
@@ -101,6 +101,8 @@ 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/team.md) is a private opt-in coordination seam on `ctx.teams`, with a durable roster, task board, and mailbox layered over continuable subagents.
|
||||
|
||||
## Where new behavior goes
|
||||
|
||||
New behavior attaches to a documented extension point. Changing the loop itself updates this map.
|
||||
|
||||
@@ -105,6 +105,8 @@ turn/end
|
||||
|
||||
seam 正是替换一个提供方就能改变整个产品的原因。文件系统与进程提供方共享同一个执行世界,因此把它们指向远程沙箱,也就把 Bash、PTY 和 LSP 一并搬了过去,无需提供方专用 fork。[subagent 提供方](subsystems/subagent.md)在同一个接口之后同样千差万别,从新建一个子 agent,到把一个轮次委派给另一个产品。
|
||||
|
||||
[实验性 Agent Teams](subsystems/team.md) 是 `ctx.teams` 上的私有显式启用协作 seam,在可继续 subagent 之上提供持久 roster、任务板和 mailbox。
|
||||
|
||||
## 新行为的归属位置
|
||||
|
||||
新行为附加到已有文档记录的扩展点。改动循环本身时,本映射随之更新。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# 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 docs/capability-seams.md
|
||||
capability-seams.md: a990a9dd4d92d10e37b82e6a63caa4a5a469c441
|
||||
capability-seams.zh.md: 441d9222835e67c4a9f657d2335860005934094b
|
||||
capability-seams.md: 710c399510b6b123123e0a9586d86bfc3a96dff9
|
||||
capability-seams.zh.md: e0559d464ba1ecd2160eaba40c24bfd829e4a0d6
|
||||
|
||||
@@ -157,6 +157,9 @@ flowchart LR
|
||||
pkg_subagent_dsh_sdk["subagent-dsh-sdk"]
|
||||
pkg_tool_subagent_control["tool-subagent-control"]
|
||||
pkg_tool_ralph["tool-ralph"]
|
||||
pkg_team["team"]
|
||||
svc_teams["ctx.teams<br/>Agent Teams coordination domain"]
|
||||
pkg_tool_team["tool-team"]
|
||||
pkg_jobs["jobs"]
|
||||
svc_jobs["ctx.jobs<br/>Background job registry"]
|
||||
pkg_jobs_local["jobs-local"]
|
||||
@@ -279,6 +282,7 @@ flowchart LR
|
||||
pkg_subprocess_e2b --> svc_subprocess
|
||||
pkg_subprocess_local --> svc_subprocess
|
||||
pkg_system_prompt --> svc_systemPrompt
|
||||
pkg_team --> svc_teams
|
||||
pkg_terminal --> svc_terminals
|
||||
pkg_terminal_bash --> svc_terminals
|
||||
pkg_token_meter --> svc_tokenMeter
|
||||
@@ -383,6 +387,7 @@ flowchart LR
|
||||
svc_systemPrompt --> pkg_tool_terminal
|
||||
svc_systemPrompt --> pkg_tool_web
|
||||
svc_systemPrompt --> pkg_tools
|
||||
svc_teams --> pkg_tool_team
|
||||
svc_terminals --> pkg_tool_terminal
|
||||
svc_tokenMeter --> pkg_compaction_basic
|
||||
svc_toolResultPruner --> pkg_compaction_basic
|
||||
@@ -456,6 +461,7 @@ flowchart LR
|
||||
| `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox), [`fs-e2b`](../packages/e2b/fs-e2b) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | tool-fs executes read/write/edit through ctx.fs; fs-sandbox fences mutations by the shared sandbox mode; fs-observation-policy contributes observed-state checks through the fs/* event gate. |
|
||||
| `ctx.compaction` | `seam` | [`compaction`](../packages/compaction/compaction) | [`compaction-basic`](../packages/compaction/compaction-basic) | [`compaction-basic`](../packages/compaction/compaction-basic) | - | The basic backend consumes post-step pressure and request-error recovery events; there is no model-facing compact tool. |
|
||||
| `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process), [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code), [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`tool-ralph`](../packages/workflow/tool-ralph) | - | Providers implement transports; the service also owns optional Activation-based continuation orchestration, tool-subagent selects one-shot or continuable delegation, tool-subagent-control delivers follow-ups, and tool-ralph requires one fresh structured-output route. |
|
||||
| `ctx.teams` | `core` | [`team`](../packages/experimental/team) | - | [`tool-team`](../packages/experimental/tool-team) | - | Owns the implicit-root roster, durable peer mailbox, shared task DAG, and continuable-child lifecycle; tool-team contributes the scoped model policy and controls. |
|
||||
| `ctx.jobs` | `seam` | [`jobs`](../packages/jobs/jobs) | [`jobs-local`](../packages/jobs/jobs-local) | [`tool-bash`](../packages/shell/tool-bash), [`tool-terminal`](../packages/terminal/tool-terminal), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-jobs`](../packages/jobs/tool-jobs) | - | Producers (background bash, PTY sends, and subagent delegations) register running work; tool-jobs is the model-facing controller that reads, lists, and kills it; jobs-local is the process-local registry. |
|
||||
| `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-fetch-http`](../packages/web/web-fetch-http) | [`tool-web`](../packages/web/tool-web) | - | Search and fetch providers register into one ctx.web seam; tool-web owns the stable model-facing names. |
|
||||
| `ctx.spillStore` | `seam` | [`spill`](../packages/spill/spill) | [`spill-local`](../packages/spill/spill-local) | [`spill-policy`](../packages/spill/spill-policy) | - | The backend saves oversized tool text and returns a model-facing locator plus retrieval hint; spill-policy is the tools/post-execute consumer that decides when to spill. |
|
||||
|
||||
@@ -159,6 +159,9 @@ flowchart LR
|
||||
pkg_subagent_dsh_sdk["subagent-dsh-sdk"]
|
||||
pkg_tool_subagent_control["tool-subagent-control"]
|
||||
pkg_tool_ralph["tool-ralph"]
|
||||
pkg_team["team"]
|
||||
svc_teams["ctx.teams<br/>Agent Teams coordination domain"]
|
||||
pkg_tool_team["tool-team"]
|
||||
pkg_jobs["jobs"]
|
||||
svc_jobs["ctx.jobs<br/>Background job registry"]
|
||||
pkg_jobs_local["jobs-local"]
|
||||
@@ -281,6 +284,7 @@ flowchart LR
|
||||
pkg_subprocess_e2b --> svc_subprocess
|
||||
pkg_subprocess_local --> svc_subprocess
|
||||
pkg_system_prompt --> svc_systemPrompt
|
||||
pkg_team --> svc_teams
|
||||
pkg_terminal --> svc_terminals
|
||||
pkg_terminal_bash --> svc_terminals
|
||||
pkg_token_meter --> svc_tokenMeter
|
||||
@@ -385,6 +389,7 @@ flowchart LR
|
||||
svc_systemPrompt --> pkg_tool_terminal
|
||||
svc_systemPrompt --> pkg_tool_web
|
||||
svc_systemPrompt --> pkg_tools
|
||||
svc_teams --> pkg_tool_team
|
||||
svc_terminals --> pkg_tool_terminal
|
||||
svc_tokenMeter --> pkg_compaction_basic
|
||||
svc_toolResultPruner --> pkg_compaction_basic
|
||||
@@ -458,6 +463,7 @@ flowchart LR
|
||||
| `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox), [`fs-e2b`](../packages/e2b/fs-e2b) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | tool-fs 通过 ctx.fs 执行读取/写入/编辑;fs-sandbox 按共享沙箱模式限制变更;fs-observation-policy 通过 fs/* 事件门禁贡献基于观测状态的检查。 |
|
||||
| `ctx.compaction` | `seam` | [`compaction`](../packages/compaction/compaction) | [`compaction-basic`](../packages/compaction/compaction-basic) | [`compaction-basic`](../packages/compaction/compaction-basic) | - | 基础后端消费步骤后的压力事件和请求错误恢复事件;不存在面向模型的压缩工具。 |
|
||||
| `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process), [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code), [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`tool-ralph`](../packages/workflow/tool-ralph) | - | 提供方实现传输;该服务还负责可选的、基于 Activation 的延续编排,tool-subagent 选择一次性或可延续委派,tool-subagent-control 传递后续消息,而 tool-ralph 要求一条全新的结构化输出路由。 |
|
||||
| `ctx.teams` | `core` | [`team`](../packages/experimental/team) | - | [`tool-team`](../packages/experimental/tool-team) | - | 负责隐式 Root roster、持久 peer mailbox、共享任务 DAG 与 continuable child 生命周期;tool-team 提供作用域化模型策略和控制工具。 |
|
||||
| `ctx.jobs` | `seam` | [`jobs`](../packages/jobs/jobs) | [`jobs-local`](../packages/jobs/jobs-local) | [`tool-bash`](../packages/shell/tool-bash), [`tool-terminal`](../packages/terminal/tool-terminal), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-jobs`](../packages/jobs/tool-jobs) | - | 生产方(后台 bash、PTY 发送和 subagent 委派)登记正在运行的工作;tool-jobs 是面向模型的控制器,用于读取、列出和终止这些工作;jobs-local 是进程本地注册表。 |
|
||||
| `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-fetch-http`](../packages/web/web-fetch-http) | [`tool-web`](../packages/web/tool-web) | - | 搜索和抓取提供方注册到同一个 ctx.web seam;tool-web 负责稳定的面向模型名称。 |
|
||||
| `ctx.spillStore` | `seam` | [`spill`](../packages/spill/spill) | [`spill-local`](../packages/spill/spill-local) | [`spill-policy`](../packages/spill/spill-policy) | - | 后端保存过大的工具文本,并返回面向模型的定位信息和取回提示;spill-policy 是 tools/post-execute 消费方,负责决定何时 spill。 |
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# 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 docs/config-catalog.md
|
||||
config-catalog.md: ad9bb6284354b934bd6e0bf3235dcd0b74f33cc5
|
||||
config-catalog.zh.md: 7038b951a14115873a032fab0066be2a990e3a3f
|
||||
config-catalog.md: 35855f511f5c574bfad46aa9aac901a1c151f868
|
||||
config-catalog.zh.md: 1cd5fc5d7af19b414a6259783cd0a7c7a18990d6
|
||||
|
||||
+56
-5
@@ -335,10 +335,12 @@ export interface Config {
|
||||
maxMessageImageBytes?: number
|
||||
/** Maximum intrinsic width multiplied by height accepted for one image. */
|
||||
maxImagePixels?: number
|
||||
/** Maximum intrinsic width and maximum intrinsic height accepted for one image. */
|
||||
maxImageDimension?: number
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/attachment/attachment-local/src/index.ts:24`](../packages/attachment/attachment-local/src/index.ts)
|
||||
Source: [`packages/attachment/attachment-local/src/index.ts:31`](../packages/attachment/attachment-local/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-bash-local"></a>
|
||||
|
||||
@@ -984,6 +986,13 @@ export interface PiAiProviderProfile {
|
||||
websocketConnectTimeoutMs?: number
|
||||
/** Maximum provider idle time while one stream read is outstanding. */
|
||||
streamIdleTimeoutMs?: number
|
||||
/**
|
||||
* Maximum base64-encoded image payload per request. When a request's
|
||||
* accumulated images exceed it, the oldest images are replaced by text
|
||||
* placeholders until the request fits, so a long session keeps completing
|
||||
* requests instead of being rejected by a request-size cap.
|
||||
*/
|
||||
maxRequestImageBytes?: number
|
||||
/** Provider-owned model-request retry policy; omission uses normal defaults. */
|
||||
retryPolicy?: RetryPolicyConfig
|
||||
}
|
||||
@@ -1117,7 +1126,7 @@ export type PiAiThinkingFormat = NonNullable<OpenAICompletionsCompat['thinkingFo
|
||||
|
||||
Depends on: `Api` (`@earendil-works/pi-ai`) · `CacheRetention` (`@earendil-works/pi-ai`) · `ChatTemplateKwargValue` (`@earendil-works/pi-ai`) · `Model` (`@earendil-works/pi-ai`) · `ModelThinkingLevel` (`@earendil-works/pi-ai`) · `OpenAICompletionsCompat` (`@earendil-works/pi-ai`) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · `ThinkingBudgets` (`@earendil-works/pi-ai`) · `Transport` (`@earendil-works/pi-ai`)
|
||||
|
||||
Source: [`packages/llm/llm-pi-ai/src/config.ts:173`](../packages/llm/llm-pi-ai/src/config.ts)
|
||||
Source: [`packages/llm/llm-pi-ai/src/config.ts:193`](../packages/llm/llm-pi-ai/src/config.ts)
|
||||
|
||||
<a id="deepseek-aidsh-llm-replay"></a>
|
||||
|
||||
@@ -2282,6 +2291,30 @@ export interface Config {
|
||||
|
||||
Source: [`packages/core/system-prompt/src/index.ts:186`](../packages/core/system-prompt/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-team"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-team`
|
||||
|
||||
Requires: `agents` · `sessions` · `sessionPersistence` · `subagents`
|
||||
|
||||
```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 queued-minus-delivered messages for one target member. */
|
||||
readonly maxPendingMessagesPerMember?: number
|
||||
/** Maximum UTF-8 bytes in one complete sender-framed delivery. */
|
||||
readonly maxMessageBytes?: number
|
||||
/** Maximum milliseconds allowed for Team-owned runtime disposal. */
|
||||
readonly disposalTimeoutMs?: number
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/experimental/team/src/types.ts:125`](../packages/experimental/team/src/types.ts)
|
||||
|
||||
<a id="deepseek-aidsh-terminal-bash"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-terminal-bash`
|
||||
@@ -2702,9 +2735,9 @@ Requires: `subagents` · `tools` · `systemPrompt`
|
||||
/** Config: how accepted reports are scheduled on the parent. */
|
||||
export interface Config {
|
||||
/**
|
||||
* Parent scheduling (default `wakeup`). `wakeup` creates one ordinary later
|
||||
* parent turn; `quiet` adds context without waking, so a parked parent learns
|
||||
* of the report only when something else wakes it.
|
||||
* Parent scheduling (default `next-step`). `next-step` wakes the parent and
|
||||
* enters at its nearest step boundary; `quiet` adds the same context without
|
||||
* waking, so a parked parent waits for another waking input.
|
||||
*/
|
||||
reportDelivery?: SubagentReportDelivery
|
||||
}
|
||||
@@ -2714,6 +2747,24 @@ Depends on: [`SubagentReportDelivery`](subsystems/subagent.md)
|
||||
|
||||
Source: [`packages/subagent/tool-subagent-report/src/index.ts:27`](../packages/subagent/tool-subagent-report/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-tool-team"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-tool-team`
|
||||
|
||||
Requires: `agents` · `teams` · `tools` · `systemPrompt`
|
||||
|
||||
```ts config-catalog
|
||||
/** Tool routing configuration. */
|
||||
export interface Config {
|
||||
/** Continuable-subagent provider used for fresh teammates. */
|
||||
readonly freshProvider?: string
|
||||
/** Continuable-subagent provider used for completed-prefix fork teammates. */
|
||||
readonly forkProvider?: string
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/experimental/tool-team/src/index.ts:17`](../packages/experimental/tool-team/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-tool-terminal"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-tool-terminal`
|
||||
|
||||
@@ -337,10 +337,12 @@ export interface Config {
|
||||
maxMessageImageBytes?: number
|
||||
/** Maximum intrinsic width multiplied by height accepted for one image. */
|
||||
maxImagePixels?: number
|
||||
/** Maximum intrinsic width and maximum intrinsic height accepted for one image. */
|
||||
maxImageDimension?: number
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/attachment/attachment-local/src/index.ts:24`](../packages/attachment/attachment-local/src/index.ts)
|
||||
来源:[`packages/attachment/attachment-local/src/index.ts:31`](../packages/attachment/attachment-local/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-bash-local"></a>
|
||||
|
||||
@@ -986,6 +988,13 @@ export interface PiAiProviderProfile {
|
||||
websocketConnectTimeoutMs?: number
|
||||
/** Maximum provider idle time while one stream read is outstanding. */
|
||||
streamIdleTimeoutMs?: number
|
||||
/**
|
||||
* Maximum base64-encoded image payload per request. When a request's
|
||||
* accumulated images exceed it, the oldest images are replaced by text
|
||||
* placeholders until the request fits, so a long session keeps completing
|
||||
* requests instead of being rejected by a request-size cap.
|
||||
*/
|
||||
maxRequestImageBytes?: number
|
||||
/** Provider-owned model-request retry policy; omission uses normal defaults. */
|
||||
retryPolicy?: RetryPolicyConfig
|
||||
}
|
||||
@@ -1119,7 +1128,7 @@ export type PiAiThinkingFormat = NonNullable<OpenAICompletionsCompat['thinkingFo
|
||||
|
||||
依赖:`Api`(`@earendil-works/pi-ai`)· `CacheRetention`(`@earendil-works/pi-ai`)· `ChatTemplateKwargValue`(`@earendil-works/pi-ai`)· `Model`(`@earendil-works/pi-ai`)· `ModelThinkingLevel`(`@earendil-works/pi-ai`)· `OpenAICompletionsCompat`(`@earendil-works/pi-ai`)· [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · `ThinkingBudgets`(`@earendil-works/pi-ai`)· `Transport`(`@earendil-works/pi-ai`)
|
||||
|
||||
来源:[`packages/llm/llm-pi-ai/src/config.ts:173`](../packages/llm/llm-pi-ai/src/config.ts)
|
||||
来源:[`packages/llm/llm-pi-ai/src/config.ts:193`](../packages/llm/llm-pi-ai/src/config.ts)
|
||||
|
||||
<a id="deepseek-aidsh-llm-replay"></a>
|
||||
|
||||
@@ -2284,6 +2293,31 @@ export interface Config {
|
||||
|
||||
来源:[`packages/core/system-prompt/src/index.ts:186`](../packages/core/system-prompt/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-team"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-team`
|
||||
|
||||
需要:`agents` · `sessions` · `sessionPersistence` · `subagents`
|
||||
|
||||
```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 queued-minus-delivered messages for one target member. */
|
||||
readonly maxPendingMessagesPerMember?: number
|
||||
/** Maximum UTF-8 bytes in one complete sender-framed delivery. */
|
||||
readonly maxMessageBytes?: number
|
||||
/** Maximum milliseconds allowed for Team-owned runtime disposal. */
|
||||
readonly disposalTimeoutMs?: number
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/experimental/team/src/types.ts:125`](../packages/experimental/team/src/types.ts)
|
||||
|
||||
|
||||
<a id="deepseek-aidsh-terminal-bash"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-terminal-bash`
|
||||
@@ -2704,9 +2738,9 @@ export interface Config {
|
||||
/** Config: how accepted reports are scheduled on the parent. */
|
||||
export interface Config {
|
||||
/**
|
||||
* Parent scheduling (default `wakeup`). `wakeup` creates one ordinary later
|
||||
* parent turn; `quiet` adds context without waking, so a parked parent learns
|
||||
* of the report only when something else wakes it.
|
||||
* Parent scheduling (default `next-step`). `next-step` wakes the parent and
|
||||
* enters at its nearest step boundary; `quiet` adds the same context without
|
||||
* waking, so a parked parent waits for another waking input.
|
||||
*/
|
||||
reportDelivery?: SubagentReportDelivery
|
||||
}
|
||||
@@ -2716,6 +2750,25 @@ export interface Config {
|
||||
|
||||
来源:[`packages/subagent/tool-subagent-report/src/index.ts:27`](../packages/subagent/tool-subagent-report/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-tool-team"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-tool-team`
|
||||
|
||||
需要:`agents` · `teams` · `tools` · `systemPrompt`
|
||||
|
||||
```ts config-catalog
|
||||
/** Tool routing configuration. */
|
||||
export interface Config {
|
||||
/** Continuable-subagent provider used for fresh teammates. */
|
||||
readonly freshProvider?: string
|
||||
/** Continuable-subagent provider used for completed-prefix fork teammates. */
|
||||
readonly forkProvider?: string
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/experimental/tool-team/src/index.ts:17`](../packages/experimental/tool-team/src/index.ts)
|
||||
|
||||
|
||||
<a id="deepseek-aidsh-tool-terminal"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-tool-terminal`
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# 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 docs/event-producer-consumer.md
|
||||
event-producer-consumer.md: 6a79e6f7ce5addc64b10efa8da7a886dcfb36dc2
|
||||
event-producer-consumer.zh.md: f7576a8e28e4f1db2c65c324595c05c98b8fe488
|
||||
event-producer-consumer.md: c7b474a15c701781a70019f0703c0d60da87bcae
|
||||
event-producer-consumer.zh.md: 2e0667288ea44f83b7030e686db194f04d550a9b
|
||||
|
||||
@@ -9,8 +9,8 @@ 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:183`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
|
||||
| `agent-preset/selected` | `emit` | [`packages/preset/agent-presets/src/types.ts:13`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `apiproxy` |
|
||||
| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), [`tool-team`](../packages/experimental/tool-team) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-team`](../packages/experimental/tool-team) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `apiproxy`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../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) |
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:205`](../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) |
|
||||
@@ -18,8 +18,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill) |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:244`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:260`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:217`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:178`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `apiproxy`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server` |
|
||||
| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:217`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`team`](../packages/experimental/team) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:178`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `apiproxy`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, [`team`](../packages/experimental/team) |
|
||||
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:278`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
|
||||
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/index.ts:30`](../packages/interaction/user-approval/src/index.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `apiproxy` |
|
||||
| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:72`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `apiproxy` |
|
||||
@@ -39,8 +39,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:64`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
|
||||
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../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`) | `apiproxy`, [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
|
||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../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-presets`](../packages/preset/agent-presets), `apiproxy`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`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), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`team`](../packages/experimental/team) |
|
||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../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-presets`](../packages/preset/agent-presets), `apiproxy`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`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), [`team`](../packages/experimental/team), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:48`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` |
|
||||
| `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:35`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
|
||||
@@ -68,7 +68,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
|
||||
| Event string | Dispatchers | Listeners |
|
||||
| --- | --- | --- |
|
||||
| `internal/dispatch` | - | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`terminal-bash`](../packages/terminal/terminal-bash), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workflow`](../packages/workflow/workflow) |
|
||||
| `internal/dispatch` | - | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`team`](../packages/experimental/team), [`terminal-bash`](../packages/terminal/terminal-bash), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workflow`](../packages/workflow/workflow) |
|
||||
| `internal/plugin` | - | `loader`, [`lsp-stdio`](../packages/lsp/lsp-stdio), `webserver` |
|
||||
| `internal/service` | - | [`agent-presets`](../packages/preset/agent-presets), `gateway` |
|
||||
| `internal/status` | - | [`agent`](../packages/core/agent) |
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:183`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
|
||||
| `agent-preset/selected` | `emit` | [`packages/preset/agent-presets/src/types.ts:13`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `apiproxy` |
|
||||
| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), [`tool-team`](../packages/experimental/tool-team) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-team`](../packages/experimental/tool-team) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `apiproxy`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../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) |
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:205`](../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) |
|
||||
@@ -20,8 +20,8 @@
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill) |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:244`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:260`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:217`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:178`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `apiproxy`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server` |
|
||||
| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:217`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`team`](../packages/experimental/team) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:178`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `apiproxy`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, [`team`](../packages/experimental/team) |
|
||||
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:278`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
|
||||
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/index.ts:30`](../packages/interaction/user-approval/src/index.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `apiproxy` |
|
||||
| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:72`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `apiproxy` |
|
||||
@@ -41,8 +41,8 @@
|
||||
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:64`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
|
||||
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../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`) | `apiproxy`, [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
|
||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../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-presets`](../packages/preset/agent-presets), `apiproxy`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`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), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`team`](../packages/experimental/team) |
|
||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../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-presets`](../packages/preset/agent-presets), `apiproxy`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`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), [`team`](../packages/experimental/team), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:48`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` |
|
||||
| `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:35`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
|
||||
@@ -70,7 +70,7 @@
|
||||
|
||||
| 事件字符串 | 派发方 | 监听方 |
|
||||
| --- | --- | --- |
|
||||
| `internal/dispatch` | - | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`terminal-bash`](../packages/terminal/terminal-bash), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workflow`](../packages/workflow/workflow) |
|
||||
| `internal/dispatch` | - | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`team`](../packages/experimental/team), [`terminal-bash`](../packages/terminal/terminal-bash), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workflow`](../packages/workflow/workflow) |
|
||||
| `internal/plugin` | - | `loader`, [`lsp-stdio`](../packages/lsp/lsp-stdio), `webserver` |
|
||||
| `internal/service` | - | [`agent-presets`](../packages/preset/agent-presets), `gateway` |
|
||||
| `internal/status` | - | [`agent`](../packages/core/agent) |
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# 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 docs/module-graph.md
|
||||
module-graph.md: 285caa985be5f2cf3cb363651d0e0a250c232873
|
||||
module-graph.zh.md: a571f53da1726637b88b95cdf07ab9e3fe952168
|
||||
module-graph.md: acc9af96bcb004acc3ce780294797df31587a6d6
|
||||
module-graph.zh.md: 0347a6f48c52e74b371d3ab7d94045dee23c4db7
|
||||
|
||||
@@ -188,6 +188,10 @@ flowchart TD
|
||||
pkg_agent_spine_demo["agent-spine-demo"]
|
||||
pkg_sdk_jsonrpc_demo["sdk-jsonrpc-demo"]
|
||||
end
|
||||
subgraph group_experimental["packages/experimental"]
|
||||
pkg_team["team"]
|
||||
pkg_tool_team["tool-team"]
|
||||
end
|
||||
subgraph group_extensions["packages/extensions"]
|
||||
pkg_client_ui_cordis["client-ui-cordis"]
|
||||
pkg_cordis_client_runner["cordis-client-runner"]
|
||||
@@ -998,6 +1002,13 @@ flowchart TD
|
||||
pkg_compaction_tool_result_pruner --> pkg_llm
|
||||
pkg_compaction_tool_result_pruner --> pkg_session
|
||||
pkg_compaction_tool_result_pruner --> pkg_token_meter
|
||||
pkg_team --> pkg_agent
|
||||
pkg_team --> pkg_brand
|
||||
pkg_team --> pkg_invariants
|
||||
pkg_team --> pkg_llm
|
||||
pkg_team --> pkg_session
|
||||
pkg_team --> pkg_session_persistence
|
||||
pkg_team --> pkg_subagent
|
||||
pkg_tool_cordis --> pkg_agent
|
||||
pkg_tool_cordis --> pkg_cordis_host_runner
|
||||
pkg_tool_cordis --> pkg_invariants
|
||||
@@ -1103,6 +1114,12 @@ flowchart TD
|
||||
pkg_agent_spine_demo --> pkg_tool_jobs
|
||||
pkg_agent_spine_demo --> pkg_tool_skill
|
||||
pkg_agent_spine_demo --> pkg_tools
|
||||
pkg_tool_team --> pkg_agent
|
||||
pkg_tool_team --> pkg_invariants
|
||||
pkg_tool_team --> pkg_session
|
||||
pkg_tool_team --> pkg_system_prompt
|
||||
pkg_tool_team --> pkg_team
|
||||
pkg_tool_team --> pkg_tools
|
||||
pkg_sdk_client --> pkg_invariants
|
||||
pkg_sdk_client --> pkg_llm
|
||||
pkg_sdk_client --> pkg_sdk_protocol
|
||||
@@ -1560,6 +1577,7 @@ flowchart TD
|
||||
| [`hooks-claude-code`](../packages/hooks/hooks-claude-code) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
|
||||
| [`web-app`](../packages/bundle/web-app) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
|
||||
| [`team`](../packages/experimental/team) | `experimental` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent) |
|
||||
| [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
|
||||
@@ -1573,6 +1591,7 @@ flowchart TD
|
||||
| [`client-connection`](../packages/client/connection) | `client` | [`attachment`](../packages/attachment/attachment), [`commands`](../packages/interaction/commands), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
|
||||
| [`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), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
|
||||
| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs-local`](../packages/jobs/jobs-local), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`shell-env`](../packages/shell/shell-env), [`skill`](../packages/skill/skill), [`skill-filesystem`](../packages/skill/skill-filesystem), [`system-prompt`](../packages/core/system-prompt), [`tool-bash`](../packages/shell/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-skill`](../packages/skill/tool-skill), [`tools`](../packages/core/tools) |
|
||||
| [`tool-team`](../packages/experimental/tool-team) | `experimental` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`team`](../packages/experimental/team), [`tools`](../packages/core/tools) |
|
||||
| [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
|
||||
| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
|
||||
| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
|
||||
|
||||
@@ -190,6 +190,10 @@ flowchart TD
|
||||
pkg_agent_spine_demo["agent-spine-demo"]
|
||||
pkg_sdk_jsonrpc_demo["sdk-jsonrpc-demo"]
|
||||
end
|
||||
subgraph group_experimental["packages/experimental"]
|
||||
pkg_team["team"]
|
||||
pkg_tool_team["tool-team"]
|
||||
end
|
||||
subgraph group_extensions["packages/extensions"]
|
||||
pkg_client_ui_cordis["client-ui-cordis"]
|
||||
pkg_cordis_client_runner["cordis-client-runner"]
|
||||
@@ -1000,6 +1004,13 @@ flowchart TD
|
||||
pkg_compaction_tool_result_pruner --> pkg_llm
|
||||
pkg_compaction_tool_result_pruner --> pkg_session
|
||||
pkg_compaction_tool_result_pruner --> pkg_token_meter
|
||||
pkg_team --> pkg_agent
|
||||
pkg_team --> pkg_brand
|
||||
pkg_team --> pkg_invariants
|
||||
pkg_team --> pkg_llm
|
||||
pkg_team --> pkg_session
|
||||
pkg_team --> pkg_session_persistence
|
||||
pkg_team --> pkg_subagent
|
||||
pkg_tool_cordis --> pkg_agent
|
||||
pkg_tool_cordis --> pkg_cordis_host_runner
|
||||
pkg_tool_cordis --> pkg_invariants
|
||||
@@ -1105,6 +1116,12 @@ flowchart TD
|
||||
pkg_agent_spine_demo --> pkg_tool_jobs
|
||||
pkg_agent_spine_demo --> pkg_tool_skill
|
||||
pkg_agent_spine_demo --> pkg_tools
|
||||
pkg_tool_team --> pkg_agent
|
||||
pkg_tool_team --> pkg_invariants
|
||||
pkg_tool_team --> pkg_session
|
||||
pkg_tool_team --> pkg_system_prompt
|
||||
pkg_tool_team --> pkg_team
|
||||
pkg_tool_team --> pkg_tools
|
||||
pkg_sdk_client --> pkg_invariants
|
||||
pkg_sdk_client --> pkg_llm
|
||||
pkg_sdk_client --> pkg_sdk_protocol
|
||||
@@ -1562,6 +1579,7 @@ flowchart TD
|
||||
| [`hooks-claude-code`](../packages/hooks/hooks-claude-code) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
|
||||
| [`web-app`](../packages/bundle/web-app) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
|
||||
| [`team`](../packages/experimental/team) | `experimental` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent) |
|
||||
| [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
|
||||
@@ -1575,6 +1593,7 @@ flowchart TD
|
||||
| [`client-connection`](../packages/client/connection) | `client` | [`attachment`](../packages/attachment/attachment), [`commands`](../packages/interaction/commands), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
|
||||
| [`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), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
|
||||
| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs-local`](../packages/jobs/jobs-local), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`shell-env`](../packages/shell/shell-env), [`skill`](../packages/skill/skill), [`skill-filesystem`](../packages/skill/skill-filesystem), [`system-prompt`](../packages/core/system-prompt), [`tool-bash`](../packages/shell/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-skill`](../packages/skill/tool-skill), [`tools`](../packages/core/tools) |
|
||||
| [`tool-team`](../packages/experimental/tool-team) | `experimental` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`team`](../packages/experimental/team), [`tools`](../packages/core/tools) |
|
||||
| [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
|
||||
| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
|
||||
| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# 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 docs/persistence-catalog.md
|
||||
persistence-catalog.md: c400298f7d37c590918820bcbda10e6550f197e8
|
||||
persistence-catalog.zh.md: 65ec0e3fbdd226c51a371dc9a90f10db5c929c7a
|
||||
persistence-catalog.md: cf796ca322027886b1a0b78d69ac1d3a98d9459f
|
||||
persistence-catalog.zh.md: 5f82254813ef6fb0b3c9244a2bd03fb2177e5559
|
||||
|
||||
@@ -706,6 +706,65 @@ Source: [`packages/core/session/src/types.ts:254`](../packages/core/session/src/
|
||||
|
||||
Source: [`packages/subagent/subagent/src/descriptor.ts:37`](../packages/subagent/subagent/src/descriptor.ts)
|
||||
|
||||
### `team/*`
|
||||
|
||||
<a id="teammember--log-only"></a>
|
||||
|
||||
#### `team/member` — log-only
|
||||
|
||||
```ts persistence-catalog
|
||||
/** Whole teammate lifecycle value, stored only in the Team Lead Session. */
|
||||
'team/member': { version: 1; teamId: TeamId; member: TeamMemberSnapshot }
|
||||
```
|
||||
|
||||
Types: [TeamId](subsystems/team.md) · [TeamMemberSnapshot](subsystems/team.md)
|
||||
|
||||
Source: [`packages/experimental/team/src/types.ts:206`](../packages/experimental/team/src/types.ts)
|
||||
|
||||
<a id="teammessagedelivered--log-only"></a>
|
||||
|
||||
#### `team/message/delivered` — log-only
|
||||
|
||||
```ts persistence-catalog
|
||||
/** Durable acknowledgement that the target Session recorded the message. */
|
||||
'team/message/delivered': {
|
||||
version: 1
|
||||
teamId: TeamId
|
||||
messageId: TeamMessageId
|
||||
targetId: SessionId
|
||||
}
|
||||
```
|
||||
|
||||
Types: [TeamId](subsystems/team.md) · [TeamMessageId](subsystems/team.md)
|
||||
|
||||
Source: [`packages/experimental/team/src/types.ts:212`](../packages/experimental/team/src/types.ts)
|
||||
|
||||
<a id="teammessagequeued--log-only"></a>
|
||||
|
||||
#### `team/message/queued` — log-only
|
||||
|
||||
```ts persistence-catalog
|
||||
/** Durable mailbox enqueue, stored before delivery is attempted. */
|
||||
'team/message/queued': { version: 1; teamId: TeamId; message: TeamMessageSnapshot }
|
||||
```
|
||||
|
||||
Types: [TeamId](subsystems/team.md) · [TeamMessageSnapshot](subsystems/team.md)
|
||||
|
||||
Source: [`packages/experimental/team/src/types.ts:210`](../packages/experimental/team/src/types.ts)
|
||||
|
||||
<a id="teamtask--log-only"></a>
|
||||
|
||||
#### `team/task` — log-only
|
||||
|
||||
```ts persistence-catalog
|
||||
/** Whole shared-task value, stored only in the Team Lead Session. */
|
||||
'team/task': { version: 1; teamId: TeamId; task: TeamTaskSnapshot }
|
||||
```
|
||||
|
||||
Types: [TeamId](subsystems/team.md) · [TeamTaskSnapshot](subsystems/team.md)
|
||||
|
||||
Source: [`packages/experimental/team/src/types.ts:208`](../packages/experimental/team/src/types.ts)
|
||||
|
||||
### `todo/*`
|
||||
|
||||
<a id="todowrite--log-only"></a>
|
||||
|
||||
@@ -708,6 +708,65 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
||||
|
||||
来源:[`packages/subagent/subagent/src/descriptor.ts:37`](../packages/subagent/subagent/src/descriptor.ts)
|
||||
|
||||
### `team/*`
|
||||
|
||||
<a id="teammember--log-only"></a>
|
||||
|
||||
#### `team/member` — log-only
|
||||
|
||||
```ts persistence-catalog
|
||||
/** Whole teammate lifecycle value, stored only in the Team Lead Session. */
|
||||
'team/member': { version: 1; teamId: TeamId; member: TeamMemberSnapshot }
|
||||
```
|
||||
|
||||
类型:[TeamId](subsystems/team.md) · [TeamMemberSnapshot](subsystems/team.md)
|
||||
|
||||
来源:[`packages/experimental/team/src/types.ts:206`](../packages/experimental/team/src/types.ts)
|
||||
|
||||
<a id="teammessagedelivered--log-only"></a>
|
||||
|
||||
#### `team/message/delivered` — log-only
|
||||
|
||||
```ts persistence-catalog
|
||||
/** Durable acknowledgement that the target Session recorded the message. */
|
||||
'team/message/delivered': {
|
||||
version: 1
|
||||
teamId: TeamId
|
||||
messageId: TeamMessageId
|
||||
targetId: SessionId
|
||||
}
|
||||
```
|
||||
|
||||
类型:[TeamId](subsystems/team.md) · [TeamMessageId](subsystems/team.md)
|
||||
|
||||
来源:[`packages/experimental/team/src/types.ts:212`](../packages/experimental/team/src/types.ts)
|
||||
|
||||
<a id="teammessagequeued--log-only"></a>
|
||||
|
||||
#### `team/message/queued` — log-only
|
||||
|
||||
```ts persistence-catalog
|
||||
/** Durable mailbox enqueue, stored before delivery is attempted. */
|
||||
'team/message/queued': { version: 1; teamId: TeamId; message: TeamMessageSnapshot }
|
||||
```
|
||||
|
||||
类型:[TeamId](subsystems/team.md) · [TeamMessageSnapshot](subsystems/team.md)
|
||||
|
||||
来源:[`packages/experimental/team/src/types.ts:210`](../packages/experimental/team/src/types.ts)
|
||||
|
||||
<a id="teamtask--log-only"></a>
|
||||
|
||||
#### `team/task` — log-only
|
||||
|
||||
```ts persistence-catalog
|
||||
/** Whole shared-task value, stored only in the Team Lead Session. */
|
||||
'team/task': { version: 1; teamId: TeamId; task: TeamTaskSnapshot }
|
||||
```
|
||||
|
||||
类型:[TeamId](subsystems/team.md) · [TeamTaskSnapshot](subsystems/team.md)
|
||||
|
||||
来源:[`packages/experimental/team/src/types.ts:208`](../packages/experimental/team/src/types.ts)
|
||||
|
||||
### `todo/*`
|
||||
|
||||
<a id="todowrite--log-only"></a>
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# 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 docs/subsystems/README.md
|
||||
README.md: a1c2262f40aebdf4b0fdcdca4b0c05bae443fe2f
|
||||
README.zh.md: 0b5c00e6321a67a11dc6f98caedc7e26fb98eec2
|
||||
README.md: b3832e44d4fc2150efc4e81aab00d1006cb816b6
|
||||
README.zh.md: 5f5a44e9f21956921472c23a141eabac236704e9
|
||||
|
||||
@@ -38,6 +38,7 @@ One page per subsystem of the DeepSeek Harness: what it is, the data structures
|
||||
| [skills.md](skills.md) | the skill service: discovery priority, `SkillSummary`/`SkillDefinition`, session-prefix catalog, model-facing `skill` loading |
|
||||
| [compaction.md](compaction.md) | the compaction seam: the `compaction/*` session events, `CompactionResult`, the `CompactionEngine` interface |
|
||||
| [subagent.md](subagent.md) | the subagent seam: the named-provider registry, `SubagentStartRequest`/`Result`/`Run`, the start-time-vs-runtime capability split |
|
||||
| [team.md](team.md) | Agent Teams: implicit Lead identity, named continuable teammates, durable peer mailbox, and shared task DAG |
|
||||
| [web.md](web.md) | the web access seam: `WebSearchRequest`/`Result`, `WebFetchRequest`/`Result`, `WebFetchBody`, provider availability, `WebError` |
|
||||
| [spill.md](spill.md) | the spill storage seam: `SaveTextSpill`, `SpillOwner`/`SpillSource`, `SpillRef`, the branded `SpillLocator` |
|
||||
| [workflow.md](workflow.md) | the workflow seam: `WorkflowStartRequest`, `WorkflowMeta`, `WorkflowRun`/`Result`, the `workflow/*` event payloads, `WorkflowError` fatality |
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
| [skills.md](skills.md) | skill(技能)服务:发现优先级、`SkillSummary`/`SkillDefinition`、会话前缀目录、面向模型的 `skill` 加载 |
|
||||
| [compaction.md](compaction.md) | 压缩(compaction)seam:`compaction/*` 会话事件、`CompactionResult`、`CompactionEngine` 接口 |
|
||||
| [subagent.md](subagent.md) | subagent seam:命名提供方注册表、`SubagentStartRequest`/`Result`/`Run`、启动时与运行时能力拆分 |
|
||||
| [team.md](team.md) | Agent Teams:隐式 Lead 身份、具名 continuable teammate、持久 peer mailbox 与共享任务 DAG |
|
||||
| [web.md](web.md) | Web 访问 seam:`WebSearchRequest`/`Result`、`WebFetchRequest`/`Result`、`WebFetchBody`、提供方可用性、`WebError` |
|
||||
| [spill.md](spill.md) | spill 存储 seam:`SaveTextSpill`、`SpillOwner`/`SpillSource`、`SpillRef`、品牌类型 `SpillLocator` |
|
||||
| [workflow.md](workflow.md) | 工作流 seam:`WorkflowStartRequest`、`WorkflowMeta`、`WorkflowRun`/`Result`、`workflow/*` 事件载荷、`WorkflowError` 致命性 |
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# 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 docs/subsystems/attachment.md
|
||||
attachment.md: ff5a802b23b0111dff4481394772438f5d68feab
|
||||
attachment.zh.md: e3db8cd58e9bf2eebad66dacb78353cad98d2fd5
|
||||
attachment.md: 21e60dbc40504f22229ef98a2dd112eda82fffdd
|
||||
attachment.zh.md: 886e569b6db9f2a5b1dca39125785d8286e22c7a
|
||||
|
||||
@@ -42,6 +42,8 @@ interface ImageAttachmentLimits {
|
||||
maxImagesPerMessage: number
|
||||
maxMessageImageBytes: number
|
||||
maxImagePixels: number
|
||||
/** Maximum intrinsic width and maximum intrinsic height in pixels for one image. */
|
||||
maxImageDimension: number
|
||||
mediaTypes: readonly ImageMediaType[]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -42,6 +42,8 @@ interface ImageAttachmentLimits {
|
||||
maxImagesPerMessage: number
|
||||
maxMessageImageBytes: number
|
||||
maxImagePixels: number
|
||||
/** Maximum intrinsic width and maximum intrinsic height in pixels for one image. */
|
||||
maxImageDimension: number
|
||||
mediaTypes: readonly ImageMediaType[]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# 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 docs/subsystems/subagent.md
|
||||
subagent.md: a683a679e6017351540ee4b73adc74375ef0a1d6
|
||||
subagent.zh.md: 61391cd297c0eb14f4c0d8eac4539b551cb60bda
|
||||
subagent.md: 07ed5ce4a1da9fd68678ec14949d9ba249d7805f
|
||||
subagent.zh.md: e1b3ab465841ab16d2dc81bb12d8c49663533aa7
|
||||
|
||||
@@ -191,7 +191,7 @@ interface ContinuableStart {
|
||||
|
||||
An optional continuable-child setup contribution can install scope-local capabilities after base child composition and before Activation publication. The registry is ordered and transactional: a failed or revoked setup rolls back the unpublished Activation, child-scope disposal releases every installation, new registrations affect the next Activation, and registration removal revokes every resident installation immediately.
|
||||
|
||||
`SubagentRuntime.reportFrom()` uses that extension point without adding a second queue or a result-bearing child wrapper. The exact live child Agent authorizes the call; callers cannot name a recipient. The manager derives the only recipient from the child's durable `parentSession`, requires that parent Agent to be live, frames the selected content as one `subagent-report` user message, and returns the message's stable `MessageId`. Quiet delivery uses `Agent.inject()` and creates no inbox occurrence or parent turn; waking delivery uses `Agent.followup()` and creates one ordinary later parent turn. Neither mode concludes the child's turn, and no final answer reports implicitly.
|
||||
`SubagentRuntime.reportFrom()` uses that extension point without adding a second queue or a result-bearing child wrapper. The exact live child Agent authorizes the call; callers cannot name a recipient. The manager derives the only recipient from the child's durable `parentSession`, requires that parent Agent to be live, frames the selected content as one `subagent-report` user message, and returns the message's stable `MessageId`. Quiet delivery uses `Agent.inject()` and does not wake the parent; next-step delivery uses `Agent.steer()`, waking an idle parent or joining a running parent's nearest step boundary. Neither mode concludes the child's turn, and no final answer reports implicitly.
|
||||
|
||||
```ts type-equiv
|
||||
/** Durable attribution for a continuable child's explicit parent report. */
|
||||
@@ -206,7 +206,7 @@ interface SubagentReportMessageSource {
|
||||
|
||||
```ts type-equiv
|
||||
/** Deployment scheduling policy for accepted child reports. */
|
||||
type SubagentReportDelivery = 'quiet' | 'wakeup'
|
||||
type SubagentReportDelivery = 'quiet' | 'next-step'
|
||||
```
|
||||
|
||||
Reporting is the child's own choice, so the manager keeps a separate account of its own: when a resident Activation settles, it delivers one notice to the child's durable direct parent describing how that epoch ended and carrying its final assistant content. That delivery is unconditional for every child whose id a caller received, happens before the ownership release that would let the parent be judged settled, and reaches a resident parent through the same waking-admission accounting as a report. A parent whose own lineage is already tearing down receives it without a wake, because waking a quiescent Agent starts a turn rather than queueing work. Its provenance is a distinct kind so a transcript never presents a runtime account as something the child wrote.
|
||||
@@ -562,6 +562,18 @@ registerContinuableSetup(contribution: ContinuableSetupContribution): () => void
|
||||
*/
|
||||
async drainContinuableDescendants(parents: readonly Agent[]): Promise<void>
|
||||
|
||||
/**
|
||||
* Release selected resident continuable direct children of one exact live
|
||||
* parent. Other children of the same parent remain admitted and resident.
|
||||
* Absent targets and a manager-less composition are accepted no-ops.
|
||||
* @param parent - exact live direct parent authorizing the selected release.
|
||||
* @param childIds - durable direct-child ids to release when resident.
|
||||
* @returns once every selected Activation released its `AgentHandle`.
|
||||
* @throws {SubagentError} `UNAUTHORIZED` when a resident target belongs to a
|
||||
* different parent or the supplied parent identity is stale.
|
||||
*/
|
||||
async drainContinuableChildren(parent: Agent, childIds: readonly SessionId[]): Promise<void>
|
||||
|
||||
/**
|
||||
* Enumerate the parent's direct session-backed subagents without loading or
|
||||
* resuming an Agent and without any query service: the listing merges the live
|
||||
|
||||
@@ -191,7 +191,7 @@ interface ContinuableStart {
|
||||
|
||||
可选的可继续 child 设置贡献可以在 child 基础组合完成后、Activation 发布前安装限定在作用域内的能力。该注册表按顺序执行且具有事务性:设置失败或被撤销时会回滚未发布的 Activation;child 作用域 dispose 时会释放所有安装;新注册项在下一个 Activation 生效;移除注册项时则会立即撤销每个驻留中的安装。
|
||||
|
||||
`SubagentRuntime.reportFrom()` 通过该扩展点实现报告,无需新增第二条队列或承载结果的 child 包装层。调用由确切的在线 child Agent 授权,调用方不能指定接收方。管理器从 child 的持久化 `parentSession` 中推导唯一接收方,要求该 parent Agent 必须在线,将选中内容封装为一条 `subagent-report` 用户消息,并返回该消息的稳定 `MessageId`。静默投递使用 `Agent.inject()`,不产生 inbox 条目实例或 parent 轮次;唤醒投递使用 `Agent.followup()`,会产生一个普通的后续 parent 轮次。两种模式都不会结束 child 轮次,最终回答也不会隐式报告。
|
||||
`SubagentRuntime.reportFrom()` 通过该扩展点实现报告,无需新增第二条队列或承载结果的 child 包装层。调用由确切的在线 child Agent 授权,调用方不能指定接收方。管理器从 child 的持久化 `parentSession` 中推导唯一接收方,要求该 parent Agent 必须在线,将选中内容封装为一条 `subagent-report` 用户消息,并返回该消息的稳定 `MessageId`。静默投递使用 `Agent.inject()`,不会唤醒 parent;next-step 投递使用 `Agent.steer()`,会唤醒空闲 parent,或加入运行中 parent 最近的 step 边界。两种模式都不会结束 child 轮次,最终回答也不会隐式报告。
|
||||
|
||||
```ts type-equiv
|
||||
/** Durable attribution for a continuable child's explicit parent report. */
|
||||
@@ -206,7 +206,7 @@ interface SubagentReportMessageSource {
|
||||
|
||||
```ts type-equiv
|
||||
/** Deployment scheduling policy for accepted child reports. */
|
||||
type SubagentReportDelivery = 'quiet' | 'wakeup'
|
||||
type SubagentReportDelivery = 'quiet' | 'next-step'
|
||||
```
|
||||
|
||||
上报是 child 自己的选择,因此管理器还保有一份属于自己的记账:当驻留 Activation 结算时,它会向该 child 持久化的直接 parent 投递一条通知,说明该 epoch 如何结束,并携带其最终 assistant 内容。对每个调用方拿到过 id 的 child,这条投递都是无条件的;它发生在会让 parent 被判定为已结算的所有权释放之前,并通过与上报相同的唤醒准入记账到达驻留 parent。若 parent 自身所在的谱系已在拆卸中,这条通知会以不唤醒的方式送达,因为唤醒一个静息 Agent 是开启一个轮次,而不是排队等待工作。其来源信息使用一个独立的 kind,因此 transcript(文本记录)绝不会把运行时的记账呈现为 child 自己写下的内容。
|
||||
@@ -564,6 +564,18 @@ registerContinuableSetup(contribution: ContinuableSetupContribution): () => void
|
||||
*/
|
||||
async drainContinuableDescendants(parents: readonly Agent[]): Promise<void>
|
||||
|
||||
/**
|
||||
* Release selected resident continuable direct children of one exact live
|
||||
* parent. Other children of the same parent remain admitted and resident.
|
||||
* Absent targets and a manager-less composition are accepted no-ops.
|
||||
* @param parent - exact live direct parent authorizing the selected release.
|
||||
* @param childIds - durable direct-child ids to release when resident.
|
||||
* @returns once every selected Activation released its `AgentHandle`.
|
||||
* @throws {SubagentError} `UNAUTHORIZED` when a resident target belongs to a
|
||||
* different parent or the supplied parent identity is stale.
|
||||
*/
|
||||
async drainContinuableChildren(parent: Agent, childIds: readonly SessionId[]): Promise<void>
|
||||
|
||||
/**
|
||||
* Enumerate the parent's direct session-backed subagents without loading or
|
||||
* resuming an Agent and without any query service: the listing merges the live
|
||||
|
||||
@@ -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 docs/subsystems/team.md
|
||||
team.md: 926434bb85d8cb3739e66fdf559c1c51d807613d
|
||||
team.zh.md: 7eeb4ef5aeeeee119ea931912390d4ff3d42bc85
|
||||
@@ -0,0 +1,183 @@
|
||||
# Agent Teams
|
||||
|
||||
English | [中文](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 literal durable forms from [`packages/experimental/team/src/types.ts`](../../packages/experimental/team/src/types.ts).
|
||||
|
||||
## Identity and roster
|
||||
|
||||
`TeamId` is the root `SessionId` under a distinct [brand](core.md#branded-ids). `TeamTaskId` is Team-local and monotonically allocated as `task-<n>`; `TeamMessageId` is globally random. A teammate's Session id remains its persistent identity, while `name` is an immutable model/UI label.
|
||||
|
||||
```ts type-equiv
|
||||
/** Whole durable value written on every teammate lifecycle change. */
|
||||
interface TeamMemberSnapshot {
|
||||
readonly id: SessionId
|
||||
readonly name: string
|
||||
readonly description: string
|
||||
readonly provider: string
|
||||
readonly context: 'fresh' | 'fork'
|
||||
readonly phase: TeamMemberPhase
|
||||
readonly error?: string
|
||||
}
|
||||
```
|
||||
|
||||
Every member starts in `provisioning` and reaches exactly one terminal roster phase, `active` or `failed`. Runtime `running`/`idle`/`inactive` status is derived separately and never rewrites this record.
|
||||
|
||||
## Durable mailbox
|
||||
|
||||
The Lead Session first stores the complete queued message. A target receipt is acknowledged only after its pending inbox item or recorded user message is durable, leaving queued-minus-delivered as the recovery mailbox.
|
||||
|
||||
```ts type-equiv
|
||||
/** One peer message retained until its target Session records it. */
|
||||
interface TeamMessageSnapshot {
|
||||
readonly id: TeamMessageId
|
||||
readonly senderId: SessionId
|
||||
readonly senderName: string
|
||||
readonly targetId: SessionId
|
||||
readonly delivery: 'quiet' | 'wakeup'
|
||||
readonly content: ContentBlock[]
|
||||
}
|
||||
```
|
||||
|
||||
The target Session keeps message identity and sender attribution on both the pending inbox item and the eventual user message. Folding that source across inbox and history is the target-side de-duplication key; the model-visible framing repeats the id and sender.
|
||||
|
||||
```ts type-equiv
|
||||
/** Source retained by the target Session for durable mailbox de-duplication. */
|
||||
interface TeamMessageSource {
|
||||
readonly kind: 'team-message'
|
||||
readonly teamId: TeamId
|
||||
readonly messageId: TeamMessageId
|
||||
readonly senderId: SessionId
|
||||
readonly senderName: string
|
||||
}
|
||||
```
|
||||
|
||||
## Shared task DAG
|
||||
|
||||
Every task event stores a complete snapshot. `revision` is the compare-and-set value and increments by one per mutation. `blockedBy` edges must name non-deleted tasks and keep the graph acyclic. `writeScopes` are normalized advisory path prefixes rather than locks.
|
||||
|
||||
```ts type-equiv
|
||||
/** Whole durable task snapshot; every mutation increments {@link revision}. */
|
||||
interface TeamTaskSnapshot {
|
||||
readonly id: TeamTaskId
|
||||
readonly revision: number
|
||||
readonly subject: string
|
||||
readonly description: string
|
||||
readonly status: TeamTaskStatus
|
||||
readonly ownerId?: SessionId
|
||||
readonly blockedBy: TeamTaskId[]
|
||||
readonly writeScopes: string[]
|
||||
}
|
||||
```
|
||||
|
||||
`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.
|
||||
|
||||
## Replay
|
||||
|
||||
`foldTeam()` replays one root Session into the roster, task board, and queued-minus-delivered mailbox that every Team operation reads. 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 that add owner name, readiness, and write-scope warnings, while pending mail stays internal to delivery and recovery. The package [README](../../packages/experimental/team/README.md) owns operation, authorization, recovery, and limit behavior.
|
||||
|
||||
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
||||
|
||||
<a id="cordis-surface"></a>
|
||||
|
||||
## Cordis API
|
||||
|
||||
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
|
||||
|
||||
<a id="ctxteams--teamservice"></a>
|
||||
|
||||
### `ctx.teams` — `TeamService`
|
||||
|
||||
Agent Teams service backed by the exact live Lead Session log.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
* List the runtime-enriched roster visible to one Team member.
|
||||
* @param agent - exact live Team member.
|
||||
* @returns Lead and teammate rows in creation order.
|
||||
*/
|
||||
listMembers(agent: Agent): TeamMemberView[]
|
||||
|
||||
/**
|
||||
* 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 spawnTeammate(caller: Agent, request: SpawnTeammateRequest): Promise<SpawnTeammateResult>
|
||||
|
||||
/**
|
||||
* Queue one durable peer message, then attempt immediate delivery.
|
||||
* @param caller - exact live sending Team member.
|
||||
* @param request - target name, content, scheduling mode, and pre-queue cancellation.
|
||||
* @returns durable message identity and immediate-delivery observation.
|
||||
*/
|
||||
async sendMessage(caller: Agent, request: SendTeamMessageRequest): Promise<SendTeamMessageResult>
|
||||
|
||||
/**
|
||||
* Create one unowned pending task in the Team Lead log.
|
||||
* @param caller - exact live Team member creating the task.
|
||||
* @param request - task text, blockers, and advisory write scopes.
|
||||
* @returns the revision-one task view.
|
||||
*/
|
||||
async createTask(caller: Agent, request: CreateTeamTaskRequest): Promise<TeamTaskView>
|
||||
|
||||
/**
|
||||
* Return one task, including a deleted tombstone.
|
||||
* @param caller - exact live Team member reading the task.
|
||||
* @param id - Team-local task identity.
|
||||
* @returns the latest task value and derived readiness diagnostics.
|
||||
*/
|
||||
getTask(caller: Agent, id: TeamTaskId): TeamTaskView
|
||||
|
||||
/**
|
||||
* List current non-deleted tasks in numeric creation order.
|
||||
* @param caller - exact live Team member reading the board.
|
||||
* @returns detached current task views.
|
||||
*/
|
||||
listTasks(caller: Agent): TeamTaskView[]
|
||||
|
||||
/**
|
||||
* Compare-and-set one authorized task transition.
|
||||
* @param caller - exact live Team member authorizing the mutation.
|
||||
* @param request - task identity, expected revision, action, and action fields.
|
||||
* @returns the committed next task revision.
|
||||
*/
|
||||
async updateTask(caller: Agent, request: UpdateTeamTaskRequest): Promise<TeamTaskView>
|
||||
|
||||
/**
|
||||
* Wait for the next Team-domain or member-status change.
|
||||
* @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<TeamWaitResult>
|
||||
|
||||
/**
|
||||
* 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' | 'idle' | '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)
|
||||
|
||||
Source: [`packages/experimental/team/src/index.ts:56`](../../packages/experimental/team/src/index.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
@@ -0,0 +1,183 @@
|
||||
# Agent Teams
|
||||
|
||||
[English](team.md) | 中文
|
||||
|
||||
实验性隐式 Root Team 领域、模型工具与宿主适配器共享的类型。[Agent Teams Agent Note](../../.agents/notes/implemented/feature/2026-08-05-agent-teams.md)负责身份、mailbox、task 与共享 checkout 决策;本页记录 [`packages/experimental/team/src/types.ts`](../../packages/experimental/team/src/types.ts) 中的字面持久形式。
|
||||
|
||||
## 身份与 roster
|
||||
|
||||
`TeamId` 是具有独立[品牌](core.md#branded-ids)的 Root `SessionId`。`TeamTaskId` 在 Team 内按 `task-<n>` 单调分配;`TeamMessageId` 是全局随机值。teammate 的 Session id 始终是持久身份,而 `name` 是不可变的模型/UI 标签。
|
||||
|
||||
```ts type-equiv
|
||||
/** Whole durable value written on every teammate lifecycle change. */
|
||||
interface TeamMemberSnapshot {
|
||||
readonly id: SessionId
|
||||
readonly name: string
|
||||
readonly description: string
|
||||
readonly provider: string
|
||||
readonly context: 'fresh' | 'fork'
|
||||
readonly phase: TeamMemberPhase
|
||||
readonly error?: string
|
||||
}
|
||||
```
|
||||
|
||||
每个 member 都从 `provisioning` 开始,并且只到达一个终态 roster phase:`active` 或 `failed`。运行时 `running`/`idle`/`inactive` 状态单独派生,绝不会重写该记录。
|
||||
|
||||
## 持久 mailbox
|
||||
|
||||
Lead Session 首先存储完整 queued message。只有 target 的 pending inbox 条目或已记录用户消息完成持久化,才会写入独立 acknowledgement event,queued-minus-delivered 因而构成恢复 mailbox。
|
||||
|
||||
```ts type-equiv
|
||||
/** One peer message retained until its target Session records it. */
|
||||
interface TeamMessageSnapshot {
|
||||
readonly id: TeamMessageId
|
||||
readonly senderId: SessionId
|
||||
readonly senderName: string
|
||||
readonly targetId: SessionId
|
||||
readonly delivery: 'quiet' | 'wakeup'
|
||||
readonly content: ContentBlock[]
|
||||
}
|
||||
```
|
||||
|
||||
target Session 会在 pending inbox 条目和最终用户消息上保留消息身份与发送者归因。跨 inbox 与历史折叠该 source 构成 target 侧去重键;模型可见的 framing 会重复 id 和发送者。
|
||||
|
||||
```ts type-equiv
|
||||
/** Source retained by the target Session for durable mailbox de-duplication. */
|
||||
interface TeamMessageSource {
|
||||
readonly kind: 'team-message'
|
||||
readonly teamId: TeamId
|
||||
readonly messageId: TeamMessageId
|
||||
readonly senderId: SessionId
|
||||
readonly senderName: string
|
||||
}
|
||||
```
|
||||
|
||||
## 共享任务 DAG
|
||||
|
||||
每条 task event 都存储完整快照。`revision` 是 compare-and-set 值,每次变更递增 1。`blockedBy` edge 必须指向未删除任务,并维持无环图。`writeScopes` 是规范化的提示性路径前缀,不是锁。
|
||||
|
||||
```ts type-equiv
|
||||
/** Whole durable task snapshot; every mutation increments {@link revision}. */
|
||||
interface TeamTaskSnapshot {
|
||||
readonly id: TeamTaskId
|
||||
readonly revision: number
|
||||
readonly subject: string
|
||||
readonly description: string
|
||||
readonly status: TeamTaskStatus
|
||||
readonly ownerId?: SessionId
|
||||
readonly blockedBy: TeamTaskId[]
|
||||
readonly writeScopes: string[]
|
||||
}
|
||||
```
|
||||
|
||||
`pending` 表示尚未开始或已经释放,`in_progress` 携带 owner,`completed` 满足 blocker,`deleted` 是保留的 tombstone。view 会添加 owner name、readiness 和 write-scope 重叠警告,但不会改变持久快照。
|
||||
|
||||
## 回放
|
||||
|
||||
`foldTeam()` 把一个 Root Session 回放成每个 Team 操作所读取的 roster、任务板与 queued-minus-delivered mailbox。它按 `TeamId` 选取记录,因此普通 fork 继承的 event 保留 ancestor id,绝不会进入新 Root 的状态。Session event 的 `seq` 与 `time` 继续负责顺序和时间记录,Team snapshot 不再重复保存它们。roster 与 task 读取以 view 形式到达调用方,附带 owner name、readiness 与 write-scope 警告,而 pending 邮件仅供投递与恢复内部使用。包 [README](../../packages/experimental/team/README.md)负责 operation、authorization、recovery 和限制行为。
|
||||
|
||||
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
||||
|
||||
<a id="cordis-surface"></a>
|
||||
|
||||
## Cordis API
|
||||
|
||||
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
|
||||
|
||||
<a id="ctxteams--teamservice"></a>
|
||||
|
||||
### `ctx.teams` — `TeamService`
|
||||
|
||||
Agent Teams service backed by the exact live Lead Session log.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
* List the runtime-enriched roster visible to one Team member.
|
||||
* @param agent - exact live Team member.
|
||||
* @returns Lead and teammate rows in creation order.
|
||||
*/
|
||||
listMembers(agent: Agent): TeamMemberView[]
|
||||
|
||||
/**
|
||||
* 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 spawnTeammate(caller: Agent, request: SpawnTeammateRequest): Promise<SpawnTeammateResult>
|
||||
|
||||
/**
|
||||
* Queue one durable peer message, then attempt immediate delivery.
|
||||
* @param caller - exact live sending Team member.
|
||||
* @param request - target name, content, scheduling mode, and pre-queue cancellation.
|
||||
* @returns durable message identity and immediate-delivery observation.
|
||||
*/
|
||||
async sendMessage(caller: Agent, request: SendTeamMessageRequest): Promise<SendTeamMessageResult>
|
||||
|
||||
/**
|
||||
* Create one unowned pending task in the Team Lead log.
|
||||
* @param caller - exact live Team member creating the task.
|
||||
* @param request - task text, blockers, and advisory write scopes.
|
||||
* @returns the revision-one task view.
|
||||
*/
|
||||
async createTask(caller: Agent, request: CreateTeamTaskRequest): Promise<TeamTaskView>
|
||||
|
||||
/**
|
||||
* Return one task, including a deleted tombstone.
|
||||
* @param caller - exact live Team member reading the task.
|
||||
* @param id - Team-local task identity.
|
||||
* @returns the latest task value and derived readiness diagnostics.
|
||||
*/
|
||||
getTask(caller: Agent, id: TeamTaskId): TeamTaskView
|
||||
|
||||
/**
|
||||
* List current non-deleted tasks in numeric creation order.
|
||||
* @param caller - exact live Team member reading the board.
|
||||
* @returns detached current task views.
|
||||
*/
|
||||
listTasks(caller: Agent): TeamTaskView[]
|
||||
|
||||
/**
|
||||
* Compare-and-set one authorized task transition.
|
||||
* @param caller - exact live Team member authorizing the mutation.
|
||||
* @param request - task identity, expected revision, action, and action fields.
|
||||
* @returns the committed next task revision.
|
||||
*/
|
||||
async updateTask(caller: Agent, request: UpdateTeamTaskRequest): Promise<TeamTaskView>
|
||||
|
||||
/**
|
||||
* Wait for the next Team-domain or member-status change.
|
||||
* @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<TeamWaitResult>
|
||||
|
||||
/**
|
||||
* 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' | 'idle' | '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)
|
||||
|
||||
Source: [`packages/experimental/team/src/index.ts:56`](../../packages/experimental/team/src/index.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
@@ -2,5 +2,5 @@
|
||||
# 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 docs/tool-catalog.md
|
||||
tool-catalog.md: 02501d0721768d38c1e51bdfb87dcc79696bfd04
|
||||
tool-catalog.zh.md: 3583662dbaf72ea78bddb6a2b7d6b18360f3f3e1
|
||||
tool-catalog.md: b3f59ed76ad1a26a4da207c52bf0e64c40148a94
|
||||
tool-catalog.zh.md: 27ae60554393fc390386b7b0aef64f6d0758cf28
|
||||
|
||||
@@ -36,6 +36,7 @@ This table connects model-visible tool names to the plugin package and service s
|
||||
| `@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 control tools over continuable background 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-subagent-report` | `report` | `ctx.subagents`, `ctx.systemPrompt`, `a live continuable in-process child Agent` | `tool/call`, `tool/result`, `a user-role message in the direct parent session` | - | Registered per continuable in-process child rather than globally, so this schema is visible only inside such a child and survives its global `toolFilter`. The same contribution installs the child-scoped `tool:report` prompt section, which this catalog does not render. The parent-facing `send_message` tool is installed independently. |
|
||||
| `@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, PTY sends, and subagents 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-tool-team` | `followup_task`, `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.teams`, `an exact live Team member Agent` | `tool/call`, `team/member`, `team/message/queued`, `team/message/delivered`, `team/task`, `tool/result` | - | All ten 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-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-workflow` | `workflow` | `ctx.tools`, `ctx.workflowEngine`, `ctx.systemPrompt`, `a calling Agent (exec.agent parents the script children)` | `tool/call`, `tool/result` | - | - |
|
||||
| `@deepseek-ai/dsh-tool-web` | `web_fetch`, `web_search` | `ctx.tools`, `ctx.web`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | web_search and web_fetch keep provider selection behind ctx.web so model-visible schemas stay stable across backend swaps. |
|
||||
@@ -1679,6 +1680,322 @@ Source: [`packages/jobs/tool-jobs/src/index.ts`](../packages/jobs/tool-jobs/src/
|
||||
|
||||
The kind-agnostic background-job controller: background bash commands, PTY sends, and subagents are read, listed, and killed through the same three tools. Loading the plugin attaches the controller that arms producers' `ctx.jobs.start()`.
|
||||
|
||||
<a id="deepseek-aidsh-tool-team"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-tool-team`
|
||||
|
||||
### `followup_task`
|
||||
|
||||
Send a durable follow-up task to another Team member and start a turn when needed.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "Team member name, or lead."
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Self-contained message for the target."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"target",
|
||||
"message"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
|
||||
|
||||
### `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 name."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"target"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
|
||||
|
||||
### `list_agents`
|
||||
|
||||
List the Lead and every durable teammate with current runtime status.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
|
||||
|
||||
### `send_message`
|
||||
|
||||
Send durable information to another Team member without starting an idle member.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "Team member name, or lead."
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Self-contained message for the target."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"target",
|
||||
"message"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-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-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
|
||||
|
||||
### `team_task_create`
|
||||
|
||||
Create one unowned pending task on the shared Team task board.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"subject": {
|
||||
"type": "string",
|
||||
"description": "Concise task title."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Complete task details and acceptance criteria."
|
||||
},
|
||||
"blocked_by": {
|
||||
"type": "array",
|
||||
"description": "Task ids that must complete first.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"write_scopes": {
|
||||
"type": "array",
|
||||
"description": "Advisory workspace-relative file or directory prefixes this task expects to modify.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"subject",
|
||||
"description"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
|
||||
|
||||
### `team_task_get`
|
||||
|
||||
Read the complete latest value of one shared task before changing or executing it.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task_id": {
|
||||
"type": "string",
|
||||
"description": "Shared task id."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"task_id"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
|
||||
|
||||
### `team_task_list`
|
||||
|
||||
List shared tasks, including readiness, owner, revision, blockers, and write-scope warnings.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "Optional exact status filter.",
|
||||
"enum": [
|
||||
"pending",
|
||||
"in_progress",
|
||||
"completed"
|
||||
]
|
||||
},
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "Optional member-name 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."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
|
||||
|
||||
### `team_task_update`
|
||||
|
||||
Compare-and-set a shared task action using the latest revision from team_task_get or team_task_list.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task_id": {
|
||||
"type": "string",
|
||||
"description": "Shared task id."
|
||||
},
|
||||
"expected_revision": {
|
||||
"type": "integer",
|
||||
"description": "Current task revision used as the CAS precondition."
|
||||
},
|
||||
"action": {
|
||||
"type": "string",
|
||||
"description": "Task transition to apply.",
|
||||
"enum": [
|
||||
"claim",
|
||||
"release",
|
||||
"edit",
|
||||
"set_dependencies",
|
||||
"complete",
|
||||
"reopen",
|
||||
"reassign",
|
||||
"delete"
|
||||
]
|
||||
},
|
||||
"subject": {
|
||||
"type": "string",
|
||||
"description": "Replacement title for edit."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Replacement details for edit."
|
||||
},
|
||||
"blocked_by": {
|
||||
"type": "array",
|
||||
"description": "Complete blocker list for set_dependencies.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"write_scopes": {
|
||||
"type": "array",
|
||||
"description": "Replacement advisory write scopes for edit.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "Member name for Lead-only reassign; omit to unassign."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"task_id",
|
||||
"expected_revision",
|
||||
"action"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
|
||||
|
||||
### `wait_agent`
|
||||
|
||||
Wait for the next teammate status, mailbox, 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.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"timeout_ms": {
|
||||
"type": "integer",
|
||||
"description": "Wait duration in milliseconds, from 10000 through 3600000. Defaults to 30000."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
|
||||
|
||||
All ten 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.
|
||||
|
||||
<a id="deepseek-aidsh-tool-todo"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-tool-todo`
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
| `@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-subagent-report` | `report` | `ctx.subagents`、`ctx.systemPrompt`、`a live continuable in-process child Agent` | `tool/call`、`tool/result`、`a user-role message in the direct parent session` | - | 按可继续的进程内子级注册,而非全局注册,因此该 schema 仅在这种子级内部可见,并且不受其全局 `toolFilter` 影响。同一份贡献还会安装子级作用域的 `tool:report` 系统提示词 section,本目录不渲染该 section。面向父级的 `send_message` 工具单独安装。 |
|
||||
| `@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 发送和 subagent 都通过相同的 3 个工具读取、列出和终止。加载该插件会挂接控制器,从而启用生产方的 `ctx.jobs.start()`。 |
|
||||
| `@deepseek-ai/dsh-tool-team` | `followup_task`、`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.teams`、`an exact live Team member Agent` | `tool/call`、`team/member`、`team/message/queued`、`team/message/delivered`、`team/task`、`tool/result` | - | 这 10 个工具限定于隐式 Team Lead 与持久 teammate 作用域。随产品发布的 dsh-base bundle 默认禁用该包;文档中的 Agent Teams profile patch 会启用它,并禁用旧 continuable child 的同名控制工具。 |
|
||||
| `@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-workflow` | `workflow` | `ctx.tools`、`ctx.workflowEngine`、`ctx.systemPrompt`、`a calling Agent (exec.agent parents the script children)` | `tool/call`、`tool/result` | - | - |
|
||||
| `@deepseek-ai/dsh-tool-web` | `web_fetch`、`web_search` | `ctx.tools`、`ctx.web`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | web_search 和 web_fetch 将提供方选择置于 ctx.web 之后,使模型可见 schema 在更换后端时保持稳定。 |
|
||||
@@ -1685,6 +1686,321 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
|
||||
|
||||
<a id="deepseek-aidsh-tool-todo"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-tool-team`
|
||||
|
||||
### `followup_task`
|
||||
|
||||
向另一名 Team member 发送持久 follow-up task,并在需要时启动一个 turn。
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "Team member name, or lead."
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Self-contained message for the target."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"target",
|
||||
"message"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
|
||||
|
||||
### `interrupt_agent`
|
||||
|
||||
中断一名 teammate 的当前 turn,同时保留其待处理 inbox。仅 Team Lead 可用。
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "Teammate name."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"target"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
|
||||
|
||||
### `list_agents`
|
||||
|
||||
列出 Lead 与所有持久 teammate,以及各自当前的运行时状态。
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
|
||||
|
||||
### `send_message`
|
||||
|
||||
向另一名 Team member 发送持久信息,但不启动 idle member。
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "Team member name, or lead."
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Self-contained message for the target."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"target",
|
||||
"message"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-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-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
|
||||
|
||||
### `team_task_create`
|
||||
|
||||
在共享 Team 任务板上创建一个无 owner 的 pending task。
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"subject": {
|
||||
"type": "string",
|
||||
"description": "Concise task title."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Complete task details and acceptance criteria."
|
||||
},
|
||||
"blocked_by": {
|
||||
"type": "array",
|
||||
"description": "Task ids that must complete first.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"write_scopes": {
|
||||
"type": "array",
|
||||
"description": "Advisory workspace-relative file or directory prefixes this task expects to modify.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"subject",
|
||||
"description"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
|
||||
|
||||
### `team_task_get`
|
||||
|
||||
在修改或执行共享任务前,读取其完整的最新值。
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task_id": {
|
||||
"type": "string",
|
||||
"description": "Shared task id."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"task_id"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
|
||||
|
||||
### `team_task_list`
|
||||
|
||||
列出共享任务,包括 readiness、owner、revision、blocker 与 write-scope warning。
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "Optional exact status filter.",
|
||||
"enum": [
|
||||
"pending",
|
||||
"in_progress",
|
||||
"completed"
|
||||
]
|
||||
},
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "Optional member-name 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."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
|
||||
|
||||
### `team_task_update`
|
||||
|
||||
使用 team_task_get 或 team_task_list 返回的最新 revision,对共享任务操作执行 compare-and-set。
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task_id": {
|
||||
"type": "string",
|
||||
"description": "Shared task id."
|
||||
},
|
||||
"expected_revision": {
|
||||
"type": "integer",
|
||||
"description": "Current task revision used as the CAS precondition."
|
||||
},
|
||||
"action": {
|
||||
"type": "string",
|
||||
"description": "Task transition to apply.",
|
||||
"enum": [
|
||||
"claim",
|
||||
"release",
|
||||
"edit",
|
||||
"set_dependencies",
|
||||
"complete",
|
||||
"reopen",
|
||||
"reassign",
|
||||
"delete"
|
||||
]
|
||||
},
|
||||
"subject": {
|
||||
"type": "string",
|
||||
"description": "Replacement title for edit."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Replacement details for edit."
|
||||
},
|
||||
"blocked_by": {
|
||||
"type": "array",
|
||||
"description": "Complete blocker list for set_dependencies.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"write_scopes": {
|
||||
"type": "array",
|
||||
"description": "Replacement advisory write scopes for edit.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "Member name for Lead-only reassign; omit to unassign."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"task_id",
|
||||
"expected_revision",
|
||||
"action"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
|
||||
|
||||
### `wait_agent`
|
||||
|
||||
等待本次调用开始后下一次 teammate 状态、mailbox 或共享任务变更。它绝不会唤醒 inactive member;若没有其他 member 正在 running 或 provisioning,则立即返回 noProgress。唤醒或超时后应重新列出状态,而不是轮询。
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"timeout_ms": {
|
||||
"type": "integer",
|
||||
"description": "Wait duration in milliseconds, from 10000 through 3600000. Defaults to 30000."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
|
||||
|
||||
这 10 个工具限定于隐式 Team Lead 与持久 teammate 作用域。随产品发布的 dsh-base bundle 默认禁用该包;文档中的 Agent Teams profile patch 会启用它,并禁用旧 continuable child 的同名控制工具。
|
||||
|
||||
|
||||
## `@deepseek-ai/dsh-tool-todo`
|
||||
|
||||
### `todo_write`
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
# Snapshot-only overlay pinning quiet report delivery. The shipped default wakes
|
||||
# the parent on every accepted report, and the runtime's settlement notice wakes
|
||||
# it again when the child's Activation ends; two independent wakes have no single
|
||||
# authored order. Quiet delivery leaves settlement as the only wake, while the
|
||||
# fixture below holds the child until the parent's spawn turn has closed.
|
||||
- id: base
|
||||
name: '@deepseek-ai/cordis-plugin-include'
|
||||
config:
|
||||
path: ./cordis.yml
|
||||
patches:
|
||||
- id: tool-subagent-report
|
||||
name: '@deepseek-ai/dsh-tool-subagent-report'
|
||||
config:
|
||||
reportDelivery: quiet
|
||||
|
||||
- id: report-fence
|
||||
name: './tests/fixtures/subagent-report-fence.ts'
|
||||
+2
-7
@@ -1,6 +1,5 @@
|
||||
# Keyless counterpart to subagent-report-quiet.cordis.yml: replace the live
|
||||
# adapter with replay, keep report delivery quiet, and fence the child behind
|
||||
# the end of its parent's spawn turn so settlement opens the next turn.
|
||||
# Keyless counterpart to subagent-report.cordis.yml: replace the live adapter
|
||||
# with replay and preserve its child and parent scheduling fence.
|
||||
- id: base
|
||||
name: '@deepseek-ai/cordis-plugin-include'
|
||||
config:
|
||||
@@ -32,10 +31,6 @@
|
||||
- passthrough-runner
|
||||
runnerFailureSignatures:
|
||||
- 'passthrough-runner: profile rejected'
|
||||
- id: tool-subagent-report
|
||||
name: '@deepseek-ai/dsh-tool-subagent-report'
|
||||
config:
|
||||
reportDelivery: quiet
|
||||
- insert:
|
||||
- id: llm-replay
|
||||
name: '@deepseek-ai/dsh-llm-replay'
|
||||
@@ -0,0 +1,10 @@
|
||||
# Snapshot-only overlay fencing the child behind its parent's spawn turn and
|
||||
# holding the parent in maintenance until settlement follows the default
|
||||
# next-step report. The resumed parent claims both notices in causal order.
|
||||
- id: base
|
||||
name: '@deepseek-ai/cordis-plugin-include'
|
||||
config:
|
||||
path: ./cordis.yml
|
||||
|
||||
- id: report-fence
|
||||
name: './tests/fixtures/subagent-report-fence.ts'
|
||||
@@ -1,11 +1,19 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { createServer } from 'node:http'
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http'
|
||||
import { mkdir, utimes, writeFile } from 'node:fs/promises'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { homedir } from 'node:os'
|
||||
import { expect, it } from 'vitest'
|
||||
import { defineAcpSnapshotSuite, type Scenario, type SnapshotSuiteOptions } from '@deepseek-ai/dsh-acp-snapshot'
|
||||
import {
|
||||
defineAcpSnapshotSuite,
|
||||
runScenario,
|
||||
type InputScript,
|
||||
type Scenario,
|
||||
type SnapshotSuiteOptions,
|
||||
} from '@deepseek-ai/dsh-acp-snapshot'
|
||||
import { resolvePwshPath } from '@deepseek-ai/dsh-pwsh-local'
|
||||
import { decodeStorageRecord } from '@deepseek-ai/dsh-session'
|
||||
|
||||
@@ -40,6 +48,7 @@ const ADVANCED_CONFIG = fileURLToPath(new URL('../advanced.cordis.yml', import.m
|
||||
const FS_CONFIG = fileURLToPath(new URL('../fs.cordis.yml', import.meta.url))
|
||||
const SESSION_QUERY_CONFIG = fileURLToPath(new URL('../session-query.cordis.yml', import.meta.url))
|
||||
const IMAGE_CONFIG = fileURLToPath(new URL('../image.cordis.yml', import.meta.url))
|
||||
const IMAGE_OFFLOAD_CONFIG = fileURLToPath(new URL('./fixtures/image-offload.cordis.yml', import.meta.url))
|
||||
const IMAGE_TEXT_ROUTE_CONFIG = fileURLToPath(new URL('../image-text-route.cordis.yml', import.meta.url))
|
||||
const PTY_CONFIG = fileURLToPath(new URL('../pty.cordis.yml', import.meta.url))
|
||||
const DEPTH_TWO_CONFIG = fileURLToPath(new URL('../depth-two.cordis.yml', import.meta.url))
|
||||
@@ -47,8 +56,8 @@ const CHILD_QUESTION_CONFIG = fileURLToPath(new URL('../child-question.cordis.ym
|
||||
const SESSION_SANDBOX_ROOT_CONFIG = fileURLToPath(new URL('../session-sandbox-root.cordis.yml', import.meta.url))
|
||||
const RETRY_CONFIG = fileURLToPath(new URL('../retry.cordis.yml', import.meta.url))
|
||||
const SESSION_TITLE_CONFIG = fileURLToPath(new URL('../session-title.cordis.yml', import.meta.url))
|
||||
const SUBAGENT_REPORT_QUIET_CONFIG = fileURLToPath(
|
||||
new URL('../subagent-report-quiet.cordis.yml', import.meta.url),
|
||||
const SUBAGENT_REPORT_CONFIG = fileURLToPath(
|
||||
new URL('../subagent-report.cordis.yml', import.meta.url),
|
||||
)
|
||||
const SUBAGENT_DURABILITY_FAILURE_CONFIG = fileURLToPath(
|
||||
new URL('../subagent-durability-failure.cordis.yml', import.meta.url),
|
||||
@@ -213,6 +222,17 @@ const SCENARIOS: Scenario[] = [
|
||||
headerClass: 'image',
|
||||
configPath: IMAGE_TEXT_ROUTE_CONFIG,
|
||||
},
|
||||
// Authored keyless replay of the oversized-image refusal: admission rejects
|
||||
// the 2001x1 fixture at the default 2000px per-side limit, the model sees a
|
||||
// recoverable tool error, and the turn still completes — the image never
|
||||
// enters durable history.
|
||||
{
|
||||
name: 'read-image-dimension',
|
||||
hasModelTurn: true,
|
||||
recorded: false,
|
||||
headerClass: 'image',
|
||||
configPath: IMAGE_CONFIG,
|
||||
},
|
||||
{
|
||||
name: 'inline-image-prompt',
|
||||
hasModelTurn: true,
|
||||
@@ -456,16 +476,15 @@ const SCENARIOS: Scenario[] = [
|
||||
configPath: SUBAGENT_DURABILITY_FAILURE_CONFIG,
|
||||
},
|
||||
// Authored child-to-parent transcript: the child calls its scope-local
|
||||
// `report`, and the runtime's unconditional settlement notice then wakes the
|
||||
// parked parent into one ordinary turn that claims both. The overlay pins
|
||||
// quiet report delivery because two independent wakes have no orderable
|
||||
// transcript; the shipped waking default is covered by package tests.
|
||||
// `report` through the shipped next-step policy. A maintenance fence holds
|
||||
// the parent until the runtime's unconditional settlement notice follows;
|
||||
// the resumed parent then claims both messages in causal order.
|
||||
{
|
||||
name: 'subagent-report',
|
||||
hasModelTurn: true,
|
||||
recorded: false,
|
||||
overridden: false,
|
||||
configPath: SUBAGENT_REPORT_QUIET_CONFIG,
|
||||
configPath: SUBAGENT_REPORT_CONFIG,
|
||||
pinsChildToolSchemas: [1],
|
||||
pinsChildSystemPrompts: [1],
|
||||
},
|
||||
@@ -645,6 +664,92 @@ defineAcpSnapshotSuite({
|
||||
hasPwsh,
|
||||
})
|
||||
|
||||
it('pins pi-ai image offload in the request sent by the assembled app', async () => {
|
||||
const requests: Record<string, unknown>[] = []
|
||||
const server = createServer((request: IncomingMessage, response: ServerResponse) => {
|
||||
let body = ''
|
||||
request.setEncoding('utf8')
|
||||
request.on('data', (chunk: string) => { body += chunk })
|
||||
request.on('end', () => {
|
||||
requests.push(JSON.parse(body) as Record<string, unknown>)
|
||||
response.writeHead(200, { 'content-type': 'text/event-stream' })
|
||||
response.end([
|
||||
'data: {"choices":[{"delta":{"role":"assistant","content":""},"index":0,"finish_reason":null}]}',
|
||||
'data: {"choices":[{"delta":{"content":"DONE"},"index":0,"finish_reason":null}]}',
|
||||
'data: {"choices":[{"delta":{},"index":0,"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":1}}',
|
||||
'data: [DONE]',
|
||||
'',
|
||||
].join('\n\n'))
|
||||
})
|
||||
})
|
||||
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve))
|
||||
const address = server.address()
|
||||
if (address === null || typeof address === 'string') throw new Error('image-offload snapshot server has no port')
|
||||
|
||||
const image = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC'
|
||||
const input: InputScript = {
|
||||
steps: [
|
||||
{ op: 'initialize' },
|
||||
{ op: 'newSession' },
|
||||
{
|
||||
op: 'promptContent',
|
||||
content: [
|
||||
{ type: 'text', text: 'Compare the older image ' },
|
||||
{ type: 'image', data: image, mimeType: 'image/png' },
|
||||
{ type: 'text', text: ' with the newer image ' },
|
||||
{ type: 'image', data: image, mimeType: 'image/png' },
|
||||
{ type: 'text', text: ', then reply with DONE.' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await runScenario(input, {
|
||||
agent: AGENT,
|
||||
mode: 'record',
|
||||
configPath: IMAGE_OFFLOAD_CONFIG,
|
||||
fixtureFile: join(SNAPSHOTS_DIR, 'image-offload-request', 'session.jsonl'),
|
||||
env: {
|
||||
DSH_SNAPSHOT_API_KEY: 'snapshot-key',
|
||||
DSH_SNAPSHOT_BASE_URL: `http://127.0.0.1:${address.port}/v1`,
|
||||
},
|
||||
})
|
||||
expect(result.stderr).toBe('')
|
||||
expect(requests).toHaveLength(1)
|
||||
const messages = requests[0]?.messages as { content?: unknown }[] | undefined
|
||||
const offloaded = messages?.find(message => JSON.stringify(message.content).includes('[image omitted'))
|
||||
expect(offloaded?.content).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"text": "Compare the older image ",
|
||||
"type": "text",
|
||||
},
|
||||
{
|
||||
"text": "[image omitted to keep the request within its image limit; older images are omitted first. If this image is still needed, read its file again when a path is available; otherwise ask the user to attach it again.]",
|
||||
"type": "text",
|
||||
},
|
||||
{
|
||||
"text": " with the newer image ",
|
||||
"type": "text",
|
||||
},
|
||||
{
|
||||
"image_url": {
|
||||
"url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC",
|
||||
},
|
||||
"type": "image_url",
|
||||
},
|
||||
{
|
||||
"text": ", then reply with DONE.",
|
||||
"type": "text",
|
||||
},
|
||||
]
|
||||
`)
|
||||
} finally {
|
||||
await new Promise<void>(resolve => server.close(() => { resolve() }))
|
||||
}
|
||||
}, 45_000)
|
||||
|
||||
it('packed ACP fixture retains every chunk row kind without changing the logical session', () => {
|
||||
const source = fixtureRecords(PACKED_CHUNKS_SOURCE)
|
||||
const packed = fixtureRecords('packed-chunks')
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user