Merge master into xtr/session-store-write-lifecycle

This commit is contained in:
_Kerman committed 2026-10-21 19:27:56 +08:00
commit 46fdc70d66
672 files changed
+13559 -5136

No files matched your search

@@ -24,8 +24,8 @@
en: 26e4b71366bebd1b
zh: 10598e12b28c586e
/agent-note-cooperative-tool-cancellation-at-the-registry-boundary/verification:
en: fbd101d4576afa92
zh: 35f5d3c2f6cad6a8
en: 983da6d0c09f5c10
zh: ca7c86117a1a4d5f
/agent-note-cooperative-tool-cancellation-at-the-registry-boundary/alternatives-considered:
en: c4d86feb135d2ac4
zh: 67c56c0bc2213949
@@ -46,7 +46,7 @@ This decision requires cancellation at the tool invocation boundary only. Making
## Verification
[`execution-signal-types.spec.ts`](../../../../packages/core/tools/tests/execution-signal-types.spec.ts) proves the required exact signal types, readonly observer and tool views, mutable-but-required around-dispatch view, and `defineTool()` inference. [`tools.spec.ts`](../../../../packages/core/tools/tests/tools.spec.ts) covers pre-aborted materialization, phase skipping, policy and wrapper races, body invocation classification, caller-signal fusion, error precedence, context retention, and quiescent drainage. [`tool-calls.spec.ts`](../../../../packages/core/agent-loop/tests/tool-calls.spec.ts) and [`contract-regressions.spec.ts`](../../../../packages/core/agent-loop/tests/contract-regressions.spec.ts) cover balanced durable results for undispatched siblings. [`ptc.spec.ts`](../../../../packages/core/tools/tests/ptc.spec.ts) and first-party integration suites cover explicit forwarding, while [`timeout-policy.spec.ts`](../../../../packages/guard/timeout-policy/tests/timeout-policy.spec.ts) preserves timeout ownership.
[`execution-signal-types.spec.ts`](../../../../packages/core/tools/tests/execution-signal-types.spec.ts) proves the required exact signal types, readonly observer and tool views, mutable-but-required around-dispatch view, and `defineTool()` inference. [`tools.spec.ts`](../../../../packages/core/tools/tests/tools.spec.ts) covers pre-aborted materialization, phase skipping, policy and wrapper races, body invocation classification, caller-signal fusion, error precedence, context retention, and quiescent drainage. [`tool-calls.spec.ts`](../../../../packages/core/agent-loop/tests/tool-calls.spec.ts) and [`contract-regressions.spec.ts`](../../../../packages/core/agent-loop/tests/contract-regressions.spec.ts) cover balanced durable results for undispatched siblings. [`ptc.spec.ts`](../../../../packages/core/tool-executor-ptc/tests/ptc.spec.ts) and first-party integration suites cover explicit forwarding, while [`timeout-policy.spec.ts`](../../../../packages/guard/timeout-policy/tests/timeout-policy.spec.ts) preserves timeout ownership.
No registry test can prove that arbitrary third-party same-process code observes the signal or stops in bounded time. Capability tests continue to prove cancellation and quiescence at the boundary that owns each side effect.
@@ -46,7 +46,7 @@ Status: implemented
## 验证
[`execution-signal-types.spec.ts`](../../../../packages/core/tools/tests/execution-signal-types.spec.ts) 证明必填的精确信号类型、观察者与工具的只读视图、环绕调度可替换但不可删除的视图,以及 `defineTool()` 推断。[`tools.spec.ts`](../../../../packages/core/tools/tests/tools.spec.ts) 覆盖进入时已中止的物化与阶段跳过、策略和包装层竞态、工具主体调用分类、调用方信号融合、错误优先级、上下文保留和完全停稳。[`tool-calls.spec.ts`](../../../../packages/core/agent-loop/tests/tool-calls.spec.ts) 与 [`contract-regressions.spec.ts`](../../../../packages/core/agent-loop/tests/contract-regressions.spec.ts) 覆盖为未调度的同批调用补齐持久化结果。[`ptc.spec.ts`](../../../../packages/core/tools/tests/ptc.spec.ts) 和第一方集成测试覆盖显式转发,[`timeout-policy.spec.ts`](../../../../packages/guard/timeout-policy/tests/timeout-policy.spec.ts) 保持超时归属。
[`execution-signal-types.spec.ts`](../../../../packages/core/tools/tests/execution-signal-types.spec.ts) 证明必填的精确信号类型、观察者与工具的只读视图、环绕调度可替换但不可删除的视图,以及 `defineTool()` 推断。[`tools.spec.ts`](../../../../packages/core/tools/tests/tools.spec.ts) 覆盖进入时已中止的物化与阶段跳过、策略和包装层竞态、工具主体调用分类、调用方信号融合、错误优先级、上下文保留和完全停稳。[`tool-calls.spec.ts`](../../../../packages/core/agent-loop/tests/tool-calls.spec.ts) 与 [`contract-regressions.spec.ts`](../../../../packages/core/agent-loop/tests/contract-regressions.spec.ts) 覆盖为未调度的同批调用补齐持久化结果。[`ptc.spec.ts`](../../../../packages/core/tool-executor-ptc/tests/ptc.spec.ts) 和第一方集成测试覆盖显式转发,[`timeout-policy.spec.ts`](../../../../packages/guard/timeout-policy/tests/timeout-policy.spec.ts) 保持超时归属。
任何注册表测试都无法证明任意第三方同进程代码会观察信号或在有界时间内停止。各能力的测试仍需在拥有相应副作用的边界证明取消与完全停稳。
@@ -54,8 +54,8 @@
en: 7865e159528e2d30
zh: 1399d54632f4d926
/agent-note-peer-principals-and-method-level-remote-access-control/decision/method-level-access-control/access-snapshot-stream:
en: 41d7ac1faf2448d7
zh: 6d523895a9abae68
en: a928a63e8c36177c
zh: c943c1fdc8c4bb45
/agent-note-peer-principals-and-method-level-remote-access-control/decision/method-level-access-control/client-face:
en: 7be28cd7e19e5377
zh: 30b5aee957271d6c
@@ -81,8 +81,8 @@
en: 89452c3c3f042e75
zh: 9bcad00a60c65303
/agent-note-peer-principals-and-method-level-remote-access-control/consequences/verification:
en: 048067eb02e6bf8a
zh: 12508b59f6b206b8
en: 2279a8c153cc5b89
zh: 63c0b6462722f0a3
/agent-note-peer-principals-and-method-level-remote-access-control/consequences/costs-and-limits:
en: 1e254cec680b1a37
zh: 06a31e057ba965aa
@@ -446,7 +446,7 @@ export const REMOTE_ACCESS_SNAPSHOT_KEY = '__DSH_REMOTE_ACCESS__'
| Behavior | Detail |
|---|---|
| First snapshot | `snapshotFor(principal)` computes a page's opening baseline. remote-access adds the global `REMOTE_ACCESS_SNAPSHOT_KEY`, set to the owner baseline `{ type: 'baseline', owner: true }`, to every index the webServer renders, through `webserver/index-inject`. A business package that renders a non-owner page (such as liveshare on the owner Host) places an assignment of that global to `snapshotFor(principal)` after every index injection row; the later assignment wins, and the Client reads the global when it mounts |
| First snapshot | `snapshotFor(principal)` computes a page's opening baseline. The webServer renders each index for the Peer the page speaks for, and `webserver/index-inject` passes that Peer to listeners; remote-access adds the global `REMOTE_ACCESS_SNAPSHOT_KEY`, set to `snapshotFor(peer.principal)`, to every index, so the owner's page receives the owner baseline `{ type: 'baseline', owner: true }` and a page rendered for a non-owner Peer (such as a liveshare viewer page on the owner Host) receives that principal's grants. The Client reads the global when it mounts |
| Subscription | Each Peer subscribes to one access snapshot stream, whose baseline replaces the injected one |
| Computation | All bindings of the policy for the principal's kind are merged by equal resource; each resource's `actions` are the endpoint actions of those bindings' roles that the same decision as a call allows (one resource, without args, conditions called without args); only a resource with at least one allowed endpoint action gets a grant. Decisions and snapshots share one source: for the same principal, action, and resource (without arguments), the snapshot answer equals `decide()` |
| Endpoint actions only | `event:<name>` actions do not enter the snapshot; the frontend decides entry visibility from endpoint actions only |
@@ -638,7 +638,7 @@ The review rule's home is the "`@Remote` marks capability methods" section of [R
| Resource resolution | A default field (top-level or first object argument) yields its resource; a lookup parameter that declares that kind as its `resource` yields its wire identity; a Context method gets its Session resource from the Agent Context identity in args without activating the Agent, and may declare other resources through `resources`; a `resources` reader overrides its kind, an array yields several resources, and `undefined` none; with nothing the call is `[global]`; a throw, an empty array, or a value that is not a non-empty string (including a Context identity) is invalid, refused by the Gateway with `resource-invalid` without asking `decide()`; a Context that declares no `resource` resolves by the ordinary rules; resources and `args` read the validated arguments, so an undeclared nested field changes no resource and no condition sees it, and a JSON argument failing validation returns `gateway/input-invalid` before the access check | Gateway host spec (gateway-access) |
| Resource registry | `typert.resources` registers one default field per kind, rejects a second registration and `global`, and withdraws the registration with its fiber; a configured lookup resolver keeps the provider's `resource`; a Host Context adapter keeps its `resource` | typert-registry spec |
| Forwarded events | A non-owner `$events` stream receives an event, whole, only when `event:<name>` is granted on its resources; an event that declares no resources is decided on `global`; an event whose reader returns `undefined` does not reach non-owners, and `decide()` is not asked; the owner receives all | Gateway host spec (gateway-access) |
| First snapshot | `snapshotFor()` returns the owner baseline or the principal's grants; remote-access injects the owner baseline into every rendered index; the Client starts from the injected baseline and rejects a malformed one | remote-access host and client specs |
| First snapshot | `snapshotFor()` returns the owner baseline or the principal's grants; remote-access injects the snapshot of the Peer each index speaks for; the Client starts from the injected baseline and rejects a malformed one | remote-access host and client specs |
| Type inference | The parameter types of `resources` readers are inferred from the method; lookup positions have wire types; a missing property, more parameters than the method has, and a `global` key fail compilation; a binding whose resource kind differs from its role's fails compilation | typert-protocol spec, remote-access spec (type tests) |
| List filtering | `session/list`, `session/search`, `session/control`, and `workspace/follow` return non-owners only items for which `allows()` is true; `workspace/follow` sends a `replace` increment when a delivered Workspace loses visibility, and the Workspace reappears once access returns; owner results are unchanged | session-controller / workspace-controller host spec |
| Access snapshots | The allowed set in a snapshot equals the Host decision for the same principal, action, and resource without arguments; it contains endpoint actions only; a resource with no allowed action gets no grant, and one whose allowed set becomes empty is removed; `invalidate()` and policy registration or removal push increments; a wait that a change ends leaves no abort listener behind; unloading the service ends every open snapshot stream; the owner receives only `{ owner: true }` | remote-access host spec |
@@ -446,7 +446,7 @@ export const REMOTE_ACCESS_SNAPSHOT_KEY = '__DSH_REMOTE_ACCESS__'
| 行为 | 说明 |
|---|---|
| 首份快照 | `snapshotFor(principal)` 计算页面打开时的 baseline。remote-access 经 `webserver/index-inject` 为 webServer 渲染的每个 index 加入全局变量 `REMOTE_ACCESS_SNAPSHOT_KEY`,值为 owner baseline `{ type: 'baseline', owner: true }`。渲染非 owner 页面的业务包(例如 owner Host 上的 liveshare)在所有 index 注入行之后放置一条把该全局变量赋值为 `snapshotFor(principal)` 的语句;后写的赋值生效,Client 在挂载时读取该全局变量 |
| 首份快照 | `snapshotFor(principal)` 计算页面打开时的 baseline。webServer 按页面所代表的 Peer 渲染 index,`webserver/index-inject` 把该 Peer 传给监听方;remote-access 为每个 index 加入全局变量 `REMOTE_ACCESS_SNAPSHOT_KEY`,值为 `snapshotFor(peer.principal)`,owner 页面因此得到 owner baseline `{ type: 'baseline', owner: true }`,为非 owner Peer 渲染的页面(例如 owner Host 上的 liveshare 读者页)得到该 principal 的 grant。Client 在挂载时读取该全局变量 |
| 订阅 | 每个 Peer 订阅一条访问快照流,其 baseline 取代注入的快照 |
| 计算 | 取该 principal kind 的 policy 的全部 binding,按资源相等归并;每个资源的 `actions` 为这些 binding 的 role 中、经与调用相同的判定(单个资源、不带 args,condition 以无 args 调用)放行的端点 action;只为至少放行一个端点 action 的资源给出 grant。判定与快照同源:同一 principal、action 与资源(不带参数)上,快照答案与 `decide()` 一致 |
| 只含端点 action | `event:<name>` action 不进入快照;前端只据端点 action 决定入口显隐 |
@@ -638,7 +638,7 @@ GlobalStandardProps.useWorkspaceAccess: (workspaceId: WorkspaceId, action: Remot
| 资源解析 | 默认字段(顶层或首个对象参数)得到对应资源;声明了该种类 `resource` 的 lookup 参数得到其 wire 身份;Context 方法从 args 中的 Agent Context 身份得到其 session 资源且不激活 Agent,另可经 `resources` 声明其他资源;`resources` 读取函数覆盖该种类,数组为多个资源,`undefined` 为无;都没有为 `[global]`;抛错、空数组或非非空字符串(含 Context 身份)为非法,Gateway 以 `resource-invalid` 拒绝且不询问 `decide()`;未声明 `resource` 的 Context 按普通规则解析;资源与 `args` 读取校验后的参数,未声明的嵌套字段不改变资源、condition 也读不到它,JSON 参数校验失败在访问检查前返回 `gateway/input-invalid` | Gateway host spec(gateway-access) |
| 资源注册表 | `typert.resources` 每个种类登记一个默认字段,拒绝重复登记与 `global`,并随 fiber 撤回;配置的 lookup resolver 保留 provider 的 `resource`;Host Context adapter 保留其 `resource` | typert-registry spec |
| 转发事件 | 非 owner 的 `$events` 流只在 `event:<name>` 于其资源上被授权时整条收到事件;未声明资源的事件按 `global` 判定;读取函数返回 `undefined` 的事件不送达非 owner,且不询问 `decide()`;owner 全部收到 | Gateway host spec(gateway-access) |
| 首份快照 | `snapshotFor()` 返回 owner baseline 或该 principal 的 grant;remote-access 为渲染的每个 index 注入 owner baseline;Client 从注入的 baseline 起步并拒绝格式错误的快照 | remote-access host 与 client spec |
| 首份快照 | `snapshotFor()` 返回 owner baseline 或该 principal 的 grant;remote-access 为每个 index 注入其所代表 Peer 的快照;Client 从注入的 baseline 起步并拒绝格式错误的快照 | remote-access host 与 client spec |
| 类型推断 | `resources` 读取函数的参数类型从方法推断;lookup 位为 wire 类型;访问不存在的属性、参数多于方法参数与 `global` 键编译失败;binding 的资源种类与 role 不符编译失败 | typert-protocol spec、remote-access spec(类型测试) |
| 列表过滤 | `session/list`、`session/search`、`session/control`、`workspace/follow` 对非 owner 只返回 `allows()` 为 true 的项;已下发工作区失去可见性时 `workspace/follow` 发送 `replace` 增量帧,访问恢复后该工作区重新出现;owner 结果不变 | session-controller / workspace-controller host spec |
| 访问快照 | 快照中的允许集合与 Host 对同一 principal、action、资源(不带参数)的判定一致;只含端点 action;没有放行 action 的资源不出 grant,放行集合变空时推送 `remove`;`invalidate()` 与 policy 注册、撤回后推送增量;被变化结束的等待不残留 abort 监听器;卸载服务即结束所有打开的快照流;owner 只收 `{ owner: true }` | remote-access host spec |
@@ -0,0 +1,28 @@
# Bilingual-pair consistency record for 2026-10-21-when-access.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-21-when-access.md
/agent-note-whenaccess-registration-that-follows-access:
en: 2ef7cfef514c138a
zh: 064e3aecbfafcd9c
/agent-note-whenaccess-registration-that-follows-access/problem:
en: 4f88d3ee7868eaa5
zh: 72a4ce2dde13bef7
/agent-note-whenaccess-registration-that-follows-access/decision:
en: a15c7c13f167e5ac
zh: 63bcc2ef60c2f344
/agent-note-whenaccess-registration-that-follows-access/decision/whenaccess:
en: c168de20989cf9a4
zh: bf3b0361beea083d
/agent-note-whenaccess-registration-that-follows-access/decision/plugin-migration:
en: 73577ed282c656f3
zh: 2f60a6824218e693
/agent-note-whenaccess-registration-that-follows-access/alternatives-considered:
en: ddc432b693608163
zh: 620294d8812b0a31
/agent-note-whenaccess-registration-that-follows-access/consequences:
en: de44fa92c49bc335
zh: 34436fe971ab013f
/agent-note-whenaccess-registration-that-follows-access/related:
en: 89073ffbfcff0641
zh: 736a180d13a08981
@@ -0,0 +1,88 @@
# Agent Note: whenAccess registration that follows access
Status: implemented
English | [中文](2026-10-21-when-access.zh.md)
## Problem
Client plugins need to switch their own registrations on and off with the current Peer's access: sidebar entries, main panels, overlays, and data streams. The existing Client access surface covers only rendering:
| Where | Existing capability | Gap |
|---|---|---|
| Components | `useRemoteAccess(action)` (global), `useSessionAccess(sessionId, action)`, `useWorkspaceAccess(workspaceId, action)`, and the Session-scope `useAccess(action)` | Readable only during render; a changed answer re-renders automatically |
| JS (`apply`, services) | `ctx.remoteAccess.source(kind, id, action)` returns a `HostObservable<boolean>` | A plugin must read `getSnapshot` and subscribe to changes itself, then register and dispose by hand |
Without a JS entry point, a plugin can only decide its registrations once by identity at `apply`, and they do not follow an access change.
## Decision
### whenAccess
The JS side has a `whenAccess` for each of the three access Hooks, from the package that publishes that Hook:
| Interface | Provider | Matching Hook |
|---|---|---|
| `ctx.remoteAccess.whenAccess(action, callback)` | remote-access | `useRemoteAccess(action)` |
| `ctx.uiSession.whenAccess(sessionId, action, callback)` | ui-session | `useSessionAccess(sessionId, action)` |
| `ctx.uiWorkspace.whenAccess(workspaceId, action, callback)` | ui-workspace | `useWorkspaceAccess(workspaceId, action)` |
```ts ignore-check
import type { AccessEffect, RemoteEndpoint } from '@deepseek-ai/dsh-api-remote-access/client'
/** The global-resource member; the Session and Workspace versions take the resource id first. */
export interface WhenAccess {
/**
* Keep `callback`'s effects installed while the current Peer may call `action`
* on the resource: run it when the answer becomes true, dispose its effects in
* reverse order when it becomes false, and run it again on the next true.
* @returns disposer for the observation and any installed effects.
*/
whenAccess(action: RemoteEndpoint, callback: () => AccessEffect): () => void
}
```
`AccessEffect` is `(() => void) | Iterable<() => void, void, void>`.
| Rule | Content |
|---|---|
| Semantics | Same as `ctx.slots.inject`: the callback returns one disposer or an iterable of disposers; an iterable installs as one transaction and disposes in reverse order; the controller belongs to the caller's fiber and disposes when the plugin unloads |
| Synchronous run | When the answer is already true, the callback runs synchronously; the owner's answer is always true, so registration order matches direct registration |
| Stream failure | A non-owner's answer turns false and the installed effects dispose |
| Callback failure | A setup failure at the call throws synchronously; a setup failure on a later grant stops the controller and is rethrown in a microtask |
| Layering | remote-access knows only `global`; the Session and Workspace versions are built over remote-access's lower-level `whenResourceAccess(kind, id, action, callback)`, as their Hooks are built over `source(kind, id, action)`. Only the package that owns a resource kind calls `source` and `whenResourceAccess`; other plugins use only the access Hooks and `whenAccess` and pass no kind. All four entry points share [`when-allowed.ts`](../../../../packages/api/remote-access/src/client/when-allowed.ts) |
### Plugin migration
| Plugin | Change | Action |
|---|---|---|
| ui-schedule | The task catalog source, delete toast overlay, right-sidebar task tab and title, main panel, sidebar entry, turn-tail card, Session-header reminder catalog, and Session row mark and hover list move into `remoteAccess.whenAccess`; the Session-header catalog reads the per-Session `schedule.list`, so its component also decides through the Session-scope `useAccess` whether to render; the task tab type and the turn definition stay registered outside | `schedule/catalog`, `schedule/list` |
| ui-plugin-manager | Host change subscriptions, refresh toast overlay, main panel (including `pluginNavigation`), and sidebar entry move into `remoteAccess.whenAccess`, and each grant refreshes a list already loaded; the controller and `pluginChanges` stay outside, and `pluginChanges` receives no updates while access is withdrawn. ui-entry-customization and voice-input use `pluginNavigation` optionally: without the service they hide the browse-plugins and open/details buttons and still start | `pluginManager/listBundles` |
| ui-settings-general | The settings shell `sidebar.settings` and the `settings.open` shortcut move into `remoteAccess.whenAccess` | `settings/describe` |
| ui-settings-account | The account state stream (including default model initialization), quota notice, settings launcher, account settings section, and `settings.models.sign-in` move into `remoteAccess.whenAccess`. On revocation the stream's disposer synchronously invalidates the account reads this grant started, closes the pending authorization tab, clears the account view, details, picture, and sign-in dialog state, ends this generation's bonus notices, which a frame of that stream begins, and then closes the stream; a frame delivered before the revocation but not yet handled is dropped. The notice controller stays outside and begins again under the next grant; the account section appears once the new stream's first frame arrives | `account/watch` |
| ui-directory-picker-native | Both directory flows move into `remoteAccess.whenAccess` | `directoryPicker/pick` |
| ui-message-feedback | The message feedback buttons stay registered and the component decides whether to render through the Session-scope `useAccess`; `messageFeedback/*` requests carry `sessionId`, which the Gateway resolves to that Session through the `session` resource's default field, so they declare nothing. The `/feedback` decoration has no Client check: the decoration registry is global while `sessionFeedback/record` is decided per Session, so without access the dialog still opens and the Host refuses the submission, which the dialog reports with its failure code | `messageFeedback/put` |
| ui-deliverables, ui-open-in-app | Unchanged: an owner-only HTTP route that cannot be read falls back as before | — |
## Alternatives considered
**Return after an owner check at the start of `apply` (the #6486 approach)**: fixed on identity; does not follow an access change.
**Trim the plugin list per connection**: a plugin cannot reload at once when access changes.
**Declare access in the plugin package manifest**: defines an access-control concept in the package manifest.
**One generic method with a resource parameter, or all three versions on remote-access**: business code would pass a kind, and remote-access would name the Session and Workspace resource kinds.
## Consequences
| Consequence | Note |
|---|---|
| Registrations follow access changes | Without a grant, a migrated plugin registers none of the corresponding entries for a non-owner; they appear after a grant and disappear after a revocation; the owner's answer is always true, so its registrations match direct registration |
| The callback must hand back disposers | An effect registered through the plugin's `ctx` inside the callback also hangs on the plugin fiber; a registration whose disposer is not yielded does not dispose on access loss, only when the plugin unloads |
| Readers receive all plugin code | Plugins load as before; the Gateway decides each call, and `whenAccess` and the Hooks control the UI |
| Test stand-ins | [`dsh-client-test-runtime`](../../../../packages/test-support/client-runtime/README.md) replaces remote-access with fixed answers through `StaticRemoteAccess`, `staticAccessPlugin`, and `TestClientOptions.access`, so plugin specs cover the refused path |
## Related
- [Peer, principal, and remote access](2026-10-12-peer-principal-and-remote-access.md): this Note extends its Client access face.
@@ -0,0 +1,88 @@
# Agent Note: 按访问状态注册的 whenAccess
Status: implemented
[English](2026-10-21-when-access.md) | 中文
## 问题
Client 插件需要按当前 Peer 的访问状态开关自己的注册:侧栏入口、主面板、浮层、数据流。现有 Client 访问面只覆盖渲染:
| 位置 | 现有能力 | 缺口 |
|---|---|---|
| 组件 | `useRemoteAccess(action)`(global)、`useSessionAccess(sessionId, action)`、`useWorkspaceAccess(workspaceId, action)`、会话作用域的 `useAccess(action)` | 只能在渲染时读,答案变化时自动重渲染 |
| JS(`apply`、服务) | `ctx.remoteAccess.source(kind, id, action)` 返回 `HostObservable<boolean>` | 插件要自己读 `getSnapshot`、订阅变化,再手动注册与撤销 |
没有 JS 侧入口时,插件只能在 `apply` 时按身份一次性决定注册与否,授权变化后注册不跟随。
## 决策
### whenAccess
JS 侧有与三个访问 Hook 一一对应的 `whenAccess`,由各自 Hook 的发布者提供:
| 接口 | 提供者 | 对应 Hook |
|---|---|---|
| `ctx.remoteAccess.whenAccess(action, callback)` | remote-access | `useRemoteAccess(action)` |
| `ctx.uiSession.whenAccess(sessionId, action, callback)` | ui-session | `useSessionAccess(sessionId, action)` |
| `ctx.uiWorkspace.whenAccess(workspaceId, action, callback)` | ui-workspace | `useWorkspaceAccess(workspaceId, action)` |
```ts ignore-check
import type { AccessEffect, RemoteEndpoint } from '@deepseek-ai/dsh-api-remote-access/client'
/** The global-resource member; the Session and Workspace versions take the resource id first. */
export interface WhenAccess {
/**
* Keep `callback`'s effects installed while the current Peer may call `action`
* on the resource: run it when the answer becomes true, dispose its effects in
* reverse order when it becomes false, and run it again on the next true.
* @returns disposer for the observation and any installed effects.
*/
whenAccess(action: RemoteEndpoint, callback: () => AccessEffect): () => void
}
```
`AccessEffect` 为 `(() => void) | Iterable<() => void, void, void>`。
| 规则 | 内容 |
|---|---|
| 语义 | 与 `ctx.slots.inject` 相同:回调返回一个 disposer 或一组 disposer;一组时按事务安装、逆序撤销;控制器属于调用方 fiber,插件卸载时撤销 |
| 同步执行 | 调用时答案为 true 则回调同步执行;owner 的答案恒为 true,注册顺序与直接注册相同 |
| 快照流失败 | 非 owner 的答案变为 false,已安装的效果撤销 |
| 回调失败 | 调用时安装失败同步抛出;之后重新授权时安装失败,控制器停止并在微任务中抛出 |
| 分层 | remote-access 只认识 global;会话版与工作区版经 remote-access 的底层方法 `whenResourceAccess(kind, id, action, callback)` 实现,与它们用 `source(kind, id, action)` 构造 Hook 相同。`source` 与 `whenResourceAccess` 只由拥有该资源种类的包调用,其他插件只用访问 Hook 与 `whenAccess`,不传 kind。四个入口共用 [`when-allowed.ts`](../../../../packages/api/remote-access/src/client/when-allowed.ts) |
### 插件迁移
| 插件 | 改法 | action |
|---|---|---|
| ui-schedule | 任务列表数据源、删除提示浮层、右栏任务 tab 与标题、主面板、侧栏入口、turn 尾部卡片、会话头部提醒、会话行标记与悬浮列表包进 `remoteAccess.whenAccess`;会话头部提醒读取按会话判定的 `schedule.list`,组件另在会话作用域里用 `useAccess` 决定是否渲染;任务 tab 类型与 turn 定义的注册留在外面 | `schedule/catalog`、`schedule/list` |
| ui-plugin-manager | Host 变更订阅、刷新提示浮层、主面板(含 `pluginNavigation`)、侧栏入口包进 `remoteAccess.whenAccess`,每次授权时刷新已加载过的列表;controller 与 `pluginChanges` 留在外面,撤权期间 `pluginChanges` 不更新。依赖 `pluginNavigation` 的 ui-entry-customization 与 voice-input 改为可选使用:服务不在时隐藏「浏览插件」与「打开 / 查看详情」按钮,插件照常启动 | `pluginManager/listBundles` |
| ui-settings-general | 设置外壳 `sidebar.settings` 与 `settings.open` 快捷键包进 `remoteAccess.whenAccess` | `settings/describe` |
| ui-settings-account | 账号状态流(含默认模型初始化)、额度提醒、设置启动器、账号设置分区与 `settings.models.sign-in` 包进 `remoteAccess.whenAccess`。撤权时账号流的 disposer 同步作废这一轮发出的账号读取,关闭待授权的浏览器标签页,清空账号视图、详情、头像与登录对话框状态,结束本轮奖励提示(提示由该流的帧开始),再关闭流;撤权前已交付但尚未处理的帧被丢弃。提示控制器留在外面,下次授权时重新开始;账号分区等新流的第一帧到达后才出现 | `account/watch` |
| ui-directory-picker-native | 两个目录选择流程包进 `remoteAccess.whenAccess` | `directoryPicker/pick` |
| ui-message-feedback | 消息反馈按钮照常注册,组件在会话作用域里用 `useAccess` 判断是否渲染;`messageFeedback/*` 的请求带 `sessionId`,Gateway 按 `session` 资源的默认字段自动解析为该 Session,无需声明。`/feedback` 装饰不做前端判断:装饰注册表是全局的,而 `sessionFeedback/record` 按会话判定;没有权限时对话框照常打开,提交被 Host 拒绝并显示失败码 | `messageFeedback/put` |
| ui-deliverables、ui-open-in-app | 不动:owner 专用 HTTP 路由读不到时走已有回退 | — |
## 考虑过的替代方案
**插件 `apply` 开头判断 owner 后 return(#6486 的做法)**:按身份写死,授权变化后不跟随。
**按连接裁剪插件列表**:授权变化后插件无法立即重新加载。
**插件包清单声明访问性**:在包清单上定义访问控制概念。
**一个带资源参数的通用方法,或三个版本都挂在 remote-access 上**:业务代码要传 kind,且 remote-access 会出现会话、工作区的资源种类名。
## 后果
| 后果 | 说明 |
|---|---|
| 注册跟随授权变化 | 迁移的插件在非 owner 无授权时不注册相应入口,授予后出现,撤销后消失;owner 的答案恒为 true,注册与直接注册相同 |
| 回调必须交还 disposer | 回调里经插件 `ctx` 注册的效果也挂在插件 fiber 上;不 yield 其 disposer 的注册在失权时不会撤销,只在插件卸载时撤销 |
| 读者拿到全部插件代码 | 插件照常加载;访问由 Gateway 逐次判定,界面由 `whenAccess` 与 Hook 控制 |
| 测试替身 | [`dsh-client-test-runtime`](../../../../packages/test-support/client-runtime/README.zh.md) 的 `StaticRemoteAccess`、`staticAccessPlugin` 与 `TestClientOptions.access` 以固定答案替换 remote-access,供插件测试无权限路径 |
## 相关
- [Peer、principal 与 remote-access](2026-10-12-peer-principal-and-remote-access.zh.md):本 Note 扩展其 Client 访问面。
+6
View File
@@ -188,6 +188,10 @@ flowchart LR
cfg --> plugin_dsh_base_mcp_resources
plugin_dsh_base_tools["tools<br/>@deepseek-ai/dsh-tools"]
cfg --> plugin_dsh_base_tools
plugin_dsh_base_tool_executor_native["tool-executor-native<br/>@deepseek-ai/dsh-tool-executor-native"]
cfg --> plugin_dsh_base_tool_executor_native
plugin_dsh_base_tool_executor_ptc["tool-executor-ptc<br/>@deepseek-ai/dsh-tool-executor-ptc"]
cfg --> plugin_dsh_base_tool_executor_ptc
plugin_dsh_base_system_prompt["system-prompt<br/>@deepseek-ai/dsh-system-prompt"]
cfg --> plugin_dsh_base_system_prompt
plugin_dsh_base_agent_loop["agent-loop<br/>@deepseek-ai/dsh-agent-loop"]
@@ -292,6 +296,8 @@ flowchart LR
| `tool-web` | `@deepseek-ai/dsh-tool-web` |
| `mcp-resources` | `@deepseek-ai/dsh-mcp-resources` |
| `tools` | `@deepseek-ai/dsh-tools` |
| `tool-executor-native` | `@deepseek-ai/dsh-tool-executor-native` |
| `tool-executor-ptc` | `@deepseek-ai/dsh-tool-executor-ptc` |
| `system-prompt` | `@deepseek-ai/dsh-system-prompt` |
| `agent-loop` | `@deepseek-ai/dsh-agent-loop` |
| `fs-sandbox` | `@deepseek-ai/dsh-fs-sandbox` |
+5
View File
@@ -0,0 +1,5 @@
- id: tool-executor-native
disabled: true
- id: tool-executor-ptc
disabled: false
+1
View File
@@ -155,6 +155,7 @@
"@deepseek-ai/dsh-subagent-spawn-in-process": "workspace:*",
"@deepseek-ai/dsh-subprocess-local": "workspace:*",
"@deepseek-ai/dsh-system-prompt": "workspace:*",
"@deepseek-ai/dsh-tool-testkit": "workspace:*",
"@deepseek-ai/dsh-tools": "workspace:*",
"@deepseek-ai/dsh-user-approval": "workspace:*",
"@types/js-yaml": "^4.0.9",
+2 -2
View File
@@ -21,8 +21,8 @@
en: 8d2cfff9bab7327f
zh: 21515ab6e6798504
/dsh-cli-behavior-reference/web-profile:
en: 850434e8ca162221
zh: 4893613f97a21eb8
en: 149c5571acd51baf
zh: 300e3c2425273a92
/dsh-cli-behavior-reference/shared-deployment-behavior:
en: f40edcf903a51f3f
zh: 8a9a915451047c8b
+1 -1
View File
@@ -130,7 +130,7 @@ The base-backed modes treat the invoking directory as the default workspace root
New sessions in base-backed profiles default to the `workspace-write` permission preset. Bash and filesystem mutations are restricted to the session workspace and platform temporary roots; reads and network access are not confined, while process visibility depends on the selected sandbox backend — bwrap runs commands in a private PID namespace that hides host processes, and Landlock and Seatbelt leave host process visibility unchanged. `DSH_PERMISSION_MODE` changes the process fallback. Stored General-settings permissions affect later Web sessions, not an already-open one. The standalone `sdk-minimal` tree instead pins `danger-full-access` and mounts no approval or permission-settings service.
`DSH_TOOLS_MODE` selects `native` or `ptc` for the process; another value fails at boot. The shipped `minimal` agent preset keeps that deployment presentation, fixes the complete system prompt to `You are a helpful software engineer assistant.`, and provides the platform-selected persistent shell, `read_image`, and `working_directory`. It supports automatic context compaction and the human `/compact` command. Select 极简模式 when creating a Web session; the shared browser, workspace, persistence, sandbox, and permission host remains in place.
Web, headless, and SDK inherit native tool execution from the base bundle; use a [PTC overlay](../config/examples/ptc.yml) to select PTC. The shipped `minimal` agent preset keeps that deployment presentation, fixes the complete system prompt to `You are a helpful software engineer assistant.`, and provides the platform-selected persistent shell, `read_image`, and `working_directory`. It supports automatic context compaction and the human `/compact` command. Select 极简模式 when creating a Web session; the shared browser, workspace, persistence, sandbox, and permission host remains in place.
## Shared deployment behavior
+1 -1
View File
@@ -132,7 +132,7 @@ dsh web --help
基于 base 的 profile 中,新会话默认使用 `workspace-write` 权限预设。Bash 和文件系统修改仅限于会话 workspace 与平台临时根目录;读取和网络访问不受限制,进程可见性则取决于所选沙箱后端——bwrap 在私有 PID 命名空间中运行命令并隐藏宿主进程,Landlock 与 Seatbelt 保持宿主进程可见性不变。`DSH_PERMISSION_MODE` 更改进程后备值。General settings 中存储的权限影响后续 Web 会话,不改变已打开的会话。独立的 `sdk-minimal` 配置树则固定为 `danger-full-access`,且不挂载 approval 或权限 settings 服务。
`DSH_TOOLS_MODE` 为进程选择 `native` 或 `ptc`;其他值会导致启动失败。随附的 `minimal` agent preset 会保留该部署的呈现方式,将完整系统提示词固定为 `You are a helpful software engineer assistant.`,并提供按平台选择的持久 shell、`read_image` 和 `working_directory`。它支持自动上下文压缩和用户 `/compact` 命令。创建 Web 会话时请选择极简模式;共享的浏览器、workspace、持久化、沙箱与权限宿主保持不变。
Web、headless 与 SDK 继承 base bundle 的原生工具执行;使用 [PTC overlay](../config/examples/ptc.yml) 可选择 PTC。随附的 `minimal` agent preset 会保留该部署的呈现方式,将完整系统提示词固定为 `You are a helpful software engineer assistant.`,并提供按平台选择的持久 shell、`read_image` 和 `working_directory`。它支持自动上下文压缩和用户 `/compact` 命令。创建 Web 会话时请选择极简模式;共享的浏览器、workspace、持久化、沙箱与权限宿主保持不变。
## 共享部署行为
+1
View File
@@ -1268,6 +1268,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
['session-title', '@deepseek-ai/dsh-session-title'],
['system-prompt', '@deepseek-ai/dsh-system-prompt'],
['tools', '@deepseek-ai/dsh-tools'],
['tool-executor-native', '@deepseek-ai/dsh-tool-executor-native'],
['mcp-resources', '@deepseek-ai/dsh-mcp-resources'],
['agent', '@deepseek-ai/dsh-agent'],
['llm-retry', '@deepseek-ai/dsh-llm-retry'],
+2 -1
View File
@@ -1,3 +1,4 @@
import { executeToolFixture } from '@deepseek-ai/dsh-tool-testkit'
import { fileURLToPath } from 'node:url'
import { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
import { ToolCallId } from '@deepseek-ai/dsh-llm'
@@ -52,7 +53,7 @@ try {
&& message.source.kind === 'skill-catalog')?.content
: undefined
const summary = (await ctx.skills.list()).find(skill => skill.name === 'dsh-badge')
const result = await ctx.tools.execute({
const result = await executeToolFixture(ctx, {
callId: ToolCallId('dsh-badge-snapshot'),
name: 'skill',
arguments: { name: 'dsh-badge' },
@@ -1,4 +1,5 @@
/** Optional bundles through the shipped Web Loader, live package manager, and real Agent loop. */
import { executeToolFixture } from '@deepseek-ai/dsh-tool-testkit'
import { randomUUID } from 'node:crypto'
import { existsSync } from 'node:fs'
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
@@ -232,7 +233,7 @@ describe.skipIf(!existsSync(join(root, 'apps/cli/lib/bin.js')))('Official option
it('loads the actual badge asset only while its bundle is selected', async () => {
const owner = await agent()
const load = (callId: string) => ctx.tools.execute({
const load = (callId: string) => executeToolFixture(ctx, {
callId: ToolCallId(callId), name: 'skill', arguments: { name: 'dsh-badge' },
agent: owner.agent, signal: new AbortController().signal,
})
@@ -555,7 +555,7 @@ describe('headless stream-json snapshots', () => {
'--permission-mode', 'auto', '--approval-policy', 'never', '--json', 'Run the CLI probe.',
],
tsconfigPath,
env: { DSH_AUTO_REVIEW_TEST_MODE: mode, DSH_TOOLS_MODE: 'native', DSH_TELEMETRY_DISABLED: '1' },
env: { DSH_AUTO_REVIEW_TEST_MODE: mode, DSH_TELEMETRY_DISABLED: '1' },
inspect: async (cwd) => {
const logs = await persistedLogs(cwd, join(cwd, '.dsh', 'sessions'))
expect(logs).toHaveLength(1)
@@ -605,7 +605,7 @@ describe('headless stream-json snapshots', () => {
const first = await runLoaderSmoke({
label: 'headless Auto denied wake', cwd, binScript: dshBinScript, configPath: autoReviewOverlayPath,
binArgs: [...args, '--permission-mode', 'auto', '--approval-policy', 'never', 'Run the CLI probe.'], tsconfigPath,
env: { DSH_AUTO_REVIEW_TEST_MODE: 'deny', DSH_TOOLS_MODE: 'native', DSH_TELEMETRY_DISABLED: '1' },
env: { DSH_AUTO_REVIEW_TEST_MODE: 'deny', DSH_TELEMETRY_DISABLED: '1' },
})
const sessionId = parseJsonl(first.stdout)[0]?.sessionId
if (typeof sessionId !== 'string') throw new Error('missing Auto Session identity')
@@ -615,7 +615,7 @@ describe('headless stream-json snapshots', () => {
const second = await runLoaderSmoke({
label: 'headless Auto authorized wake', cwd, binScript: dshBinScript, configPath: autoReviewOverlayPath,
binArgs: [...args, '--session-id', sessionId, 'I authorize the exact CLI probe. Retry it.'], tsconfigPath,
env: { DSH_AUTO_REVIEW_TEST_MODE: 'allow', DSH_TOOLS_MODE: 'native', DSH_TELEMETRY_DISABLED: '1' },
env: { DSH_AUTO_REVIEW_TEST_MODE: 'allow', DSH_TELEMETRY_DISABLED: '1' },
inspect: async (directory) => {
const logs = await persistedLogs(directory, join(directory, '.dsh', 'sessions'))
expect(logs).toHaveLength(1)
@@ -40,7 +40,7 @@ interface ResolutionEvidence {
sourceToolsCjs: string | null
sourceLeafCjs: string
sourceLeafExplicitCjs: string
scheduler: boolean
executor: boolean
modules: string[]
}
@@ -139,7 +139,7 @@ export function testProfileResolution(mode: ExampleMode): void {
const staleTools = join(root, 'stale-tools')
const staleToolsLink = join(sharedModules, '@deepseek-ai', 'dsh-tools')
await writePackage(staleTools, { name: '@deepseek-ai/dsh-tools', version: '0.0.0', exports }, {
'index.mjs': "export default class Tools {}\nexport const TOOL_RUNTIME_SCHEDULER = Symbol()\nthrow new Error('STALE_DSH_TOOLS')",
'index.mjs': "export default class Tools {}\nthrow new Error('STALE_DSH_TOOLS')",
'index.cjs': "throw new Error('STALE_DSH_TOOLS')",
})
const sourceDir = join(sourcePackageDir, 'src')
@@ -183,7 +183,7 @@ export function testProfileResolution(mode: ExampleMode): void {
'probe.mjs': [
"import { createRequire } from 'node:module'",
"import { getEnvironmentData } from 'node:worker_threads'",
"import Tools, { TOOL_RUNTIME_SCHEDULER } from '@deepseek-ai/dsh-tools'",
"import Tools from '@deepseek-ai/dsh-tools'",
`import { leaf } from '${leafName}'`,
`import { leaf as bridgeLeaf } from '${bridgeName}'`,
`import { external } from '${externalName}'`,
@@ -214,7 +214,7 @@ export function testProfileResolution(mode: ExampleMode): void {
' sourceToolsInstance: ctx.tools instanceof SourceTools,',
` profileToolsCjs: ${toolsCjsExpression},`,
' sourceToolsCjs, sourceLeafCjs, sourceLeafExplicitCjs,',
" scheduler: typeof ctx.tools[TOOL_RUNTIME_SCHEDULER]?.prepare === 'function',",
" executor: typeof ctx.tools.execute === 'function',",
' modules: [...ctx.loader.internal.loadCache.keys()]',
' .filter(url => /\\/packages\\/core\\/(?:tools|agent-loop)\\//.test(url)),',
' }',
@@ -292,7 +292,7 @@ export function testProfileResolution(mode: ExampleMode): void {
expect(evidence.sourceLeafCjs).toBe(evidence.cjs.filename)
expect(evidence.sourceLeafExplicitCjs).toBe(realpathSync.native(join(sourceDevLeaf, 'index.cjs')))
expect(evidence.sourceLeafCjs).not.toBe(evidence.sourceLeafExplicitCjs)
expect(evidence.scheduler).toBe(true)
expect(evidence.executor).toBe(true)
expect(evidence.execArgv).toEqual(mode === 'src' ? launch.args.slice(0, 2) : [])
for (const name of ['tools', 'agent-loop']) {
const selected = mode === 'src' ? 'src/index.ts' : 'lib/index.js'
@@ -1,3 +1,4 @@
import { executeModelToolFixture } from '@deepseek-ai/dsh-tool-testkit'
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
@@ -96,7 +97,7 @@ function runCode(
signal: AbortSignal = testToolSignal,
agent?: Agent,
): Promise<ToolExecutionResult> {
return harness.tools.execute({
return executeModelToolFixture(harness, {
callId: ToolCallId(`keyless-code-${++keylessCall}`),
name: RUN_CODE_NAME,
arguments: { code, description: 'Run the e2e program' },
@@ -308,7 +309,7 @@ describe('PTC mode typed values: keyless real-process contracts', () => {
return { provider: provider.id, names: inspected.data.tools.map(tool => tool.name) };
`, testToolSignal, agent))
expect(value).toEqual({ provider: 'Tool', names: ['cordis_inspect_list', 'cordis_inspect_query', 'run_code'] })
expect(value).toEqual({ provider: 'Tool', names: ['cordis_inspect_list', 'cordis_inspect_query'] })
})
})
@@ -58,7 +58,6 @@ describe.skipIf(!existsSync(join(repoRoot, 'apps/cli/lib/bin.js')))('dsh SOURCE
DSH_HOME: join(root, 'home'),
DSH_AGENTS_HOME: join(root, 'agents'),
DSH_TELEMETRY_DISABLED: '1',
DSH_TOOLS_MODE: 'native',
DSH_CLI_MOCK_FAILURE: '0',
},
input: '',
@@ -85,7 +85,6 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('SDK native tool updates with rea
retryPolicy: { mode: 'normal', maxRetries: 0 } satisfies RetryPolicyConfig,
models: [{ id: 'deepseek-flash', systemPromptUpdate: 'in-history', toolUpdate: 'addition-only' }],
} },
{ id: 'tools', config: { mode: 'native' } },
{ id: 'session-log-deepseek', disabled: true },
{ id: 'plugin-package-inventory-deepseek', disabled: true },
{ insert: [{ id: 'sdk-dynamic-tool-cache-fixture', name: fixturePath, config: { evidencePath, callsPath, sampleLabel, updatePrompt } }] },
@@ -58,7 +58,7 @@ describe('Python SDK dsh profile keyless smoke', () => {
{ label: 'reports max-token turns with mapping enabled through env', envValue: 'true', editorEnabled: false },
{ label: 'reports max-token turns with mapping disabled through env', envValue: 'false', editorEnabled: false },
{ label: 'allows an explicit patch to enable str_replace_editor', envValue: undefined, editorEnabled: true },
{ label: 'uses desktop tools through the PTC environment switch', envValue: undefined, editorEnabled: false, ptc: true },
{ label: 'uses the PTC executor overlay despite the retired environment selector', envValue: undefined, editorEnabled: false, ptc: true },
])('$label', async (scenario) => {
const { envValue, editorEnabled } = scenario
const ptc = 'ptc' in scenario && scenario.ptc
@@ -92,6 +92,7 @@ describe('Python SDK dsh profile keyless smoke', () => {
'--profile',
'sdk',
...(editorEnabled ? ['--patch', editorPatch] : []),
...(ptc ? ['--patch', join(repoRoot, 'apps/cli/config/examples/ptc.yml')] : []),
], {
cwd: repoRoot,
env: {
@@ -99,7 +100,7 @@ describe('Python SDK dsh profile keyless smoke', () => {
DSH_HOME: join(root, '.dsh'),
DSH_PERMISSION_MODE: 'danger-full-access',
DSH_TELEMETRY_DISABLED: '1',
DSH_TOOLS_MODE: ptc ? 'ptc' : 'native',
DSH_TOOLS_MODE: ptc ? 'native' : 'ptc',
DEEPSEEK_API_KEY: 'keyless-smoke-no-call',
DEEPSEEK_BASE_URL: `http://127.0.0.1:${address.port}`,
...(envValue === undefined ? {} : { DSH_MAX_TOKENS_AS_SUCCESS: envValue }),
@@ -58,7 +58,7 @@ it.skipIf(!process.env.DEEPSEEK_API_KEY || process.platform === 'win32')(`agent
], {
cwd: root, env: {
PATH: path, DSH_HOME: join(root, 'home'), DSH_AGENTS_HOME: join(root, 'agents'),
DSH_PERMISSION_MODE: 'danger-full-access', DSH_TELEMETRY_DISABLED: '1', DSH_TOOLS_MODE: 'native',
DSH_PERMISSION_MODE: 'danger-full-access', DSH_TELEMETRY_DISABLED: '1',
DSH_PRIMARY_RUNTIME: carrier === 'sdk' ? undefined : '',
},
timeout: 110_000, killSignal: 'SIGKILL', reject: false,
@@ -49,7 +49,7 @@ it('delivers an overdue reminder with the SDK route and lets the client resume t
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('model fixture has no port')
const env = { ...process.env, DSH_TOOLS_MODE: 'native', DSH_PERMISSION_MODE: 'danger-full-access', DSH_TELEMETRY_DISABLED: '1',
const env = { ...process.env, DSH_PERMISSION_MODE: 'danger-full-access', DSH_TELEMETRY_DISABLED: '1',
DEEPSEEK_API_KEY: 'keyless-schedule-fixture', DEEPSEEK_BASE_URL: `http://127.0.0.1:${String(address.port)}` }
clearAmbientProxyEnv(env)
const first = new DeepSeekHarness({ cwd: root, dshHome: join(root, '.dsh'), patches: [patch],
@@ -3,6 +3,7 @@ import { createServer } from 'node:http'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { DeepSeekHarness, type SdkUserQuestionRequest } from '@deepseek-ai/dsh-sdk-client'
import { clearAmbientProxyEnv } from '../../../../../scripts/test-proxy-environment.ts'
import { expect, it, onTestFinished } from 'vitest'
@@ -47,13 +48,14 @@ it.each([
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('model fixture has no port')
const env = { ...process.env, DSH_TOOLS_MODE: mode, DSH_PERMISSION_MODE: 'danger-full-access', DSH_TELEMETRY_DISABLED: '1',
const env = { ...process.env, DSH_PERMISSION_MODE: 'danger-full-access', DSH_TELEMETRY_DISABLED: '1',
DEEPSEEK_API_KEY: 'keyless-question-fixture', DEEPSEEK_BASE_URL: `http://127.0.0.1:${String(address.port)}` }
clearAmbientProxyEnv(env)
const seen: SdkUserQuestionRequest[] = []
const entered = Promise.withResolvers<AbortSignal>()
const harness = state.harness = new DeepSeekHarness({
cwd: root, dshHome: join(root, '.dsh'), env, model: 'deepseek-v4-pro', maxTokens: 1234, initializeTimeoutMs: 30_000,
patches: mode === 'ptc' ? [fileURLToPath(new URL('../../../config/examples/ptc.yml', import.meta.url))] : [],
onUserQuestion: async (request, { signal }) => {
expect(signal.aborted).toBe(false)
seen.push(request)
@@ -1,4 +1,6 @@
/** Test-only IPC assertions over real Creator presets and profile management. */
import { executeModelToolFixture } from '@deepseek-ai/dsh-tool-testkit'
export const inject = ['agents', 'agentPresets', 'tools', 'pluginManager', 'permissionPresets']
export function apply(ctx, config) {
@@ -30,7 +32,7 @@ export function apply(ctx, config) {
try {
const first = await make(`${phase}-first`)
const before = names(first)
const manage = args => ctx.tools.execute({ name: 'plugin_manager', arguments: args, agent: first,
const manage = args => executeModelToolFixture(ctx, { name: 'plugin_manager', arguments: args, agent: first,
callId: `${phase}-manage`, signal: new AbortController().signal })
ctx.permissionPresets.set(first.session, 'workspace-write')
first.session.append('turn/start', { turn: 1 })
@@ -45,7 +47,7 @@ export function apply(ctx, config) {
const second = await make(`${phase}-second`)
const after = names(first)
const other = names(second)
const ping = await ctx.tools.execute({ name: 'mcp__demo__ping', arguments: {}, agent: first,
const ping = await executeModelToolFixture(ctx, { name: 'mcp__demo__ping', arguments: {}, agent: first,
callId: `${phase}-ping`, signal: new AbortController().signal })
const removed = phase === 'restart'
? JSON.parse((await manage({ action: 'remove_bundle', target: '@test/creator-mcp' })).value) : undefined
+11 -10
View File
@@ -1,3 +1,4 @@
import { executeToolFixture } from '@deepseek-ai/dsh-tool-testkit'
import { agentPresetProjectionDefinition } from '@deepseek-ai/dsh-agent-preset-registry'
import { subagentModelSelectionProjectionDefinition } from '@deepseek-ai/dsh-tool-subagent/src/model-selection-state.ts'
import { createRequire } from 'node:module'
@@ -629,7 +630,7 @@ describe('the shipped Web composition', () => {
expect(scoped).toContain('project-proof')
// The preset's own loader tool resolves the global-layer skill.
const loaded = await ctx.tools.execute({
const loaded = await executeToolFixture(ctx, {
callId: ToolCallId('preset-skills-load'),
name: 'skill',
arguments: { name: 'dsh-badge' },
@@ -787,7 +788,7 @@ describe('a user preset declared from the shipped cordis rows', () => {
]))
}
const signal = new AbortController().signal
const listed = await copyCtx.tools.execute({
const listed = await executeToolFixture(copyCtx, {
callId: ToolCallId('copied-preset-inspect-list'),
name: 'cordis_inspect_list',
arguments: {},
@@ -802,7 +803,7 @@ describe('a user preset declared from the shipped cordis rows', () => {
// The `Tool` provider is the one built over the host context: it
// answers with the shared registry's view of the REQUESTING agent,
// so the copy sees its own preset's tools, not the host's empty set.
const queried = await copyCtx.tools.execute({
const queried = await executeToolFixture(copyCtx, {
callId: ToolCallId('copied-preset-inspect-tools'),
name: 'cordis_inspect_query',
arguments: { platform: 'host', provider: 'Tool', method: 'listTools' },
@@ -819,12 +820,12 @@ describe('a user preset declared from the shipped cordis rows', () => {
expect(queriedText).toContain(`"name": "${toolName}"`)
}
// The `Config` provider reads the booted profile tree: the shipped `tools` row declares a Config,
// The `Config` provider reads the booted profile tree: the Agent loop declares a Config,
// and the bootstrap include row is a carrier. The name filter keeps each page small.
type ConfigRow = { id: string; patchId: string; name: string; status: string }
type ConfigPage = { data: { entries: ConfigRow[]; total: number; nextOffset: number | null } }
const listConfigs = async (input: Record<string, string | number>, callId: string): Promise<ConfigPage['data']> => {
const page = await copyCtx.tools.execute({
const page = await executeToolFixture(copyCtx, {
callId: ToolCallId(callId),
name: 'cordis_inspect_query',
arguments: { platform: 'host', provider: 'Config', method: 'listConfigs', input },
@@ -834,16 +835,16 @@ describe('a user preset declared from the shipped cordis rows', () => {
expect(page.isError, resultText(page)).toBe(false)
return (JSON.parse(resultText(page)) as ConfigPage).data
}
const toolsRows = await listConfigs({ name: '@deepseek-ai/dsh-tools' }, 'copied-preset-inspect-configs')
const toolsRows = await listConfigs({ name: '@deepseek-ai/dsh-agent-loop' }, 'copied-preset-inspect-configs')
const toolsRow = toolsRows.entries[0]
expect(toolsRow).toMatchObject({ patchId: 'tools', status: 'schema' })
expect(toolsRow).toMatchObject({ patchId: 'agent-loop', status: 'schema' })
expect(toolsRows).toMatchObject({ total: 1, nextOffset: null })
const includes = await listConfigs({ name: 'cordis:include' }, 'copied-preset-inspect-includes')
expect(includes.entries.find(entry => entry.patchId === 'include')).toMatchObject({ status: 'tree' })
const firstPage = await listConfigs({ limit: 5 }, 'copied-preset-inspect-page')
expect(firstPage.entries).toHaveLength(5)
expect(firstPage.nextOffset).toBe(5)
const projected = await copyCtx.tools.execute({
const projected = await executeToolFixture(copyCtx, {
callId: ToolCallId('copied-preset-inspect-config'),
name: 'cordis_inspect_query',
arguments: { platform: 'host', provider: 'Config', method: 'listConfigs', input: { entry: toolsRow!.id } },
@@ -855,7 +856,7 @@ describe('a user preset declared from the shipped cordis rows', () => {
const { data } = JSON.parse(resultText(projected)) as { data: Projected }
expect(data.status).toBe('schema')
expect(data.schema.$defs).toHaveProperty('loaderExpression')
expect(JSON.stringify(data.schema)).toContain('"mode"')
expect(JSON.stringify(data.schema)).toContain('"maxParallelToolCalls"')
} finally {
await copied.dispose()
await shipped.dispose()
@@ -954,7 +955,7 @@ describe('a delegated child', () => {
] as const
for (const [name, args] of calls) {
expect(toolNames(ctx, child.agent)).toContain(name)
const result = await ctx.tools.execute({
const result = await executeToolFixture(ctx, {
callId: ToolCallId(`child-${name}`), signal: new AbortController().signal,
name, arguments: args, agent: child.agent,
})
+2 -1
View File
@@ -30,6 +30,7 @@
"devDependencies": {
"@deepseek-ai/cordis-plugin-loader": "workspace:~",
"@deepseek-ai/cordis-plugin-include": "workspace:~",
"@deepseek-ai/dsh-skill": "workspace:*"
"@deepseek-ai/dsh-skill": "workspace:*",
"@deepseek-ai/dsh-tool-executor-native": "workspace:*"
}
}
+1 -1
View File
@@ -104,7 +104,7 @@ async function main(): Promise<void> {
if (process.connected) process.send?.({ type: 'platform-session', session })
})
const url = ctx.connection.authenticatedUrl(`${ctx.webServer.protocol}//127.0.0.1:${String(ctx.webServer.port)}`)
if (process.connected) process.send?.({ type: 'ready', url, injections: ctx.webServer.collectIndexInjections() }, (error) => { if (error !== null) console.error(error) })
if (process.connected) process.send?.({ type: 'ready', url, injections: ctx.webServer.collectIndexInjections(ctx.connection.browserPeer) }, (error) => { if (error !== null) console.error(error) })
}
/** Upper bound of the startup diagnostic carried over IPC; the head holds the message and stack. */
+2 -1
View File
@@ -1,3 +1,4 @@
import * as NativeToolExecutor from '@deepseek-ai/dsh-tool-executor-native'
import { cp, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, sep } from 'node:path'
@@ -29,7 +30,7 @@ it('loads Desktop Office CLI paths and removes them on disposal', async () => {
expect('default' in desktopOffice).toBe(false)
expect(ctx.loader.unwrapExports(desktopOffice)).toBe(desktopOffice)
const modules = new Map<string, unknown>([
['agents', AgentRegistry], ['systemPrompt', SystemPrompt], ['tools', ToolRuntime],
['agents', AgentRegistry], ['systemPrompt', SystemPrompt], ['tools', ToolRuntime], ['toolExecutor', NativeToolExecutor],
['skills', SkillRegistry], ['office', desktopOffice],
])
ctx.loader.internal = {
+1
View File
@@ -48,6 +48,7 @@
"@deepseek-ai/dsh-experimental-webworker-runtime": "workspace:*",
"@deepseek-ai/dsh-remote-mock": "workspace:*",
"@deepseek-ai/dsh-session-projection-cache": "workspace:*",
"@deepseek-ai/dsh-tool-testkit": "workspace:*",
"@deepseek-ai/dsh-util-values": "workspace:*",
"@types/node": "^22.20.1",
"@types/ws": "8.18.1",
+2 -1
View File
@@ -1,3 +1,4 @@
import { executeToolFixture } from '@deepseek-ai/dsh-tool-testkit'
// Session-header background jobs driven by a real `ctx.jobs` entry. No model
// call is involved.
import { readFile, writeFile } from 'node:fs/promises'
@@ -87,7 +88,7 @@ describe.skipIf(MODE === 'record')('web e2e: background job list', () => {
const trigger = page.getByRole('button', { name: '1 background job running' })
expect(await trigger.count()).toBe(0)
const started = await scaffold.ctx.tools.execute({
const started = await executeToolFixture(scaffold.ctx, {
signal: new AbortController().signal,
callId: ToolCallId('background-job-list-e2e'),
name: 'bash',
+4 -4
View File
@@ -56,9 +56,9 @@ it.skipIf(process.platform !== 'darwin')('keeps system requests pending until fr
await scaffold.ctx.settings.mutate('cua', [{ op: 'set', path: ['allowedApps'], value: [] }])
// Application consent remains independent of the session's tool-approval policy.
const grant = scaffold.ctx.tools.execute({ agent, signal: new AbortController().signal,
callId: ToolCallId('cua-app-approval'), name: 'run_code',
arguments: { code: 'return await tools.cua({ method: "getApp", params: { selector: "Fixture" } })',
description: 'Request application access' } })
calls: [{ type: 'tool-call', id: ToolCallId('cua-app-approval'), name: 'run_code',
arguments: JSON.stringify({ code: 'return await tools.cua({ method: "getApp", params: { selector: "Fixture" } })',
description: 'Request application access' }) }] })
const approval = page.locator('[data-approval-key]')
await Promise.race([approval.waitFor(), grant.then((result) => {
throw new Error(`Application selection completed before consent: ${JSON.stringify(result)}`)
@@ -73,7 +73,7 @@ it.skipIf(process.platform !== 'darwin')('keeps system requests pending until fr
await compareOrRefreshGolden(fileURLToPath(new URL('./expected/cua-permissions/application-approval.expected.md', import.meta.url)),
await captureStableAria(page, '[data-approval-key]', scaffold.workspaceCwd), webSnapshotMode())
await approval.getByRole('button', { name: 'Always allow', exact: true }).click()
expect((await grant).isError).toBe(false)
expect((await grant).results).toMatchObject([{ result: { isError: false } }])
await approval.waitFor({ state: 'hidden' })
await page.getByRole('button', { name: 'Plugins', exact: true }).click()
const card = page.locator('[data-plugin-package="@deepseek-ai/dsh-cua-bundle"]')
@@ -0,0 +1,81 @@
# NavScenario: first run bash to
Session file:
```text
{{sessionFile}}
```
> NavScenario: first run bash to print exactly NAVIGATION_OK, then read nav-a.md and nav-b.md using two read calls in ONE assistant message, then reply with the single word FIRST_DONE and stop.
<details><summary>3 previous messages</summary>
**Tool: `bash`**
```json
{
"command": "echo NAVIGATION_OK",
"description": "Print NAVIGATION_OK"
}
```
**Result**
```
NAVIGATION_OK
```
**Tool: `read`**
```json
{
"file_path": "nav-a.md"
}
```
**Result**
```
<path>{{cwd}}/workspace/nav-a.md</path>
<type>file</type>
<content>
1: # alpha nav
(End of file - total 1 lines)
</content>
```
**Tool: `read`**
```json
{
"file_path": "nav-b.md"
}
```
**Result**
```
<path>{{cwd}}/workspace/nav-b.md</path>
<type>file</type>
<content>
1: # beta nav
(End of file - total 1 lines)
</content>
```
</details>
FIRST_DONE
> Reply in markdown with: a level-2 heading "Navigation Summary", a bulleted list of exactly two items, and a fenced code block containing echo WATERFALL. Then stop.
## Navigation Summary
- alpha nav
- beta nav
```
echo WATERFALL
```
+2 -1
View File
@@ -1,4 +1,5 @@
/** Background-job menu geometry over the normal Browser dock in the shipped Web composition. */
import { executeToolFixture } from '@deepseek-ai/dsh-tool-testkit'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import { join } from 'node:path'
@@ -116,7 +117,7 @@ describe.skipIf(MODE === 'record')('web e2e: jobs menu layering', () => {
const opened = scaffold.ctx.agents.get(SEED_ID)
if (opened === undefined) throw new Error('opened session has no live Agent')
agent = opened
const started = await scaffold.ctx.tools.execute({
const started = await executeToolFixture(scaffold.ctx, {
signal: new AbortController().signal,
callId: ToolCallId('jobs-menu-layering-e2e'),
name: 'bash',
+2 -1
View File
@@ -1,3 +1,4 @@
import { executeToolFixture } from '@deepseek-ai/dsh-tool-testkit'
// Session-header job list driven by a real background bash run: the job row
// arrives over the job roster stream, expanding it opens the output
// observation stream, and the panel shows the process's real output while it
@@ -87,7 +88,7 @@ describe.skipIf(MODE === 'record')('web e2e: live job stream', () => {
const trigger = page.getByRole('button', { name: '1 background job running' })
expect(await trigger.count()).toBe(0)
const started = await scaffold.ctx.tools.execute({
const started = await executeToolFixture(scaffold.ctx, {
signal: new AbortController().signal,
callId: ToolCallId('live-job-stream-e2e'),
name: 'bash',
+3 -2
View File
@@ -1,3 +1,4 @@
import { executeToolFixture } from '@deepseek-ai/dsh-tool-testkit'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
@@ -97,14 +98,14 @@ describe('minimal agent preset', () => {
const stateDir = join(scaffold.workspaceCwd, 'persistent-state')
await mkdir(stateDir)
const signal = new AbortController().signal
await scaffold.ctx.tools.execute({
await executeToolFixture(scaffold.ctx, {
signal,
callId: ToolCallId('minimal-bash-state-setup'),
name: 'bash',
arguments: { command: `cd ${JSON.stringify(stateDir)} && export DSH_MINIMAL_STATE=PERSISTED` },
agent: agentHandle.agent,
})
const bash = await scaffold.ctx.tools.execute({
const bash = await executeToolFixture(scaffold.ctx, {
signal,
callId: ToolCallId('minimal-bash-state-read'),
name: 'bash',
+66 -3
View File
@@ -6,7 +6,7 @@
// under the standard discipline: turn 1 produces a bash call plus two
// parallel reads in one assistant message (tool-call density for the
// trajectory ledger/timing lanes), turn 2 a markdown-rich reply.
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import { join } from 'node:path'
import type { Browser, Page, Response } from 'playwright'
@@ -14,7 +14,7 @@ import { chromium } from 'playwright'
import { strFromU8, unzipSync } from 'fflate'
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, onTestFailed } from 'vitest'
import { parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
import { SESSION_FORMAT_VERSION, type SessionEvent } from '@deepseek-ai/dsh-session'
import { SESSION_FORMAT_VERSION, SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
import {
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
launchWebScaffold, recordFixture, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
@@ -32,6 +32,7 @@ const TERMINAL_EXPECTED = join(SNAPSHOT_DIR, 'terminal-card.expected.md')
const CODE_CARD_DIR = fileURLToPath(new URL('./expected/navigation-panes', import.meta.url))
const COMMAND_WRAP_EXPECTED = join(CODE_CARD_DIR, 'command-wrap.expected.md')
const CODE_CARD_EXPECTED = join(CODE_CARD_DIR, 'code-card.expected.md')
const COPY_MARKDOWN_EXPECTED = join(CODE_CARD_DIR, 'copy-markdown.expected.md')
const MODE = webSnapshotMode()
const SEED_ID = 'navigation-panes-web-e2e'
const EXPORTED_LOG_FILE = `session.v${SESSION_FORMAT_VERSION}.jsonl`
@@ -424,6 +425,66 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
}
}, 120_000)
it.skipIf(MODE === 'record')('copies the complete Session as Markdown from the Session Header', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-copy-markdown'))
await ensureSeedOpen(page)
await page.context().grantPermissions(['clipboard-read', 'clipboard-write'])
expect(await page.evaluate(() => typeof ClipboardItem)).toBe('function')
expect(await page.evaluate(() => typeof navigator.clipboard.write)).toBe('function')
await page.evaluate(() => {
const clipboard = navigator.clipboard
const originalWrite = clipboard.write.bind(clipboard)
Reflect.set(globalThis, '__dshClipboardWrites', 0)
Object.defineProperty(clipboard, 'write', {
configurable: true,
value: (...args: Parameters<typeof originalWrite>) => {
Reflect.set(globalThis, '__dshClipboardWrites', Number(Reflect.get(globalThis, '__dshClipboardWrites')) + 1)
return originalWrite(...args)
},
})
})
const markdownResponse = page.waitForResponse(response =>
response.request().method() === 'GET'
&& new URL(response.url()).pathname === '/api/session.markdown', { timeout: 30_000 })
const copyButton = page.getByRole('button', { name: 'More actions' })
await copyButton.click()
await page.getByRole('menuitem', { name: 'Copy Session as Markdown' }).click()
const response = await markdownResponse
expect(response.status()).toBe(200)
expect(response.headers()['content-type']).toBe('text/markdown; charset=utf-8')
await page.getByText('Session copied as Markdown.', { exact: true }).waitFor({ timeout: 30_000 })
expect(await page.evaluate(() => Number(Reflect.get(globalThis, '__dshClipboardWrites')))).toBe(1)
const sourcePath = (await scaffold.ctx.sessionPersistence.stat(SessionId(SEED_ID)))?.location?.path
if (sourcePath === undefined) throw new Error('seeded Session has no materialized artifact location')
expect((await stat(sourcePath)).isFile()).toBe(true)
expect(sourcePath.startsWith(`${scaffold.persistenceRoot}/`)).toBe(true)
const markdown = await page.evaluate(() => navigator.clipboard.readText())
expect(markdown).toContain(`Session file:\n\n\`\`\`text\n${sourcePath}\n\`\`\``)
const normalized = markdown
.split(sourcePath).join('{{sessionFile}}')
.split(scaffold.workspaceCwd).join('{{cwd}}')
// compareOrRefreshGolden appends its own newline; the clipboard text already ends with exactly one.
await compareOrRefreshGolden(COPY_MARKDOWN_EXPECTED, normalized.replace(/\n$/u, ''), MODE)
expect(normalized).toContain('Session file:\n\n```text\n{{sessionFile}}\n```')
expect(normalized).toContain('# NavScenario: first run bash to')
expect(normalized).toContain('> NavScenario: first run bash to print exactly NAVIGATION_OK')
expect(normalized).toContain('<details><summary>3 previous messages</summary>')
expect(normalized).toContain('**Tool: `bash`**')
expect(normalized).toContain('NAVIGATION_OK')
expect(normalized).toContain('FIRST_DONE')
expect(normalized).toContain('Navigation Summary')
expect(normalized).not.toContain('The user wants me to follow')
expect(normalized).not.toContain('{{system}}')
expect(normalized).not.toContain('request/header')
expect(normalized.endsWith('\n')).toBe(true)
expect(normalized.endsWith('\n\n')).toBe(false)
await copyButton.click()
expect(await page.getByRole('menuitem', { name: 'Copy Session as Markdown' }).isDisabled()).toBe(false)
await page.keyboard.press('Escape')
}, 60_000)
it.skipIf(MODE === 'record')('focuses the ledger by dragging an overview interval', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-timeline'))
await ensureSeedOpen(page)
@@ -561,7 +622,9 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
}, 60_000)
it.skipIf(MODE === 'record')('keeps the recorded fixture inventory exact', async () => {
await assertFixtureInventory(CODE_CARD_DIR, ['code-card.expected.md', 'command-wrap.expected.md'])
await assertFixtureInventory(CODE_CARD_DIR, [
'code-card.expected.md', 'command-wrap.expected.md', 'copy-markdown.expected.md',
])
await assertFixtureInventory(SNAPSHOT_DIR, [
'session.v3.jsonl', 'search-id-results.expected.md', 'search-results.expected.md', 'trajectory.expected.md',
'terminal-card.expected.md', 'timing.expected.md',
+521 -26
View File
@@ -68,9 +68,43 @@ async function capMetrics(field: Locator): Promise<{ textLines: number; scrolls:
})
}
// The options carry long descriptions on purpose: the squeeze assertion below
// needs option copy that WRAPS, which is the only text layout that reproduces a
// collapsed row painting its copy outside its own box.
/**
* Read how the composer draws one long-copy element: the lines it shows,
* whether CSS clipped it, whether its text overflows the line budget, and what
* its toggle does while that toggle is visible.
* @param element - a question heading or an option description.
* @returns the drawn state of the copy.
*/
function readCopy(element: HTMLElement): { lines: number; clipped: boolean; overflows: boolean; toggle: 'expand' | 'collapse' | undefined } {
const style = getComputedStyle(element)
const toggle = element.tagName === 'H2'
? element.nextElementSibling
: element.closest('[data-option-row]')?.querySelector(':scope > button:not([role])')
return {
// A clipped box keeps its full budget and gives the dropped line back through its bottom margin.
lines: Math.round((element.getBoundingClientRect().height + parseFloat(style.marginBottom)) / parseFloat(style.lineHeight)),
clipped: style.maskImage !== 'none',
overflows: element.scrollHeight > element.clientHeight + 0.5,
toggle: toggle instanceof HTMLElement && getComputedStyle(toggle).visibility === 'visible'
? toggle.getAttribute('aria-expanded') === 'true' ? 'collapse' : 'expand'
: undefined,
}
}
/**
* Tell whether an expanded copy's toggle sits below the copy's last text line.
* @param toggle - the copy's toggle button.
* @param copy - the expanded question heading or option description.
* @returns whether the toggle starts at or below the bottom of the text; false without the copy.
*/
function belowText(toggle: HTMLElement, copy: Element | null): boolean {
if (copy === null) return false
const textBottom = copy.getBoundingClientRect().bottom - parseFloat(getComputedStyle(copy).paddingBottom)
return toggle.getBoundingClientRect().top >= textBottom - 0.5
}
// The options carry descriptions on purpose: the squeeze assertion below
// verifies that each description keeps to its two-line budget in the card scrollport.
const PROMPT = 'Use the ask_user_question tool to ask me exactly one multi-select question with id "color", question "Which color do you prefer?", header "Pick one", and two options: label "Blue" with description "A cool recessive hue that reads as calm and trustworthy in long reading sessions and dense dashboards.", and label "Green" with description "A restful mid-spectrum hue with the highest perceived brightness, easiest on the eye over long sessions." Set multi_select to true. After I answer, reply with the single word DONE and stop.'
function isRecord(value: unknown): value is Record<string, unknown> {
@@ -338,53 +372,85 @@ describe('web e2e: resident question composer round trip', () => {
// This golden owns the stable question surface; the answered-state
// golden below owns the resulting transcript.
const snapshot = await captureStableAria(page, '[data-question-key]', scaffold.workspaceCwd)
await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
const descriptions = await page.locator('[data-question-key] [role="checkbox"]').evaluateAll(nodes =>
nodes.map(node => ({
label: node.getAttribute('aria-label'),
description: (node.getAttribute('aria-describedby') ?? '').split(/\s+/)
.filter(Boolean).map(id => document.getElementById(id)?.textContent ?? '').join(' '),
})))
await compareOrRefreshGolden(UI_EXPECTED, `${snapshot}\nChoice descriptions: ${JSON.stringify(descriptions)}`, MODE)
const sidebar = await captureStableAria(page, '[role="treeitem"][aria-selected="true"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(SIDEBAR_EXPECTED, sidebar, MODE)
}
// Squeezed card: the option rows are the capped card's scroll content, so
// shrinking the seat must push overflow into the option list, never
// collapse a row below the height its own copy needs — a collapsed row
// paints its centered copy outside the row box, over the title and the
// neighbouring rows. Measured on the live composer at seat heights that
// force the cap, then restored for the answer gesture below. Replay only:
// record mode must reach the recording write below, not abort on layout.
// Squeezed card: the question and compact option rows share one scrollport,
// while navigation and submission stay outside it. Measured on the live
// composer at seat heights that force the cap, then restored for the answer
// gesture below. Replay only: record mode must reach the recording write
// below, not abort on layout.
const originalViewport = page.viewportSize() ?? { width: 1680, height: 1000 }
if (MODE !== 'record') {
const original = page.viewportSize() ?? { width: 1680, height: 1000 }
const scrollStates: boolean[] = []
for (const height of [520, 440, 380]) {
await page.setViewportSize({ width: 900, height })
await page.setViewportSize({ width: 640, height })
const squeeze = await composer.evaluate((card) => {
// Role/ARIA selectors, not the CSS-module class names: the built
// client hashes those.
const rows = [...card.querySelectorAll<HTMLElement>(
'[role="radio"], [role="checkbox"], [aria-expanded]',
)]
const rows = [...card.querySelectorAll<HTMLElement>('[data-option-row]')]
const spill = rows.map(row => Math.max(...[...row.children].map((child) => {
const box = row.getBoundingClientRect()
const inner = child.getBoundingClientRect()
return Math.max(box.top - inner.top, inner.bottom - box.bottom)
})))
const list = card.querySelector<HTMLElement>('[data-question-scroll]')
const title = card.querySelector('h2')
return {
rows: rows.length,
spill: Math.max(...spill),
// Wrapped option text is what overflows a collapsed row, and a
// scrolling list proves the seat is genuinely capped. Without both,
// the spill assertion would hold vacuously.
wrappedRows: rows.filter(row => row.getBoundingClientRect().height > 42).length,
scrolls: list === null ? false : list.scrollHeight > list.clientHeight,
titleInScrollport: list?.contains(title) === true,
}
})
expect(squeeze.rows).toBeGreaterThan(0)
expect(squeeze.wrappedRows).toBeGreaterThan(0)
expect(squeeze.scrolls).toBe(true)
for (const description of await composer.locator('[data-option-description]').all()) {
const copy = await description.evaluate(readCopy)
expect(copy.lines).toBeLessThanOrEqual(2)
expect(copy.clipped).toBe(copy.overflows)
expect(copy.toggle).toBe(copy.clipped ? 'expand' : undefined)
}
scrollStates.push(squeeze.scrolls)
expect(squeeze.titleInScrollport).toBe(true)
expect(await composer.getByRole('button', { name: 'Submit', exact: true }).isVisible()).toBe(true)
// Sub-pixel tolerance: every row's copy stays inside its border box.
expect(squeeze.spill).toBeLessThan(0.6)
}
await page.setViewportSize(original)
expect(scrollStates).toContain(true)
await page.setViewportSize(originalViewport)
}
// The narrowest supported card clips Blue's description to one line; its
// toggle expands the text in place without choosing Blue.
await page.setViewportSize({ width: 390, height: 700 })
const blue = composer.getByRole('checkbox', { name: 'Blue' })
const blueText = composer.locator('[data-option-description]').first()
const blueToggle = composer.locator(`button[aria-controls=${JSON.stringify(await blueText.getAttribute('id'))}]`)
await expect.poll(() => blueText.evaluate(readCopy)).toEqual({ lines: 1, clipped: true, overflows: true, toggle: 'expand' })
// A clipped description's dropped line takes no clicks: the custom row's top edge belongs to it.
expect(await composer.locator('[data-option-description]').last().evaluate(readCopy)).toMatchObject({ clipped: true })
expect(await composer.locator('textarea').evaluate((field) => {
const row = field.closest('[class*="customRow"]')
if (!(row instanceof HTMLElement)) return false
const box = row.getBoundingClientRect()
const hit = document.elementFromPoint(box.left + box.width / 2, box.top + 2)
return hit !== null && row.contains(hit)
})).toBe(true)
await blueToggle.click()
expect(await blue.getAttribute('aria-checked')).toBe('false')
expect(await blueToggle.getAttribute('aria-expanded')).toBe('true')
expect(await blueText.evaluate(readCopy)).toMatchObject({ clipped: false, overflows: false, toggle: 'collapse' })
await blueToggle.click()
// A collapsed copy's scroll timeline reports the overflow from the next frame.
await expect.poll(() => blueText.evaluate(readCopy)).toEqual({ lines: 1, clipped: true, overflows: true, toggle: 'expand' })
await page.setViewportSize(originalViewport)
// Multi-line custom answer: the field is a textarea whose hidden mirror
// owns the box height, so a soft-wrapped or line-broken draft GROWS the
// field instead of scrolling one line, and Shift+Enter breaks the line
@@ -415,7 +481,6 @@ describe('web e2e: resident question composer round trip', () => {
await custom.fill('')
}
const blue = composer.getByRole('checkbox', { name: 'Blue' })
await blue.click()
await custom.fill('Include accessibility notes')
expect(await blue.getAttribute('aria-checked')).toBe('true')
@@ -536,6 +601,436 @@ describe('web e2e: resident question composer round trip', () => {
await expect.poll(() => page.locator('[data-question-key]').count(), { timeout: 10_000 }).toBe(0)
}, 60_000)
it.skipIf(MODE === 'record')('keeps short option explanations beside their labels', async () => {
const sessionId = answeredSession
expect(sessionId).toBeDefined()
const agent = scaffold.ctx.agents.get(sessionId as SessionId)
expect(agent).toBeDefined()
const controller = new AbortController()
const asked = scaffold.ctx.userQuestions.ask({
agent: agent as NonNullable<typeof agent>,
signal: controller.signal,
questions: [{
id: 'reasoning',
header: '确认思考等级',
question: '选择 HF provider 的思考等级?',
options: [
{ label: '高(high)', description: '倾向更充分的推理,通常更慢、消耗更多 token,需模型支持。' },
{ label: '中(medium)', description: '平衡推理深度与速度;需模型支持。' },
{ label: '低(low)', description: '优先速度与较低 token 消耗;需模型支持。' },
{ label: '先查看当前配置', description: '先确认 HF provider 的模型及其支持的思考参数,再决定。' },
],
}],
})
const outcome = asked.then(value => ({ value }), (error: unknown) => ({ error }))
const composer = page.locator('[data-question-key]')
const original = page.viewportSize() ?? { width: 1680, height: 1000 }
let completed = false
try {
await page.setViewportSize({ width: 1328, height: 720 })
await composer.getByRole('heading', { name: '选择 HF provider 的思考等级?' }).waitFor()
expect(await composer.getByRole('heading').evaluate(readCopy))
.toEqual({ lines: 1, clipped: false, overflows: false, toggle: undefined })
expect(await composer.getByRole('button', { name: 'Expand', exact: true }).count()).toBe(0)
const rows = await composer.locator('[data-option-row]').evaluateAll(options => options.map((row) => {
const label = row.querySelector<HTMLElement>('[data-option-line] > span:first-child')
const description = row.querySelector<HTMLElement>('[data-option-description]')
if (label === null || description === null) return null
const style = getComputedStyle(description)
return {
sameLine: description.getBoundingClientRect().top < label.getBoundingClientRect().bottom,
fontSize: style.fontSize,
lineHeight: style.lineHeight,
maskImage: style.maskImage,
}
}))
expect(rows).toHaveLength(4)
expect(rows).toEqual(Array.from({ length: 4 }, () => ({
sameLine: true, fontSize: '13px', lineHeight: '20px', maskImage: 'none',
})))
await composer.getByRole('radio', { name: '中(medium)' }).click()
await composer.getByRole('button', { name: 'Submit', exact: true }).click()
expect(await outcome).toEqual({ value: { answers: [{ id: 'reasoning', selected: ['中(medium)'] }] } })
completed = true
} finally {
controller.abort()
const cleanup = await Promise.allSettled([outcome, page.setViewportSize(original)])
if (completed) {
const failure = cleanup.find((result): result is PromiseRejectedResult => result.status === 'rejected')
if (failure !== undefined) throw failure.reason
}
}
}, 60_000)
it.skipIf(MODE === 'record')('keeps long questions answerable in short and zoomed viewports', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-question-long-copy'))
const sessionId = answeredSession
expect(sessionId).toBeDefined()
const agent = scaffold.ctx.agents.get(sessionId as SessionId)
expect(agent).toBeDefined()
const question = Array.from({ length: 8 }, () => 'The release gate reports that the installed application graph still contains an experimental package through a default dependency. Which approach should we use to preserve explicit installation while keeping the supported application graph valid?').join(' ')
const detail = 'Review the shipped profile, package ownership, upgrade impact, and the path users follow before choosing. The answer must preserve the existing release invariant, avoid a hidden compatibility path, and remain understandable to people who did not participate in the implementation discussion.'
const description = Array.from({ length: 48 }, (_, index) =>
`Migration step ${String(index + 1)} removes the experimental client from shipped templates, requires an explicit profile install, names the affected users, and records the cross-platform validation before rollout.`).join(' ')
const controller = new AbortController()
const asked = scaffold.ctx.userQuestions.ask({
agent: agent as NonNullable<typeof agent>,
signal: controller.signal,
questions: [{
id: 'release',
header: 'Release decision',
question,
detail,
multiSelect: true,
options: [
{ label: 'Explicit install', description },
{ label: 'Keep the default edge', description: 'Keep the current setup.' },
{ label: 'Move the package', description: 'Move it to a separately installed package with an explicit profile entry.' },
],
}, { id: 'followup', question: 'Who should own the rollout?', options: [{ label: 'Release team' }] }],
})
const outcome = asked.then(value => ({ value }), (error: unknown) => ({ error }))
const composer = page.locator('[data-question-key]')
const original = page.viewportSize() ?? { width: 1680, height: 1000 }
let bodyCompleted = false
try {
await composer.waitFor({ timeout: 30_000 })
const heading = composer.getByRole('heading', { name: question })
const descriptionOf = (label: string) => composer.locator('[data-option-row]', {
has: page.getByRole('checkbox', { name: label, exact: true }),
}).locator('[data-option-description]')
const toggleFor = async (copy: Locator) =>
composer.locator(`button[aria-controls=${JSON.stringify(await copy.getAttribute('id'))}]`)
const readMetrics = () => composer.evaluate((card) => {
const scroll = card.querySelector<HTMLElement>('[data-question-scroll]')
const title = card.querySelector<HTMLElement>('h2')
const submit = [...card.querySelectorAll<HTMLButtonElement>('button')]
.find(button => button.textContent === 'Next')
if (scroll === null || title === null || submit === undefined) return null
const submitBox = submit.getBoundingClientRect()
return {
footerFits: submitBox.top >= -0.5 && submitBox.bottom <= window.innerHeight + 0.5,
scrolls: scroll.scrollHeight > scroll.clientHeight,
titleInScrollport: scroll.contains(title),
}
})
// Cover common desktop sizes plus the CSS-pixel equivalents of a
// progressively zoomed 640x420 window without changing browser-global zoom.
const scrollStates: boolean[] = []
for (const viewport of [
{ width: 1440, height: 900 },
{ width: 900, height: 600 },
{ width: 640, height: 420 },
{ width: 512, height: 336 },
{ width: 427, height: 280 },
{ width: 390, height: 700 },
]) {
await page.setViewportSize(viewport)
await expect.poll(readMetrics).not.toBeNull()
const metrics = await readMetrics()
expect(metrics?.footerFits).toBe(true)
expect(metrics?.titleInScrollport).toBe(true)
scrollStates.push(metrics?.scrolls ?? false)
// Past its three-line budget the question keeps two lines; past its
// two-line budget a description keeps one. Copy within budget shows in full.
await expect.poll(() => heading.evaluate(readCopy))
.toEqual({ lines: 2, clipped: true, overflows: true, toggle: 'expand' })
expect(await descriptionOf('Explicit install').evaluate(readCopy))
.toEqual({ lines: 1, clipped: true, overflows: true, toggle: 'expand' })
expect(await descriptionOf('Keep the default edge').evaluate(readCopy))
.toEqual({ lines: 1, clipped: false, overflows: false, toggle: undefined })
for (const copy of await composer.locator('[data-option-description]').all()) {
const drawn = await copy.evaluate(readCopy)
expect(drawn.clipped).toBe(drawn.overflows)
expect(drawn.toggle).toBe(drawn.clipped ? 'expand' : undefined)
expect(drawn.lines).toBeLessThanOrEqual(drawn.clipped ? 1 : 2)
}
if (viewport.width === 512) {
expect(await descriptionOf('Move the package').evaluate(readCopy))
.toEqual({ lines: 2, clipped: false, overflows: false, toggle: undefined })
}
const textMetrics = await composer.locator('[data-option-description]').evaluateAll(nodes => nodes.map((node) => {
const style = getComputedStyle(node)
const rect = node.getBoundingClientRect()
return { fontSize: style.fontSize, lineHeight: style.lineHeight,
fitsViewport: rect.left >= 0 && rect.right <= innerWidth }
}))
expect(textMetrics).toEqual(Array.from({ length: 3 }, () => ({
fontSize: '13px', lineHeight: '20px', fitsViewport: true,
})))
}
expect(scrollStates).toContain(false)
expect(scrollStates).toContain(true)
// A clipped question's dropped line takes no clicks: the detail's first line belongs to the detail.
expect(await composer.getByText(detail, { exact: true }).evaluate((text) => {
const box = text.getBoundingClientRect()
const hit = document.elementFromPoint(box.left + 8, box.top + 3)
return hit !== null && text.contains(hit)
})).toBe(true)
// The question expands in place; its choices move down inside the
// scrollport while navigation and submission stay reachable.
const questionToggle = await toggleFor(heading)
expect(await questionToggle.getAttribute('aria-expanded')).toBe('false')
expect(await questionToggle.getAttribute('aria-label')).toBe('Expand')
expect(await questionToggle.getAttribute('aria-describedby')).toBe(await heading.getAttribute('id'))
await questionToggle.click()
expect(await questionToggle.getAttribute('aria-expanded')).toBe('true')
expect(await questionToggle.getAttribute('aria-label')).toBe('Collapse')
const expandedQuestion = await heading.evaluate(readCopy)
expect(expandedQuestion).toMatchObject({ clipped: false, overflows: false, toggle: 'collapse' })
expect(expandedQuestion.lines).toBeGreaterThan(3)
expect(await questionToggle.evaluate(belowText, await heading.elementHandle())).toBe(true)
expect(await readMetrics()).toEqual({ footerFits: true, scrolls: true, titleInScrollport: true })
await questionToggle.click()
await expect.poll(() => heading.evaluate(readCopy)).toEqual({ lines: 2, clipped: true, overflows: true, toggle: 'expand' })
const detailText = composer.getByText(detail, { exact: true })
expect(await detailText.evaluate(node => node.closest('[data-question-scroll]') !== null)).toBe(true)
await detailText.scrollIntoViewIfNeeded()
expect(await detailText.isVisible()).toBe(true)
expect(await composer.getByRole('button', { name: 'Next', exact: true }).isVisible()).toBe(true)
// A description expands in place without choosing its option, and its
// toggle stays described by the text it controls.
const explicit = composer.getByRole('checkbox', { name: 'Explicit install' })
const explicitText = descriptionOf('Explicit install')
const descriptionToggle = await toggleFor(explicitText)
const descriptionId = await explicitText.getAttribute('id')
expect(await explicit.getAttribute('aria-describedby')).toBe(descriptionId)
expect(await descriptionToggle.getAttribute('aria-describedby')).toBe(descriptionId)
expect(await descriptionToggle.getAttribute('aria-label')).toBe('Expand')
await descriptionToggle.click()
expect(await explicit.getAttribute('aria-checked')).toBe('false')
const expandedDescription = await explicitText.evaluate(readCopy)
expect(expandedDescription).toMatchObject({ clipped: false, overflows: false, toggle: 'collapse' })
expect(expandedDescription.lines).toBeGreaterThan(2)
expect(await descriptionToggle.evaluate(belowText, await explicitText.elementHandle())).toBe(true)
expect(await explicitText.evaluate((text) => {
const row = text.closest('[data-option-row]') as HTMLElement
return [...row.querySelectorAll<HTMLElement>('*')].every(child =>
child.getBoundingClientRect().bottom <= row.getBoundingClientRect().bottom + 0.5)
})).toBe(true)
// The toggle's icon keeps non-text contrast on the card in both themes.
const palette: string[] = []
const originalDarkTheme = await page.locator('body').getAttribute('data-ds-dark-theme')
try {
for (const dark of [false, true]) {
await page.evaluate((value) => { document.body.toggleAttribute('data-ds-dark-theme', value) }, dark)
const colors = await descriptionToggle.evaluate((toggle) => {
const card = toggle.closest('section')
return card === null ? null : { text: getComputedStyle(toggle).color, surface: getComputedStyle(card).backgroundColor }
})
if (colors === null) throw new Error('The question card is unavailable')
const channels = (color: string) => color.match(/[\d.]+/g)?.slice(0, 3).map(Number) ?? []
const luminance = (color: string) => channels(color).reduce((total, channel, index) => {
const normalized = channel / 255
const linear = normalized <= 0.04045 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4
return total + linear * [0.2126, 0.7152, 0.0722][index]!
}, 0)
const foreground = luminance(colors.text)
const background = luminance(colors.surface)
expect((Math.max(foreground, background) + 0.05) / (Math.min(foreground, background) + 0.05))
.toBeGreaterThanOrEqual(3)
palette.push(colors.surface)
}
expect(palette[0]).not.toBe(palette[1])
} finally {
await page.evaluate((previous) => {
if (previous === null) document.body.removeAttribute('data-ds-dark-theme')
else document.body.setAttribute('data-ds-dark-theme', previous)
}, originalDarkTheme)
}
await descriptionToggle.click()
await expect.poll(() => explicitText.evaluate(readCopy))
.toEqual({ lines: 1, clipped: true, overflows: true, toggle: 'expand' })
await composer.getByRole('checkbox', { name: 'Explicit install' }).click()
const answer = composer.getByRole('textbox')
await answer.fill('Keep the migration steps explicit.')
await answer.press('Enter')
await composer.getByRole('heading', { name: 'Who should own the rollout?' }).waitFor()
expect(await answer.evaluate(node => document.activeElement === node)).toBe(true)
await page.keyboard.type('Release owner')
expect(await answer.inputValue()).toBe('Release owner')
await composer.getByRole('button', { name: 'Submit', exact: true }).click()
expect(await outcome).toEqual({
value: {
answers: [{
id: 'release',
selected: ['Explicit install'],
custom: 'Keep the migration steps explicit.',
}, { id: 'followup', selected: [], custom: 'Release owner' }],
},
})
await expect.poll(() => page.locator('[data-question-key]').count(), { timeout: 10_000 }).toBe(0)
bodyCompleted = true
} finally {
controller.abort()
const cleanup = await Promise.allSettled([outcome, page.setViewportSize(original)])
if (bodyCompleted) {
const failure = cleanup.find((result): result is PromiseRejectedResult => result.status === 'rejected')
if (failure !== undefined) throw failure.reason
}
}
}, 60_000)
it.skipIf(MODE === 'record')('re-evaluates long copy when the chat column or the window changes width', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-question-chat-width'))
const sessionId = answeredSession
expect(sessionId).toBeDefined()
const agent = scaffold.ctx.agents.get(sessionId as SessionId)
expect(agent).toBeDefined()
// Sized for the chat-width drag range: the question takes three lines or
// fewer in the widest chat column and more in the narrowest, and the staged
// description takes two lines or fewer and more.
const question = `${Array.from({ length: 2 }, () => 'The rollout plan must name the release owner, the verification lanes that gate it, and the rollback path before the candidate is cut.').join(' ')} Which plan should we adopt?`
const staged = 'Stage the rollout behind the release channel first, keep the experimental client out of every shipped template, ask plugin authors to declare the dependency explicitly, and publish the upgrade note together with the cross-platform validation results.'
const explicit = Array.from({ length: 24 }, (_, index) =>
`Step ${String(index + 1)} removes the experimental client from shipped templates and records the validation before rollout.`).join(' ')
const controller = new AbortController()
const asked = scaffold.ctx.userQuestions.ask({
agent: agent as NonNullable<typeof agent>,
signal: controller.signal,
questions: [{
id: 'rollout',
header: 'Rollout decision',
question,
options: [
{ label: 'Explicit install', description: explicit },
{ label: 'Stage the rollout', description: staged },
{ label: 'Keep the default edge', description: 'Keep the current setup.' },
],
}],
})
const outcome = asked.then(value => ({ value }), (error: unknown) => ({ error }))
const composer = page.locator('[data-question-key]')
const frame = page.locator('[style*="grid-template-columns"]').first()
const original = page.viewportSize() ?? { width: 1680, height: 1000 }
let bodyCompleted = false
try {
await page.setViewportSize({ width: 1680, height: 1000 })
await composer.waitFor({ timeout: 30_000 })
const heading = composer.getByRole('heading', { name: question })
const descriptionOf = (label: string) => composer.locator('[data-option-row]', {
has: page.getByRole('radio', { name: label, exact: true }),
}).locator('[data-option-description]')
const toggleFor = async (copy: Locator) =>
composer.locator(`button[aria-controls=${JSON.stringify(await copy.getAttribute('id'))}]`)
// Settled layout: no AppFrame column transition in flight, then every
// collapsed copy drawn as its own overflow decides, the expanded copy
// still expanded, and the actions reachable.
const settle = async () => {
await expect.poll(() => frame.evaluate(node => node.hasAttribute('data-animating'))).toBe(false)
const read = async () => {
const card = await composer.locator('section').evaluate((section) => {
const scroll = section.querySelector<HTMLElement>('[data-question-scroll]')
const submit = [...section.querySelectorAll<HTMLButtonElement>('button')]
.find(button => button.textContent === 'Submit')
const submitBox = submit?.getBoundingClientRect()
return {
width: Math.round(section.getBoundingClientRect().width),
actionsReachable: submitBox !== undefined && submitBox.top >= -0.5 && submitBox.bottom <= window.innerHeight + 0.5,
titleInScrollport: scroll?.contains(section.querySelector('h2')) === true,
}
})
return {
card,
question: await heading.evaluate(readCopy),
explicit: await descriptionOf('Explicit install').evaluate(readCopy),
staged: await descriptionOf('Stage the rollout').evaluate(readCopy),
short: await descriptionOf('Keep the default edge').evaluate(readCopy),
}
}
await expect.poll(async () => {
const state = await read()
const decided = (copy: Awaited<ReturnType<typeof read>>['question'], budget: number) =>
copy.clipped === copy.overflows && copy.toggle === (copy.clipped ? 'expand' : undefined)
&& copy.lines <= (copy.clipped ? budget - 1 : budget)
return state.card.actionsReachable && state.card.titleInScrollport
&& decided(state.question, 3) && decided(state.staged, 2) && decided(state.short, 2)
&& !state.explicit.clipped && !state.explicit.overflows && state.explicit.toggle === 'collapse'
}).toBe(true)
return await read()
}
// The right transcript handle widens the content column by twice its
// outward travel. The composer seat covers the handle's lower part, so
// the gesture grabs it beside the transcript.
const dragChatWidth = async (travel: number) => {
const box = await page.locator('[data-width-handle="right"]').boundingBox()
if (box === null) throw new Error('The chat width handle is not rendered')
const x = box.x + box.width / 2
const y = box.y + 120
await page.mouse.move(x, y)
await page.mouse.down()
try {
await page.mouse.move(x + travel, y, { steps: 8 })
} finally {
await page.mouse.up()
}
}
// Expanded copy is the user's choice and survives every width change below.
const explicitToggle = await toggleFor(descriptionOf('Explicit install'))
await explicitToggle.click()
expect(await explicitToggle.getAttribute('aria-expanded')).toBe('true')
expect(await composer.getByRole('radio', { name: 'Explicit install', exact: true }).getAttribute('aria-checked')).toBe('false')
await dragChatWidth(-400)
const narrow = await settle()
expect(narrow.question).toEqual({ lines: 2, clipped: true, overflows: true, toggle: 'expand' })
expect(narrow.staged).toEqual({ lines: 1, clipped: true, overflows: true, toggle: 'expand' })
await dragChatWidth(600)
const wide = await settle()
expect(wide.card.width).toBeGreaterThan(narrow.card.width)
expect(wide.question).toMatchObject({ clipped: false, overflows: false, toggle: undefined })
expect(wide.staged).toEqual({ lines: 2, clipped: false, overflows: false, toggle: undefined })
expect(wide.short).toEqual({ lines: 1, clipped: false, overflows: false, toggle: undefined })
// The right sidebar narrows the conversation column, and collapsing it restores the wide state.
await page.getByRole('button', { name: 'Open right sidebar', exact: true }).click()
const withSidebar = await settle()
expect(withSidebar.card.width).toBeLessThan(wide.card.width)
await page.getByRole('button', { name: 'Collapse right sidebar', exact: true }).click()
expect(await settle()).toEqual(wide)
// A smaller window clamps the dragged width; restoring the window restores it.
await page.setViewportSize({ width: 900, height: 700 })
const smallWindow = await settle()
expect(smallWindow.card.width).toBeLessThan(wide.card.width)
expect(smallWindow.question).toEqual({ lines: 2, clipped: true, overflows: true, toggle: 'expand' })
expect(smallWindow.staged).toEqual({ lines: 1, clipped: true, overflows: true, toggle: 'expand' })
await page.setViewportSize({ width: 1680, height: 1000 })
expect(await settle()).toEqual(wide)
await explicitToggle.click()
await expect.poll(() => descriptionOf('Explicit install').evaluate(readCopy))
.toEqual({ lines: 1, clipped: true, overflows: true, toggle: 'expand' })
await composer.getByRole('radio', { name: 'Stage the rollout', exact: true }).click()
await composer.getByRole('button', { name: 'Submit', exact: true }).click()
expect(await outcome).toEqual({ value: { answers: [{ id: 'rollout', selected: ['Stage the rollout'] }] } })
await expect.poll(() => page.locator('[data-question-key]').count(), { timeout: 10_000 }).toBe(0)
bodyCompleted = true
} finally {
controller.abort()
// The dragged width is a persisted preference; dropping it and resizing republishes the default.
const cleanup = await Promise.allSettled([
outcome,
page.evaluate(() => { localStorage.removeItem('dsh.conversation.contentWidth') })
.then(() => page.setViewportSize(original)),
])
if (bodyCompleted) {
const failure = cleanup.find((result): result is PromiseRejectedResult => result.status === 'rejected')
if (failure !== undefined) throw failure.reason
}
}
}, 90_000)
})
describe.skipIf(MODE === 'record')('web e2e: cancelled question transcript', () => {
+2 -1
View File
@@ -1,3 +1,4 @@
import { executeToolFixture } from '@deepseek-ai/dsh-tool-testkit'
// Web e2e scenario: fresh round trip. A real chromium types a prompt into the
// real composer; the wire, Remote gateway, agent loop, and the REAL bash tool (echo
// in the temp workspace) all run; the model adapter is dsh-llm-replay (keyless)
@@ -249,7 +250,7 @@ describe('web e2e: fresh round trip through the real assembly', () => {
setup: agentCtx => probe.ctx.agentPresets.mount(agentCtx).then(() => undefined),
})
try {
const result = await probe.ctx.tools.execute({
const result = await executeToolFixture(probe.ctx, {
signal: AbortSignal.timeout(5_000),
callId: ToolCallId('web-url-probe'),
name: 'bash',
-8
View File
@@ -384,13 +384,6 @@ export interface LaunchOptions {
paceMs?: number
/** Synthetic model capacity for UI scenarios whose seeded history must remain uncompacted. */
replayContextWindow?: number
/**
* Tool presentation mode patched onto the shipped `tools` row (`code`
* collapses the wire to run_code + the SDK prompt section). Omit for the
* yml default. The PTC runtime row is always in the tree, so no extra
* insertion is needed.
*/
toolsMode?: 'native' | 'ptc'
/**
* Keep the shipped DeepSeek adapter mounted while masking the process
* environment's DEEPSEEK_API_KEY for this scaffold lifetime. This is the
@@ -689,7 +682,6 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
{ id: 'agent-preset-registry', config: { default: options.agentPresets.default } },
{ insert: (options.agentPresets.definitions ?? []).map(config => ({ id: `preset-${config.id}`, name: '@deepseek-ai/dsh-agent-preset', config })) },
],
...options.toolsMode === undefined ? [] : [{ id: 'tools', config: { mode: options.toolsMode } }],
...options.deepSeekSearch === undefined
? []
: [{
+4 -3
View File
@@ -1,4 +1,5 @@
/** Keyless assembled-Web evidence for conversational Schedule delivery. */
import { executeToolFixture } from '@deepseek-ai/dsh-tool-testkit'
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
@@ -354,7 +355,7 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
source: { kind: 'user' },
})
await workspace.attachSession(afterHandle.agent.id)
const afterCreated = await scaffold.ctx.tools.execute({
const afterCreated = await executeToolFixture(scaffold.ctx, {
signal: AbortSignal.timeout(10_000),
callId: ToolCallId('schedule-after-create'),
name: 'schedule_create',
@@ -420,7 +421,7 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
})
await scaffold.ctx.schedule.delete({ sessionId: everyHandle.agent.id, id: wake.id })
await workspace.attachSession(everyHandle.agent.id)
const everyListed = await scaffold.ctx.tools.execute({
const everyListed = await executeToolFixture(scaffold.ctx, {
signal: AbortSignal.timeout(10_000),
callId: ToolCallId('schedule-every-list'),
name: 'schedule_list',
@@ -1181,7 +1182,7 @@ describe.skipIf(MODE === 'record')('web e2e: active Schedule catalog', () => {
})
const time = clock.format(new Date(Date.now() + 3_600_000))
const prompt = 'Inspect the daily calendar rule'
const result = await scaffold.ctx.tools.execute({
const result = await executeToolFixture(scaffold.ctx, {
signal: AbortSignal.timeout(10_000), callId: ToolCallId('schedule-daily-catalog-create'),
name: 'schedule_create', arguments: { prompt, title: prompt, daily: { time, time_zone: timeZone } }, agent,
})
+5 -4
View File
@@ -1,3 +1,4 @@
import { executeToolFixture, executeModelToolFixture } from '@deepseek-ai/dsh-tool-testkit'
// Boots the shipped Web composition over the built dist this lane already uses
// and asserts its catalog, defaults, Loader lifecycle, and one complete Auto
// producer-to-tool path. Browser scenarios in this lane own visual behavior.
@@ -679,7 +680,7 @@ it('lets a preset producer reach the background-job registry', async () => {
// `tool-bash` is a preset row and `tasks` is a host registry; the producer
// resolves it with `ctx.get`, so a registry hidden behind a preset realm
// fails here — with every task control still listed in the catalog above.
const started = await ctx.tools.execute({
const started = await executeModelToolFixture(ctx, {
signal,
callId: ToolCallId('shipped-bash-background'),
name: 'bash',
@@ -701,7 +702,7 @@ it('lets a preset producer reach the background-job registry', async () => {
// The controller reads what the producer started: same registry, one
// owner. A per-preset registry would list nothing here even on success.
const listed = await ctx.tools.execute({
const listed = await executeToolFixture(ctx, {
signal,
callId: ToolCallId('shipped-task-list'),
name: 'job_list',
@@ -714,7 +715,7 @@ it('lets a preset producer reach the background-job registry', async () => {
// The full round trip: the output a host-plane producer wrote is collected
// through a preset-plane control, which is the linkage the realm severed.
const collected = await ctx.tools.execute({
const collected = await executeToolFixture(ctx, {
signal,
callId: ToolCallId('shipped-task-output'),
name: 'job_output',
@@ -1036,7 +1037,7 @@ it('rolls back a failed shipped Auto initialization before publishing or interce
// contribution performs no review, so success proves the failed shipped
// integration left no pre-execute listener behind. It is not supported
// Auto execution behavior.
const result = await ctx.tools.execute({
const result = await executeToolFixture(ctx, {
signal: new AbortController().signal,
callId: ToolCallId('shipped-auto-init-rollback-write'),
name: 'write',
+52
View File
@@ -1103,6 +1103,58 @@ describe('web e2e: shipped right Sidebar', () => {
expect(tripwire.warnings).toEqual([])
}, 90_000)
it('keeps a dragged tab when a middle click is chorded with the primary button', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-right-middle-drag'))
const column = await resetSidebar(page)
await proseChip(page).click()
await expect.poll(() => tabTitles(column)).toEqual(['Files', SAMPLE_NAME])
const sample = column.getByRole('tab').filter({ hasText: SAMPLE_NAME })
const from = await pointIn(sample, 0.25, 0.5)
const to = await pointIn(column.getByRole('tab').filter({ hasText: 'Files' }), 0.25, 0.5)
await page.mouse.move(from.x, from.y)
await page.mouse.down()
try {
await page.mouse.move(to.x, to.y, { steps: 8 })
await column.locator('[data-dockkit-caret="0"]').waitFor({ state: 'visible' })
await page.mouse.down({ button: 'middle' })
await page.mouse.up({ button: 'middle' })
} finally {
await page.mouse.up()
}
await expect.poll(() => tabTitles(column)).toEqual([SAMPLE_NAME, 'Files'])
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
})
it('closes background and active tabs with the middle button and keeps the last guide', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-right-middle-close'))
const column = await resetSidebar(page)
await proseChip(page).click()
await expect.poll(() => tabTitles(column)).toEqual(['Files', SAMPLE_NAME])
const sample = column.getByRole('tab').filter({ hasText: SAMPLE_NAME })
const files = column.getByRole('tab').filter({ hasText: 'Files' })
const selected = () => column.locator('[role="tab"][aria-selected="true"] [data-dockkit-tab-title]').allTextContents()
await expect.poll(selected).toEqual([SAMPLE_NAME])
await files.click({ button: 'middle' })
await expect.poll(() => tabTitles(column)).toEqual([SAMPLE_NAME])
expect(await selected()).toEqual([SAMPLE_NAME])
await column.locator('[data-dockkit-add-tab]').click()
await expect.poll(() => tabTitles(column)).toEqual([SAMPLE_NAME, 'Start'])
await sample.click()
await expect.poll(selected).toEqual([SAMPLE_NAME])
await sample.click({ button: 'middle' })
await expect.poll(() => tabTitles(column)).toEqual(['Start'])
expect(await selected()).toEqual(['Start'])
await column.getByRole('tab').click({ button: 'middle' })
expect(await tabTitles(column)).toEqual(['Start'])
expect(await column.locator('[data-sidebar-right-open]').count()).toBe(1)
expect(await column.locator('[data-dockkit-tab-quiet]').count()).toBe(1)
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
})
it('preserves the open surface after a reload', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-right-reload'))
const frame = page.locator('[class*="frame"]').first()
+15 -6
View File
@@ -603,7 +603,7 @@ describe('dsh web keyless CLI smoke', () => {
}
}, 120_000)
it('DSH_TOOLS_MODE=ptc collapses the provider wire tools to run_code with the SDK prompt section', async () => {
it.each(['native', 'ptc'] as const)('selects the %s executor through profile overlays and ignores the retired environment selector', async (mode) => {
requireDist()
const workspace = mkdtempSync(join(tmpdir(), 'dsh-web-ptc-'))
@@ -632,14 +632,17 @@ describe('dsh web keyless CLI smoke', () => {
const tsxLoader = pathToFileURL(createRequire(join(REPO_ROOT, 'package.json')).resolve('tsx')).href
const child = spawn(
process.execPath,
['--import', tsxLoader, join(REPO_ROOT, 'apps/cli/src/bin.ts'), 'web', '--no-open', '--port', '0'],
['--import', tsxLoader, join(REPO_ROOT, 'apps/cli/src/bin.ts'), 'web',
...mode === 'ptc' ? ['--patch', join(REPO_ROOT, 'apps/cli/config/examples/ptc.yml')] : [],
'--no-open', '--port', '0',
],
{
cwd: workspace,
env: {
...process.env,
DEEPSEEK_API_KEY: 'keyless-web-ptc',
DEEPSEEK_BASE_URL: `http://127.0.0.1:${address.port}`,
DSH_TOOLS_MODE: 'ptc',
DSH_TOOLS_MODE: mode === 'native' ? 'ptc' : 'native',
DSH_HOME: join(workspace, '.dsh'),
DSH_AGENTS_HOME: join(workspace, '.agents'),
TSX_TSCONFIG_PATH: join(REPO_ROOT, 'tsconfig.json'),
@@ -662,9 +665,15 @@ describe('dsh web keyless CLI smoke', () => {
setTimeout(() => { reject(new Error('provider request not received in 10s')) }, 10_000).unref()
}),
])
expect(captured.tools?.map(tool => tool.name)).toEqual(['run_code'])
expect(captured.system).toContain('## Writing code for run_code')
expect(captured.system).toContain('declare const tools')
if (mode === 'ptc') {
expect(captured.tools?.map(tool => tool.name)).toEqual(['run_code'])
expect(captured.system).toContain('## Writing code for run_code')
expect(captured.system).toContain('declare const tools')
} else {
expect(captured.tools?.map(tool => tool.name)).not.toContain('run_code')
expect(captured.tools?.length).toBeGreaterThan(0)
expect(captured.system).not.toContain('## Writing code for run_code')
}
} finally {
const closed = child.exitCode === null
? new Promise<void>((resolveClose) => { child.once('close', () => { resolveClose() }) })
+9 -1
View File
@@ -224,6 +224,8 @@ describe.skipIf(MODE === 'record')('web e2e: retained steering and a post-stop m
let hostClock: MockInstance<typeof Date.now> | undefined
const sessionEvents: SessionEvent[] = []
const requestInputs: string[][] = []
let firstRequestSignal: AbortSignal | undefined
const questionStarted = Promise.withResolvers<undefined>()
const releaseQuestion = Promise.withResolvers<undefined>()
const FOLLOWUP = 'Also acknowledge this second message.'
const RESPONSE = "Great, let's move forward. BANANA!"
@@ -239,9 +241,13 @@ describe.skipIf(MODE === 'record')('web e2e: retained steering and a post-stop m
message.role === 'user'
? message.content.flatMap(block => block.type === 'text' ? [block.text] : [])
: []))
if (requestInputs.length === 1) firstRequestSignal = options.signal
for await (const chunk of next()) {
yield chunk
if (requestInputs.length === 1 && chunk.type === 'block-start') await releaseQuestion.promise
if (requestInputs.length === 1 && chunk.type === 'block-start') {
questionStarted.resolve(undefined)
await releaseQuestion.promise
}
}
}, { prepend: true })
browser = await chromium.launch()
@@ -271,11 +277,13 @@ describe.skipIf(MODE === 'record')('web e2e: retained steering and a post-stop m
const input = page.locator('[data-composer-input][contenteditable="true"]').first()
await input.fill(PROMPT)
await input.press('Enter')
await questionStarted.promise
await expect.poll(() => requestInputs.length, { timeout: 10_000 }).toBe(1)
await input.fill(STEER)
await input.press('Meta+Enter')
await page.locator('[data-pending-steering]').filter({ hasText: STEER }).waitFor({ timeout: 10_000 })
await page.getByRole('button', { name: 'Stop generating', exact: true }).click()
await expect.poll(() => firstRequestSignal?.aborted).toBe(true)
releaseQuestion.resolve(undefined)
await expect.poll(() => sessionEvents.filter(event => event.type === 'turn/end').length, { timeout: 10_000 }).toBe(1)
expect(claimedMessages(sessionEvents, STEER)).toHaveLength(0)
@@ -10,7 +10,7 @@ import { createUserMessage, LlmAdapter } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm'
import { SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import { defineContentToolFixture } from '@deepseek-ai/dsh-tool-testkit'
import { assertBuiltBenchmarkRuntime } from '../support/built-worker.ts'
import { PARENT_ID, response, resultText, syntheticHistory, TIME_ZERO, WORKLOAD } from './workload.ts'
+1
View File
@@ -42,6 +42,7 @@
"@deepseek-ai/dsh-storage-domain": "workspace:*",
"@deepseek-ai/dsh-storage-json": "workspace:*",
"@deepseek-ai/dsh-subagent": "workspace:*",
"@deepseek-ai/dsh-tool-testkit": "workspace:*",
"@deepseek-ai/dsh-tools": "workspace:*",
"@deepseek-ai/dsh-token-meter": "workspace:*",
"@deepseek-ai/dsh-typert-protocol": "workspace:*",
+2 -2
View File
@@ -24,8 +24,8 @@
en: e89802f066e4955d
zh: ea2fb4bef94e086d
/deepseek-harness-architecture/turn-flow:
en: d2e54d2e197cee82
zh: e085148bb4ae8edd
en: 1a79a70b62c04cdb
zh: c86d765e21430405
/deepseek-harness-architecture/session-log:
en: 8a7f19b95d426c99
zh: d5bb0538404e20ea
+3 -1
View File
@@ -109,7 +109,7 @@ Factory setup follows SessionStore entry and `session/created`, preceding `agent
One inbox feeds the driver; injected context waits for a waking message. AgentLoop’s durable `inbox` projection exposes pending input without live Agents.
The loop snapshots Session model selection before assembly, sharing it through `AssembleContext.modelSelection` and `agent/pre-step.modelSelection`; retries retain that selection, and omitted reasoning effort uses adapter defaults. `agent/pre-step` decides the accepted input. Listeners may rewrite or reject claimed messages; a rejected or empty first claim closes a durable turn without a step. An enter decision may set `startsRequestSeries`: the loop logs a fresh `request/header` (reason `series`, or `change` with `startsSeries: true` when the envelope also changed). Wrapping listeners preserve that declaration with `{ ...decision, messages }`. After assembly and `step/start`, the deprecated `agent/request` hook adjusts controls without changing the provider, model, or effort selection, and `prepareCall()` binds the adapter before the system prompt and accepted users are committed; cancellation during either async phase commits neither. Prepared call capabilities govern prompt admission, not preceding `request/context`. Each attempt refreshes registered runtime facts, reconciles the bound prompt, admits users once, and restores removed runtime context, logs header/context as needed, and freezes the request before streaming the bound call. Retries do not repeat assembly or `agent/pre-step`. Surface replacements and image-offload decisions after attachment start a new request series, including during the first resumed pre-step; unchanged resume continues the series. The system prompt travels as `system/message` history: empty text clears it, capable routes append updates, and other routes consolidate it at the first system node. [Agent-loop](../packages/core/agent-loop/README.md#understand-the-implementation) owns prompt admission and replacement details.
Before assembly, the loop snapshots Session model selection in `AssembleContext.modelSelection` and `agent/pre-step.modelSelection`; retries retain it; omitted reasoning effort uses adapter defaults. `agent/pre-step` listeners accept, rewrite, or reject claimed messages; a rejected or empty first claim closes a durable turn without a step. An enter decision may set `startsRequestSeries`: the loop logs a fresh `request/header` (reason `series`, or `change` with `startsSeries: true` when the envelope also changed). Wrapping listeners preserve that declaration with `{ ...decision, messages }`. After assembly and `step/start`, the deprecated `agent/request` hook adjusts controls without changing the provider, model, or effort selection, and `prepareCall()` binds the adapter before the system prompt and accepted users are committed; cancellation during either async phase commits neither. Prepared call capabilities govern prompt admission, not preceding `request/context`. Each attempt refreshes registered runtime facts, reconciles the bound prompt, admits users once, and restores removed runtime context, logs header/context as needed, and freezes the request before streaming the bound call. Retries do not repeat assembly or `agent/pre-step`. Surface replacements and image-offload decisions after attachment start a new request series, including during the first resumed pre-step; unchanged resume continues the series. The system prompt travels as `system/message` history: empty text clears it, capable routes append updates, and other routes consolidate it at the first system node. [Agent-loop](../packages/core/agent-loop/README.md#understand-the-implementation) owns prompt admission and replacement details.
The loop sends immutable requests with live cancellation and reuses freeze evidence for frozen identities; [agent-loop](../packages/core/agent-loop/README.md) owns construction and cancellation causes.
@@ -117,6 +117,8 @@ Failed steps [record missing tool results](../packages/core/agent-loop/README.md
Details: the [sequence diagram](agent-lifecycle.md), the [tool pipeline](tool-execution-pipeline.md), and [cancellation and error recovery](subsystems/core.md#the-agent-handle).
[`ctx.tools.execute()`](subsystems/tools.md) receives raw batches and concurrency limits.
## Session log
The session log is the source of the context the model sees. `deriveMessages()` projects model history from it. Each `assistant/message` embeds the exact compact timed stream that produced its assembled content; `assistant/attempt` retains settled failed, retried, cancelled, and stream-error attempts without adding model history. Fork, resume, transcripts, telemetry, and persistence all derive from these durable settlements, while live UI incrementality comes from `agent/assistant-stream`; a hard process loss before settlement leaves no durable attempt stream ([decision](../.agents/notes/implemented/architecture/2026-09-01-v2-embedded-assistant-streams.md)).
+3 -1
View File
@@ -113,7 +113,7 @@ turn/end
输入通过同一个 inbox 到达驱动器;注入的上下文等待一条唤醒消息。AgentLoop 的持久 `inbox` 投影使待处理输入在没有活跃 Agent 时仍可读取。
循环在组装前快照 Session 模型选择,并通过 `AssembleContext.modelSelection` 与 `agent/pre-step.modelSelection` 传递该选择;重试保留该选择,省略推理强度时使用适配器默认值。`agent/pre-step` 决定接纳的输入。监听器可以改写或拒绝已领取消息;首次领取被拒绝或为空时,关闭不含步骤的持久轮次。enter 决策可设置 `startsRequestSeries`:循环记录新的 `request/header`(原因为 `series`,或在封装同时变化时为携带 `startsSeries: true` 的 `change`)。包装监听器通过 `{ ...decision, messages }` 保留该声明。组装与 `step/start` 之后,已弃用的 `agent/request` hook 在不改变提供方、模型或推理强度选择的前提下调整控制参数,`prepareCall()` 绑定适配器,再提交系统提示词与已接纳用户消息;在任一异步阶段取消都不会提交这两者。提示词准入依据已准备调用的能力,而非先前的 `request/context`。每次尝试刷新已注册运行时事实、协调绑定的提示词、仅接纳一次用户消息并恢复被移除的运行时上下文、按需记录 header/context,再冻结请求并通过绑定调用发起流式请求。重试不重复组装或 `agent/pre-step`。附接后的 surface 替换和图片省略决定开启新请求序列,包括恢复后的首次 pre-step 中发生的替换;未变化的恢复延续序列。系统提示词通过 `system/message` 历史传递:空文本清除提示词,具备能力的路由追加更新,其他路由将其归并到首个系统节点。[Agent-loop](../packages/core/agent-loop/README.zh.md#understand-the-implementation) 说明提示词准入与替换的细节。
循环在组装前将 Session 模型选择快照到 `AssembleContext.modelSelection` 与 `agent/pre-step.modelSelection`;重试保留该选择,省略推理强度时使用适配器默认值。`agent/pre-step` 监听器接纳、改写或拒绝已领取消息;首次领取被拒绝或为空时,关闭不含步骤的持久轮次。enter 决策可设置 `startsRequestSeries`:循环记录新的 `request/header`(原因为 `series`,或在封装同时变化时为携带 `startsSeries: true` 的 `change`)。包装监听器通过 `{ ...decision, messages }` 保留该声明。组装与 `step/start` 之后,已弃用的 `agent/request` hook 在不改变提供方、模型或推理强度选择的前提下调整控制参数,`prepareCall()` 绑定适配器,再提交系统提示词与已接纳用户消息;在任一异步阶段取消都不会提交这两者。提示词准入依据已准备调用的能力,而非先前的 `request/context`。每次尝试刷新已注册运行时事实、协调绑定的提示词、仅接纳一次用户消息并恢复被移除的运行时上下文、按需记录 header/context,再冻结请求并通过绑定调用发起流式请求。重试不重复组装或 `agent/pre-step`。附接后的 surface 替换和图片省略决定开启新请求序列,包括恢复后的首次 pre-step 中发生的替换;未变化的恢复延续序列。系统提示词通过 `system/message` 历史传递:空文本清除提示词,具备能力的路由追加更新,其他路由将其归并到首个系统节点。[Agent-loop](../packages/core/agent-loop/README.zh.md#understand-the-implementation) 说明提示词准入与替换的细节。
循环发送不可变请求,同时保持取消有效,仅对完全冻结的对象复用冻结证据;[agent-loop](../packages/core/agent-loop/README.zh.md) 负责请求构建与取消原因。
@@ -121,6 +121,8 @@ turn/end
详情见[时序图](agent-lifecycle.zh.md)、[工具流水线](tool-execution-pipeline.zh.md)和[取消与错误恢复](subsystems/core.zh.md#the-agent-handle)。
[`ctx.tools.execute()`](subsystems/tools.zh.md) 接收原始调用批次和并发上限。
## 会话日志
会话日志是模型所见上下文的来源。`deriveMessages()` 从中投影出模型历史。每个 `assistant/message` 都嵌入产生其组装内容的精确紧凑带时间 stream;`assistant/attempt` 保留已到达 settlement 的失败、重试、取消与 stream error attempt,且不添加模型历史。fork、恢复、transcript(文本记录)、遥测与持久化都从这些持久 settlement 派生,实时 UI 增量则来自 `agent/assistant-stream`;如果进程在 settlement 前硬中断,则不会留下持久 attempt stream(见[决策](../.agents/notes/implemented/architecture/2026-09-01-v2-embedded-assistant-streams.zh.md))。
+2 -2
View File
@@ -6,5 +6,5 @@
en: 41d772192075e133
zh: 5ef3da69b4de1d8a
/capability-seams-and-core-services:
en: 753b3da3abc6aeea
zh: aab02ec16c3f744b
en: f2546ac3454d220d
zh: 642a473c0405e81f
+5 -4
View File
@@ -166,7 +166,7 @@ flowchart LR
pkg_tools["tools"]
pkg_tool_terminal["tool-terminal"]
pkg_tool_web["tool-web"]
svc_tools["ctx.tools<br/>Tool registry and guarded execution pipeline"]
svc_tools["ctx.tools<br/>Scoped tool registry and executor selection"]
pkg_tool_ask_user["tool-ask-user"]
pkg_tool_cordis["tool-cordis"]
pkg_tool_skill["tool-skill"]
@@ -235,6 +235,7 @@ flowchart LR
svc_ptcRuntime["ctx.ptcRuntime<br/>PTC execution seam"]
pkg_ptc_runtime_node["ptc-runtime-node"]
pkg_experimental_ptc_runtime_python["experimental-ptc-runtime-python"]
pkg_tool_executor_ptc["tool-executor-ptc"]
pkg_workflow_ptc["workflow-ptc"]
pkg_fs["fs"]
svc_fs["ctx.fs<br/>Filesystem provider seam"]
@@ -516,7 +517,7 @@ flowchart LR
svc_pluginMarket --> pkg_plugin_market
svc_pluginRegistryProbe --> pkg_client_ui_plugin_manager
svc_profileContext --> pkg_plugin_manager
svc_ptcRuntime --> pkg_tools
svc_ptcRuntime --> pkg_tool_executor_ptc
svc_ptcRuntime --> pkg_workflow_ptc
svc_remoteAccess --> pkg_api_gateway
svc_remoteAccess --> pkg_api_session_controller
@@ -677,7 +678,7 @@ flowchart LR
| `ctx.sessionReferenceResolver` | `core` | [`session-reference`](../packages/context/session-reference) | - | - | - | Projects bounded current-surface conversation snapshots into durable untrusted message context; host adapters own mention syntax. |
| `ctx.sessionTitle` | `seam` | [`session-title`](../packages/session/session-title) | [`session-title-first-prompt-llm`](../packages/session/session-title-first-prompt-llm), [`session-title-all-prompts-llm`](../packages/experimental/session-title-all-prompts-llm) | - | - | Owns the deterministic fallback, latest-title fold, and sole optional asynchronous provider registration. |
| `ctx.systemPrompt` | `core` | [`system-prompt`](../packages/core/system-prompt) | - | [`agent-loop`](../packages/core/agent-loop), [`tools`](../packages/core/tools), [`tool-fs`](../packages/fs/tool-fs), [`tool-terminal`](../packages/experimental/tool-terminal), [`tool-web`](../packages/web/tool-web) | - | Collects prompt sections and model-facing tool schemas for each step. |
| `ctx.tools` | `core` | [`tools`](../packages/core/tools) | - | [`agent-loop`](../packages/core/agent-loop), [`tool-ask-user`](../packages/interaction/tool-ask-user), [`tool-bash`](../packages/shell/tool-bash), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-fs`](../packages/fs/tool-fs), [`tool-terminal`](../packages/experimental/tool-terminal), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-todo`](../packages/todo/tool-todo), [`tool-web`](../packages/web/tool-web) | - | Registers capabilities, owns PTC mode transport, and routes calls through pre-policy, monotonic guards, around dispatch, post-policy, and final-result observation. |
| `ctx.tools` | `core` | [`tools`](../packages/core/tools) | - | [`agent-loop`](../packages/core/agent-loop), [`tool-ask-user`](../packages/interaction/tool-ask-user), [`tool-bash`](../packages/shell/tool-bash), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-fs`](../packages/fs/tool-fs), [`tool-terminal`](../packages/experimental/tool-terminal), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-todo`](../packages/todo/tool-todo), [`tool-web`](../packages/web/tool-web) | - | Registers capabilities and scoped executors. Native and PTC providers own descriptions and complete raw call batches; their optional shared pipeline handles tool policy and dispatch. |
| `ctx.userQuestions` | `seam` | [`user-questions`](../packages/interaction/user-questions) | - | [`tool-ask-user`](../packages/interaction/tool-ask-user) | - | UI front ends provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise. |
| `ctx.planMode` | `core` | [`plan-mode`](../packages/plan/plan-mode) | - | - | - | Folds logged plan/mode state, flushes user selections at turn boundaries, renders deployment-owned guidance, registers /plan, and keeps the plan-exit schema stable across transitions. |
| `ctx.agentPresets` | `core` | [`agent-preset-registry`](../packages/preset/agent-preset-registry) | - | - | - | Eagerly mounts YAML-declared preset revisions, binds Agents and cold readers to scoped contributions, and retains retired revisions until their last user releases them. |
@@ -699,7 +700,7 @@ flowchart LR
| `ctx.sandboxPolicy` | `core` | [`sandbox-policy`](../packages/sandbox/sandbox-policy) | - | [`bash-sandbox`](../packages/shell/bash-sandbox), [`fs-sandbox`](../packages/fs/fs-sandbox), [`terminal-bash`](../packages/terminal/terminal-bash) | - | The one home for the deployment default mode + workspace root; only the sandboxed executor and provider read the service (the tool layers use the pure `sandbox/mode` fold it also exports). Both enforcing families read it so bash and fs cannot confine to different roots. |
| `ctx.approval` | `seam` | [`user-approval`](../packages/interaction/user-approval) | - | [`tools`](../packages/core/tools), [`tool-bash`](../packages/shell/tool-bash), [`acp`](../packages/acp/acp) | - | One-shot permission decisions dispatched over the `approval/request` waterfall; answerers are listeners (the ACP bridge for its own agents), absence fails closed to `unavailable`. |
| `ctx.permissionPresets` | `core` | [`permission-presets`](../packages/interaction/permission-presets) | - | - | - | User-facing preset table (`workspace-write`/`danger-full-access`) bundling the sandbox-mode and approval-policy knobs; a switch writes one `permission/preset` event through to both knob events. |
| `ctx.ptcRuntime` | `seam` | [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime) | [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), [`experimental-ptc-runtime-python`](../packages/experimental/ptc-runtime-python) | [`tools`](../packages/core/tools), [`workflow-ptc`](../packages/workflow/workflow-ptc) | - | Runs programs against host-provided async bindings; tools owns PTC presentation and workflow-ptc owns workflow orchestration. |
| `ctx.ptcRuntime` | `seam` | [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime) | [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), [`experimental-ptc-runtime-python`](../packages/experimental/ptc-runtime-python) | [`tool-executor-ptc`](../packages/core/tool-executor-ptc), [`workflow-ptc`](../packages/workflow/workflow-ptc) | - | Runs programs against host-provided async bindings; tool-executor-ptc owns PTC presentation and workflow-ptc owns workflow orchestration. |
| `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox), [`fs-ssh`](../packages/ssh/fs-ssh) | [`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/experimental/tool-ralph) | - | Providers prepare local children or execute external tasks; one activation manager owns both lifecycles. The delegation tool selects a backend, control tools deliver local follow-ups, and workflows await activation results. |
+5 -4
View File
@@ -168,7 +168,7 @@ flowchart LR
pkg_tools["tools"]
pkg_tool_terminal["tool-terminal"]
pkg_tool_web["tool-web"]
svc_tools["ctx.tools<br/>Tool registry and guarded execution pipeline"]
svc_tools["ctx.tools<br/>Scoped tool registry and executor selection"]
pkg_tool_ask_user["tool-ask-user"]
pkg_tool_cordis["tool-cordis"]
pkg_tool_skill["tool-skill"]
@@ -237,6 +237,7 @@ flowchart LR
svc_ptcRuntime["ctx.ptcRuntime<br/>PTC execution seam"]
pkg_ptc_runtime_node["ptc-runtime-node"]
pkg_experimental_ptc_runtime_python["experimental-ptc-runtime-python"]
pkg_tool_executor_ptc["tool-executor-ptc"]
pkg_workflow_ptc["workflow-ptc"]
pkg_fs["fs"]
svc_fs["ctx.fs<br/>Filesystem provider seam"]
@@ -518,7 +519,7 @@ flowchart LR
svc_pluginMarket --> pkg_plugin_market
svc_pluginRegistryProbe --> pkg_client_ui_plugin_manager
svc_profileContext --> pkg_plugin_manager
svc_ptcRuntime --> pkg_tools
svc_ptcRuntime --> pkg_tool_executor_ptc
svc_ptcRuntime --> pkg_workflow_ptc
svc_remoteAccess --> pkg_api_gateway
svc_remoteAccess --> pkg_api_session_controller
@@ -679,7 +680,7 @@ flowchart LR
| `ctx.sessionReferenceResolver` | `core` | [`session-reference`](../packages/context/session-reference) | - | - | - | 将当前表层中有界的对话快照投影为持久但不可信的消息上下文;Host 适配器负责提及语法。 |
| `ctx.sessionTitle` | `seam` | [`session-title`](../packages/session/session-title) | [`session-title-first-prompt-llm`](../packages/session/session-title-first-prompt-llm), [`session-title-all-prompts-llm`](../packages/experimental/session-title-all-prompts-llm) | - | - | 负责确定性回退、最新标题折叠区,以及唯一的可选异步提供方注册。 |
| `ctx.systemPrompt` | `core` | [`system-prompt`](../packages/core/system-prompt) | - | [`agent-loop`](../packages/core/agent-loop), [`tools`](../packages/core/tools), [`tool-fs`](../packages/fs/tool-fs), [`tool-terminal`](../packages/experimental/tool-terminal), [`tool-web`](../packages/web/tool-web) | - | 为每个步骤收集提示词各部分和面向模型的工具 schema。 |
| `ctx.tools` | `core` | [`tools`](../packages/core/tools) | - | [`agent-loop`](../packages/core/agent-loop), [`tool-ask-user`](../packages/interaction/tool-ask-user), [`tool-bash`](../packages/shell/tool-bash), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-fs`](../packages/fs/tool-fs), [`tool-terminal`](../packages/experimental/tool-terminal), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-todo`](../packages/todo/tool-todo), [`tool-web`](../packages/web/tool-web) | - | 注册能力,负责 PTC mode 传输,并让调用依次经过策略前处理、单调守卫、环绕分派、策略后处理和最终结果观测。 |
| `ctx.tools` | `core` | [`tools`](../packages/core/tools) | - | [`agent-loop`](../packages/core/agent-loop), [`tool-ask-user`](../packages/interaction/tool-ask-user), [`tool-bash`](../packages/shell/tool-bash), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-fs`](../packages/fs/tool-fs), [`tool-terminal`](../packages/experimental/tool-terminal), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-todo`](../packages/todo/tool-todo), [`tool-web`](../packages/web/tool-web) | - | 注册能力和作用域执行器。原生与 PTC 提供者负责工具描述及完整原始调用批次;它们可选用共享流水线处理工具策略和分发。 |
| `ctx.userQuestions` | `seam` | [`user-questions`](../packages/interaction/user-questions) | - | [`tool-ask-user`](../packages/interaction/tool-ask-user) | - | UI 前端提供当前生效的人工回答提供方;tool-ask-user 在提供方无关的 ask() promise 上暂停工具调用。 |
| `ctx.planMode` | `core` | [`plan-mode`](../packages/plan/plan-mode) | - | - | - | 折叠已记录的计划/模式状态,在轮次边界刷新用户选择,渲染由部署方拥有的指导信息,注册 /plan,并在状态转换期间保持计划退出 schema 稳定。 |
| `ctx.agentPresets` | `core` | [`agent-preset-registry`](../packages/preset/agent-preset-registry) | - | - | - | 立即挂载 YAML 声明的 preset 版本,把 Agent 和冷读取绑定到作用域内的贡献,并保留已替换的版本,直到最后一个使用者释放它。 |
@@ -701,7 +702,7 @@ flowchart LR
| `ctx.sandboxPolicy` | `core` | [`sandbox-policy`](../packages/sandbox/sandbox-policy) | - | [`bash-sandbox`](../packages/shell/bash-sandbox), [`fs-sandbox`](../packages/fs/fs-sandbox), [`terminal-bash`](../packages/terminal/terminal-bash) | - | 统一保存部署默认模式和工作区根目录;只有沙箱执行器和提供方读取该服务(工具层使用它同时导出的纯 `sandbox/mode` 折叠区)。两类强制执行组件都读取该服务,因此 bash 与 fs 不会限制到不同的根目录。 |
| `ctx.approval` | `seam` | [`user-approval`](../packages/interaction/user-approval) | - | [`tools`](../packages/core/tools), [`tool-bash`](../packages/shell/tool-bash), [`acp`](../packages/acp/acp) | - | 一次性权限决策通过 `approval/request` waterfall(瀑布式事件)分派;回答方是监听器(即 ACP 为自身 agent 提供的桥接),没有回答方时以 `unavailable` 关闭失败。 |
| `ctx.permissionPresets` | `core` | [`permission-presets`](../packages/interaction/permission-presets) | - | - | - | 面向用户的预设表(`workspace-write`/`danger-full-access`),将沙箱模式与审批策略选项组合在一起;一次切换会写入一个 `permission/preset` 事件,并贯通到两个选项事件。 |
| `ctx.ptcRuntime` | `seam` | [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime) | [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), [`experimental-ptc-runtime-python`](../packages/experimental/ptc-runtime-python) | [`tools`](../packages/core/tools), [`workflow-ptc`](../packages/workflow/workflow-ptc) | - | 使用 Host 提供的异步绑定运行程序;tools 负责 PTC 呈现,workflow-ptc 负责工作流编排。 |
| `ctx.ptcRuntime` | `seam` | [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime) | [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), [`experimental-ptc-runtime-python`](../packages/experimental/ptc-runtime-python) | [`tool-executor-ptc`](../packages/core/tool-executor-ptc), [`workflow-ptc`](../packages/workflow/workflow-ptc) | - | 使用 Host 提供的异步绑定运行程序;tool-executor-ptc 负责 PTC 呈现,workflow-ptc 负责工作流编排。 |
| `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox), [`fs-ssh`](../packages/ssh/fs-ssh) | [`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/experimental/tool-ralph) | - | 提供方准备本地子级或执行外部任务;一个 activation 管理器持有两类生命周期。委派工具选择后端,控制工具投递本地后续消息,工作流等待 activation 结果。 |
+29 -44
View File
@@ -162,11 +162,13 @@ export interface Config {
## `@deepseek-ai/dsh-agent-tool-presentation`
- `inject`: `tools`
- `refs`: [`ToolPresentationMode`](subsystems/tools.md)
- `source`: [`packages/core/agent-tool-presentation/src/index.ts:38`](../packages/core/agent-tool-presentation/src/index.ts)
- `source`: [`packages/core/agent-tool-presentation/src/index.ts:25`](../packages/core/agent-tool-presentation/src/index.ts)
```ts config-catalog
/** Plugin config. */
/**
* Plugin config.
* @deprecated Select the native or PTC executor plugin directly instead of configuring a mode.
*/
export interface Config {
/**
* The form this agent's model sees. `native` sends every visible schema,
@@ -175,7 +177,7 @@ export interface Config {
* without this row already gets, so an omitted value would mean the row was
* composed for nothing.
*/
mode: ToolPresentationMode
mode: 'native' | 'ptc'
}
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-agent-tool-presentation -->
@@ -1706,7 +1708,7 @@ export interface Config {
## `@deepseek-ai/dsh-host-webserver`
- `source`: [`packages/host/webserver/src/index.ts:76`](../packages/host/webserver/src/index.ts)
- `source`: [`packages/host/webserver/src/index.ts:79`](../packages/host/webserver/src/index.ts)
```ts config-catalog
/** Web server listen, TLS, and response-compression config. */
@@ -2890,7 +2892,7 @@ export interface Config {
## `@deepseek-ai/dsh-session-log-export`
- `inject`: `commands` · `connection`
- `source`: [`packages/session-query/session-log-export/src/index.ts:46`](../packages/session-query/session-log-export/src/index.ts)
- `source`: [`packages/session-query/session-log-export/src/index.ts:48`](../packages/session-query/session-log-export/src/index.ts)
```ts config-catalog
/** Session-log archive policy. */
@@ -3270,7 +3272,7 @@ export interface Config {
## `@deepseek-ai/dsh-spill-policy`
- `inject`: `tools`
- `source`: [`packages/spill/spill-policy/src/index.ts:25`](../packages/spill/spill-policy/src/index.ts)
- `source`: [`packages/spill/spill-policy/src/index.ts:26`](../packages/spill/spill-policy/src/index.ts)
```ts config-catalog
/** Optional result-retention budget. */
@@ -3849,6 +3851,23 @@ export interface Config {
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-tool-bash-persistent -->
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-tool-executor-ptc -->
<a id="deepseek-aidsh-tool-executor-ptc"></a>
## `@deepseek-ai/dsh-tool-executor-ptc`
- `inject`: `tools` · `systemPrompt` · `ptcRuntime`
- `source`: [`packages/core/tool-executor-ptc/src/config.ts:5`](../packages/core/tool-executor-ptc/src/config.ts)
```ts config-catalog
/** Program execution limits. */
export interface Config {
/** Maximum overlapping concurrency-safe program subcalls. */
maxParallelSubCalls: number
}
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-tool-executor-ptc -->
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-tool-fs -->
<a id="deepseek-aidsh-tool-fs"></a>
@@ -4320,43 +4339,6 @@ export interface Config {
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-tool-workspace-dependencies -->
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-tools -->
<a id="deepseek-aidsh-tools"></a>
## `@deepseek-ai/dsh-tools`
- `inject`: `systemPrompt`
- `source`: [`packages/core/tools/src/index.ts:675`](../packages/core/tools/src/index.ts)
```ts config-catalog
/** Plugin config: how the registered tools are presented to the model. */
export interface Config {
/**
* Model presentation. `native` (default) sends every visible schema; `ptc`
* sends only `run_code` plus a generated SDK prompt and collapses the
* executor to the same surface (a model-direct call may only name
* `run_code`; `run_code` SDK sub-dispatches keep every visible tool).
* PTC mode requires a `ctx.ptcRuntime` whose `language`
* has a registered SDK renderer (TypeScript or Python) and fails prompt
* assembly when it is absent or has no renderer. Under `ptc`, native names
* in `toolOrder` are invalid.
*/
mode?: ToolPresentationMode
/**
* Concurrency cap for a `run_code` program's overlapping sub-calls
* (default 10, the loop scheduler's own default). Sub-calls follow the
* native scheduling contract — only calls whose tools classify
* concurrency-safe overlap; exclusive calls form barriers — so `1`
* restores strictly serial dispatch. Must be a positive integer.
*/
maxParallelSubCalls?: number
}
/** How the registry presents its tools to the model (see {@link Config.mode}). */
export type ToolPresentationMode = 'native' | 'ptc'
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-tools -->
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-typert-loader -->
<a id="deepseek-aidsh-typert-loader"></a>
@@ -4801,9 +4783,11 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
| `@deepseek-ai/dsh-terminal` | — | [`packages/terminal/terminal/src/index.ts`](../packages/terminal/terminal/src/index.ts) |
| `@deepseek-ai/dsh-tool-call-timeout-policy` | `tools` | [`packages/guard/timeout-policy/src/index.ts`](../packages/guard/timeout-policy/src/index.ts) |
| `@deepseek-ai/dsh-tool-cordis` | `tools` · `cordisInspect` | [`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts) |
| `@deepseek-ai/dsh-tool-executor-native` | `tools` | [`packages/core/tool-executor-native/src/index.ts`](../packages/core/tool-executor-native/src/index.ts) |
| `@deepseek-ai/dsh-tool-schedule` | `tools` | [`packages/schedule/tool-schedule/src/index.ts`](../packages/schedule/tool-schedule/src/index.ts) |
| `@deepseek-ai/dsh-tool-subagent-control` | `tools` · `subagents` · `agents` | [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/subagent/tool-subagent-control/src/index.ts) |
| `@deepseek-ai/dsh-tool-working-directory` | `tools` · `workingDirectory` | [`packages/session/tool-working-directory/src/index.ts`](../packages/session/tool-working-directory/src/index.ts) |
| `@deepseek-ai/dsh-tools` | `systemPrompt` | [`packages/core/tools/src/index.ts`](../packages/core/tools/src/index.ts) |
| `@deepseek-ai/dsh-user-questions` | — | [`packages/interaction/user-questions/src/index.ts`](../packages/interaction/user-questions/src/index.ts) |
| `@deepseek-ai/dsh-webhook` | `agents` · `agentDefaultModel` · `agentPresets` · `permissionPresets` · `sessionTitle` · `workspaceRegistry` | [`packages/experimental/webhook/src/index.ts`](../packages/experimental/webhook/src/index.ts) |
| `@deepseek-ai/dsh-workspace` | `storageDomain` · `sessionPersistence` | [`packages/workspace/workspace/src/index.ts`](../packages/workspace/workspace/src/index.ts) |
@@ -4900,6 +4884,7 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them.
| `@deepseek-ai/dsh-session-title-llm` | — | [`packages/session/session-title-llm/src/index.ts`](../packages/session/session-title-llm/src/index.ts) |
| `@deepseek-ai/dsh-ssh-helper-runtime` | — | [`packages/ssh/ssh-helper-runtime/src/index.ts`](../packages/ssh/ssh-helper-runtime/src/index.ts) |
| `@deepseek-ai/dsh-timeout` | — | [`packages/util/timeout/src/index.ts`](../packages/util/timeout/src/index.ts) |
| `@deepseek-ai/dsh-tool-testkit` | — | [`packages/test-support/tool-testkit/src/index.ts`](../packages/test-support/tool-testkit/src/index.ts) |
| `@deepseek-ai/dsh-typert-generator` | — | [`packages/typert/generator/src/index.ts`](../packages/typert/generator/src/index.ts) |
| `@deepseek-ai/dsh-typert-protocol` | — | [`packages/typert/protocol/src/index.ts`](../packages/typert/protocol/src/index.ts) |
| `@deepseek-ai/dsh-typert-registry` | — | [`packages/typert/registry/src/index.ts`](../packages/typert/registry/src/index.ts) |
+29 -44
View File
@@ -164,11 +164,13 @@ export interface Config {
## `@deepseek-ai/dsh-agent-tool-presentation`
- `inject`: `tools`
- `refs`: [`ToolPresentationMode`](subsystems/tools.zh.md)
- `source`: [`packages/core/agent-tool-presentation/src/index.ts:38`](../packages/core/agent-tool-presentation/src/index.ts)
- `source`: [`packages/core/agent-tool-presentation/src/index.ts:25`](../packages/core/agent-tool-presentation/src/index.ts)
```ts config-catalog
/** Plugin config. */
/**
* Plugin config.
* @deprecated Select the native or PTC executor plugin directly instead of configuring a mode.
*/
export interface Config {
/**
* The form this agent's model sees. `native` sends every visible schema,
@@ -177,7 +179,7 @@ export interface Config {
* without this row already gets, so an omitted value would mean the row was
* composed for nothing.
*/
mode: ToolPresentationMode
mode: 'native' | 'ptc'
}
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-agent-tool-presentation -->
@@ -1708,7 +1710,7 @@ export interface Config {
## `@deepseek-ai/dsh-host-webserver`
- `source`: [`packages/host/webserver/src/index.ts:76`](../packages/host/webserver/src/index.ts)
- `source`: [`packages/host/webserver/src/index.ts:79`](../packages/host/webserver/src/index.ts)
```ts config-catalog
/** Web server listen, TLS, and response-compression config. */
@@ -2892,7 +2894,7 @@ export interface Config {
## `@deepseek-ai/dsh-session-log-export`
- `inject`: `commands` · `connection`
- `source`: [`packages/session-query/session-log-export/src/index.ts:46`](../packages/session-query/session-log-export/src/index.ts)
- `source`: [`packages/session-query/session-log-export/src/index.ts:48`](../packages/session-query/session-log-export/src/index.ts)
```ts config-catalog
/** Session-log archive policy. */
@@ -3272,7 +3274,7 @@ export interface Config {
## `@deepseek-ai/dsh-spill-policy`
- `inject`: `tools`
- `source`: [`packages/spill/spill-policy/src/index.ts:25`](../packages/spill/spill-policy/src/index.ts)
- `source`: [`packages/spill/spill-policy/src/index.ts:26`](../packages/spill/spill-policy/src/index.ts)
```ts config-catalog
/** Optional result-retention budget. */
@@ -3851,6 +3853,23 @@ export interface Config {
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-tool-bash-persistent -->
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-tool-executor-ptc -->
<a id="deepseek-aidsh-tool-executor-ptc"></a>
## `@deepseek-ai/dsh-tool-executor-ptc`
- `inject`: `tools` · `systemPrompt` · `ptcRuntime`
- `source`: [`packages/core/tool-executor-ptc/src/config.ts:5`](../packages/core/tool-executor-ptc/src/config.ts)
```ts config-catalog
/** Program execution limits. */
export interface Config {
/** Maximum overlapping concurrency-safe program subcalls. */
maxParallelSubCalls: number
}
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-tool-executor-ptc -->
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-tool-fs -->
<a id="deepseek-aidsh-tool-fs"></a>
@@ -4322,43 +4341,6 @@ export interface Config {
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-tool-workspace-dependencies -->
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-tools -->
<a id="deepseek-aidsh-tools"></a>
## `@deepseek-ai/dsh-tools`
- `inject`: `systemPrompt`
- `source`: [`packages/core/tools/src/index.ts:675`](../packages/core/tools/src/index.ts)
```ts config-catalog
/** Plugin config: how the registered tools are presented to the model. */
export interface Config {
/**
* Model presentation. `native` (default) sends every visible schema; `ptc`
* sends only `run_code` plus a generated SDK prompt and collapses the
* executor to the same surface (a model-direct call may only name
* `run_code`; `run_code` SDK sub-dispatches keep every visible tool).
* PTC mode requires a `ctx.ptcRuntime` whose `language`
* has a registered SDK renderer (TypeScript or Python) and fails prompt
* assembly when it is absent or has no renderer. Under `ptc`, native names
* in `toolOrder` are invalid.
*/
mode?: ToolPresentationMode
/**
* Concurrency cap for a `run_code` program's overlapping sub-calls
* (default 10, the loop scheduler's own default). Sub-calls follow the
* native scheduling contract — only calls whose tools classify
* concurrency-safe overlap; exclusive calls form barriers — so `1`
* restores strictly serial dispatch. Must be a positive integer.
*/
maxParallelSubCalls?: number
}
/** How the registry presents its tools to the model (see {@link Config.mode}). */
export type ToolPresentationMode = 'native' | 'ptc'
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-tools -->
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-typert-loader -->
<a id="deepseek-aidsh-typert-loader"></a>
@@ -4803,9 +4785,11 @@ export interface Config {
| `@deepseek-ai/dsh-terminal` | — | [`packages/terminal/terminal/src/index.ts`](../packages/terminal/terminal/src/index.ts) |
| `@deepseek-ai/dsh-tool-call-timeout-policy` | `tools` | [`packages/guard/timeout-policy/src/index.ts`](../packages/guard/timeout-policy/src/index.ts) |
| `@deepseek-ai/dsh-tool-cordis` | `tools` · `cordisInspect` | [`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts) |
| `@deepseek-ai/dsh-tool-executor-native` | `tools` | [`packages/core/tool-executor-native/src/index.ts`](../packages/core/tool-executor-native/src/index.ts) |
| `@deepseek-ai/dsh-tool-schedule` | `tools` | [`packages/schedule/tool-schedule/src/index.ts`](../packages/schedule/tool-schedule/src/index.ts) |
| `@deepseek-ai/dsh-tool-subagent-control` | `tools` · `subagents` · `agents` | [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/subagent/tool-subagent-control/src/index.ts) |
| `@deepseek-ai/dsh-tool-working-directory` | `tools` · `workingDirectory` | [`packages/session/tool-working-directory/src/index.ts`](../packages/session/tool-working-directory/src/index.ts) |
| `@deepseek-ai/dsh-tools` | `systemPrompt` | [`packages/core/tools/src/index.ts`](../packages/core/tools/src/index.ts) |
| `@deepseek-ai/dsh-user-questions` | — | [`packages/interaction/user-questions/src/index.ts`](../packages/interaction/user-questions/src/index.ts) |
| `@deepseek-ai/dsh-webhook` | `agents` · `agentDefaultModel` · `agentPresets` · `permissionPresets` · `sessionTitle` · `workspaceRegistry` | [`packages/experimental/webhook/src/index.ts`](../packages/experimental/webhook/src/index.ts) |
| `@deepseek-ai/dsh-workspace` | `storageDomain` · `sessionPersistence` | [`packages/workspace/workspace/src/index.ts`](../packages/workspace/workspace/src/index.ts) |
@@ -4902,6 +4886,7 @@ export interface Config {
| `@deepseek-ai/dsh-session-title-llm` | — | [`packages/session/session-title-llm/src/index.ts`](../packages/session/session-title-llm/src/index.ts) |
| `@deepseek-ai/dsh-ssh-helper-runtime` | — | [`packages/ssh/ssh-helper-runtime/src/index.ts`](../packages/ssh/ssh-helper-runtime/src/index.ts) |
| `@deepseek-ai/dsh-timeout` | — | [`packages/util/timeout/src/index.ts`](../packages/util/timeout/src/index.ts) |
| `@deepseek-ai/dsh-tool-testkit` | — | [`packages/test-support/tool-testkit/src/index.ts`](../packages/test-support/tool-testkit/src/index.ts) |
| `@deepseek-ai/dsh-typert-generator` | — | [`packages/typert/generator/src/index.ts`](../packages/typert/generator/src/index.ts) |
| `@deepseek-ai/dsh-typert-protocol` | — | [`packages/typert/protocol/src/index.ts`](../packages/typert/protocol/src/index.ts) |
| `@deepseek-ai/dsh-typert-registry` | — | [`packages/typert/registry/src/index.ts`](../packages/typert/registry/src/index.ts) |
+5 -4
View File
@@ -37,12 +37,13 @@ export function apply(ctx: Context) {
// the model. ToolCallId brands the correlation id a provider would issue.
void (async () => {
const result = await ctx.tools.execute({
callId: brandString<ToolCallId>('demo-1'),
name: 'greet',
arguments: { name: 'Cordis' },
calls: [{
type: 'tool-call', id: brandString<ToolCallId>('demo-1'),
name: 'greet', arguments: JSON.stringify({ name: 'Cordis' }),
}],
signal: new AbortController().signal,
})
console.log('tool replied:', JSON.stringify(result.content))
console.log('tool replied:', JSON.stringify(result.results[0]?.result.content))
})()
}
```
@@ -37,12 +37,13 @@ export function apply(ctx: Context) {
// the model. ToolCallId brands the correlation id a provider would issue.
void (async () => {
const result = await ctx.tools.execute({
callId: brandString<ToolCallId>('demo-1'),
name: 'greet',
arguments: { name: 'Cordis' },
calls: [{
type: 'tool-call', id: brandString<ToolCallId>('demo-1'),
name: 'greet', arguments: JSON.stringify({ name: 'Cordis' }),
}],
signal: new AbortController().signal,
})
console.log('tool replied:', JSON.stringify(result.content))
console.log('tool replied:', JSON.stringify(result.results[0]?.result.content))
})()
}
```
@@ -12,8 +12,8 @@
en: bf79c6acf35c90a5
zh: 9f2ce6c7f6493b17
/package-surface-rules/package-json-exports-uses-the-standard-key-set:
en: 3604bf435be1c430
zh: 169621c8a0c2dc4c
en: 692ded0ff903609d
zh: c871ab58ca92ad70
/package-surface-rules/dependency-declarations-and-version-consistency-are-enforced-by-the-dependency-scan:
en: e68bfcde70f845b4
zh: 73e5c4fb067c4fa1
@@ -78,6 +78,7 @@ A `package.json` `exports` map uses only the standard key set, in which every ke
- Standard keys, each with one meaning repo-wide, across `packages/*/*/package.json` and `apps/*/package.json`: `.` (the package entry), `./package.json` (toolchain access, not a semantic export), `./src/*` (source passthrough for tests and tools, whose documented status is still open), `./client` (the browser entry, paired with the `dsh.client` declaration), `./types` (the type entry), `./typert` (the typert type runtime), `./remote` (the `@Remote` call surface), `./locale/*.json` (locale resources), `./cordis.patch.yml` (the bundle patch layer declared by `dsh.bundle`). Counts are not listed here because they rot on the next package; the recompute command below prints them.
- Keys outside that set (`./brand` in 5 packages, `./worker` in 4, `./startup`, `./runner`, and `./protocol` in 2 each, plus many one-off keys) have no documented meaning; they are pending review, not a pattern to copy.
- `dsh-tools` additionally exposes `./pipeline` for reusable invocation stages and batch scheduling. Its main and pipeline bundles share runtime chunks to preserve error-class identity; workspace constraints check the export targets and publication files.
- Judged condition: does a change add an export key outside the standard set?
- Exception: none by default; a new key changes the owning document and the check first, with the domain owner's confirmation.
- Recompute the distribution:
@@ -78,6 +78,7 @@ import { isSurfaceEligibleType } from '../../core/session/src/surface.ts'
- 标准键,每个键在全仓只有一个含义,统计口径是 `packages/*/*/package.json` 与 `apps/*/package.json`:`.`(包主入口)、`./package.json`(工具链访问,不是语义导出)、`./src/*`(测试与工具的源码透传,文档状态仍待定)、`./client`(浏览器入口,与 `dsh.client` 声明配对)、`./types`(类型入口)、`./typert`(typert 类型运行时)、`./remote`(`@Remote` 调用面)、`./locale/*.json`(本地化资源)、`./cordis.patch.yml`(`dsh.bundle` 声明的 bundle 补丁层)。这里不列各键的包数,因为下一个包就会让它过期;下面的复算命令会打印当前数字。
- 集合之外的键(`./brand` 5 个包、`./worker` 4 个、`./startup`、`./runner`、`./protocol` 各 2 个,另有大量一次性键)没有文档含义,属待核项,不是可照抄的先例。
- `dsh-tools` 另行提供 `./pipeline`,用于复用调用阶段与批量调度。主入口与 pipeline bundle 共享运行时 chunk,以保持错误类的身份一致;工作区检查验证导出目标与发布文件。
- 判定条件:改动是否新增了标准集合以外的 export 键?
- 例外:无默认例外;新增键须先改正家文档与检查,并取得对应领域专家确认。
- 复算分布:
+8 -8
View File
@@ -65,7 +65,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:56`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`schedule`](../packages/schedule/schedule), `server`, `session-controller`, [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry) |
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:65`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts: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-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `session/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-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter), [`tool-executor-ptc`](../packages/core/tool-executor-ptc), [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) |
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:75`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`emit`) | `remotes` |
| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:296`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
@@ -75,14 +75,14 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:158`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) |
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | `browser-use-runtime`, [`working-directory`](../packages/session/working-directory) |
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:209`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:165`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `claude-code-mods`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:177`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `claude-code-mods`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) |
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:154`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:191`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:199`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) |
| `tools/change` | `emit` | [`packages/core/tools/src/events.ts:56`](../packages/core/tools/src/events.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `tools/execute` | `waterfall` | [`packages/core/tools/src/events.ts:29`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `claude-code-mods`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/events.ts:41`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `claude-code-mods`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) |
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/events.ts:18`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tool-executor-ptc/src/runtime-types.ts:46`](../packages/core/tool-executor-ptc/src/runtime-types.ts) | [`tool-executor-ptc`](../packages/core/tool-executor-ptc) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
| `tools/result` | `emit` | [`packages/core/tools/src/events.ts:49`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) |
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:174`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes`, `server` |
| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:39`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `remote-access`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` |
| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:42`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `remote-access`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` |
| `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) |
| `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:68`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) |
| `workflow/end` | `emit` | [`packages/workflow/workflow/src/index.ts:89`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | - |
+8 -8
View File
@@ -67,7 +67,7 @@
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:56`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`schedule`](../packages/schedule/schedule), `server`, `session-controller`, [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry) |
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:65`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts: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-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `session/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-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter), [`tool-executor-ptc`](../packages/core/tool-executor-ptc), [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) |
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:75`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`emit`) | `remotes` |
| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:296`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
@@ -77,14 +77,14 @@
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:158`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) |
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | `browser-use-runtime`, [`working-directory`](../packages/session/working-directory) |
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:209`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:165`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `claude-code-mods`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:177`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `claude-code-mods`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) |
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:154`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:191`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:199`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) |
| `tools/change` | `emit` | [`packages/core/tools/src/events.ts:56`](../packages/core/tools/src/events.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `tools/execute` | `waterfall` | [`packages/core/tools/src/events.ts:29`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `claude-code-mods`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/events.ts:41`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `claude-code-mods`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) |
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/events.ts:18`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tool-executor-ptc/src/runtime-types.ts:46`](../packages/core/tool-executor-ptc/src/runtime-types.ts) | [`tool-executor-ptc`](../packages/core/tool-executor-ptc) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
| `tools/result` | `emit` | [`packages/core/tools/src/events.ts:49`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) |
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:174`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes`, `server` |
| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:39`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `remote-access`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` |
| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:42`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `remote-access`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` |
| `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) |
| `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:68`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) |
| `workflow/end` | `emit` | [`packages/workflow/workflow/src/index.ts:89`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | - |
+20 -5
View File
@@ -47,6 +47,8 @@ flowchart TD
pkg_scope["scope"]
pkg_session["session"]
pkg_system_prompt["system-prompt"]
pkg_tool_executor_native["tool-executor-native"]
pkg_tool_executor_ptc["tool-executor-ptc"]
pkg_tools["tools"]
end
subgraph group_goal["packages/goal"]
@@ -445,6 +447,7 @@ flowchart TD
pkg_loader_smoke["loader-smoke"]
pkg_remote_mock["remote-mock"]
pkg_session_snapshot["session-snapshot"]
pkg_tool_testkit["tool-testkit"]
end
subgraph group_typert["packages/typert"]
pkg_typert_generator["typert-generator"]
@@ -726,14 +729,10 @@ flowchart TD
pkg_llm_deepseek_account --> pkg_timeout
pkg_tools --> pkg_agent
pkg_tools --> pkg_llm
pkg_tools --> pkg_ptc_runtime
pkg_tools --> pkg_sandbox
pkg_tools --> pkg_sandbox_policy
pkg_tools --> pkg_scope
pkg_tools --> pkg_session
pkg_tools --> pkg_system_prompt
pkg_tools --> pkg_user_approval
pkg_tools --> pkg_working_directory
pkg_command_goal --> pkg_commands
pkg_command_goal --> pkg_goal
pkg_command_goal --> pkg_llm
@@ -830,6 +829,17 @@ flowchart TD
pkg_agent_loop --> pkg_system_prompt
pkg_agent_loop --> pkg_tools
pkg_agent_tool_presentation --> pkg_tools
pkg_tool_executor_native --> pkg_tools
pkg_tool_executor_ptc --> pkg_agent
pkg_tool_executor_ptc --> pkg_llm
pkg_tool_executor_ptc --> pkg_ptc_runtime
pkg_tool_executor_ptc --> pkg_sandbox
pkg_tool_executor_ptc --> pkg_sandbox_policy
pkg_tool_executor_ptc --> pkg_scope
pkg_tool_executor_ptc --> pkg_session
pkg_tool_executor_ptc --> pkg_tools
pkg_tool_executor_ptc --> pkg_user_approval
pkg_tool_executor_ptc --> pkg_working_directory
pkg_tool_goal --> pkg_agent
pkg_tool_goal --> pkg_goal
pkg_tool_goal --> pkg_llm
@@ -1041,6 +1051,8 @@ flowchart TD
pkg_llm_replay --> pkg_deepseek_llm_api_extensions
pkg_llm_replay --> pkg_llm
pkg_llm_replay --> pkg_session
pkg_tool_testkit --> pkg_llm
pkg_tool_testkit --> pkg_tools
pkg_tool_workflow --> pkg_agent
pkg_tool_workflow --> pkg_jobs
pkg_tool_workflow --> pkg_llm
@@ -1673,7 +1685,7 @@ flowchart TD
| [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`http-proxy`](../packages/util/http-proxy), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`llm-deepseek-account`](../packages/llm/llm-deepseek-account) | `llm` | [`deepseek-account`](../packages/credentials/deepseek-account), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`timeout`](../packages/util/timeout) |
| [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) |
| [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/interaction/user-approval) |
| [`command-goal`](../packages/goal/command-goal) | `goal` | [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm) |
| [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
| [`web-deepseek-api`](../packages/web/web-deepseek-api) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`deepseek-account`](../packages/credentials/deepseek-account), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`web`](../packages/web/web) |
@@ -1694,6 +1706,8 @@ flowchart TD
| [`terminal-bash`](../packages/terminal/terminal-bash) | `terminal` | [`agent`](../packages/core/agent), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess), [`terminal`](../packages/terminal/terminal), [`working-directory`](../packages/session/working-directory) |
| [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`agent-tool-presentation`](../packages/core/agent-tool-presentation) | `core` | [`tools`](../packages/core/tools) |
| [`tool-executor-native`](../packages/core/tool-executor-native) | `core` | [`tools`](../packages/core/tools) |
| [`tool-executor-ptc`](../packages/core/tool-executor-ptc) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) |
| [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) |
| [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
@@ -1735,6 +1749,7 @@ flowchart TD
| [`tool-pwsh-persistent`](../packages/shell/tool-pwsh-persistent) | `shell` | [`agent`](../packages/core/agent), [`output-retention`](../packages/util/output-retention), [`terminal`](../packages/terminal/terminal), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
| [`ssh`](../packages/ssh/ssh) | `ssh` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-local`](../packages/sandbox/sandbox-local), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session-projection`](../packages/session/session-projection), [`subprocess`](../packages/subprocess/subprocess), [`subprocess-local`](../packages/subprocess/subprocess-local), [`util-values`](../packages/util/values) |
| [`llm-replay`](../packages/test-support/llm-replay) | `test-support` | [`compaction`](../packages/compaction/compaction), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`tool-testkit`](../packages/test-support/tool-testkit) | `test-support` | [`llm`](../packages/llm/llm), [`tools`](../packages/core/tools) |
| [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | `llm` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`session`](../packages/core/session) |
| [`token-meter`](../packages/llm/token-meter) | `llm` | [`compaction`](../packages/compaction/compaction), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
+20 -5
View File
@@ -49,6 +49,8 @@ flowchart TD
pkg_scope["scope"]
pkg_session["session"]
pkg_system_prompt["system-prompt"]
pkg_tool_executor_native["tool-executor-native"]
pkg_tool_executor_ptc["tool-executor-ptc"]
pkg_tools["tools"]
end
subgraph group_goal["packages/goal"]
@@ -447,6 +449,7 @@ flowchart TD
pkg_loader_smoke["loader-smoke"]
pkg_remote_mock["remote-mock"]
pkg_session_snapshot["session-snapshot"]
pkg_tool_testkit["tool-testkit"]
end
subgraph group_typert["packages/typert"]
pkg_typert_generator["typert-generator"]
@@ -728,14 +731,10 @@ flowchart TD
pkg_llm_deepseek_account --> pkg_timeout
pkg_tools --> pkg_agent
pkg_tools --> pkg_llm
pkg_tools --> pkg_ptc_runtime
pkg_tools --> pkg_sandbox
pkg_tools --> pkg_sandbox_policy
pkg_tools --> pkg_scope
pkg_tools --> pkg_session
pkg_tools --> pkg_system_prompt
pkg_tools --> pkg_user_approval
pkg_tools --> pkg_working_directory
pkg_command_goal --> pkg_commands
pkg_command_goal --> pkg_goal
pkg_command_goal --> pkg_llm
@@ -832,6 +831,17 @@ flowchart TD
pkg_agent_loop --> pkg_system_prompt
pkg_agent_loop --> pkg_tools
pkg_agent_tool_presentation --> pkg_tools
pkg_tool_executor_native --> pkg_tools
pkg_tool_executor_ptc --> pkg_agent
pkg_tool_executor_ptc --> pkg_llm
pkg_tool_executor_ptc --> pkg_ptc_runtime
pkg_tool_executor_ptc --> pkg_sandbox
pkg_tool_executor_ptc --> pkg_sandbox_policy
pkg_tool_executor_ptc --> pkg_scope
pkg_tool_executor_ptc --> pkg_session
pkg_tool_executor_ptc --> pkg_tools
pkg_tool_executor_ptc --> pkg_user_approval
pkg_tool_executor_ptc --> pkg_working_directory
pkg_tool_goal --> pkg_agent
pkg_tool_goal --> pkg_goal
pkg_tool_goal --> pkg_llm
@@ -1043,6 +1053,8 @@ flowchart TD
pkg_llm_replay --> pkg_deepseek_llm_api_extensions
pkg_llm_replay --> pkg_llm
pkg_llm_replay --> pkg_session
pkg_tool_testkit --> pkg_llm
pkg_tool_testkit --> pkg_tools
pkg_tool_workflow --> pkg_agent
pkg_tool_workflow --> pkg_jobs
pkg_tool_workflow --> pkg_llm
@@ -1675,7 +1687,7 @@ flowchart TD
| [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`http-proxy`](../packages/util/http-proxy), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`llm-deepseek-account`](../packages/llm/llm-deepseek-account) | `llm` | [`deepseek-account`](../packages/credentials/deepseek-account), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`timeout`](../packages/util/timeout) |
| [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) |
| [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/interaction/user-approval) |
| [`command-goal`](../packages/goal/command-goal) | `goal` | [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm) |
| [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
| [`web-deepseek-api`](../packages/web/web-deepseek-api) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`deepseek-account`](../packages/credentials/deepseek-account), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`web`](../packages/web/web) |
@@ -1696,6 +1708,8 @@ flowchart TD
| [`terminal-bash`](../packages/terminal/terminal-bash) | `terminal` | [`agent`](../packages/core/agent), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess), [`terminal`](../packages/terminal/terminal), [`working-directory`](../packages/session/working-directory) |
| [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`agent-tool-presentation`](../packages/core/agent-tool-presentation) | `core` | [`tools`](../packages/core/tools) |
| [`tool-executor-native`](../packages/core/tool-executor-native) | `core` | [`tools`](../packages/core/tools) |
| [`tool-executor-ptc`](../packages/core/tool-executor-ptc) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) |
| [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) |
| [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
@@ -1737,6 +1751,7 @@ flowchart TD
| [`tool-pwsh-persistent`](../packages/shell/tool-pwsh-persistent) | `shell` | [`agent`](../packages/core/agent), [`output-retention`](../packages/util/output-retention), [`terminal`](../packages/terminal/terminal), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
| [`ssh`](../packages/ssh/ssh) | `ssh` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-local`](../packages/sandbox/sandbox-local), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session-projection`](../packages/session/session-projection), [`subprocess`](../packages/subprocess/subprocess), [`subprocess-local`](../packages/subprocess/subprocess-local), [`util-values`](../packages/util/values) |
| [`llm-replay`](../packages/test-support/llm-replay) | `test-support` | [`compaction`](../packages/compaction/compaction), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`tool-testkit`](../packages/test-support/tool-testkit) | `test-support` | [`llm`](../packages/llm/llm), [`tools`](../packages/core/tools) |
| [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | `llm` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`session`](../packages/core/session) |
| [`token-meter`](../packages/llm/token-meter) | `llm` | [`compaction`](../packages/compaction/compaction), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
+20 -20
View File
@@ -249,11 +249,11 @@
en: 03f36237cbf9e267
zh: 83fe2e69bbbfd3c1
/session-persistence-event-catalog/events/tool/tool-ptc-dispatch-log-only:
en: 3d5d72b99e702a8c
zh: e8c3b82cd5361c4a
en: a62b38c00afed655
zh: 793bef93e856eafa
/session-persistence-event-catalog/events/tool/tool-ptc-dispatch-start-log-only:
en: de38cf70d81ad7c3
zh: dd0a2a8be099271d
en: da841b1ed3b22c87
zh: 4c9a4d4bc5bd76b6
/session-persistence-event-catalog/events/tool/tool-result-surface:
en: 283e3cf1fdd57f05
zh: 0bc3a15ae4f47aaf
@@ -1164,14 +1164,14 @@
en: bb11552744e477b8
zh: afb881f3ebf97e68
/session-persistence-event-catalog/resolved-persistence-types/principal:
en: d1e45e03f161dc7c
zh: 4f80af59ca4967c5
en: 6d0d81e0e833403e
zh: fa47cd75ee75f948
/session-persistence-event-catalog/resolved-persistence-types/ptcdispatcheventdata:
en: 81e66c5a07e17dd6
zh: 0ecf3cfceba14d2b
en: c0f7b22b41cb0608
zh: de14a1b5cac17665
/session-persistence-event-catalog/resolved-persistence-types/ptcdispatchstarteventdata:
en: fbbf650635960ec8
zh: 9457953f4cbe07c6
en: aca35dac4f9d2add
zh: 6a4bdb95e3c7b0ce
/session-persistence-event-catalog/resolved-persistence-types/reasoningblock:
en: 07e04c3e336f880f
zh: 6f8986ede9994fa1
@@ -1284,11 +1284,11 @@
en: 036b2595215410bb
zh: 41792808ebac1fa3
/session-persistence-event-catalog/resolved-persistence-types/toolremovalblock:
en: f43a635a14568780
zh: 8d22226b045122eb
en: fdac0df45529e14c
zh: c5e38de83a0e0a42
/session-persistence-event-catalog/resolved-persistence-types/toolrestriction:
en: d7d3c0238d9b373e
zh: f49d93ee54e7a780
en: 9d64c653e33bf2b6
zh: 7bb427b48756f30f
/session-persistence-event-catalog/resolved-persistence-types/toolresultmessage:
en: 2258683c3cfa4ce3
zh: 5663aa76c911761b
@@ -1443,8 +1443,8 @@
en: a1f188c9058cf621
zh: eb43c0f045fb9afb
/session-persistence-event-catalog/resolved-persistence-types/code-name-reason:
en: 404a76066a59f3c6
zh: ec9d510bdbac9786
en: 39c67256532a22f9
zh: 59f823e98630c5e2
/session-persistence-event-catalog/resolved-persistence-types/commandid-kind-sourceeventseq-text:
en: 87b03e1c73d2a326
zh: d7b9f06863cad891
@@ -1674,8 +1674,8 @@
en: c53129cf7214286c
zh: 8c5183b6dabbf714
/session-persistence-event-catalog/resolved-persistence-types/kind-ptc-mode:
en: 8de77980e11a0562
zh: 6de199e70a666a71
en: b4fd9b219667fb26
zh: bc56baa64fd7ffd8
/session-persistence-event-catalog/resolved-persistence-types/kind-repeat-tool-reminder:
en: e3a0ef98a197374e
zh: 95e9363529a62cc3
@@ -1830,8 +1830,8 @@
en: 08be4349548406a9
zh: 8e7cf9966198ef78
/session-persistence-event-catalog/resolved-persistence-types/kind-tool-registry:
en: 809a23c25ded8497
zh: e9cd23e281f8d31f
en: c59d020828d8211f
zh: 154a30cb69e79489
/session-persistence-event-catalog/resolved-persistence-types/kind-user:
en: 43f0c6042d410afd
zh: a0b68de4e5d220bd
+11 -11
View File
@@ -1116,7 +1116,7 @@ Source: [`packages/core/session/src/types.ts:362`](../packages/core/session/src/
'tool/ptc-dispatch': PtcDispatchEventData
```
Source: [`packages/core/tools/src/types.ts:61`](../packages/core/tools/src/types.ts)
Source: [`packages/core/tool-executor-ptc/src/types.ts:61`](../packages/core/tool-executor-ptc/src/types.ts)
<a id="toolptc-dispatch-start--log-only"></a>
@@ -1139,7 +1139,7 @@ Source: [`packages/core/tools/src/types.ts:61`](../packages/core/tools/src/types
'tool/ptc-dispatch-start': PtcDispatchStartEventData
```
Source: [`packages/core/tools/src/types.ts:45`](../packages/core/tools/src/types.ts)
Source: [`packages/core/tool-executor-ptc/src/types.ts:45`](../packages/core/tool-executor-ptc/src/types.ts)
<a id="toolresult--surface"></a>
@@ -4442,7 +4442,7 @@ Sources: [`packages/typert/protocol/src/types.ts:457`](../packages/typert/protoc
<a id="persistence-type-sha256-cef171adf021a1867d244618a3dbbaafc22beb17c7f3df3ee203393271c8c6f6"></a>
<a id="persistence-type-packagescoretoolssrctypestsptcdispatcheventdata"></a>
<a id="persistence-type-packagescoretool-executor-ptcsrctypestsptcdispatcheventdata"></a>
<a id="persistence-type-ptcdispatcheventdata"></a>
@@ -4450,7 +4450,7 @@ Sources: [`packages/typert/protocol/src/types.ts:457`](../packages/typert/protoc
SHA-256: `cef171adf021a1867d244618a3dbbaafc22beb17c7f3df3ee203393271c8c6f6`
Sources: [`packages/core/tools/src/types.ts:21`](../packages/core/tools/src/types.ts)
Sources: [`packages/core/tool-executor-ptc/src/types.ts:21`](../packages/core/tool-executor-ptc/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -4466,7 +4466,7 @@ Sources: [`packages/core/tools/src/types.ts:21`](../packages/core/tools/src/type
<a id="persistence-type-sha256-2fe5026daf14bd4ff2b7bfb7cf9791c302d9cbba06802178b6dfec00b892698f"></a>
<a id="persistence-type-packagescoretoolssrctypestsptcdispatchstarteventdata"></a>
<a id="persistence-type-packagescoretool-executor-ptcsrctypestsptcdispatchstarteventdata"></a>
<a id="persistence-type-ptcdispatchstarteventdata"></a>
@@ -4474,7 +4474,7 @@ Sources: [`packages/core/tools/src/types.ts:21`](../packages/core/tools/src/type
SHA-256: `2fe5026daf14bd4ff2b7bfb7cf9791c302d9cbba06802178b6dfec00b892698f`
Sources: [`packages/core/tools/src/types.ts:12`](../packages/core/tools/src/types.ts)
Sources: [`packages/core/tool-executor-ptc/src/types.ts:12`](../packages/core/tool-executor-ptc/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -5185,7 +5185,7 @@ Sources: [`packages/llm/llm/src/types.ts:127`](../packages/llm/llm/src/types.ts)
<a id="persistence-type-sha256-3cfc3a56502da1f8c6153c2c56657bab4b749056968634dd0991d325ec1c919f"></a>
<a id="persistence-type-packagescoretoolssrcindextstoolrestriction"></a>
<a id="persistence-type-packagescoretoolssrccontractststoolrestriction"></a>
<a id="persistence-type-toolrestriction"></a>
@@ -5193,7 +5193,7 @@ Sources: [`packages/llm/llm/src/types.ts:127`](../packages/llm/llm/src/types.ts)
SHA-256: `3cfc3a56502da1f8c6153c2c56657bab4b749056968634dd0991d325ec1c919f`
Sources: [`packages/core/tools/src/index.ts:701`](../packages/core/tools/src/index.ts)
Sources: [`packages/core/tools/src/contracts.ts:326`](../packages/core/tools/src/contracts.ts)
| Property | Presence | Type |
|---|---|---|
@@ -5979,7 +5979,7 @@ SHA-256: `cbbe8377ee744964537dbafd83607219e58ba79a13f6db0d0645c7ad3d5e8a09`
SHA-256: `a81059ca06999da1e712414520c2c65e491d052fcf9640fae20aa38f0a5dc735`
Sources: [`packages/core/session/src/types.ts:384`](../packages/core/session/src/types.ts) · [`packages/core/tools/src/types.ts:27`](../packages/core/tools/src/types.ts)
Sources: [`packages/core/session/src/types.ts:384`](../packages/core/session/src/types.ts) · [`packages/core/tool-executor-ptc/src/types.ts:27`](../packages/core/tool-executor-ptc/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -6960,7 +6960,7 @@ Sources: [`packages/session/session-title/src/types.ts:30`](../packages/session/
SHA-256: `dba54c09c6e7aafc9acbed7792ecc920d99f92d0784aeeed10af55f7fe0bc8cd`
Sources: [`packages/core/tools/src/ptc.ts:15`](../packages/core/tools/src/ptc.ts)
Sources: [`packages/core/tool-executor-ptc/src/types.ts:68`](../packages/core/tool-executor-ptc/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -7542,7 +7542,7 @@ SHA-256: `c5228b7a55498d84f529591744a0ab26e0b93ade18a70a26db1377a1ecfa6b49`
SHA-256: `8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543`
Sources: [`packages/core/tools/src/index.ts:35`](../packages/core/tools/src/index.ts)
Sources: [`packages/core/tools/src/index.ts:18`](../packages/core/tools/src/index.ts)
| Property | Presence | Type |
|---|---|---|
+11 -11
View File
@@ -1118,7 +1118,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
'tool/ptc-dispatch': PtcDispatchEventData
```
来源:[`packages/core/tools/src/types.ts:61`](../packages/core/tools/src/types.ts)
来源:[`packages/core/tool-executor-ptc/src/types.ts:61`](../packages/core/tool-executor-ptc/src/types.ts)
<a id="toolptc-dispatch-start--log-only"></a>
@@ -1141,7 +1141,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
'tool/ptc-dispatch-start': PtcDispatchStartEventData
```
来源:[`packages/core/tools/src/types.ts:45`](../packages/core/tools/src/types.ts)
来源:[`packages/core/tool-executor-ptc/src/types.ts:45`](../packages/core/tool-executor-ptc/src/types.ts)
<a id="toolresult--surface"></a>
@@ -4444,7 +4444,7 @@ SHA-256: `deebfc7c62177ea840f23c6362b8615bc78ffa93377dd5d243c04e541c413af5`
<a id="persistence-type-sha256-cef171adf021a1867d244618a3dbbaafc22beb17c7f3df3ee203393271c8c6f6"></a>
<a id="persistence-type-packagescoretoolssrctypestsptcdispatcheventdata"></a>
<a id="persistence-type-packagescoretool-executor-ptcsrctypestsptcdispatcheventdata"></a>
<a id="persistence-type-ptcdispatcheventdata"></a>
@@ -4452,7 +4452,7 @@ SHA-256: `deebfc7c62177ea840f23c6362b8615bc78ffa93377dd5d243c04e541c413af5`
SHA-256: `cef171adf021a1867d244618a3dbbaafc22beb17c7f3df3ee203393271c8c6f6`
来源:[`packages/core/tools/src/types.ts:21`](../packages/core/tools/src/types.ts)
来源:[`packages/core/tool-executor-ptc/src/types.ts:21`](../packages/core/tool-executor-ptc/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -4468,7 +4468,7 @@ SHA-256: `cef171adf021a1867d244618a3dbbaafc22beb17c7f3df3ee203393271c8c6f6`
<a id="persistence-type-sha256-2fe5026daf14bd4ff2b7bfb7cf9791c302d9cbba06802178b6dfec00b892698f"></a>
<a id="persistence-type-packagescoretoolssrctypestsptcdispatchstarteventdata"></a>
<a id="persistence-type-packagescoretool-executor-ptcsrctypestsptcdispatchstarteventdata"></a>
<a id="persistence-type-ptcdispatchstarteventdata"></a>
@@ -4476,7 +4476,7 @@ SHA-256: `cef171adf021a1867d244618a3dbbaafc22beb17c7f3df3ee203393271c8c6f6`
SHA-256: `2fe5026daf14bd4ff2b7bfb7cf9791c302d9cbba06802178b6dfec00b892698f`
来源:[`packages/core/tools/src/types.ts:12`](../packages/core/tools/src/types.ts)
来源:[`packages/core/tool-executor-ptc/src/types.ts:12`](../packages/core/tool-executor-ptc/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -5187,7 +5187,7 @@ SHA-256: `43e311235aa537df1bacdd92d144df8a952337a76e9ac892cdf670f1a797bad8`
<a id="persistence-type-sha256-3cfc3a56502da1f8c6153c2c56657bab4b749056968634dd0991d325ec1c919f"></a>
<a id="persistence-type-packagescoretoolssrcindextstoolrestriction"></a>
<a id="persistence-type-packagescoretoolssrccontractststoolrestriction"></a>
<a id="persistence-type-toolrestriction"></a>
@@ -5195,7 +5195,7 @@ SHA-256: `43e311235aa537df1bacdd92d144df8a952337a76e9ac892cdf670f1a797bad8`
SHA-256: `3cfc3a56502da1f8c6153c2c56657bab4b749056968634dd0991d325ec1c919f`
来源:[`packages/core/tools/src/index.ts:701`](../packages/core/tools/src/index.ts)
来源:[`packages/core/tools/src/contracts.ts:326`](../packages/core/tools/src/contracts.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -5981,7 +5981,7 @@ SHA-256: `cbbe8377ee744964537dbafd83607219e58ba79a13f6db0d0645c7ad3d5e8a09`
SHA-256: `a81059ca06999da1e712414520c2c65e491d052fcf9640fae20aa38f0a5dc735`
来源:[`packages/core/session/src/types.ts:384`](../packages/core/session/src/types.ts) · [`packages/core/tools/src/types.ts:27`](../packages/core/tools/src/types.ts)
来源:[`packages/core/session/src/types.ts:384`](../packages/core/session/src/types.ts) · [`packages/core/tool-executor-ptc/src/types.ts:27`](../packages/core/tool-executor-ptc/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -6962,7 +6962,7 @@ SHA-256: `b81e9de81a481b24e1e1f9adf038bfc59e57b4f35ce62f8f1e45284186f4f812`
SHA-256: `dba54c09c6e7aafc9acbed7792ecc920d99f92d0784aeeed10af55f7fe0bc8cd`
来源:[`packages/core/tools/src/ptc.ts:15`](../packages/core/tools/src/ptc.ts)
来源:[`packages/core/tool-executor-ptc/src/types.ts:68`](../packages/core/tool-executor-ptc/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -7544,7 +7544,7 @@ SHA-256: `c5228b7a55498d84f529591744a0ab26e0b93ade18a70a26db1377a1ecfa6b49`
SHA-256: `8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543`
来源:[`packages/core/tools/src/index.ts:35`](../packages/core/tools/src/index.ts)
来源:[`packages/core/tools/src/index.ts:18`](../packages/core/tools/src/index.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
+6 -6
View File
@@ -190,7 +190,7 @@
{"digest":"2eafefdc6f79a4922d68a787a09eaf4dd2b2369c3777fba4c05e800b5678ac8e","names":["packages/experimental/agent-team/src/types.ts#TeamMessageSnapshot"],"sources":["packages/experimental/agent-team/src/types.ts:129"]},
{"digest":"2f1e6a7ebace43d727ae138ddcd352eb3813960009e5805b8c5ac84300c97716","names":["packages/attachment/attachment/src/types.ts#ImageMediaType"],"sources":["packages/attachment/attachment/src/types.ts:8"]},
{"digest":"2f3f201c8846e19c20bb75c02280e3d4eb3c57509e4715f0c0a4d1d4f91a7a06","names":[],"sources":[]},
{"digest":"2fe5026daf14bd4ff2b7bfb7cf9791c302d9cbba06802178b6dfec00b892698f","names":["packages/core/tools/src/types.ts#PtcDispatchStartEventData"],"sources":["packages/core/tools/src/types.ts:12"]},
{"digest":"2fe5026daf14bd4ff2b7bfb7cf9791c302d9cbba06802178b6dfec00b892698f","names":["packages/core/tool-executor-ptc/src/types.ts#PtcDispatchStartEventData"],"sources":["packages/core/tool-executor-ptc/src/types.ts:12"]},
{"digest":"30d5a537b296685770b1241c4b4bbde19d5a31ff81aa23df73fab5e90b8fc65f","names":["packages/session/session-title/src/types.ts#SessionTitleSource"],"sources":["packages/session/session-title/src/types.ts:28"]},
{"digest":"312aee2f0c42aabbae6cc60d92d596d8453c0aeb061c5868d1755a69ef49e69a","names":[],"sources":[]},
{"digest":"31907de9563751822af6d9be02bafaf26faa098bbd1cb412524134d577cc1648","names":["packages/compaction/compaction/src/checkpoint.ts#CompactionCheckpointSource"],"sources":["packages/compaction/compaction/src/checkpoint.ts:22"]},
@@ -220,7 +220,7 @@
{"digest":"3bec87f9e6ab3da03346f49923d3c51621debc0159df68605e543c3b2a026f90","names":[],"sources":["packages/llm/llm/src/types.ts:471"]},
{"digest":"3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1","names":[],"sources":[]},
{"digest":"3c2535ca0a2062f172f15344f8040755002b5ac506cf94fc4ebaa822b15feb8c","names":[],"sources":[]},
{"digest":"3cfc3a56502da1f8c6153c2c56657bab4b749056968634dd0991d325ec1c919f","names":["packages/core/tools/src/index.ts#ToolRestriction"],"sources":["packages/core/tools/src/index.ts:701"]},
{"digest":"3cfc3a56502da1f8c6153c2c56657bab4b749056968634dd0991d325ec1c919f","names":["packages/core/tools/src/contracts.ts#ToolRestriction"],"sources":["packages/core/tools/src/contracts.ts:326"]},
{"digest":"3d7d07060a7be77984feb651ea2bf1925d3e0477911d9d3ed86330d1e8ca6182","names":["packages/feedback/message-feedback/src/types.ts#MessageFeedbackItem"],"sources":["packages/feedback/message-feedback/src/types.ts:20"]},
{"digest":"3e0eff5946255ff05d88a10839c60305135801f97794ea48d409f8503303317e","names":[],"sources":[]},
{"digest":"3e2ece4967cfebe95f450a93f45523dff99d6749f6269f2f60f84dfecfcf3693","names":[],"sources":[]},
@@ -416,7 +416,7 @@
{"digest":"8e8e91646a5cae3fc78f7532a0013ff2088ae47077642c2edd507a508209b8af","names":["packages/schedule/schedule/src/types.ts#OneShotScheduleDispatchChange"],"sources":["packages/schedule/schedule/src/types.ts:237"]},
{"digest":"8f324cea3916c0517261e88167def8efb3a9994c1135e23d40ed9fd48f8c7bd4","names":[],"sources":[]},
{"digest":"8f6d609794bf5afc01d7ccf3e03811d32f4b8047f58a2c9a98db0154c2ad7cb9","names":[],"sources":[]},
{"digest":"8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543","names":[],"sources":["packages/core/tools/src/index.ts:35"]},
{"digest":"8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543","names":[],"sources":["packages/core/tools/src/index.ts:18"]},
{"digest":"904bf499d98be4a3a5f836fcac533b6c4899569b018071e6604e1d32657d00fd","names":[],"sources":[]},
{"digest":"930c1cd289def119d443e19dd90008916bf035f0a4d23e07241e7f050f2b89b7","names":["packages/interaction/user-approval/src/types.ts#ApprovalOutcome"],"sources":["packages/interaction/user-approval/src/types.ts:32"]},
{"digest":"935c8d486292b13c87b57ca280eb8c58b5b2f5239be86fcd4334c33fdeb87682","names":[],"sources":[]},
@@ -471,7 +471,7 @@
{"digest":"a6da9ad5d42ae83a6833a8288b076688e8e0801633ea36cbdd8a30a6d9f3160c","names":["packages/workflow/tool-workflow/src/types.ts#ToolWorkflowRunStartData"],"sources":["packages/workflow/tool-workflow/src/types.ts:14"]},
{"digest":"a720757f704a2512e9d2f20da4a093e582472176236a75ab8b8eb7aaff17d85a","names":[],"sources":[]},
{"digest":"a7cf43ce7c2a4c038feed1885cd7a00d5c6ee2d90a7e0d56b46f78a3e1ca327f","names":[],"sources":[]},
{"digest":"a81059ca06999da1e712414520c2c65e491d052fcf9640fae20aa38f0a5dc735","names":[],"sources":["packages/core/session/src/types.ts:384","packages/core/tools/src/types.ts:27"]},
{"digest":"a81059ca06999da1e712414520c2c65e491d052fcf9640fae20aa38f0a5dc735","names":[],"sources":["packages/core/session/src/types.ts:384","packages/core/tool-executor-ptc/src/types.ts:27"]},
{"digest":"a853902cfab417b8f08aa51ad6855fea46f34ed4588a74e47403035111d0fb86","names":[],"sources":["packages/interaction/commands/src/types.ts:105"]},
{"digest":"a8a2b9011fc067862df7ce40748868467e5778283d7ac193c13f77684a914c57","names":[],"sources":[]},
{"digest":"a9059a81430b320792bbb7c2ee1f8f6c77ff71e40ee44c18aec72e157c522694","names":[],"sources":[]},
@@ -570,7 +570,7 @@
{"digest":"cda2345a9be9d139df49ccbd16dcba716f3440178bf20ff8b801fda1f5eb77dc","names":[],"sources":[]},
{"digest":"cdca80b8dad24d4e7b1d5a288a8475ba8d8d46be385bd2da85f122ee836f1670","names":[],"sources":[]},
{"digest":"cdee10e87dbed8440d8b6935888624884878efc7af95402e94171b29f173dced","names":[],"sources":["packages/llm/llm-retry/src/types.ts:17"]},
{"digest":"cef171adf021a1867d244618a3dbbaafc22beb17c7f3df3ee203393271c8c6f6","names":["packages/core/tools/src/types.ts#PtcDispatchEventData"],"sources":["packages/core/tools/src/types.ts:21"]},
{"digest":"cef171adf021a1867d244618a3dbbaafc22beb17c7f3df3ee203393271c8c6f6","names":["packages/core/tool-executor-ptc/src/types.ts#PtcDispatchEventData"],"sources":["packages/core/tool-executor-ptc/src/types.ts:21"]},
{"digest":"cffcdc19881ab61ec0a9ce18743996f7ac44ac8889f34dfac36ae745574aa22f","names":["packages/goal/goal/src/domain.ts#GoalClearChangeMeta"],"sources":["packages/goal/goal/src/domain.ts:35"]},
{"digest":"d0fbb0ca1bbba059830826f33f801da1aced35a2504376d61e020bdfec8d50c0","names":[],"sources":[]},
{"digest":"d28765f87d6b6ab408e0b2f3e7e297791b004b806d8e302c49903ebae815feb2","names":[],"sources":[]},
@@ -590,7 +590,7 @@
{"digest":"db06ef35b27a38425046b2228fa6bce9582abe140d781d363137eb2f4d233a1c","names":[],"sources":[]},
{"digest":"db6c5246911d47a7969f6b4cb47c7d0dbbde75c402710b22ace3672fa97bebe0","names":[],"sources":[]},
{"digest":"db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965","names":[],"sources":[]},
{"digest":"dba54c09c6e7aafc9acbed7792ecc920d99f92d0784aeeed10af55f7fe0bc8cd","names":[],"sources":["packages/core/tools/src/ptc.ts:15"]},
{"digest":"dba54c09c6e7aafc9acbed7792ecc920d99f92d0784aeeed10af55f7fe0bc8cd","names":[],"sources":["packages/core/tool-executor-ptc/src/types.ts:68"]},
{"digest":"dbdafceb9a900183cccc80596498d386f3bf7bddead2b8cfde68453aa4739edc","names":[],"sources":[]},
{"digest":"dd2780e7e07684332f7a9ab047237798d260e7eaffb1321cb38eca695d184c3d","names":[],"sources":[]},
{"digest":"dd5ee8762bd8a14b6c6bffedd1753e4e78505bb0f9ea1621018d03a54eef59d5","names":[],"sources":[]},
+3 -3
View File
@@ -14,9 +14,9 @@
/session-persistence/crash-recovery-preserves-an-interrupted-turn:
en: 534044a0ce482d52
zh: d2e8a7dfeb066946
/session-persistence/sessionlocation-refusal-diagnostics-artifact-target:
en: 886589f052a916d7
zh: 2ca126aa700c933c
/session-persistence/sessionlocation-per-session-artifact-metadata:
en: e310ff426d848262
zh: 8a7667349b0549a7
/session-persistence/sessionheader-metadata-beside-the-log:
en: 5795ae03d2d163cd
zh: a46ceeb22cfbb6a1
+12 -6
View File
@@ -113,16 +113,16 @@ Repair therefore writes only under write ownership: a live session's write handl
Read-only observation is `open(id, 'read')`: the handle serves validated contiguous prefix slices, never a torn tail, and repeated reads on one handle never observe an older state than a prior read. There is no persistence-side prepared-Session cache: session-query owns its cold-read cache, keying one balanced cold Session per id on the `stat().revision` change token and re-reading only when the token changes. The [handle-based persistence Agent Note](../../.agents/notes/implemented/architecture/2026-08-27-handle-based-session-persistence.md) owns this lifecycle; the archived [Session preparation record](../../.agents/notes/archived/architecture/2026-08-05-session-preparation.md) documents the original publication-boundary `SessionPreparation` decision.
## `SessionLocation` — refusal-diagnostics artifact target
## `SessionLocation` — per-Session artifact metadata
`SessionLocation` is not a consumer-facing query: log access goes through a session handle's `read`. It survives only as refusal diagnostics, letting a `SessionFormatUnsupportedError` name the raw log a build refused to interpret. JSONL supplies the absolute transcript path inside its project/session directory; a backend without one artifact per session supplies nothing.
`SessionLocation` is observational metadata for diagnostics and export; log access still goes through a session handle's `read`. Materialized persistence snapshots carry the selected artifact path, and a `SessionFormatUnsupportedError` can name the raw log a build refused to interpret. JSONL supplies the absolute transcript path inside its project/session directory; a backend without one artifact per session supplies nothing.
```ts type-equiv
/**
* A backend-resolved, per-session local artifact location. Carried only by
* refusal diagnostics ({@link SessionFormatUnsupportedError}) so a user can
* find the raw log a build refused to interpret; it is not a consumer-facing
* query — log access goes through a session handle's `read`.
* A backend-resolved, per-Session local artifact path used by observational
* snapshots and refusal diagnostics. It names a backend target for the Session
* and does not authorize reads; log access still goes through a session
* handle's `read`. It is not persisted Session content.
*/
interface SessionLocation {
/** Backend-specific artifact kind, for example `jsonl`. */
@@ -319,6 +319,12 @@ interface SessionPersistenceSnapshot {
readonly eventCount?: number
/** Physical artifact byte size, when the backend can provide it cheaply (JSONL); otherwise absent. */
readonly sizeBytes?: number
/**
* Backend-resolved location of the selected materialized per-Session
* artifact, for diagnostics and export. Absent when the backend has no
* materialized per-Session file, including an unmaterialized pending Session.
*/
readonly location?: SessionLocation
}
```
+12 -6
View File
@@ -113,16 +113,16 @@ interface SessionHandle extends AsyncDisposable {
只读观察即 `open(id, 'read')`:句柄提供经过验证的连续前缀切片,绝不返回撕裂尾部,且同一句柄上的重复读取绝不会观察到比先前读取更旧的状态。持久化侧不存在已准备 Session 缓存:session-query 拥有自己的冷读缓存,按 `stat().revision` 变更令牌为每个 id 缓存一个已配平的冷 Session,仅在令牌变化时重新读取。该生命周期由[基于句柄的持久化 Agent Note](../../.agents/notes/implemented/architecture/2026-08-27-handle-based-session-persistence.zh.md)定义;已归档的 [Session 准备阶段记录](../../.agents/notes/archived/architecture/2026-08-05-session-preparation.md)记载了发布边界 `SessionPreparation` 最初的决策。
## `SessionLocation`——拒绝诊断的产物目标
## `SessionLocation`——每一 Session 的产物元数据
`SessionLocation` 不是面向消费者的查询:日志访问走会话句柄的 `read`。它仅作为拒绝诊断存在,使 `SessionFormatUnsupportedError` 能指出本构建拒绝解读的原始日志。JSONL 提供其项目/会话目录内 transcript(文本记录)的绝对路径;没有逐会话工件的后端则不提供。
`SessionLocation` 用于诊断与导出的观察元数据;日志访问仍走会话句柄的 `read`。已物化的持久化快照携带选中产物路径,`SessionFormatUnsupportedError` 也能指出本构建拒绝解读的原始日志。JSONL 提供其项目/会话目录内 transcript(文本记录)的绝对路径;没有逐会话工件的后端则不提供。
```ts type-equiv
/**
* A backend-resolved, per-session local artifact location. Carried only by
* refusal diagnostics ({@link SessionFormatUnsupportedError}) so a user can
* find the raw log a build refused to interpret; it is not a consumer-facing
* query — log access goes through a session handle's `read`.
* A backend-resolved, per-Session local artifact path used by observational
* snapshots and refusal diagnostics. It names a backend target for the Session
* and does not authorize reads; log access still goes through a session
* handle's `read`. It is not persisted Session content.
*/
interface SessionLocation {
/** Backend-specific artifact kind, for example `jsonl`. */
@@ -319,6 +319,12 @@ interface SessionPersistenceSnapshot {
readonly eventCount?: number
/** Physical artifact byte size, when the backend can provide it cheaply (JSONL); otherwise absent. */
readonly sizeBytes?: number
/**
* Backend-resolved location of the selected materialized per-Session
* artifact, for diagnostics and export. Absent when the backend has no
* materialized per-Session file, including an unmaterialized pending Session.
*/
readonly location?: SessionLocation
}
```
+5 -2
View File
@@ -14,9 +14,12 @@
/tools/toolrestriction-one-scope-s-live-filter-over-what-it-inherits:
en: 8c3fc4ca26f7e314
zh: 2e27263cb25243b9
/tools/executors-and-raw-call-batches:
en: 14522295539ea2fb
zh: 572c462f2c6b70ad
/tools/execution-extensible-waterfalls-plus-monotonic-policy:
en: dcac8fa88ef3e719
zh: aae2f4ce8852d8d8
en: 356bf8128685d0fe
zh: a28541da29c3deed
/tools/the-enforced-raw-json-schema-subset:
en: 28e354bfa2f9cebb
zh: be1f75abccf8afdf
+145 -79
View File
@@ -30,7 +30,7 @@ interface ToolDefinition extends ToolSchema {
/**
* Run one accepted call and return only its canonical lossless-JSON value.
* Async work must observe or forward `exec.signal` and settle only after its
* owned work reaches quiescence. The registry preserves caller cancellation
* owned work reaches quiescence. The executor preserves caller cancellation
* through around-dispatch signal replacement and does not abandon this
* promise, but it cannot hard-kill same-process code.
* @param args - losslessly snapshotted, frozen model arguments.
@@ -49,12 +49,12 @@ interface ToolDefinition extends ToolSchema {
*/
projectContent?(exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): ContentBlock[] | undefined
/**
* Synchronous last-mile transform for model-facing content. The registry
* Synchronous last-mile transform for model-facing content. The executor
* snapshots this callback when execution starts and invokes it exactly once
* for every normalized outcome, including pipeline failures that bypass
* `tools/post-execute`, immediately before lossless materialization.
* Returning `undefined` preserves the content; every other result field
* remains registry-owned. The callback must be total and must not throw.
* remains executor-owned. The callback must be total and must not throw.
* @param exec - immutable execution identity and arguments.
* @param result - complete normalized outcome before materialization.
* @returns replacement content, or `undefined` to preserve it.
@@ -177,9 +177,91 @@ interface ToolRestriction {
}
```
## Executors and raw call batches
`ctx.tools.execute()` delegates a complete raw model-call batch to the nearest scoped executor. The service selects the current provider when each batch starts; prompt assembly returns only model input. `agent` is absent for standalone service calls; `log` is absent for programmatic calls outside a model step, such as initialization hooks. An Agent without `log` still scopes tool access but produces no root call/result events. Tool-owned events may still use its Session.
```ts type-equiv
/** Model-call schemas produced by an executor. */
type ToolModelInput = Pick<ToolProviderResult, 'schemas' | 'knownNames'>
```
```ts type-equiv
/** Raw model calls, with optional Agent ownership and model-step logging. */
interface ToolBatchInput {
readonly calls: readonly ToolCallBlock[]
readonly signal: AbortSignal
/**
* Positive integer overriding the executor's concurrency limit for this batch.
* Applies to concurrency-safe root calls; program subcall limits remain separate.
* Undefined uses the executor's configured limit.
*/
readonly maxParallelToolCalls?: number
/**
* Owning Agent supplies scoped tools and execution context, including outside a turn.
* Undefined for standalone calls outside an Agent, such as a service invocation.
*/
readonly agent?: Agent
/**
* Committed model step receiving root call/result events; requires `agent`.
* Undefined when `agent` is absent or for calls outside a model response, such
* as session initialization hooks. Without `log`, root call/result events are
* omitted while `agent`, when provided, still supplies scope and execution context.
*/
readonly log?: {
readonly turn: number
readonly step: number
}
}
```
```ts type-equiv
/** One settled result correlated with its original model call. */
interface ToolCallResult {
readonly callId: ToolCallId
readonly result: ToolExecutionResult
}
```
```ts type-equiv
/** Model-ordered outcomes and context accepted for the next step. */
interface ToolBatchResult {
readonly results: readonly ToolCallResult[]
readonly additionalContexts: readonly UserMessage[]
readonly concluded: boolean
}
```
```ts type-equiv
/** A plugin owns argument parsing, dispatch, scheduling, cancellation, and logging. */
interface ToolExecutor {
/** Guidance registered and disposed with this executor; absent means no executor-owned sections. */
readonly sections?: readonly ToolPromptSection[]
/**
* Describe the model's callable tools.
* @param tools - currently visible capability definitions, after scoped restrictions.
* @returns schemas and names used to validate tool ordering.
*/
describe(tools: readonly ToolDefinition[]): ToolModelInput
/**
* Execute a complete batch. With log, commit root call/result events to the
* Agent's Session; without log, omit those events. Tool-owned events may still
* use the owning Agent's Session. Return
* one outcome per input call in input order, including skipped calls on abort.
* Never settle while started work is still running. Internal failures reject
* with ToolBatchExecutionError after draining, preserving committed outcomes
* and context; callers must stop dependent work and repair missing log results.
* An explicit maxParallelToolCalls takes precedence over executor configuration.
* @param input - original calls, cancellation, optional concurrency override, Agent and logging step.
* @returns settled results, deferred context, and whether a success concludes the turn.
*/
execute(input: ToolBatchInput): Promise<ToolBatchResult>
}
```
## Execution: extensible waterfalls plus monotonic policy
`ctx.tools.execute()` accepts a caller-owned `ToolExecutionInput` with a required readonly `signal`, materializes its parsed JSON arguments once into a pipeline-owned `ToolExecution`, and runs that call through `tools/pre-execute` (the reorderable allow/deny/ask waterfall) → registered monotonic guards → `tools/execute` (around-dispatch wrappers) → `projectContent` → `tools/post-execute` (inspect/replace the result) → optional definition-owned `finalizeContent` → `tools/result` (the immutable authoritative outcome). Only the `tools/execute` view may replace the required signal. The outcome is a `ToolExecutionResult`.
Built-in executors pass parsed `ToolExecutionInput` values to the shared `ToolExecutionPipeline`. The pipeline owns argument snapshots, execution identities, policy waterfalls, dispatch, and final-result notification, returning one `ToolExecutionResult` per invocation. Custom executors can implement their own execution; the public `ToolExecutor` requires only descriptions and batch execution.
```ts type-equiv
/** Opaque call identity that permits correlation without exposing mutable execution state. */
@@ -188,8 +270,8 @@ type ToolExecutionToken = symbol & { readonly [toolExecutionTokenBrand]: true }
```ts type-equiv
/**
* Caller-supplied description of one tool call. {@link ToolRuntime.execute}
* adds the registry-owned token to form a pipeline {@link ToolExecution};
* Parsed input to the shared invocation pipeline. The pipeline
* adds its own token to form a {@link ToolExecution};
* callers do not choose that token.
*/
interface ToolExecutionInput {
@@ -211,9 +293,9 @@ interface ToolExecutionInput {
* mode sets this on SDK sub-dispatches so commit-style observers can wait for
* the outer `run_code` outcome without receiving its live mutable execution.
* The token also marks the call as a transport sub-dispatch rather than a
* model-direct call: under `mode: 'ptc'`, only calls WITH a parent may
* model-direct call: with the PTC executor selected, only calls with a parent may
* execute a native tool name — a model-direct call (no parent) is denied as
* `UNKNOWN_TOOL` before the policy pipeline. See {@link ToolRuntime.execute}.
* `UNKNOWN_TOOL` before the policy pipeline.
*/
readonly parent?: ToolExecutionToken
/** Required caller-owned cancellation for this invocation. */
@@ -225,7 +307,7 @@ A tool body receives the runtime extension. `deferContext()` attaches context to
```ts type-equiv
/**
* Runtime context handed to a tool implementation after the registry has
* Runtime context handed to a tool implementation after the executor has
* accepted a {@link ToolExecution}. {@link deferContext} attaches context to
* this execution's own result — a composite tool ferries nested-dispatch
* context back to the outer result, and a leaf tool may mint a fresh
@@ -294,16 +376,16 @@ interface PtcDispatchLog {
```ts type-equiv
/**
* One pending tool call inside the registry pipeline. Parsed arguments cross
* One pending tool call inside the executor pipeline. Parsed arguments cross
* one lossless-JSON materialization boundary before policy and are deep-frozen;
* call identity, the caller signal, and the registry-assigned {@link token} are
* readonly. The registry freezes the complete object before `tools/result`
* call identity, the caller signal, and the executor-assigned {@link token} are
* readonly. The executor freezes the complete object before `tools/result`
* observers run.
*/
interface ToolExecution extends ToolExecutionInput {
/** Root model-requested call, resolved for every root and nested execution. */
readonly rootCallId: ToolCallId
/** Registry-assigned identity shared with nested calls only as their opaque `parent` token. */
/** Executor-assigned identity shared with nested calls only as their opaque `parent` token. */
readonly token: ToolExecutionToken
}
```
@@ -312,7 +394,7 @@ interface ToolExecution extends ToolExecutionInput {
/**
* Around-dispatch view of a {@link ToolExecution}. A `tools/execute` wrapper
* may replace the signal for its delegated lifetime, but it cannot remove it.
* The registry fuses every replacement with the captured caller signal.
* The executor fuses every replacement with the captured caller signal.
*/
interface ToolDispatchExecution extends Omit<ToolExecution, 'signal'> {
/** Cancellation signal visible to the next wrapper or tool body. */
@@ -501,26 +583,13 @@ The full presentation field docs live in [`packages/core/tools/src/presentation.
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`) — the language sides differ only in locale-specific paired document paths. 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="ctxtools--toolruntime"></a>
<a id="ctxtools--toolsservice"></a>
### `ctx.tools` — `ToolRuntime`
### `ctx.tools` — `ToolsService`
Tool registry and execution pipeline. Scoped registrations shadow globals; one visibility resolver feeds presentation, lookup, and dispatch.
Concrete tool registry and scoped executor dispatcher.
```ts cordis-catalog
/**
* Present the calling scope's tools in `mode` instead of the deployment
* default. Nearest scope on the chain wins, so a preset's standing
* declaration covers every agent joined under it.
*
* Scoped only, and one declaration per scope: this is how an agent preset
* composes PTC mode agents beside native ones in the same process, and a
* process-global override would be the `mode` config field instead.
* @param mode - the presentation the covered agents' models see.
* @returns the exact disposer that restores the deployment default.
*/
presentAs(mode: ToolPresentationMode): () => void
/**
* Register globally or in the calling agent scope. Scoped tools shadow
* globals; duplicates within one layer and the reserved `run_code` name fail.
@@ -538,18 +607,6 @@ register(definition: ToolDefinition): () => void
*/
restrict(filter: ToolRestriction): () => void
/**
* Register a monotonic guard after the extensible `tools/pre-execute`
* waterfall. A plain-context guard applies globally; one registered through
* `agent.ctx` applies only to that agent. Any matching guard may deny by
* returning a reason, while no guard can force-allow a call another guard
* denied. The exact effect disposer is returned for ordered ownership and
* HMR cleanup.
* @param guard - synchronous check; a returned string denies the execution.
* @returns the exact disposer that unregisters the guard.
*/
guard(guard: ToolGuard): () => void
/**
* Look up a tool as one scope sees it (scoped
* shadows global; a restricted-away global reads as absent). Presenters pass
@@ -561,6 +618,13 @@ guard(guard: ToolGuard): () => void
*/
get(name: string, scope?: ScopeKey): ToolDefinition | undefined
/**
* Read the visible capability definitions for descriptions and program bindings.
* @param scope - viewing Agent or preset; omitted selects the global view.
* @returns definitions after scoped shadowing and restrictions.
*/
definitions(scope?: ScopeKey): readonly ToolDefinition[]
/**
* Project visible definitions onto the allowlisted model-facing schema fields,
* excluding execution and presentation callbacks.
@@ -570,29 +634,34 @@ get(name: string, scope?: ScopeKey): ToolDefinition | undefined
schemas(scope?: ScopeKey): ToolSchema[]
/**
* Classify a pending call through the caller's visible tool definition. Only
* an exact `true` is parallel; unknown, hidden, undeclared, invalid, or
* throwing classifiers are exclusive.
* @param exec - call name, parsed arguments, and optional agent scope.
* @returns the fail-closed scheduling mode.
* Provide the executor for the calling scope; an unscoped contribution is the deployment default.
* Rendering any executor-owned section requires the viewing scope's selection to remain available.
* @param executor - initialized provider owned by the contributing plugin.
* @returns the effect disposer. Unloading leaves the selection unavailable until it is replaced.
*/
executionMode(exec: ToolExecutionInput): ToolExecutionMode
setExecutor(executor: ToolExecutor): () => void
/**
* Execute through pre-policy, guards, around-dispatch, post-policy,
* definition-owned content finalization, and final notification. Tool and
* listener failures resolve as materialized error results; an invisible tool
* reports `UNKNOWN_TOOL`. The returned outcome is the same lossless, frozen
* snapshot final observers receive. Cancellation
* arriving after entry and before final result materialization skips a
* not-yet-started body with `ABORTED_BEFORE_DISPATCH` or replaces a
* successful started outcome with `ABORTED`; already-started work is still
* drained and may retain a tool-owned structured error.
* @param exec - the typed same-process call input. The registry assigns its
* correlation token before policy begins.
* @returns the materialized final result.
* Delegate a raw call batch to its Agent's provider or the deployment default.
* Select at dispatch; accepted batches settle through that provider even if it unloads.
* @param input - original calls, optional owning Agent, and optional logging step.
* @returns the selected executor's settled batch; unavailable selections reject.
*/
async execute(exec: ToolExecutionInput): Promise<ToolExecutionResult>
async execute(input: ToolBatchInput): Promise<ToolBatchResult>
/**
* Register a monotonic denial in the calling scope.
* @param guard - evaluated after extensible pre-execute policy by the shared pipeline.
* @returns the effect disposer that removes this guard.
*/
guard(guard: ToolGuard): () => void
/**
* Check global and inherited guards after pre-execute policy and approval resolve.
* @param exec - invocation whose guards are checked before dispatch.
* @returns the first denial reason, or undefined when no guard denies.
*/
checkGuards(exec: ToolExecution): string | undefined
```
Types: [ScopeKey](scope.md)
@@ -607,22 +676,19 @@ Source: [`packages/core/tools/src/index.ts`](../../packages/core/tools/src/index
#### `tools/change` — emit
A tool was registered or unregistered, or a scoped restriction changed (the available tool set changed — possibly for one scope only). An UNFILTERED registry-subject notification, deliberately not scope-filtered dispatch: a global change concerns every agent's next assembly, so a scoped listener subscribing here sees every change, not just its own scope's.
A tool was registered or unregistered, or a scoped restriction changed. Unfiltered registry notification: scoped listeners receive changes from every scope because a global contribution can affect every agent's next assembly.
```ts cordis-catalog
/**
* A tool was registered or unregistered, or a scoped restriction changed
* (the available tool set changed — possibly for one scope only). An
* UNFILTERED registry-subject notification, deliberately not scope-filtered
* dispatch: a global change concerns every agent's next assembly, so a
* scoped listener subscribing here sees every change, not just its own
* scope's.
* A tool was registered or unregistered, or a scoped restriction changed.
* Unfiltered registry notification: scoped listeners receive changes from every
* scope because a global contribution can affect every agent's next assembly.
* @mode emit
*/
'tools/change'(): void
```
Source: [`packages/core/tools/src/index.ts`](../../packages/core/tools/src/index.ts)
Source: [`packages/core/tools/src/events.ts`](../../packages/core/tools/src/events.ts)
<a id="toolsexecute--waterfall"></a>
@@ -641,12 +707,12 @@ Around-dispatch waterfall for timeout, retry, or metrics. `next()` returns a nor
* @param exec - the allowed call about to dispatch (name, parsed arguments, caller agent, signal).
* @mode waterfall
*/
'tools/execute'(this: Scoped<ToolRuntime>, exec: ToolDispatchExecution, next: () => Promise<ToolExecutionResult>): Promise<ToolExecutionResult>
'tools/execute'(this: Scoped<ToolsService>, exec: ToolDispatchExecution, next: () => Promise<ToolExecutionResult>): Promise<ToolExecutionResult>
```
Types: [Scoped](scope.md)
Source: [`packages/core/tools/src/index.ts`](../../packages/core/tools/src/index.ts)
Source: [`packages/core/tools/src/events.ts`](../../packages/core/tools/src/events.ts)
<a id="toolspost-execute--waterfall"></a>
@@ -666,12 +732,12 @@ Accept, replace, enrich, or block a normalized dispatch result. `next()` accepts
* @param result - the dispatch outcome a listener may accept, replace, or block.
* @mode waterfall
*/
'tools/post-execute'(this: Scoped<ToolRuntime>, exec: ToolExecution, result: Readonly<ToolExecutionResult>, next: () => Promise<PostToolDecision>): Promise<PostToolDecision>
'tools/post-execute'(this: Scoped<ToolsService>, exec: ToolExecution, result: Readonly<ToolExecutionResult>, next: () => Promise<PostToolDecision>): Promise<PostToolDecision>
```
Types: [Scoped](scope.md)
Source: [`packages/core/tools/src/index.ts`](../../packages/core/tools/src/index.ts)
Source: [`packages/core/tools/src/events.ts`](../../packages/core/tools/src/events.ts)
<a id="toolspre-execute--waterfall"></a>
@@ -690,12 +756,12 @@ Allow, deny, cancel, or ask before dispatch. `next()` delegates to allow; `cance
* @param exec - the pending call (name, parsed arguments, caller agent).
* @mode waterfall
*/
'tools/pre-execute'(this: Scoped<ToolRuntime>, exec: ToolExecution, next: () => Promise<PreToolDecision>): Promise<PreToolDecision>
'tools/pre-execute'(this: Scoped<ToolsService>, exec: ToolExecution, next: () => Promise<PreToolDecision>): Promise<PreToolDecision>
```
Types: [Scoped](scope.md)
Source: [`packages/core/tools/src/index.ts`](../../packages/core/tools/src/index.ts)
Source: [`packages/core/tools/src/events.ts`](../../packages/core/tools/src/events.ts)
<a id="toolsptc-dispatch-log--waterfall"></a>
@@ -717,12 +783,12 @@ Allow a listener to replace content in the DURABLE LOG COPY of one `run_code` su
* @param dispatch - the parent execution, sub-call identity, and the settled content to log.
* @mode waterfall
*/
'tools/ptc-dispatch-log'(this: Scoped<ToolRuntime>, dispatch: PtcDispatchLog, next: () => Promise<ContentBlock[]>): Promise<ContentBlock[]>
'tools/ptc-dispatch-log'(this: Scoped<ToolsService>, dispatch: PtcDispatchLog, next: () => Promise<ContentBlock[]>): Promise<ContentBlock[]>
```
Types: [ContentBlock](llm-streaming.md) · [Scoped](scope.md)
Source: [`packages/core/tools/src/index.ts`](../../packages/core/tools/src/index.ts)
Source: [`packages/core/tool-executor-ptc/src/runtime-types.ts`](../../packages/core/tool-executor-ptc/src/runtime-types.ts)
<a id="toolsresult--emit"></a>
@@ -738,10 +804,10 @@ Observe the frozen, lossless-JSON final outcome. Listener failures are contained
* @param result - a deep-frozen snapshot of the final returned result.
* @mode emit
*/
'tools/result'(this: Scoped<ToolRuntime>, exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): undefined
'tools/result'(this: Scoped<ToolsService>, exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): undefined
```
Types: [Scoped](scope.md)
Source: [`packages/core/tools/src/index.ts`](../../packages/core/tools/src/index.ts)
Source: [`packages/core/tools/src/events.ts`](../../packages/core/tools/src/events.ts)
<!-- END GENERATED cordis-surface -->
+145 -79
View File
@@ -30,7 +30,7 @@ interface ToolDefinition extends ToolSchema {
/**
* Run one accepted call and return only its canonical lossless-JSON value.
* Async work must observe or forward `exec.signal` and settle only after its
* owned work reaches quiescence. The registry preserves caller cancellation
* owned work reaches quiescence. The executor preserves caller cancellation
* through around-dispatch signal replacement and does not abandon this
* promise, but it cannot hard-kill same-process code.
* @param args - losslessly snapshotted, frozen model arguments.
@@ -49,12 +49,12 @@ interface ToolDefinition extends ToolSchema {
*/
projectContent?(exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): ContentBlock[] | undefined
/**
* Synchronous last-mile transform for model-facing content. The registry
* Synchronous last-mile transform for model-facing content. The executor
* snapshots this callback when execution starts and invokes it exactly once
* for every normalized outcome, including pipeline failures that bypass
* `tools/post-execute`, immediately before lossless materialization.
* Returning `undefined` preserves the content; every other result field
* remains registry-owned. The callback must be total and must not throw.
* remains executor-owned. The callback must be total and must not throw.
* @param exec - immutable execution identity and arguments.
* @param result - complete normalized outcome before materialization.
* @returns replacement content, or `undefined` to preserve it.
@@ -177,9 +177,91 @@ interface ToolRestriction {
}
```
## 执行器与原始调用批次
`ctx.tools.execute()` 把完整的原始模型调用批次委托给最近作用域的执行器。服务在每个批次开始时选择当前提供者;提示词组装仅返回模型输入。`agent` 在独立服务调用中不存在;`log` 在模型步骤之外的程序调用(例如初始化钩子)中不存在。有 Agent 而没有 `log` 时,调用仍受该 Agent 的工具作用域约束,但不记录根调用/结果事件。工具自身的事件仍可使用 Agent 的 Session。
```ts type-equiv
/** Model-call schemas produced by an executor. */
type ToolModelInput = Pick<ToolProviderResult, 'schemas' | 'knownNames'>
```
```ts type-equiv
/** Raw model calls, with optional Agent ownership and model-step logging. */
interface ToolBatchInput {
readonly calls: readonly ToolCallBlock[]
readonly signal: AbortSignal
/**
* Positive integer overriding the executor's concurrency limit for this batch.
* Applies to concurrency-safe root calls; program subcall limits remain separate.
* Undefined uses the executor's configured limit.
*/
readonly maxParallelToolCalls?: number
/**
* Owning Agent supplies scoped tools and execution context, including outside a turn.
* Undefined for standalone calls outside an Agent, such as a service invocation.
*/
readonly agent?: Agent
/**
* Committed model step receiving root call/result events; requires `agent`.
* Undefined when `agent` is absent or for calls outside a model response, such
* as session initialization hooks. Without `log`, root call/result events are
* omitted while `agent`, when provided, still supplies scope and execution context.
*/
readonly log?: {
readonly turn: number
readonly step: number
}
}
```
```ts type-equiv
/** One settled result correlated with its original model call. */
interface ToolCallResult {
readonly callId: ToolCallId
readonly result: ToolExecutionResult
}
```
```ts type-equiv
/** Model-ordered outcomes and context accepted for the next step. */
interface ToolBatchResult {
readonly results: readonly ToolCallResult[]
readonly additionalContexts: readonly UserMessage[]
readonly concluded: boolean
}
```
```ts type-equiv
/** A plugin owns argument parsing, dispatch, scheduling, cancellation, and logging. */
interface ToolExecutor {
/** Guidance registered and disposed with this executor; absent means no executor-owned sections. */
readonly sections?: readonly ToolPromptSection[]
/**
* Describe the model's callable tools.
* @param tools - currently visible capability definitions, after scoped restrictions.
* @returns schemas and names used to validate tool ordering.
*/
describe(tools: readonly ToolDefinition[]): ToolModelInput
/**
* Execute a complete batch. With log, commit root call/result events to the
* Agent's Session; without log, omit those events. Tool-owned events may still
* use the owning Agent's Session. Return
* one outcome per input call in input order, including skipped calls on abort.
* Never settle while started work is still running. Internal failures reject
* with ToolBatchExecutionError after draining, preserving committed outcomes
* and context; callers must stop dependent work and repair missing log results.
* An explicit maxParallelToolCalls takes precedence over executor configuration.
* @param input - original calls, cancellation, optional concurrency override, Agent and logging step.
* @returns settled results, deferred context, and whether a success concludes the turn.
*/
execute(input: ToolBatchInput): Promise<ToolBatchResult>
}
```
## 执行:可扩展的 waterfall(瀑布式事件)加单调策略
`ctx.tools.execute()` 接受由调用方拥有且包含必需 readonly `signal` 的 `ToolExecutionInput`,将其解析后的 JSON 参数一次性物化为流水线拥有的 `ToolExecution`,然后让调用依次经过 `tools/pre-execute`(可重排的 allow/deny/ask waterfall)→ 已注册的单调 guard → `tools/execute`(环绕分派包装层)→ `projectContent` → `tools/post-execute`(检查/替换结果)→ 可选且由定义拥有的 `finalizeContent` → `tools/result`(不可变的权威结果)。只有 `tools/execute` 视图可以替换必需的 signal。最终产出为 `ToolExecutionResult`。
内置执行器把解析后的 `ToolExecutionInput` 交给共享 `ToolExecutionPipeline`。流水线负责参数快照、执行身份、策略瀑布、工具分发和最终结果通知,每次返回一个 `ToolExecutionResult`。自定义执行器可以独立实现这些行为;公开的 `ToolExecutor` 只要求工具描述和批次执行方法。
```ts type-equiv
/** Opaque call identity that permits correlation without exposing mutable execution state. */
@@ -188,8 +270,8 @@ type ToolExecutionToken = symbol & { readonly [toolExecutionTokenBrand]: true }
```ts type-equiv
/**
* Caller-supplied description of one tool call. {@link ToolRuntime.execute}
* adds the registry-owned token to form a pipeline {@link ToolExecution};
* Parsed input to the shared invocation pipeline. The pipeline
* adds its own token to form a {@link ToolExecution};
* callers do not choose that token.
*/
interface ToolExecutionInput {
@@ -211,9 +293,9 @@ interface ToolExecutionInput {
* mode sets this on SDK sub-dispatches so commit-style observers can wait for
* the outer `run_code` outcome without receiving its live mutable execution.
* The token also marks the call as a transport sub-dispatch rather than a
* model-direct call: under `mode: 'ptc'`, only calls WITH a parent may
* model-direct call: with the PTC executor selected, only calls with a parent may
* execute a native tool name — a model-direct call (no parent) is denied as
* `UNKNOWN_TOOL` before the policy pipeline. See {@link ToolRuntime.execute}.
* `UNKNOWN_TOOL` before the policy pipeline.
*/
readonly parent?: ToolExecutionToken
/** Required caller-owned cancellation for this invocation. */
@@ -225,7 +307,7 @@ interface ToolExecutionInput {
```ts type-equiv
/**
* Runtime context handed to a tool implementation after the registry has
* Runtime context handed to a tool implementation after the executor has
* accepted a {@link ToolExecution}. {@link deferContext} attaches context to
* this execution's own result — a composite tool ferries nested-dispatch
* context back to the outer result, and a leaf tool may mint a fresh
@@ -294,16 +376,16 @@ interface PtcDispatchLog {
```ts type-equiv
/**
* One pending tool call inside the registry pipeline. Parsed arguments cross
* One pending tool call inside the executor pipeline. Parsed arguments cross
* one lossless-JSON materialization boundary before policy and are deep-frozen;
* call identity, the caller signal, and the registry-assigned {@link token} are
* readonly. The registry freezes the complete object before `tools/result`
* call identity, the caller signal, and the executor-assigned {@link token} are
* readonly. The executor freezes the complete object before `tools/result`
* observers run.
*/
interface ToolExecution extends ToolExecutionInput {
/** Root model-requested call, resolved for every root and nested execution. */
readonly rootCallId: ToolCallId
/** Registry-assigned identity shared with nested calls only as their opaque `parent` token. */
/** Executor-assigned identity shared with nested calls only as their opaque `parent` token. */
readonly token: ToolExecutionToken
}
```
@@ -312,7 +394,7 @@ interface ToolExecution extends ToolExecutionInput {
/**
* Around-dispatch view of a {@link ToolExecution}. A `tools/execute` wrapper
* may replace the signal for its delegated lifetime, but it cannot remove it.
* The registry fuses every replacement with the captured caller signal.
* The executor fuses every replacement with the captured caller signal.
*/
interface ToolDispatchExecution extends Omit<ToolExecution, 'signal'> {
/** Cancellation signal visible to the next wrapper or tool body. */
@@ -501,26 +583,13 @@ type ObjectJsonSchema = JsonSchemaNode & { type: 'object' }
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`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.zh.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxtools--toolruntime"></a>
<a id="ctxtools--toolsservice"></a>
### `ctx.tools` — `ToolRuntime`
### `ctx.tools` — `ToolsService`
Tool registry and execution pipeline. Scoped registrations shadow globals; one visibility resolver feeds presentation, lookup, and dispatch.
Concrete tool registry and scoped executor dispatcher.
```ts cordis-catalog
/**
* Present the calling scope's tools in `mode` instead of the deployment
* default. Nearest scope on the chain wins, so a preset's standing
* declaration covers every agent joined under it.
*
* Scoped only, and one declaration per scope: this is how an agent preset
* composes PTC mode agents beside native ones in the same process, and a
* process-global override would be the `mode` config field instead.
* @param mode - the presentation the covered agents' models see.
* @returns the exact disposer that restores the deployment default.
*/
presentAs(mode: ToolPresentationMode): () => void
/**
* Register globally or in the calling agent scope. Scoped tools shadow
* globals; duplicates within one layer and the reserved `run_code` name fail.
@@ -538,18 +607,6 @@ register(definition: ToolDefinition): () => void
*/
restrict(filter: ToolRestriction): () => void
/**
* Register a monotonic guard after the extensible `tools/pre-execute`
* waterfall. A plain-context guard applies globally; one registered through
* `agent.ctx` applies only to that agent. Any matching guard may deny by
* returning a reason, while no guard can force-allow a call another guard
* denied. The exact effect disposer is returned for ordered ownership and
* HMR cleanup.
* @param guard - synchronous check; a returned string denies the execution.
* @returns the exact disposer that unregisters the guard.
*/
guard(guard: ToolGuard): () => void
/**
* Look up a tool as one scope sees it (scoped
* shadows global; a restricted-away global reads as absent). Presenters pass
@@ -561,6 +618,13 @@ guard(guard: ToolGuard): () => void
*/
get(name: string, scope?: ScopeKey): ToolDefinition | undefined
/**
* Read the visible capability definitions for descriptions and program bindings.
* @param scope - viewing Agent or preset; omitted selects the global view.
* @returns definitions after scoped shadowing and restrictions.
*/
definitions(scope?: ScopeKey): readonly ToolDefinition[]
/**
* Project visible definitions onto the allowlisted model-facing schema fields,
* excluding execution and presentation callbacks.
@@ -570,29 +634,34 @@ get(name: string, scope?: ScopeKey): ToolDefinition | undefined
schemas(scope?: ScopeKey): ToolSchema[]
/**
* Classify a pending call through the caller's visible tool definition. Only
* an exact `true` is parallel; unknown, hidden, undeclared, invalid, or
* throwing classifiers are exclusive.
* @param exec - call name, parsed arguments, and optional agent scope.
* @returns the fail-closed scheduling mode.
* Provide the executor for the calling scope; an unscoped contribution is the deployment default.
* Rendering any executor-owned section requires the viewing scope's selection to remain available.
* @param executor - initialized provider owned by the contributing plugin.
* @returns the effect disposer. Unloading leaves the selection unavailable until it is replaced.
*/
executionMode(exec: ToolExecutionInput): ToolExecutionMode
setExecutor(executor: ToolExecutor): () => void
/**
* Execute through pre-policy, guards, around-dispatch, post-policy,
* definition-owned content finalization, and final notification. Tool and
* listener failures resolve as materialized error results; an invisible tool
* reports `UNKNOWN_TOOL`. The returned outcome is the same lossless, frozen
* snapshot final observers receive. Cancellation
* arriving after entry and before final result materialization skips a
* not-yet-started body with `ABORTED_BEFORE_DISPATCH` or replaces a
* successful started outcome with `ABORTED`; already-started work is still
* drained and may retain a tool-owned structured error.
* @param exec - the typed same-process call input. The registry assigns its
* correlation token before policy begins.
* @returns the materialized final result.
* Delegate a raw call batch to its Agent's provider or the deployment default.
* Select at dispatch; accepted batches settle through that provider even if it unloads.
* @param input - original calls, optional owning Agent, and optional logging step.
* @returns the selected executor's settled batch; unavailable selections reject.
*/
async execute(exec: ToolExecutionInput): Promise<ToolExecutionResult>
async execute(input: ToolBatchInput): Promise<ToolBatchResult>
/**
* Register a monotonic denial in the calling scope.
* @param guard - evaluated after extensible pre-execute policy by the shared pipeline.
* @returns the effect disposer that removes this guard.
*/
guard(guard: ToolGuard): () => void
/**
* Check global and inherited guards after pre-execute policy and approval resolve.
* @param exec - invocation whose guards are checked before dispatch.
* @returns the first denial reason, or undefined when no guard denies.
*/
checkGuards(exec: ToolExecution): string | undefined
```
Types: [ScopeKey](scope.zh.md)
@@ -607,22 +676,19 @@ Source: [`packages/core/tools/src/index.ts`](../../packages/core/tools/src/index
#### `tools/change` — emit
A tool was registered or unregistered, or a scoped restriction changed (the available tool set changed — possibly for one scope only). An UNFILTERED registry-subject notification, deliberately not scope-filtered dispatch: a global change concerns every agent's next assembly, so a scoped listener subscribing here sees every change, not just its own scope's.
A tool was registered or unregistered, or a scoped restriction changed. Unfiltered registry notification: scoped listeners receive changes from every scope because a global contribution can affect every agent's next assembly.
```ts cordis-catalog
/**
* A tool was registered or unregistered, or a scoped restriction changed
* (the available tool set changed — possibly for one scope only). An
* UNFILTERED registry-subject notification, deliberately not scope-filtered
* dispatch: a global change concerns every agent's next assembly, so a
* scoped listener subscribing here sees every change, not just its own
* scope's.
* A tool was registered or unregistered, or a scoped restriction changed.
* Unfiltered registry notification: scoped listeners receive changes from every
* scope because a global contribution can affect every agent's next assembly.
* @mode emit
*/
'tools/change'(): void
```
Source: [`packages/core/tools/src/index.ts`](../../packages/core/tools/src/index.ts)
Source: [`packages/core/tools/src/events.ts`](../../packages/core/tools/src/events.ts)
<a id="toolsexecute--waterfall"></a>
@@ -641,12 +707,12 @@ Around-dispatch waterfall for timeout, retry, or metrics. `next()` returns a nor
* @param exec - the allowed call about to dispatch (name, parsed arguments, caller agent, signal).
* @mode waterfall
*/
'tools/execute'(this: Scoped<ToolRuntime>, exec: ToolDispatchExecution, next: () => Promise<ToolExecutionResult>): Promise<ToolExecutionResult>
'tools/execute'(this: Scoped<ToolsService>, exec: ToolDispatchExecution, next: () => Promise<ToolExecutionResult>): Promise<ToolExecutionResult>
```
Types: [Scoped](scope.zh.md)
Source: [`packages/core/tools/src/index.ts`](../../packages/core/tools/src/index.ts)
Source: [`packages/core/tools/src/events.ts`](../../packages/core/tools/src/events.ts)
<a id="toolspost-execute--waterfall"></a>
@@ -666,12 +732,12 @@ Accept, replace, enrich, or block a normalized dispatch result. `next()` accepts
* @param result - the dispatch outcome a listener may accept, replace, or block.
* @mode waterfall
*/
'tools/post-execute'(this: Scoped<ToolRuntime>, exec: ToolExecution, result: Readonly<ToolExecutionResult>, next: () => Promise<PostToolDecision>): Promise<PostToolDecision>
'tools/post-execute'(this: Scoped<ToolsService>, exec: ToolExecution, result: Readonly<ToolExecutionResult>, next: () => Promise<PostToolDecision>): Promise<PostToolDecision>
```
Types: [Scoped](scope.zh.md)
Source: [`packages/core/tools/src/index.ts`](../../packages/core/tools/src/index.ts)
Source: [`packages/core/tools/src/events.ts`](../../packages/core/tools/src/events.ts)
<a id="toolspre-execute--waterfall"></a>
@@ -690,12 +756,12 @@ Allow, deny, cancel, or ask before dispatch. `next()` delegates to allow; `cance
* @param exec - the pending call (name, parsed arguments, caller agent).
* @mode waterfall
*/
'tools/pre-execute'(this: Scoped<ToolRuntime>, exec: ToolExecution, next: () => Promise<PreToolDecision>): Promise<PreToolDecision>
'tools/pre-execute'(this: Scoped<ToolsService>, exec: ToolExecution, next: () => Promise<PreToolDecision>): Promise<PreToolDecision>
```
Types: [Scoped](scope.zh.md)
Source: [`packages/core/tools/src/index.ts`](../../packages/core/tools/src/index.ts)
Source: [`packages/core/tools/src/events.ts`](../../packages/core/tools/src/events.ts)
<a id="toolsptc-dispatch-log--waterfall"></a>
@@ -717,12 +783,12 @@ Allow a listener to replace content in the DURABLE LOG COPY of one `run_code` su
* @param dispatch - the parent execution, sub-call identity, and the settled content to log.
* @mode waterfall
*/
'tools/ptc-dispatch-log'(this: Scoped<ToolRuntime>, dispatch: PtcDispatchLog, next: () => Promise<ContentBlock[]>): Promise<ContentBlock[]>
'tools/ptc-dispatch-log'(this: Scoped<ToolsService>, dispatch: PtcDispatchLog, next: () => Promise<ContentBlock[]>): Promise<ContentBlock[]>
```
Types: [ContentBlock](llm-streaming.zh.md) · [Scoped](scope.zh.md)
Source: [`packages/core/tools/src/index.ts`](../../packages/core/tools/src/index.ts)
Source: [`packages/core/tool-executor-ptc/src/runtime-types.ts`](../../packages/core/tool-executor-ptc/src/runtime-types.ts)
<a id="toolsresult--emit"></a>
@@ -738,10 +804,10 @@ Observe the frozen, lossless-JSON final outcome. Listener failures are contained
* @param result - a deep-frozen snapshot of the final returned result.
* @mode emit
*/
'tools/result'(this: Scoped<ToolRuntime>, exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): undefined
'tools/result'(this: Scoped<ToolsService>, exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): undefined
```
Types: [Scoped](scope.zh.md)
Source: [`packages/core/tools/src/index.ts`](../../packages/core/tools/src/index.ts)
Source: [`packages/core/tools/src/events.ts`](../../packages/core/tools/src/events.ts)
<!-- END GENERATED cordis-surface -->
+3 -4
View File
@@ -561,10 +561,9 @@ decide(request: RemoteAccessRequest<Principal>): RemoteAccessDecision
allows(resource: RemoteAccessResource): boolean
/**
* The baseline a page opens with before its snapshot stream connects. A
* business package rendering a non-owner page assigns this value to
* {@link REMOTE_ACCESS_SNAPSHOT_KEY} after every index injection row,
* replacing the owner baseline the webServer index carries.
* The baseline a page opens with before its snapshot stream connects; every
* index the webServer renders carries it under
* {@link REMOTE_ACCESS_SNAPSHOT_KEY} for the Peer the page speaks for.
* @param principal - the page's caller identity.
* @returns the owner baseline for the owner; otherwise the principal's current grants.
*/
+3 -4
View File
@@ -561,10 +561,9 @@ decide(request: RemoteAccessRequest<Principal>): RemoteAccessDecision
allows(resource: RemoteAccessResource): boolean
/**
* The baseline a page opens with before its snapshot stream connects. A
* business package rendering a non-owner page assigns this value to
* {@link REMOTE_ACCESS_SNAPSHOT_KEY} after every index injection row,
* replacing the owner baseline the webServer index carries.
* The baseline a page opens with before its snapshot stream connects; every
* index the webServer renders carries it under
* {@link REMOTE_ACCESS_SNAPSHOT_KEY} for the Peer the page speaks for.
* @param principal - the page's caller identity.
* @returns the owner baseline for the owner; otherwise the principal's current grants.
*/
+4 -4
View File
@@ -3,8 +3,8 @@
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/web-server.md
/http-server:
en: c9b2f67d3248c496
zh: 5fa36d2837ff4f63
en: 56b268a69964f651
zh: c8ac8f16bf58a98a
/http-server/routes:
en: e949c3738a35b58f
zh: 138e3aaeb319ddc4
@@ -12,5 +12,5 @@
en: 4b162bd9f280eb06
zh: 3bea98dfee88c582
/http-server/the-service:
en: cc36972b8458279f
zh: c129044935d44769
en: bbf84a6cd0460dc2
zh: cc63ce45b0675c6d
+19 -14
View File
@@ -2,7 +2,7 @@
English | [中文](web-server.zh.md)
[dsh-host-webserver](../../packages/host/webserver) is the browser HTTP carrier for the GUI host: one `node:http` server — `node:https` when `tls` is configured — providing `ctx.webServer`, a named-route registry, optional gzip response compression, index.html transform callbacks, and one fallback handler that a plugin may claim. It is not part of the agent loop and not a capability seam; it knows no harness concepts, and another plugin registers every feature route, including the `/api` bridge, plugin bundles, and the HMR event stream ([layering reference](../../packages/boot/app-boot/README.md)). It serves browsers only: Electron loads the built files over `file://` and sends fetch requests through an IPC bridge instead of this server.
[dsh-host-webserver](../../packages/host/webserver) is the browser HTTP carrier for the GUI host: one `node:http` server — `node:https` when `tls` is configured — providing `ctx.webServer`, a named-route registry, optional gzip response compression, index.html transform callbacks, and one fallback handler that a plugin may claim. It is not part of the agent loop and not a capability seam; its only harness concept is the Peer an index render speaks for, and another plugin registers every feature route, including the `/api` bridge, plugin bundles, and the HMR event stream ([layering reference](../../packages/boot/app-boot/README.md)). It serves browsers only: Electron loads the built files over `file://` and sends fetch requests through an IPC bridge instead of this server.
Source: [`packages/host/webserver/src/index.ts`](../../packages/host/webserver/src/index.ts)
@@ -81,7 +81,7 @@ No composition can bind every interface at once: the unspecified address is reje
## The service
`WebServer` (`ctx.webServer`) listens immediately on activation; a listen failure (EADDRINUSE…) rejects initialization, and the boot process reports the failed fiber. The `tls` pair is read once before the listener binds, and an unreadable, empty, or mismatched pair rejects initialization the same way instead of falling back to HTTP. `register(route)` adds one named route and returns its disposer; a duplicate `(kind, path)` throws because route patterns are a composition-level contract and a collision is a misconfiguration. Gzip wraps eligible socket-backed responses inside the server, so route handlers retain direct `ServerResponse` ownership and no response-writing API is added to the service. Existing content encodings, `Cache-Control: no-transform`, ranges, SSE, ZIP, and the packaged `.gz` Worker image remain identity responses. `collectIndexInjections()` gathers structured `IndexInjection` rows over one `webserver/index-inject` emit, and `renderIndex(html)` renders them into successful root and configured index responses before applying the raw `tapIndex(transform)` escape-hatch transforms in registration order; [dsh-client-modules](../../packages/client/modules) answers the event with the boot manifest rows. `port` reads the listening port, including the port assigned by the OS when `config.port` is 0, and `protocol` reports the scheme the listener serves (`http:` or `https:`).
`WebServer` (`ctx.webServer`) listens immediately on activation; a listen failure (EADDRINUSE…) rejects initialization, and the boot process reports the failed fiber. The `tls` pair is read once before the listener binds, and an unreadable, empty, or mismatched pair rejects initialization the same way instead of falling back to HTTP. `register(route)` adds one named route and returns its disposer; a duplicate `(kind, path)` throws because route patterns are a composition-level contract and a collision is a misconfiguration. Gzip wraps eligible socket-backed responses inside the server, so route handlers retain direct `ServerResponse` ownership and no response-writing API is added to the service. Existing content encodings, `Cache-Control: no-transform`, ranges, SSE, ZIP, and the packaged `.gz` Worker image remain identity responses. `collectIndexInjections(peer)` gathers structured `IndexInjection` rows over one `webserver/index-inject` emit that passes the Peer the page speaks for, and `renderIndex(html, peer)` renders them into successful root and configured index responses before applying the raw `tapIndex(transform)` escape-hatch transforms in registration order; [dsh-client-modules](../../packages/client/modules) answers the event with the boot manifest rows. `port` reads the listening port, including the port assigned by the OS when `config.port` is 0, and `protocol` reports the scheme the listener serves (`http:` or `https:`).
A request whose handling throws (a malformed %-escape hitting `decodeURIComponent`, a client dropping mid-body) is logged as a warning and answered 400 — or the socket destroyed when headers are already out — never a process exit. Disposal pairs `close()` with `closeAllConnections()` because a handler may hold its response open (SSE) and such connections never end on their own; without the force-close, teardown would hang. A TLS socket counts as a connection only after its handshake, so the server tracks raw TLS sockets beside upgraded ones and destroys both at disposal. The package never prints: the URL line belongs to the shell. Per-package operational detail, including the dev-mode bundle watch pipeline, stays in the [README](../../packages/host/webserver/README.md).
@@ -235,20 +235,23 @@ tapIndex(transform: (html: string) => string): () => void
applyIndexTaps(html: string): string
/**
* Gather the structured injection table: one `webserver/index-inject` emit,
* every subscriber pushes its current rows. Fresh per call, so subscribers
* read live state (module graph, theme preference) at emit time.
* Gather the structured injection table for one Peer: one
* `webserver/index-inject` emit, every subscriber pushes its current rows.
* Fresh per call, so subscribers read live state (module graph, theme
* preference, the Peer's access) at emit time.
* @param peer - the Peer the page speaks for.
* @returns rows in subscriber activation order.
*/
collectIndexInjections(): IndexInjection[]
collectIndexInjections(peer: PeerScope): IndexInjection[]
/**
* Render one index.html body: the structured injection table first, then
* the raw `tapIndex` transforms over the result.
* Render one index.html body for one Peer: the structured injection table
* first, then the raw `tapIndex` transforms over the result.
* @param html - the raw index.html body.
* @param peer - the Peer the page speaks for.
* @returns the transformed body.
*/
renderIndex(html: string): string
renderIndex(html: string, peer: PeerScope): string
```
Source: [`packages/host/webserver/src/index.ts`](../../packages/host/webserver/src/index.ts)
@@ -285,17 +288,19 @@ Source: [`packages/client/connection/src/index.ts`](../../packages/client/connec
#### `webserver/index-inject` — emit
Collect the structured index injection table. Emitted on every index render and every worker boot-payload request; listeners push their current rows, so a row's data is read fresh at emit time.
Collect the structured index injection table for the page `peer` speaks for. Emitted on every index render and every worker boot-payload request; listeners push their current rows, so a row's data is read fresh at emit time.
```ts cordis-catalog
/**
* Collect the structured index injection table. Emitted on every index
* render and every worker boot-payload request; listeners push their
* current rows, so a row's data is read fresh at emit time.
* Collect the structured index injection table for the page `peer` speaks
* for. Emitted on every index render and every worker boot-payload
* request; listeners push their current rows, so a row's data is read
* fresh at emit time.
* @param table - Mutable row table; listeners append in activation order.
* @param peer - The Peer the rendered page speaks for.
* @mode emit
*/
'webserver/index-inject'(table: IndexInjection[]): void
'webserver/index-inject'(table: IndexInjection[], peer: PeerScope): void
```
Source: [`packages/host/webserver/src/index.ts`](../../packages/host/webserver/src/index.ts)
+19 -14
View File
@@ -2,7 +2,7 @@
[English](web-server.md) | 中文
[dsh-host-webserver](../../packages/host/webserver) 是 GUI Host 的浏览器 HTTP 载体:一个 `node:http` 服务器——配置 `tls` 时改用 `node:https`——提供 `ctx.webServer`、具名路由注册表、可选的 gzip 响应压缩、index.html 转换回调,以及一个可由插件认领的回退处理器。它不属于 agent loop(智能体循环),也不是能力 seam;它不了解任何 harness 概念。其他插件负责注册所有功能路由,包括 `/api` 桥接、插件 bundle 和 HMR(热模块替换)事件流([分层说明](../../packages/boot/app-boot/README.zh.md))。该服务器只服务浏览器:Electron 通过 `file://` 加载已构建文件,并经 IPC 桥接发送 fetch 请求,不使用本服务器。
[dsh-host-webserver](../../packages/host/webserver) 是 GUI Host 的浏览器 HTTP 载体:一个 `node:http` 服务器——配置 `tls` 时改用 `node:https`——提供 `ctx.webServer`、具名路由注册表、可选的 gzip 响应压缩、index.html 转换回调,以及一个可由插件认领的回退处理器。它不属于 agent loop(智能体循环),也不是能力 seam;它唯一涉及的 harness 概念是 index 渲染所代表的 Peer。其他插件负责注册所有功能路由,包括 `/api` 桥接、插件 bundle 和 HMR(热模块替换)事件流([分层说明](../../packages/boot/app-boot/README.zh.md))。该服务器只服务浏览器:Electron 通过 `file://` 加载已构建文件,并经 IPC 桥接发送 fetch 请求,不使用本服务器。
源码:[`packages/host/webserver/src/index.ts`](../../packages/host/webserver/src/index.ts)
@@ -81,7 +81,7 @@ interface TlsConfig {
## 服务
`WebServer`(`ctx.webServer`)在激活时立即监听;监听失败(EADDRINUSE 等)会使初始化被拒绝,启动进程会报告失败的 fiber。`tls` 证书对在监听器绑定前读取一次;文件不可读、为空或二者不匹配时同样会使初始化被拒绝,而不是回退到 HTTP。`register(route)` 添加一条具名路由并返回其 disposer;重复的 `(kind, path)` 抛出异常,因为路由模式是组合层约定,冲突即配置错误。Gzip 在服务器内部包装符合条件且基于 socket 的响应,因此 route handler 继续直接持有 `ServerResponse`,服务也不新增响应写出 API。已有内容编码、`Cache-Control: no-transform`、范围响应、SSE、ZIP 与打包后的 `.gz` Worker 镜像均保持 identity 响应。`collectIndexInjections()` 经一次 `webserver/index-inject` emit 收集结构化 `IndexInjection` 行,`renderIndex(html)` 把它们渲染进成功的根路径和配置 index 响应,随后再按注册顺序应用原始的 `tapIndex(transform)` 逃生口转换;[dsh-client-modules](../../packages/client/modules) 以启动 manifest(元数据清单)行回应该事件。`port` 读取监听端口,包括 `config.port` 为 0 时操作系统分配的端口,`protocol` 则报告监听器提供的协议(`http:` 或 `https:`)。
`WebServer`(`ctx.webServer`)在激活时立即监听;监听失败(EADDRINUSE 等)会使初始化被拒绝,启动进程会报告失败的 fiber。`tls` 证书对在监听器绑定前读取一次;文件不可读、为空或二者不匹配时同样会使初始化被拒绝,而不是回退到 HTTP。`register(route)` 添加一条具名路由并返回其 disposer;重复的 `(kind, path)` 抛出异常,因为路由模式是组合层约定,冲突即配置错误。Gzip 在服务器内部包装符合条件且基于 socket 的响应,因此 route handler 继续直接持有 `ServerResponse`,服务也不新增响应写出 API。已有内容编码、`Cache-Control: no-transform`、范围响应、SSE、ZIP 与打包后的 `.gz` Worker 镜像均保持 identity 响应。`collectIndexInjections(peer)` 经一次传入页面所代表 Peer 的 `webserver/index-inject` emit 收集结构化 `IndexInjection` 行,`renderIndex(html, peer)` 把它们渲染进成功的根路径和配置 index 响应,随后再按注册顺序应用原始的 `tapIndex(transform)` 逃生口转换;[dsh-client-modules](../../packages/client/modules) 以启动 manifest(元数据清单)行回应该事件。`port` 读取监听端口,包括 `config.port` 为 0 时操作系统分配的端口,`protocol` 则报告监听器提供的协议(`http:` 或 `https:`)。
处理过程中抛出异常的请求(畸形的 % 转义撞上 `decodeURIComponent`、客户端在请求体中途断开)会记录为警告并应答 400(响应头已发出时则销毁 socket),绝不导致进程退出。dispose(资源释放)把 `close()` 与 `closeAllConnections()` 配对使用,因为处理器可能像 SSE(Server-Sent Events)那样保持响应打开,而这类连接永远不会自行结束;没有强制关闭,拆卸就会挂起。TLS socket 只有完成握手后才计入连接,因此服务器会把原始 TLS socket 与升级后的 socket 一并跟踪,并在 dispose 时销毁两者。该包从不打印输出:URL 行归 shell 所有。逐包运维细节(含开发模式的 bundle 监视流水线)留在 [README](../../packages/host/webserver/README.zh.md) 中。
@@ -235,20 +235,23 @@ tapIndex(transform: (html: string) => string): () => void
applyIndexTaps(html: string): string
/**
* Gather the structured injection table: one `webserver/index-inject` emit,
* every subscriber pushes its current rows. Fresh per call, so subscribers
* read live state (module graph, theme preference) at emit time.
* Gather the structured injection table for one Peer: one
* `webserver/index-inject` emit, every subscriber pushes its current rows.
* Fresh per call, so subscribers read live state (module graph, theme
* preference, the Peer's access) at emit time.
* @param peer - the Peer the page speaks for.
* @returns rows in subscriber activation order.
*/
collectIndexInjections(): IndexInjection[]
collectIndexInjections(peer: PeerScope): IndexInjection[]
/**
* Render one index.html body: the structured injection table first, then
* the raw `tapIndex` transforms over the result.
* Render one index.html body for one Peer: the structured injection table
* first, then the raw `tapIndex` transforms over the result.
* @param html - the raw index.html body.
* @param peer - the Peer the page speaks for.
* @returns the transformed body.
*/
renderIndex(html: string): string
renderIndex(html: string, peer: PeerScope): string
```
Source: [`packages/host/webserver/src/index.ts`](../../packages/host/webserver/src/index.ts)
@@ -285,17 +288,19 @@ Source: [`packages/client/connection/src/index.ts`](../../packages/client/connec
#### `webserver/index-inject` — emit
Collect the structured index injection table. Emitted on every index render and every worker boot-payload request; listeners push their current rows, so a row's data is read fresh at emit time.
Collect the structured index injection table for the page `peer` speaks for. Emitted on every index render and every worker boot-payload request; listeners push their current rows, so a row's data is read fresh at emit time.
```ts cordis-catalog
/**
* Collect the structured index injection table. Emitted on every index
* render and every worker boot-payload request; listeners push their
* current rows, so a row's data is read fresh at emit time.
* Collect the structured index injection table for the page `peer` speaks
* for. Emitted on every index render and every worker boot-payload
* request; listeners push their current rows, so a row's data is read
* fresh at emit time.
* @param table - Mutable row table; listeners append in activation order.
* @param peer - The Peer the rendered page speaks for.
* @mode emit
*/
'webserver/index-inject'(table: IndexInjection[]): void
'webserver/index-inject'(table: IndexInjection[], peer: PeerScope): void
```
Source: [`packages/host/webserver/src/index.ts`](../../packages/host/webserver/src/index.ts)
+10 -10
View File
@@ -9,8 +9,8 @@
en: 6cc2cd9ced70bb09
zh: 1c0e6cb4ba2ed637
/tool-schema-catalog/tool-package-map:
en: 19883cdd5d6181cb
zh: 7d0cac18206184ec
en: 10fe940a3d17cde7
zh: ef1e8863c28e3c05
/tool-schema-catalog/deepseek-ai-dsh-plugin-manager:
en: e0143916bb634762
zh: e0143916bb634762
@@ -60,14 +60,14 @@
en: e713a4a11a15d13c
zh: e713a4a11a15d13c
/tool-schema-catalog/deepseek-ai-dsh-tool-ask-user/ask-user-question:
en: 055268ba4dcd32f9
zh: a1a3c4bfcbf698ed
/tool-schema-catalog/deepseek-ai-dsh-tools:
en: 76fbbf0fe38aef10
zh: 76fbbf0fe38aef10
/tool-schema-catalog/deepseek-ai-dsh-tools/run-code:
en: a2368a9d8136276e
zh: d1a4918fc45a7c87
en: 2f86adf8ae823afb
zh: a5dad3d821e75e55
/tool-schema-catalog/deepseek-ai-dsh-tool-executor-ptc:
en: 8c8643ef46fd075f
zh: 8c8643ef46fd075f
/tool-schema-catalog/deepseek-ai-dsh-tool-executor-ptc/run-code:
en: a5d49232a604a2fb
zh: 720040d11a3e31c2
/tool-schema-catalog/deepseek-ai-dsh-plan-mode:
en: b500b530845e26a9
zh: b500b530845e26a9
+8 -8
View File
@@ -20,7 +20,7 @@ This table connects model-visible tool names to the plugin package and service s
| `@deepseek-ai/dsh-mcp-resources` | `list_mcp_resource_templates`, `list_mcp_resources`, `read_mcp_resource` | `ctx.tools`, `ctx.mcpResources` | `tool/call`, `tool/result` | - | - |
| `@deepseek-ai/dsh-experimental-browser-use-stagehand-native` | `stagehand_act`, `stagehand_extract`, `stagehand_navigate`, `stagehand_observe`, `stagehand_screenshot`, `stagehand_tabs` | `ctx.browserUse`, `ctx.agents`, `ctx.tools`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | - |
| `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`, `ctx.userQuestions` | `tool/call`, `tool/result after an answer or timeout`, `late user/message` | - | ask_user_question keeps the original blocking behavior by default; set `mode: timed` to opt into a foreground timeout and pending result while the question remains answerable. In timed mode, `timeout: -1` keeps that call blocking indefinitely. |
| `@deepseek-ai/dsh-tools` | `run_code` | `ctx.workingDirectory (Agent executions)`, `ctx.tools`, `ctx.ptcRuntime (execution time)`, `ctx.systemPrompt` | `tool/call`, `one tool/ptc-dispatch-start + tool/ptc-dispatch pair per bridged sub-call`, `tool/result` | - | Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: ptc` (see the PTC mode Agent Note). Under `ptc` it is the registry's only wire contribution; the other visible capabilities are declared in a generated SDK section in the loaded runtime's language, and a program calls them through bindings scheduled under the native concurrency contract (submission-ordered starts and policy; concurrency-safe bodies overlap up to `maxParallelSubCalls`) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result. |
| `@deepseek-ai/dsh-tool-executor-ptc` | `run_code` | `ctx.workingDirectory (Agent executions)`, `ctx.tools`, `ctx.ptcRuntime`, `ctx.systemPrompt` | `tool/call`, `one tool/ptc-dispatch-start + tool/ptc-dispatch pair per bridged sub-call`, `tool/result` | - | The PTC executor exposes only run_code to the model and declares other visible capabilities in a generated SDK in the runtime’s language. Program bindings enter the shared guarded tool pipeline in submission order; concurrency-safe bodies overlap up to maxParallelSubCalls, and nested results remain linked to the outer program. |
| `@deepseek-ai/dsh-plan-mode` | `exit_plan_mode` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agents`, `ctx.userQuestions (execution time, opportunistic)` | `tool/call`, `plan/mode inactive on an approved review`, `tool/result` | - | exit_plan_mode stays in the model-facing schema while planning is inactive so transitions add no tool-catalog churn on top of the plan-policy change. Its execute path rejects calls outside plan mode; in plan mode it presents the plan over the user-questions seam (approve / keep planning with feedback), and approval logs plan mode inactive at the step boundary. |
| `@deepseek-ai/dsh-tool-bash` | `bash` | `ctx.workingDirectory`, `ctx.tools`, `ctx.shell`, `ctx.systemPrompt`, `ctx.shellEnv`, `ctx.jobs for run_in_background and the job-backed foreground path` | `tool/call`, `tool/result` | - | The bash tool is the model-facing consumer of the bash executor seam. With a job registry composed every call registers with the generic `ctx.jobs` runtime as it starts, collected/stopped through the `job_*` tools from `@deepseek-ai/dsh-tool-jobs`; without one, or with `enableRunInBackground: false`, the tool registers a foreground-only schema without the `run_in_background` parameter. |
| `@deepseek-ai/dsh-tool-present` | `present` | `ctx.workingDirectory`, `ctx.tools`, `ctx.fs`, `ctx.agents` | `tool/call`, `deliverables/presented after a successful final result`, `tool/result` | - | Deliveries belong to the calling Session; Web ui-deliverables supplies source-file opening and cards. |
@@ -571,13 +571,13 @@ Source: [`packages/interaction/tool-ask-user/src/index.ts`](../packages/interact
ask_user_question keeps the original blocking behavior by default; set `mode: timed` to opt into a foreground timeout and pending result while the question remains answerable. In timed mode, `timeout: -1` keeps that call blocking indefinitely.
<a id="deepseek-aidsh-tools"></a>
<a id="deepseek-aidsh-tool-executor-ptc"></a>
## `@deepseek-ai/dsh-tools`
## `@deepseek-ai/dsh-tool-executor-ptc`
### `run_code`
Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the program to execute, and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Curate the program output. Image-bearing subtool results are attached after the run.
Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the BODY of an async function (erasable syntax only; top-level `await` and `return` work), and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return is program output — curate it. Image-bearing subtool results are attached after the run. The program is the body of an async TypeScript function; only erasable syntax is accepted, with no enum or namespaces. Type annotations are advisory. Top-level await and return are supported. Emit output with console.log(...) or return a lossless JSON value. Only printed values and the returned value become program output. Each call runs in a fresh Node process. Node APIs are available through await import(...). Relative paths use the supplied working directory; process.env starts empty. Direct filesystem access follows this execution's sandbox policy. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.
```json
{
@@ -585,7 +585,7 @@ Execute a TypeScript program against the available tools. Takes two required arg
"properties": {
"code": {
"type": "string",
"description": "The TypeScript program to execute, following this tool's execution instructions."
"description": "The program: the body of an async TypeScript function."
},
"description": {
"type": "string",
@@ -593,7 +593,7 @@ Execute a TypeScript program against the available tools. Takes two required arg
},
"timeoutMs": {
"type": "number",
"description": "Positive elapsed-time budget in milliseconds, capped by the deployment maximum."
"description": "Positive elapsed-time budget in milliseconds, including nested tool and approval waits. Default 120000; capped at 600000. Zero does not disable the deadline."
},
"sandbox_permissions": {
"type": "string",
@@ -615,9 +615,9 @@ Execute a TypeScript program against the available tools. Takes two required arg
}
```
Source: [`packages/core/tools/src/ptc.ts`](../packages/core/tools/src/ptc.ts)
Source: [`packages/core/tool-executor-ptc/src/ptc.ts`](../packages/core/tool-executor-ptc/src/ptc.ts)
Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: ptc` (see the PTC mode Agent Note). Under `ptc` it is the registry's only wire contribution; the other visible capabilities are declared in a generated SDK section in the loaded runtime's language, and a program calls them through bindings scheduled under the native concurrency contract (submission-ordered starts and policy; concurrency-safe bodies overlap up to `maxParallelSubCalls`) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result.
The PTC executor exposes only run_code to the model and declares other visible capabilities in a generated SDK in the runtime’s language. Program bindings enter the shared guarded tool pipeline in submission order; concurrency-safe bodies overlap up to maxParallelSubCalls, and nested results remain linked to the outer program.
<a id="deepseek-aidsh-plan-mode"></a>
+9 -9
View File
@@ -24,7 +24,7 @@
| `@deepseek-ai/dsh-mcp-resources` | `list_mcp_resource_templates`, `list_mcp_resources`, `read_mcp_resource` | `ctx.tools`, `ctx.mcpResources` | `tool/call`, `tool/result` | - | - |
| `@deepseek-ai/dsh-experimental-browser-use-stagehand-native` | `stagehand_act`、`stagehand_extract`、`stagehand_navigate`、`stagehand_observe`、`stagehand_screenshot`、`stagehand_tabs` | `ctx.browserUse`、`ctx.agents`、`ctx.tools`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | - |
| `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`、`ctx.userQuestions` | `tool/call`、`tool/result after an answer or timeout`、`late user/message` | - | ask_user_question 默认保持原有阻塞行为;设置 `mode: timed` 后才启用前台超时与 pending 结果,同时问题仍可回答;timed 模式内 `timeout: -1` 让本次调用无限期阻塞。 |
| `@deepseek-ai/dsh-tools` | `run_code` | `ctx.workingDirectory (Agent executions)`、`ctx.tools`、`ctx.ptcRuntime (execution time)`、`ctx.systemPrompt` | `tool/call`、`one tool/ptc-dispatch-start + tool/ptc-dispatch pair per bridged sub-call`、`tool/result` | - | 在 `mode: ptc` 下,它由工具注册表所有,作为可过滤能力层之外的保留传输机制(参见 PTC mode Agent Note)。在 `ptc` 下,它是注册表对协议格式(wire format)的唯一贡献;其他可见能力在使用已加载运行时语言生成的 SDK 章节中声明。程序通过 binding 调用这些能力,调用按照原生并发约定调度:启动顺序和策略遵循提交顺序,并发安全的函数体最多重叠执行 `maxParallelSubCalls` 个。调用会重新进入完整且受守卫保护的工具流水线,并将每个嵌套执行关联到此外层结果。 |
| `@deepseek-ai/dsh-tool-executor-ptc` | `run_code` | `ctx.workingDirectory (Agent executions)`、`ctx.tools`、`ctx.ptcRuntime`、`ctx.systemPrompt` | `tool/call`、`one tool/ptc-dispatch-start + tool/ptc-dispatch pair per bridged sub-call`、`tool/result` | - | PTC 执行器只向模型公开 run_code,并在运行时语言生成的 SDK 中声明其他可见能力。程序绑定按提交顺序进入共享的受守卫工具流水线;并行安全的函数体最多重叠 maxParallelSubCalls 个,嵌套结果保持与外层程序关联。 |
| `@deepseek-ai/dsh-plan-mode` | `exit_plan_mode` | `ctx.tools`、`ctx.systemPrompt`、`ctx.agents`、`ctx.userQuestions (execution time, opportunistic)` | `tool/call`、`plan/mode inactive on an approved review`、`tool/result` | - | 规划未激活时,exit_plan_mode 仍保留在面向模型的 schema 中,这样状态转换不会在规划策略变更之外额外造成工具目录变动。其执行路径会拒绝规划模式之外的调用;在规划模式下,它通过用户交互 seam 提交计划(批准/根据反馈继续规划),批准后会在步骤边界记录规划模式已停用。 |
| `@deepseek-ai/dsh-tool-bash` | `bash` | `ctx.workingDirectory`、`ctx.tools`、`ctx.shell`、`ctx.systemPrompt`、`ctx.shellEnv`、`ctx.jobs for run_in_background and the job-backed foreground path` | `tool/call`、`tool/result` | - | bash 工具是 bash 执行器 seam 面向模型的消费方。组合中有 job 注册表时,每次调用一启动就注册到通用 `ctx.jobs` 运行时,并通过 `job_*` 工具(来自 `@deepseek-ai/dsh-tool-jobs`)收集/停止;没有注册表或 `enableRunInBackground: false` 时,工具注册不带 `run_in_background` 参数的纯前台 schema。 |
| `@deepseek-ai/dsh-tool-present` | `present` | `ctx.workingDirectory`、`ctx.tools`, `ctx.fs`, `ctx.agents` | `tool/call`, `deliverables/presented 在成功的最终结果之后`, `tool/result` | - | 交付归调用方 Session 所有;Web ui-deliverables 提供源文件打开与卡片。 |
@@ -41,7 +41,7 @@
| `@deepseek-ai/dsh-tool-lsp` | `lsp` | `ctx.workingDirectory`、`ctx.tools`、`ctx.lsp`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,因此其模型可见 schema 在更换提供方时保持稳定。运行时要求已注册提供方,例如 `@deepseek-ai/dsh-lsp-stdio`;如果没有提供方,查询会返回结构化 `LSP_UNAVAILABLE` 错误,而不会改变 schema。 |
| `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`、`ctx.workflowEngine`、`ctx.subagents`、`ctx.systemPrompt`、`a calling Agent (exec.agent parents every fresh round)` | `tool/call`、`tool/result`、`workflow and child session events during execution` | - | 固定的前台工作流会在每个 Round 启动一个全新的结构化子级;模型只能选择不可变目标和可选的 Round 上限。 |
| `@deepseek-ai/dsh-tool-skill` | `skill` | `ctx.workingDirectory`、`ctx.tools`、`ctx.agents`、`ctx.skills` | `tool/call`、`tool/result`、`user/message replacement catalogs via agent.inject()` | - | - |
| `@deepseek-ai/dsh-tool-session-query` | `session_event_read`、`session_event_search`、`session_event_trace`、`session_search`、`session_trace` | `ctx.tools`、`ctx.systemPrompt`、`ctx.sessionQuery`、`ctx.agents`、`a calling Agent for workspace authority` | `tool/call`、`tool/result` | - | 这 5 个只读工具会隐藏提供方游标,并根据不可变的调用 agent 会话为每个结果授权。该包需要选择启用;需要强制截止时间或限制行内输出的组合还会挂载通用超时或 spill 策略。 |
| `@deepseek-ai/dsh-tool-session-query` | `session_event_read`、`session_event_search`、`session_event_trace`、`session_search`、`session_trace` | `ctx.tools`、`ctx.systemPrompt`、`ctx.sessionQuery`、`a calling Agent for workspace authority` | `tool/call`、`tool/result` | - | 这 5 个只读工具会隐藏提供方游标,并根据不可变的调用 agent 会话为每个结果授权。该包需要选择启用;需要强制截止时间或限制行内输出的组合还会挂载通用超时或 spill 策略。 |
| `@deepseek-ai/dsh-experimental-tool-subagent-oar` | `subagent_oar` | `ctx.tools`, `ctx.subagents` | `tool/call`, `tool/result`, `parent subagent catalog and completion notices` | - | subagent_oar 工具在为外部提供方配置的别名之间选择。本目录展示 Codex 和 Claude Code 别名;每个部署自行选择提供方。原生模型和权限设置由所选提供方的配置决定。 |
| `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`ctx.sessionProjections`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 加载时的 `toolName` 选择委派名称(默认为 `subagent`);`modelSelection` 增加可选路由字段。`modelDiscovery` 默认关闭,每个作用域由一个实例注册共享发现工具。Web 与 Desktop preset 启用 spawn 发现和路由字段,并为 `subagent_fork` 保持路由字段关闭。Host 设置服务为每个新顶层 Session 记录授权;子 Session 继承该授权。没有已记录策略时,发现和显式选择只允许当前父级路由。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 |
| `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`、`list_agents`、`send_message` | `ctx.tools`、`ctx.subagents`、`ctx.agents` | `tool/call`、`tool/result`、`child session events through ctx.subagents` | - | 这些是用于发现、中断托管 subagent 和向本地子代理发送消息的全局命名工具:绑定提供方的 `tool-subagent` 实例注册不同的委派工具;本包的一个实例注册 `send_message`、`interrupt_agent` 和 `list_agents`。 |
@@ -575,13 +575,13 @@
ask_user_question 默认保持原有阻塞行为;设置 `mode: timed` 后才启用前台超时与 pending 结果,同时问题仍可回答;timed 模式内 `timeout: -1` 让本次调用无限期阻塞。
<a id="deepseek-aidsh-tools"></a>
<a id="deepseek-aidsh-tool-executor-ptc"></a>
## `@deepseek-ai/dsh-tools`
## `@deepseek-ai/dsh-tool-executor-ptc`
### `run_code`
针对可用工具执行 TypeScript 程序。接受两个必填参数:`code`,要执行的程序;以及 `description`,简要说明该程序做什么。请根据系统提示词中的声明,以 `await tools.name(args)` 形式调用工具。请筛选程序输出。含图片的子工具结果会在运行结束后附加。
针对可用工具执行 TypeScript 程序。接受两个必填参数:`code`,异步函数的函数体(仅限可擦除语法,支持顶层 `await` 和 `return`);以及 `description`,简要说明程序做什么。请按系统提示词中的声明,以 `await tools.name(args)` 调用工具。只有打印或返回的内容构成程序输出,请筛选这些内容。含图片的子工具结果会在运行结束后附加。程序是异步 TypeScript 函数的函数体,只接受可擦除语法,不支持 enum 或 namespace。类型标注只作提示。支持顶层 await 和 return。使用 console.log(...) 或返回无损 JSON 值来输出。只有打印值和返回值成为程序输出。每次调用在新的 Node 进程中运行,可通过 await import(...) 使用 Node API。相对路径以传入的工作目录为准;process.env 初始为空。直接文件系统访问遵循本次执行的沙箱策略。每个程序从 Session 的当前目录启动,运行中保留初始目录。沙箱提权只为本次完整程序执行授权。嵌套工具保留自身策略与审批。只有在获得拒绝证据后才请求更宽访问。先前效果可能已经发生,请在显式重试前检查。程序不会自动重放。
```json
{
@@ -589,7 +589,7 @@ ask_user_question 默认保持原有阻塞行为;设置 `mode: timed` 后才
"properties": {
"code": {
"type": "string",
"description": "The TypeScript program to execute, following this tool's execution instructions."
"description": "The program: the body of an async TypeScript function."
},
"description": {
"type": "string",
@@ -597,7 +597,7 @@ ask_user_question 默认保持原有阻塞行为;设置 `mode: timed` 后才
},
"timeoutMs": {
"type": "number",
"description": "Positive elapsed-time budget in milliseconds, capped by the deployment maximum."
"description": "Positive elapsed-time budget in milliseconds, including nested tool and approval waits. Default 120000; capped at 600000. Zero does not disable the deadline."
},
"sandbox_permissions": {
"type": "string",
@@ -619,9 +619,9 @@ ask_user_question 默认保持原有阻塞行为;设置 `mode: timed` 后才
}
```
来源:[`packages/core/tools/src/ptc.ts`](../packages/core/tools/src/ptc.ts)
来源:[`packages/core/tool-executor-ptc/src/ptc.ts`](../packages/core/tool-executor-ptc/src/ptc.ts)
在 `mode: ptc` 下,它由工具注册表所有,作为可过滤能力层之外的保留传输机制(参见 PTC mode Agent Note)。在 `ptc` 下,它是注册表对协议格式的唯一贡献;其他可见能力在使用已加载运行时语言生成的 SDK 章节中声明。程序通过 binding 调用这些能力,调用按照原生并发约定调度:启动顺序和策略遵循提交顺序,并发安全的函数体最多重叠执行 `maxParallelSubCalls` 个。调用会重新进入完整且受守卫保护的工具流水线,并将每个嵌套执行关联到此外层结果。
PTC 执行器只向模型公开 run_code,并在运行时语言生成的 SDK 中声明其他可见能力。程序绑定按提交顺序进入共享的受守卫工具流水线;并行安全的函数体最多重叠 maxParallelSubCalls 个,嵌套结果保持与外层程序关联。
<a id="deepseek-aidsh-plan-mode"></a>
+2 -2
View File
@@ -6,5 +6,5 @@
en: 41d772192075e133
zh: 0eb190f00f396176
/tool-execution-pipeline:
en: f2811b12640c6f8e
zh: 9e0323523dcbb04c
en: a1103f6055f81a4b
zh: 6eefe6bfa59b1692
+5 -4
View File
@@ -3,7 +3,7 @@
# Tool Execution Pipeline
This graph shows where policy, hooks, sandboxing, filesystem guards, result rewriting, final-outcome observation, and UI rendering run without changing the loop. The `tools/pre-execute` waterfall runs first, monotonic guards run next, and the `tools/execute` and `tools/post-execute` waterfalls follow; the three waterfalls may transform a call. Definition-owned `projectContent` installs prepared content before post-execute; `finalizeContent` and `tools/result` run afterward.
Built-in executors own raw batch parsing, scheduling, cancellation, and Session call/result logging. This graph shows where their shared invocation pipeline applies policy, hooks, sandboxing, filesystem guards, result rewriting, final-outcome observation, and UI rendering without changing the loop. The `tools/pre-execute` waterfall runs first, monotonic guards run next, and the `tools/execute` and `tools/post-execute` waterfalls follow; the three waterfalls may transform a call. Definition-owned `projectContent` installs prepared content before post-execute; `finalizeContent` and `tools/result` run afterward.
```mermaid
flowchart TD
@@ -20,14 +20,15 @@ flowchart TD
owned["Tool-owned session events<br/><code>todo/write</code>, <code>fs/observed</code>, <code>hook/invoked</code>, <code>hook/result</code>, <code>tool/ptc-dispatch</code>"]
project["ToolDefinition.projectContent<br/>execution-prepared text and images"]
post["<code>tools/post-execute</code> waterfall<br/>accept, block, replace, add context"]
normalized["Registry outer normalization<br/>pipeline/result snapshot throws become isError"]
normalized["Invocation pipeline normalization<br/>pipeline/result snapshot throws become isError"]
finalize["ToolDefinition.finalizeContent<br/>last content-only invariant"]
final["<code>tools/result</code> synchronous notification<br/>frozen authoritative outcome"]
context["Active-batch additionalContexts FIFO<br/>injected user/message after recorded tool results"]
toolResult["Session event: <code>tool/result</code><br/>single model-facing outcome"]
allResults["Tool batch settled<br/>recorded tool/result events complete"]
presentResult["UI completed card<br/>presentResult(args, result)"]
model --> toolCall
executor["Selected ToolExecutor.execute<br/>raw batch, Agent, optional logging step"]
model --> executor --> toolCall
toolCall --> presentCall
toolCall --> pre
pre -->|allow| guards
@@ -60,6 +61,6 @@ flowchart TD
allResults --> context
```
Filesystem read-before-edit checks stay below `tool-fs` on `fs/*` events. Generic pre/post waterfalls host hooks and approval policy; `ctx.approval` resolves asks before monotonic guards, and owner policy that must not be reordered remains a registered guard. Around-dispatch concerns such as timeouts wrap `tools/execute`. The registry losslessly snapshots the candidate result and normalizes a snapshot failure before the visible definition's snapshotted `finalizeContent` callback enforces its synchronous content-only invariant. `tools/result` then observes the immutable, lossless-JSON outcome. This lets hooks span tool families without coupling the tools to one policy service. PTC mode sends both the reserved `run_code` transport and its serialized sub-calls through the pipeline; sub-calls carry the parent token, log `tool/ptc-dispatch`, return denials as binding rejections, and omit `additionalContexts` to preserve call/result adjacency.
Filesystem read-before-edit checks stay below `tool-fs` on `fs/*` events. Generic pre/post waterfalls host hooks and approval policy; `ctx.approval` resolves asks before monotonic guards, and owner policy that must not be reordered remains a registered guard. Around-dispatch concerns such as timeouts wrap `tools/execute`. The pipeline losslessly snapshots the candidate result and normalizes a snapshot failure before the visible definition's snapshotted `finalizeContent` callback enforces its synchronous content-only invariant. `tools/result` then observes the immutable, lossless-JSON outcome. This lets hooks span tool families without coupling the tools to one policy service. PTC mode sends both the reserved `run_code` transport and its ordered sub-calls through the pipeline; sub-calls carry the parent token, log `tool/ptc-dispatch`, return denials as binding rejections, and omit `additionalContexts` to preserve call/result adjacency.
Maintenance mode: curated Mermaid flow; exact tool schemas and event signatures live in generated catalogs.
+4 -3
View File
@@ -5,7 +5,7 @@
[English](tool-execution-pipeline.md) | 中文
此图展示策略、钩子、沙箱、文件系统守卫、结果重写、最终结果观察和 UI 渲染在不改变循环的情况下何时运行。`tools/pre-execute` waterfall(瀑布式事件)首先运行,随后是单调守卫,然后运行 `tools/execute` 和 `tools/post-execute` waterfall;这三个 waterfall 可以改写一次调用。由定义自身控制的 `projectContent` 在执行后策略之前安装已准备内容;`finalizeContent` 和 `tools/result` 在此之后运行。
内置执行器负责原始批次解析、调度、取消和 Session 调用/结果日志。此图展示其共享调用流水线如何执行策略、钩子、沙箱、文件系统守卫、结果改写、最终结果观测和 UI 呈现,而无需改动循环。`tools/pre-execute` 瀑布先执行,然后是单调守卫,再是 `tools/execute` 和 `tools/post-execute` 瀑布;三个瀑布可以转换调用。定义拥有的 `projectContent` 在后置策略之前安装准备好的内容,`finalizeContent` 和 `tools/result` 在之后执行。
```mermaid
flowchart TD
@@ -22,14 +22,15 @@ flowchart TD
owned["Tool-owned session events<br/><code>todo/write</code>, <code>fs/observed</code>, <code>hook/invoked</code>, <code>hook/result</code>, <code>tool/ptc-dispatch</code>"]
project["ToolDefinition.projectContent<br/>execution-prepared text and images"]
post["<code>tools/post-execute</code> waterfall<br/>accept, block, replace, add context"]
normalized["Registry outer normalization<br/>pipeline/result snapshot throws become isError"]
normalized["Invocation pipeline normalization<br/>pipeline/result snapshot throws become isError"]
finalize["ToolDefinition.finalizeContent<br/>last content-only invariant"]
final["<code>tools/result</code> synchronous notification<br/>frozen authoritative outcome"]
context["Active-batch additionalContexts FIFO<br/>injected user/message after recorded tool results"]
toolResult["Session event: <code>tool/result</code><br/>single model-facing outcome"]
allResults["Tool batch settled<br/>recorded tool/result events complete"]
presentResult["UI completed card<br/>presentResult(args, result)"]
model --> toolCall
executor["Selected ToolExecutor.execute<br/>raw batch, Agent, optional logging step"]
model --> executor --> toolCall
toolCall --> presentCall
toolCall --> pre
pre -->|allow| guards
@@ -9,8 +9,8 @@
en: d202616bf6e03fa4
zh: cc276adda4b952be
/sdk-desktop-tools-and-context/change:
en: 85ac2825c52e2503
zh: fd6fdbdca131c778
en: ef89799953ecb457
zh: 86fdb487b9a15a6f
/sdk-desktop-tools-and-context/migration:
en: 9caef4f8b153622a
zh: 1b4901f5b18b72fd
@@ -9,7 +9,7 @@ English | [中文](guide.zh.md)
## Change
The existing `sdk` profile now includes `present`, `ask_user_question`, schedule tools, file-reference guidance and time context. It does not start a Web server. Standard tool calling remains the default; `DSH_TOOLS_MODE=ptc` selects programmatic tool calling in the same profile. `sdk-minimal` is unchanged.
The existing `sdk` profile now includes `present`, `ask_user_question`, schedule tools, file-reference guidance and time context. It does not start a Web server. Native tool execution remains the default; select PTC through an [executor overlay](../tool-executor-plugins/guide.md). `sdk-minimal` is unchanged.
## Migration
@@ -9,7 +9,7 @@ description: 现有 SDK profile 包含原生桌面工具和模型上下文。
## 变更
现有 `sdk` profile 现在包含 `present`、`ask_user_question`、定时任务工具、文件引用说明和时间上下文。它不启动 Web server。默认仍使用标准工具调用;`DSH_TOOLS_MODE=ptc` 在同一 profile 中选择程序化工具调用。`sdk-minimal` 保持不变。
现有 `sdk` profile 现在包含 `present`、`ask_user_question`、定时任务工具、文件引用说明和时间上下文。它不启动 Web server。默认仍使用原生工具执行;通过[执行器 overlay](../tool-executor-plugins/guide.zh.md) 选择 PTC。`sdk-minimal` 保持不变。
## 迁移
@@ -0,0 +1,16 @@
# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-rc.2/tool-executor-plugins/guide.md
/:
en: 3b1ca9e0f78709e0
zh: 13f23adcc722f319
/mount-a-tool-executor:
en: 73002a2963b9ea27
zh: a67ea382d7c70c61
/mount-a-tool-executor/change:
en: fac0b27c9452c4f2
zh: 2f53fd013ff460ba
/mount-a-tool-executor/migration:
en: 4d91c0177d06f4bd
zh: e93047b58a5aef4c
@@ -0,0 +1,35 @@
---
kind: upgrade-guide
description: "Tool execution modes move from dsh-tools configuration into explicit executor plugins."
---
# Mount a tool executor
English | [中文](guide.zh.md)
## Change
`@deepseek-ai/dsh-tools` keeps tool registration and executor selection, but rejects the removed `mode` and `maxParallelSubCalls` configuration fields at load time. Native and PTC execution require `@deepseek-ai/dsh-tool-executor-native` or `@deepseek-ai/dsh-tool-executor-ptc`. Shipped profiles mount an executor; custom compositions must add one. The existing `dsh-agent-tool-presentation` selector remains supported but is deprecated. Enabled `tool-executor-native` and `tool-executor-ptc` rows are required at startup; invalid executor configuration now stops startup. Web, headless, and SDK no longer read `DSH_TOOLS_MODE`; all inherit native execution from base.
## Migration
1. Keep the `dsh-tools` row and remove its execution configuration. Mount one native or PTC executor in the intended deployment or preset scope. For shipped profile patches, target the executor row instead of the `tools` row.
2. Move `maxParallelSubCalls` to the PTC executor configuration and mount a compatible `ctx.ptcRuntime` provider. The executor declares this service as a required injection; a missing runtime stops startup, and unloading the runtime withdraws the executor. `agent-loop.maxParallelToolCalls` and saved overrides stay in place with default `10`. The loop forwards the current limit to each batch; live edits apply to the next batch instead of the next tool group.
3. Custom executors implement `describe` and `execute`. Programmatic callers pass raw `calls` and `signal`, optional `agent`, and optional `log: { turn, step }`; read single-call outcomes from `results`. Omit `log` outside a committed model step.
4. Import PTC SDK renderers and `CodeRunFailedError` from `@deepseek-ai/dsh-tool-executor-ptc`. Run a task with the selected interaction mode and confirm its tool results appear in the Session.
5. Import `ToolExecutionPipeline` and `executeToolBatch` from `@deepseek-ai/dsh-tools/pipeline`. Complete calls, including single calls, use batches; custom schedulers call `prepare`, `dispatch`, and `complete` directly. Add `@deepseek-ai/dsh-tool-testkit` as a development dependency for tool fixtures and import them from that package. Neither is exported by the tool registry entry.
6. Import shared dispatch events and the `ptc-mode` message-source declaration from `@deepseek-ai/dsh-tool-executor-ptc/types`; Client code uses its `/client` re-export. Host policy extensions use `import type` from `@deepseek-ai/dsh-tool-executor-ptc` for `PtcDispatchLog` and `tools/ptc-dispatch-log`.
7. Remove `DSH_TOOLS_MODE` from launch environments. For PTC, save the following as `ptc.yml` and pass it with `--patch`; native execution needs no overlay. The same patch works with `--profile web` and `--profile sdk`; TypeScript and Python SDK clients pass its path through `patches`. Confirm that the model receives only `run_code` in PTC mode.
```yaml
- id: tool-executor-native
disabled: true
- id: tool-executor-ptc
disabled: false
```
```bash
dsh --profile headless --patch ./ptc.yml "task"
```
@@ -0,0 +1,35 @@
---
kind: upgrade-guide
description: "工具执行模式从 dsh-tools 配置移至显式执行器插件。"
---
# 挂载工具执行器
[English](guide.md) | 中文
## 变更
`@deepseek-ai/dsh-tools` 保留工具注册和执行器选择,但加载时会拒绝已移除的 `mode` 和 `maxParallelSubCalls` 配置字段。原生和 PTC 执行分别要求 `@deepseek-ai/dsh-tool-executor-native` 或 `@deepseek-ai/dsh-tool-executor-ptc`。内置 profile 已挂载执行器,自定义组合必须添加一个。现有 `dsh-agent-tool-presentation` 选择器仍受支持,但已弃用。启用的 `tool-executor-native` 和 `tool-executor-ptc` 条目属于启动必需项;无效的执行器配置会终止启动。Web、headless 与 SDK 不再读取 `DSH_TOOLS_MODE`,均继承 base 的原生执行。
## 迁移
1. 保留 `dsh-tools` 条目并移除其执行配置。在目标部署或预设作用域挂载一个原生或 PTC 执行器。内置 profile 的补丁应修改执行器条目,而非 `tools` 条目。
2. 把 `maxParallelSubCalls` 移到 PTC 执行器配置,并挂载兼容的 `ctx.ptcRuntime` 提供方。执行器将该服务声明为必需注入;缺少运行时会阻止启动,卸载运行时会撤回执行器。`agent-loop.maxParallelToolCalls` 和已保存的覆盖值保留原位,默认值为 `10`。循环向每个批次传入当前上限;实时修改从下一个批次生效,不再从下一个工具组生效。
3. 自定义执行器实现 `describe` 和 `execute`。程序调用方传入原始 `calls` 和 `signal`,以及可选的 `agent` 和 `log: { turn, step }`;从 `results` 读取单次结果。不属于已提交模型步骤的调用应省略 `log`。
4. 从 `@deepseek-ai/dsh-tool-executor-ptc` 导入 PTC SDK 渲染器和 `CodeRunFailedError`。用所选交互模式运行任务,确认工具结果出现在 Session 中。
5. 从 `@deepseek-ai/dsh-tools/pipeline` 导入 `ToolExecutionPipeline` 和 `executeToolBatch`。完整执行(包括单次调用)使用批次;自定义调度器直接调用 `prepare`、`dispatch` 和 `complete`。把 `@deepseek-ai/dsh-tool-testkit` 添加为开发依赖,并从该包导入工具 fixture。工具注册表主入口不再导出这些成员。
6. 从 `@deepseek-ai/dsh-tool-executor-ptc/types` 导入共享分派事件与 `ptc-mode` 消息来源声明;Client 代码使用其 `/client` 重导出。Host 策略扩展通过 `@deepseek-ai/dsh-tool-executor-ptc` 的 `import type` 获取 `PtcDispatchLog` 与 `tools/ptc-dispatch-log`。
7. 从启动环境中移除 `DSH_TOOLS_MODE`。需要 PTC 时,将以下内容保存为 `ptc.yml` 并通过 `--patch` 加载;原生执行无需 overlay。同一补丁也适用于 `--profile web` 和 `--profile sdk`;TypeScript 和 Python SDK 客户端通过 `patches` 传入其路径。确认 PTC 模式下模型只收到 `run_code`。
```yaml
- id: tool-executor-native
disabled: true
- id: tool-executor-ptc
disabled: false
```
```bash
dsh --profile headless --patch ./ptc.yml "task"
```
@@ -0,0 +1,16 @@
# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-rc.2/webserver-index-peer/guide.md
/:
en: 4182112137303167
zh: 8451ce873cfca164
/webserver-index-rendering-takes-a-peer:
en: 7986c2aaf1511f53
zh: a6f78fc166e1ea19
/webserver-index-rendering-takes-a-peer/change:
en: 07a11388305a0124
zh: 662003b29c238d30
/webserver-index-rendering-takes-a-peer/migration:
en: 7ef144b29ffb2f39
zh: defb62ecc4ba6c2e
@@ -0,0 +1,17 @@
---
kind: upgrade-guide
description: "WebServer index rendering takes the Peer the page speaks for, and the `webserver/index-inject` event passes that Peer to listeners."
---
# WebServer index rendering takes a Peer
English | [中文](guide.zh.md)
## Change
`renderIndex(html)` and `collectIndexInjections()` on `ctx.webServer` from `@deepseek-ai/dsh-host-webserver` took no Peer, and every index carried the owner's access snapshot. They are now `renderIndex(html, peer)` and `collectIndexInjections(peer)`, and the `webserver/index-inject` event passes the same Peer as its second argument, so the remote-access snapshot in the index belongs to that Peer. Code that calls either method breaks at compile time; listeners that read only the table keep working.
## Migration
1. Pass the Peer the page speaks for to every `ctx.webServer.renderIndex(html)` and `ctx.webServer.collectIndexInjections()` call. A page served to the owner's browser passes `ctx.connection.browserPeer`.
2. In a test that emits `webserver/index-inject` itself, pass a Peer after the table: `ctx.emit('webserver/index-inject', table, peer)`.
3. Run your TypeScript build and confirm no call to either method reports a missing argument.
@@ -0,0 +1,17 @@
---
kind: upgrade-guide
description: "WebServer 渲染 index 时需要传入页面所代表的 Peer,`webserver/index-inject` 事件会把该 Peer 传给监听方。"
---
# WebServer 渲染 index 需要传入 Peer
[English](guide.md) | 中文
## 变更
`@deepseek-ai/dsh-host-webserver` 中 `ctx.webServer` 的 `renderIndex(html)` 与 `collectIndexInjections()` 不接收 Peer,每个 index 携带的都是 owner 的访问快照。现在它们改为 `renderIndex(html, peer)` 与 `collectIndexInjections(peer)`,`webserver/index-inject` 事件以第二个参数传入同一个 Peer,index 中的 remote-access 快照因此属于该 Peer。调用这两个方法的代码会在编译时报错;只读取表格的监听方不受影响。
## 迁移
1. 每处 `ctx.webServer.renderIndex(html)` 与 `ctx.webServer.collectIndexInjections()` 调用都传入页面所代表的 Peer。提供给 owner 浏览器的页面传入 `ctx.connection.browserPeer`。
2. 自行 emit `webserver/index-inject` 的测试在表格之后传入一个 Peer:`ctx.emit('webserver/index-inject', table, peer)`。
3. 运行 TypeScript 构建,确认这两个方法的调用都不再报缺少参数。
+2
View File
@@ -224,6 +224,8 @@
"devDependencies": {
"@deepseek-ai/dsh-hooks-claude-code": "workspace:*",
"@deepseek-ai/dsh-hooks-codex": "workspace:*",
"@deepseek-ai/dsh-tool-executor-native": "workspace:*",
"@deepseek-ai/dsh-tool-executor-ptc": "workspace:*",
"@deepseek-ai/dsh-tool-ralph": "workspace:*",
"@deepseek-ai/dsh-tool-terminal": "workspace:*",
"@deepseek-ai/dsh-agent": "workspace:*",
+1
View File
@@ -63,6 +63,7 @@
"@deepseek-ai/dsh-session-projection": "workspace:*",
"@deepseek-ai/dsh-subagent": "workspace:*",
"@deepseek-ai/dsh-token-meter": "workspace:*",
"@deepseek-ai/dsh-tool-testkit": "workspace:*",
"@deepseek-ai/dsh-tools": "workspace:*",
"@deepseek-ai/dsh-user-approval": "workspace:*"
}
+1 -1
View File
@@ -9,7 +9,7 @@ import { ToolCallId, type StreamChunk } from '@deepseek-ai/dsh-llm'
import { SESSION_FORMAT_VERSION, SessionId } from '@deepseek-ai/dsh-session'
import type { SessionHeader } from '@deepseek-ai/dsh-session'
import { SessionPersistenceRevision, type SessionPersistenceSnapshot } from '@deepseek-ai/dsh-session-persistence'
import { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import { defineContentToolFixture } from '@deepseek-ai/dsh-tool-testkit'
import { makeBridgeHarness, textResponse, type BridgeHarness } from './harness.ts'
import { startHttpMcpFixture } from '../../../mcp/mcp-client/tests/http-fixture.ts'
Loaded 100 of 672 files, more files were not shown because too many files have changed in this diff. Show more