@dsh-enhanced/assistant-delivery 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -44,8 +44,6 @@ Patch 通过 `inject: [assistantPolicy]` 固定加载策略依赖;入站 Agent
44
44
  | `defaultAgentPreset` | `standard` | 新 session 解析并挂载的 preset;该内置 preset 提供 Bash、文件、检索、Skills 等完整编码能力 |
45
45
  | `policyRef` | `owner-dm` | 固化在 binding 上的策略引用标签 |
46
46
  | `agentProvider` / `agentModel` | `deepseek-official` / `deepseek-v4-flash` | 渠道 Agent 的部署默认模型;会话可用 `/model` 覆盖 |
47
- | `toolCapableProviders` | `[deepseek-official]` | 尚未发布 registry metadata 的上游原生工具 provider 审计列表;在 `unknownRouteToolCalls: deny` 下作为白名单生效 |
48
- | `unknownRouteToolCalls` | `allow` | 未发布任何工具能力声明的 route 如何准入;`deny` 切换为严格白名单。provider 显式声明 `none` 时两种取值都 fail closed |
49
47
  | `agentMaxOutputTokens` | `8192` | 单轮模型输出上限 |
50
48
  | `modelPickerTtlMs` | `900000` | `/model` 选择卡片的签名提交有效期;范围 1 分钟至 24 小时 |
51
49
  | `permissionPickerTtlMs` | `900000` | `/permissions` 三档权限卡片的签名提交有效期;范围 1 分钟至 24 小时 |
@@ -106,6 +104,7 @@ rules:
106
104
  - 429/明确 5xx 未发送应给出 `retryable` 与可选 `retryAfterMs`;永久 4xx 进入 dead。
107
105
  - 请求可能已到平台但 response 丢失时返回 `unknown`。若声明 `reconcileUnknownSend` capability,必须实现对账,并使用独立的有界对账 attempt;连续无法判定达到上限后进入 dead,释放同 lane 后续消息。未声明该能力时记录保持 `unknown_after_send`,不会被领取为空对账 attempt,直到 owner 显式 retry/cancel。
108
106
  - receipt 必须带同一个 channel、account、provider message id;状态只允许单调 `accepted→delivered→read`。
107
+ - `progress()` 收到的工具参数/结果仅是最多 1500 字符的展示预览,并会脱敏常见 credential 字段与 token 形态;畸形或原始输入过大的参数只产生状态标记,不回显 raw text。它不包含任何 reasoning/thinking 内容或 provider 原始错误。adapter 仍必须按当前受众决定是否展示明细,群聊等共享受众应保持 status-only。
109
108
  - adapter 的 socket、timer、SDK client 和 listener 必须由 `start()` disposer 释放。
110
109
 
111
110
  内部身份始终是结构化的 `ExternalPrincipalKey`、`ConversationRef` 与 `ConversationBinding`,不会从 `platform:chat:thread` 字符串反解析路由。DM 不允许 thread;group 入口要求渠道提供显式且稳定的 thread,可以是真实 provider root,也可以是渠道命名空间内按 principal 派生的顶层合成 lane。binding 仍固定精确 principal,任何同 conversation 的另一 principal 都会在进入 session 前 dead-letter,不能共享既有 owner 上下文。Lark 顶层群消息使用按发送者派生的合成 lane,真实回复串继续按 `root_id` 隔离。
@@ -150,7 +149,7 @@ rules:
150
149
 
151
150
  `/model` 从宿主 `ctx.llm` 的实时 provider/model 目录生成受限的 typed `model-picker` intent,不发起模型生成;每个模型会显式关联它自己的 `effortIds`,而不是共享全局 effort 并集。支持它的渠道可以据此显示“分组、模型、effort”三级联动选择和确认按钮,不支持它的渠道会把该 intent 明确拒绝而不会降级为任意卡片 JSON。持久 selection 会先投影为不含 `updatedAt/version` 的纯路由再进入严格 intent 边界;如果目录中的异常数据仍使卡片校验失败,系统会立即退回完整纯文本目录,确定性校验错误不会重试。某个 provider 或模型能力解析失败不影响其他项,最多载入 20 个 provider、50 个模型、每模型 20 档 effort,并且全目录最多 20 个不同 effort id。
152
151
 
153
- 卡片提交后,Delivery 会再次核对 active binding、精确 principal/chat 和 Policy,并用实时 `resolveModelInfo()` 验证 provider/model 及该模型支持的 effort;通过后把三项选择按 canonical conversation 持久化到 Delivery SQLite。选择从下一条普通消息生效,保留当前 session 上下文,跨 `/new` 和 Host 重启仍有效。卡片可选择“默认(由模型决定)”;`/model use` 保留为无卡片渠道和排错时的文字后备,`/model reset` 删除会话覆盖并恢复 `agentProvider` / `agentModel`。模型目录按宿主约定是建议性的,最终调用是否成功仍由对应 adapter 和账号认证决定。
152
+ 卡片提交后,Delivery 会再次核对 active binding、精确 principal/chat、原始 provider message id 和 Policy,并用实时 `resolveModelInfo()` 验证 provider/model 及该模型支持的 effort;通过后把三项选择按 canonical conversation 持久化到 Delivery SQLite。渠道可以等待该 durable settlement:同实例由完成通知即时唤醒,共享数据库的另一实例完成时由有界轮询感知,从而把原卡更新成不可交互的最终结果。选择从下一条普通消息生效,保留当前 session 上下文,跨 `/new` 和 Host 重启仍有效。卡片可选择“默认(由模型决定)”;`/model use` 保留为无卡片渠道和排错时的文字后备,`/model reset` 删除会话覆盖并恢复 `agentProvider` / `agentModel`。模型目录按宿主约定是建议性的,最终调用是否成功仍由对应 adapter 和账号认证决定。若某个已持久化的显式 effort 在下一条普通消息前被 DSH 核心判为不支持,Delivery 会在任何 prompt 或 durable dispatch 标记之前,以 version CAS 仅清除该 effort、重新预检同一 provider/model 的默认档位;CAS 冲突会重试而不会覆盖更晚的 `/model` 选择。模型不会被静默替换,也不会重放或重复计费。
154
153
 
155
154
  `/permissions`(别名 `/permission`)只允许当前 active binding 的精确 owner 使用,命令本身不会进入 Agent 或触发 LLM。支持 typed `permission-picker` 的私聊渠道会显示“请求批准(ask)/帮我批准(auto)/完全访问权限(full)”三档卡片并标出当前档位;不支持卡片或渲染失败时自动退回含同等信息的文字命令。前两档都使用 `workspace-write + ask`,区别是持久 reviewer 为 `user` 或 `auto-review`。`full` 使用 `danger-full-access + never + none`,可访问网络及任意文件;卡片按钮带原生风险确认,文字入口则由 `/permission full` 显示橙色警告,必须再次发送 `/permission full confirm` 才切换。
156
155
 
@@ -164,7 +163,7 @@ rules:
164
163
 
165
164
  权限命令在第一次 reviewer/policy/sandbox mutation 前还必须先取得 Inbox 的 durable dispatch/recovery marker;每个异步边界后都复核 abort 与租约。若进程退出、续租失败、flush 未确认、依赖尚未就绪或回复入队失败,该 marker 会绕过普通 `maxAttempts`,在原 binding 串行 lane 中继续恢复,直到确认已持久化目标并补发成功终态,或安全收敛到 ask 后补发失败/取消终态。`/stop` 和 `/new` 会在 abort 前把正在执行的权限命令原子改标为 cancelled recovery;旧命令不会以 `processor-ambiguous` 终止,也不会从原始文本重新制造一次可能已取消的 full 提权。
166
165
 
167
- 普通消息真正 resume Agent 时,Delivery 会在 preset 挂载完成后检查最终 scoped tool schemas。非空工具时,provider 通过 `@dsh-enhanced/llm-route-capabilities` 声明 `native`/`bridge` 即放行。DSH 的 `generate` 契约对所有 adapter 都接受 `tools`,因此工具调用是 adapter 基线能力:未发布任何声明的 route(pi-ai 等内置/网关 provider 从不发布 registry metadata)默认按 `unknownRouteToolCalls: allow` 准入,避免整类可用 route 被误拒。需要严格白名单的部署设为 `deny`,此时只放行 `toolCapableProviders`。provider 显式声明 `none` 时始终 fail closed,不受上述任何一项影响。Claude、Cursor、Grok 和 Codex CLI 等 text-only route 因此会在 `followup()`、认证和子进程之前返回可操作的 `/model` 提示;显式 opt-in 的 Codex direct route 声明 `native`,可把 function call 交回同一个 DSH Agent Loop 执行。fresh session 创建本身不执行模型,仍可先持久建立 binding。最终工具为空时不要求能力声明。
166
+ 普通消息真正 resume Agent 时,Delivery 先解析并挂载 binding 固定的 preset,再把该 Agent 的最终 scoped tool schemas 原样交给当前 provider/model。工具和 Skills 属于 Agent/preset,模型切换不会改变它们的可见集合,Delivery 也不维护 provider 工具白名单或部署侧能力名单。协议元数据缺失、`native` 和 `bridge` 都会放行;只有 adapter 主动声明 `toolCalls: none` 且最终 scope 非空时,才会在 prompt 发送前因“未实现统一 DSH tool-call 协议”失败关闭。这个检查描述 adapter 的传输实现,不是模型权限或能力分级;实际工具执行仍逐次经过 ToolRuntime、Policy、sandbox、审批、身份与预算检查。fresh session 创建本身不执行模型,仍可先持久建立 binding。
168
167
 
169
168
  只注册两个模型工具:
170
169
 
package/cordis.patch.yml CHANGED
@@ -21,8 +21,6 @@
21
21
  policyRef: owner-dm
22
22
  agentProvider: deepseek-official
23
23
  agentModel: deepseek-v4-flash
24
- toolCapableProviders:
25
- - deepseek-official
26
24
  agentMaxOutputTokens: 8192
27
25
  modelPickerTtlMs: 900000
28
26
  permissionPickerTtlMs: 900000
@@ -26,11 +26,16 @@ interface DshDeliveryRuntimeOptions {
26
26
  policyRef: string;
27
27
  provider: string;
28
28
  model: string;
29
- toolCapableProviders: ReadonlySet<string>;
30
- unknownRouteToolCalls: 'allow' | 'deny';
31
29
  maxOutputTokens: number;
32
30
  permissionPickerTtlMs: number;
33
31
  getModelSelection(conversation: ConversationRef): ConversationModelSelection | undefined;
32
+ /** Atomically remove a stale explicit effort without overwriting a newer model choice. */
33
+ clearStaleModelReasoningEffort(conversation: ConversationRef, expected: ConversationModelSelection): {
34
+ applied: false;
35
+ } | {
36
+ applied: true;
37
+ selection: ConversationModelSelection;
38
+ };
34
39
  imageMaterializer: Pick<InboundImageMaterializer, 'materialize'>;
35
40
  isInboundAuthorized(binding: Readonly<ConversationBinding>, envelope: Readonly<InboundEnvelope>): boolean;
36
41
  isPermissionController(binding: Readonly<ConversationBinding>, envelope: Readonly<InboundEnvelope>): boolean;
@@ -60,15 +65,14 @@ interface ModelCommandReply {
60
65
  fallbackText?: string;
61
66
  }
62
67
  /**
63
- * Convert one durable session fact into a deliberately narrow, user-visible progress update.
64
- * Raw reasoning chunks, tool arguments, result content, and error details never cross this boundary.
68
+ * Convert one durable session fact into a bounded, user-visible progress update.
69
+ * Reasoning/thinking content and provider error messages never cross this boundary. Tool calls/results
70
+ * contribute bounded previews with common credential shapes redacted; the channel still decides
71
+ * whether its current audience is private enough to render those previews.
65
72
  *
66
- * The `reasoning` block of a durable `assistant/message` is the assistant's own settled summary of
67
- * the step, not a streaming fragment, so it is the one reasoning form safe to surface. Only some
68
- * providers emit it at all: subscription CLIs declare an effort capability but still return text
69
- * only, and the ACP thought channel is not mapped to DSH reasoning yet. `step/start` therefore also
70
- * yields a neutral phase label, so a turn reports progress even on providers that never reason
71
- * out loud.
73
+ * A DSH `reasoning` block is explicitly thinking content and is not guaranteed to be a public
74
+ * summary, even after assembly into `assistant/message`. `step/start` therefore provides the only
75
+ * reasoning-independent phase label, so a turn reports progress without exposing hidden thought.
72
76
  */
73
77
  export declare function deliveryProgressFromSessionEvent(event: SessionEvent): DeliveryProgressUpdate | undefined;
74
78
  export declare function permissionStateHash(events: readonly SessionEvent[], tableFingerprint?: string): string;
@@ -88,6 +92,13 @@ export declare class DshDeliveryRuntime implements DeliveryInboundRuntime {
88
92
  /** Wait for only the turn that claims Delivery's exact message identity. */
89
93
  private followupTurn;
90
94
  private setupAgent;
95
+ /**
96
+ * DSH validates an explicit reasoning effort before calling an adapter. That is normally the
97
+ * right contract, but a persisted external-channel selection can outlive a provider's live
98
+ * model directory. Clear only that stale persisted effort before a prompt crosses the durable
99
+ * dispatch boundary; TraeX still rechecks the newly created ACP session afterwards.
100
+ */
101
+ private resolveExecutionRoute;
91
102
  private reconcileNativeFullPermissionReviewer;
92
103
  private runPermissionCommand;
93
104
  createSession(input: {
@@ -1 +1 @@
1
- {"version":3,"file":"agent-runtime.d.ts","sourceRoot":"","sources":["../src/agent-runtime.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,OAAO,EAEL,KAAK,KAAK,EAGX,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAA;AAUlE,OAAO,EAA2B,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAMrF,OAAO,EAKL,KAAK,sBAAsB,EAC5B,MAAM,gCAAgC,CAAA;AAEvC,OAAO,KAAK,EACV,qBAAqB,EACrB,oBAAoB,EAEpB,oBAAoB,EACpB,sBAAsB,EACtB,sBAAsB,EACvB,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAA;AACnE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AAS1D,OAAO,KAAK,EACV,mBAAmB,EACnB,0BAA0B,EAC1B,eAAe,EACf,sBAAsB,EACtB,eAAe,EACf,iBAAiB,EACjB,aAAa,EACb,sBAAsB,EAEvB,MAAM,YAAY,CAAA;AAEnB,OAAO,QAAQ,sBAAsB,CAAC;IACpC,UAAU,gBAAgB;QACxB,QAAQ,EAAE;YACR,IAAI,EAAE,UAAU,CAAA;YAChB,OAAO,EAAE,MAAM,CAAA;YACf,OAAO,EAAE,MAAM,CAAA;YACf,OAAO,EAAE,MAAM,CAAA;YACf,KAAK,EAAE,WAAW,CAAA;SACnB,CAAA;KACF;CACF;AAED,UAAU,yBAAyB;IACjC,gBAAgB,EAAE,MAAM,CAAA;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,eAAe,IAAI,IAAI,CAAC,YAAY,EAAE,OAAO,GAAG,SAAS,CAAC,GAAG,SAAS,CAAA;IACtE,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,oBAAoB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IACzC,qBAAqB,EAAE,OAAO,GAAG,MAAM,CAAA;IACvC,eAAe,EAAE,MAAM,CAAA;IACvB,qBAAqB,EAAE,MAAM,CAAA;IAC7B,iBAAiB,CAAC,YAAY,EAAE,eAAe,GAAG,0BAA0B,GAAG,SAAS,CAAA;IACxF,iBAAiB,EAAE,IAAI,CAAC,wBAAwB,EAAE,aAAa,CAAC,CAAA;IAChE,mBAAmB,CACjB,OAAO,EAAE,QAAQ,CAAC,mBAAmB,CAAC,EACtC,QAAQ,EAAE,QAAQ,CAAC,eAAe,CAAC,GAClC,OAAO,CAAA;IACV,sBAAsB,CACpB,OAAO,EAAE,QAAQ,CAAC,mBAAmB,CAAC,EACtC,QAAQ,EAAE,QAAQ,CAAC,eAAe,CAAC,GAClC,OAAO,CAAA;IACV,iGAAiG;IACjG,wBAAwB,CACtB,OAAO,EAAE,QAAQ,CAAC,mBAAmB,CAAC,EACtC,QAAQ,EAAE,QAAQ,CAAC,eAAe,CAAC,GAClC,OAAO,CAAA;IACV,iBAAiB,CAAC,YAAY,EAAE,eAAe,GAAG,MAAM,CAAA;IACxD,kBAAkB,CAAC,KAAK,EAAE;QACxB,YAAY,EAAE,eAAe,CAAA;QAC7B,aAAa,EAAE,MAAM,CAAA;QACrB,KAAK,CAAC,EAAE,aAAa,CAAA;KACtB,GAAG;QAAE,OAAO,EAAE,KAAK,CAAA;KAAE,GAAG;QAAE,OAAO,EAAE,IAAI,CAAC;QAAC,SAAS,CAAC,EAAE,0BAA0B,CAAA;KAAE,CAAA;IAClF,gBAAgB,EAAE,MAAM,CAAA;IACxB,QAAQ,CACN,OAAO,EAAE,QAAQ,CAAC,mBAAmB,CAAC,EACtC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,sBAAsB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAAA;IAChB,YAAY,CACV,OAAO,EAAE,QAAQ,CAAC,mBAAmB,CAAC,EACtC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,iBAAiB,EACxB,cAAc,CAAC,EAAE,MAAM,GACtB,IAAI,CAAA;IACP,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAAA;CACrE;AAED,UAAU,iBAAiB;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,UAAU,GAAG,cAAc,GAAG,mBAAmB,GAAG,OAAO,CAAA;IACpE,WAAW,CAAC,EAAE,iBAAiB,CAAA;IAC/B,gBAAgB,CAAC,EAAE,sBAAsB,CAAA;IACzC,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAmHD;;;;;;;;;;GAUG;AACH,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,YAAY,GAAG,sBAAsB,GAAG,SAAS,CA0CxG;AAkMD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,EAAE,gBAAgB,SAAK,GAAG,MAAM,CAalG;AAED,wBAAgB,2BAA2B,CAAC,YAAY,EAAE,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAU5G;AA2CD,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAUvG;AA8SD,qBAAa,kBAAmB,YAAW,sBAAsB;IAK7D,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAN1B,QAAQ,CAAC,eAAe,EAAG,UAAU,CAAS;IAC9C,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAA0C;gBAG7D,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,sBAAsB,EAC9B,OAAO,EAAE,yBAAyB;IAG/C,YAAY,CAAC,OAAO,EAAE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAwBrG,OAAO,CAAC,mBAAmB;IAwB3B,OAAO,CAAC,iBAAiB;YAMX,yBAAyB;IAwBvC,4EAA4E;IAC5E,OAAO,CAAC,YAAY;YA2CN,UAAU;YAqBV,qCAAqC;YASrC,oBAAoB;IAmb5B,aAAa,CAAC,KAAK,EAAE;QACzB,QAAQ,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAA;QACnC,UAAU,EAAE,MAAM,CAAA;QAClB,QAAQ,CAAC,EAAE,QAAQ,CAAC,mBAAmB,CAAC,CAAA;QACxC,MAAM,EAAE,WAAW,CAAA;KACpB,GAAG,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAqEvF,OAAO,CACX,OAAO,EAAE,QAAQ,CAAC,mBAAmB,CAAC,EACtC,QAAQ,EAAE,QAAQ,CAAC,eAAe,CAAC,EACnC,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,QAAQ,CAAC,qBAAqB,CAAC,GACvC,OAAO,CAAC,oBAAoB,CAAC;IAmDhC,OAAO,CAAC,mBAAmB;IAiB3B,OAAO,CAAC,WAAW;YAkBL,iBAAiB;IA2LzB,OAAO,CACX,OAAO,EAAE,QAAQ,CAAC,mBAAmB,CAAC,EACtC,QAAQ,EAAE,QAAQ,CAAC,eAAe,CAAC,EACnC,WAAW,EAAE,WAAW,EACxB,QAAQ,CAAC,EAAE,QAAQ,CAAC,sBAAsB,CAAC,EAC3C,eAAe,CAAC,EAAE,sBAAsB,GACvC,OAAO,CAAC,oBAAoB,CAAC;YA+ClB,iBAAiB;CA8PhC"}
1
+ {"version":3,"file":"agent-runtime.d.ts","sourceRoot":"","sources":["../src/agent-runtime.ts"],"names":[],"mappings":"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"}
@@ -3,7 +3,7 @@ import { mkdir, stat } from 'node:fs/promises';
3
3
  import { installModelSelection, } from '@deepseek-ai/dsh-agent';
4
4
  import { contentHasImage, createUserMessage, ReasoningEffortId, } from '@deepseek-ai/dsh-llm';
5
5
  import { effectiveSandboxMode, setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy';
6
- import { SessionId } from '@deepseek-ai/dsh-session';
6
+ import { isAppendSurfaceEvent, SessionId } from '@deepseek-ai/dsh-session';
7
7
  import { effectiveApprovalPolicy, setApprovalPolicy, } from '@deepseek-ai/dsh-user-approval';
8
8
  import { approvalReviewerOf, setApprovalReviewer, waitForApprovalReviewerSessionEventReady, } from '@dsh-enhanced/assistant-policy';
9
9
  import { resolveLlmRouteCapability } from '@dsh-enhanced/llm-route-capabilities';
@@ -22,6 +22,16 @@ class UserSessionCancellation extends Error {
22
22
  const MAX_CATALOG_MODELS = 50;
23
23
  const MAX_PROGRESS_TODOS = 20;
24
24
  const MAX_PROGRESS_TEXT_CHARS = 240;
25
+ const MAX_PROGRESS_TOOL_PREVIEW_CHARS = 1_500;
26
+ const MAX_PROGRESS_JSON_INPUT_CHARS = 32_768;
27
+ const MAX_PROGRESS_JSON_DEPTH = 16;
28
+ const MAX_PROGRESS_JSON_NODES = 500;
29
+ const MAX_PROGRESS_RESULT_BLOCKS = 100;
30
+ const REDACTED_PROGRESS_VALUE = '[REDACTED]';
31
+ const TRUNCATED_PROGRESS_VALUE = '[TRUNCATED]';
32
+ const PROGRESS_ASSIGNMENT_KEY = '([A-Za-z_][A-Za-z0-9_.-]{0,127})';
33
+ const PROGRESS_ASSIGNMENT_START = new RegExp(`(^|[^A-Za-z0-9_])(["']?)${PROGRESS_ASSIGNMENT_KEY}\\2(\\s*[:=]\\s*)`, 'giu');
34
+ const ESCAPED_QUOTED_PROGRESS_ASSIGNMENT_START = new RegExp(`(^|[^A-Za-z0-9_])\\\\+(["'])${PROGRESS_ASSIGNMENT_KEY}\\\\+\\2(\\s*[:=]\\s*)`, 'giu');
25
35
  class DurableAgentIdentityError extends Error {
26
36
  }
27
37
  class ApprovalReviewerReaderUnavailableError extends Error {
@@ -30,16 +40,16 @@ class ApprovalReviewerReaderUnavailableError extends Error {
30
40
  this.name = 'ApprovalReviewerReaderUnavailableError';
31
41
  }
32
42
  }
33
- class ToolCapabilityAdmissionError extends Error {
43
+ class AdapterToolCallProtocolError extends Error {
34
44
  provider;
35
45
  model;
36
46
  presetId;
37
47
  constructor(provider, model, presetId) {
38
- super(`assistant-delivery: ${provider}/${model} cannot execute the tools mounted by preset ${presetId}`);
48
+ super(`assistant-delivery: adapter ${provider}/${model} declares no DSH tool-call protocol`);
39
49
  this.provider = provider;
40
50
  this.model = model;
41
51
  this.presetId = presetId;
42
- this.name = 'ToolCapabilityAdmissionError';
52
+ this.name = 'AdapterToolCallProtocolError';
43
53
  }
44
54
  }
45
55
  class ImageCapabilityAdmissionError extends Error {
@@ -52,32 +62,18 @@ class ImageCapabilityAdmissionError extends Error {
52
62
  this.name = 'ImageCapabilityAdmissionError';
53
63
  }
54
64
  }
55
- function requireToolCapability(llm, provider, model, presetId, toolCount, declaredToolCapableProviders, unknownRouteToolCalls) {
65
+ function requireAdapterToolCallProtocol(llm, provider, model, presetId, toolCount) {
56
66
  if (toolCount === 0)
57
67
  return;
58
- const capability = resolveLlmRouteCapability(llm, provider, model);
59
- if (capability?.toolCalls === 'native' || capability?.toolCalls === 'bridge')
60
- return;
61
- // A provider-owned declaration is authoritative in both directions: an
62
- // explicit `none` still fails closed even when the route is allowlisted or
63
- // unknown routes are admitted.
64
- if (capability?.toolCalls === 'none')
65
- throw new ToolCapabilityAdmissionError(provider, model, presetId);
66
- // Only routes with no declaration at all reach the host policy. The DSH
67
- // `generate` contract accepts `tools` for every adapter, so tool calling is
68
- // the adapter baseline rather than an opt-in: gateway and built-in providers
69
- // (pi-ai and friends) never publish registry metadata, and denying them by
70
- // default made every such route unusable behind a tool-mounting preset.
71
- if (declaredToolCapableProviders.has(provider))
72
- return;
73
- if (unknownRouteToolCalls === 'allow')
74
- return;
75
- throw new ToolCapabilityAdmissionError(provider, model, presetId);
68
+ if (resolveLlmRouteCapability(llm, provider, model)?.toolCalls === 'none') {
69
+ throw new AdapterToolCallProtocolError(provider, model, presetId);
70
+ }
76
71
  }
77
- function toolCapabilityReply(error) {
72
+ function adapterToolCallProtocolReply(error) {
78
73
  return {
79
- text: `当前模型 ${error.provider}/${error.model} 已声明不支持工具调用,无法执行 preset “${error.presetId}” 暴露的工具。`
80
- + '请发送 /model 切换到支持工具调用的 provider,或改用不挂载工具的 preset。',
74
+ text: `当前路由 ${error.provider}/${error.model} 的 adapter 明确声明未实现统一 DSH tool-call 协议,`
75
+ + `因此无法承载 preset “${error.presetId}” 已挂载的工具。`
76
+ + '这不是模型权限差异;请升级或修复该 provider adapter,使其实现 native/bridge 协议。',
81
77
  format: 'plain',
82
78
  };
83
79
  }
@@ -97,16 +93,214 @@ async function requireImageCapability(llm, provider, model, signal) {
97
93
  function boundedProgressText(value) {
98
94
  return [...value].slice(0, MAX_PROGRESS_TEXT_CHARS).join('');
99
95
  }
96
+ function boundedProgressToolPreview(value) {
97
+ const trimmed = value.trim();
98
+ const characters = [...trimmed];
99
+ return characters.length <= MAX_PROGRESS_TOOL_PREVIEW_CHARS
100
+ ? trimmed
101
+ : `${characters.slice(0, MAX_PROGRESS_TOOL_PREVIEW_CHARS - 1).join('')}…`;
102
+ }
103
+ function redactProgressText(value) {
104
+ const common = redactEscapedKeyProgressAssignments(value)
105
+ .replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?(?:-----END [^-]*PRIVATE KEY-----|$)/giu, '[REDACTED PRIVATE KEY]')
106
+ .replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^/\s@]+@/giu, '$1[REDACTED]@')
107
+ .replace(/\b((?:proxy-)?authorization\s*[:=]\s*)(?:basic|bearer)\s+[^\s,;]+/giu, '$1[REDACTED]');
108
+ return redactProgressAssignments(common)
109
+ .replace(/\b(?:github_pat_[A-Za-z0-9_]{20,}|gh[pousr]_[A-Za-z0-9]{20,}|(?:sk|xai)-[A-Za-z0-9_-]{8,})\b/gu, REDACTED_PROGRESS_VALUE)
110
+ .replace(/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/gu, REDACTED_PROGRESS_VALUE);
111
+ }
112
+ function isSensitiveProgressKey(key) {
113
+ const words = key.normalize('NFKC')
114
+ .replace(/([a-z\d])([A-Z])/gu, '$1_$2')
115
+ .toLowerCase()
116
+ .split(/[^a-z\d]+/gu)
117
+ .filter(Boolean);
118
+ const joined = words.join('');
119
+ return words.some(word => [
120
+ 'auth', 'authorization', 'bearer', 'cookie', 'cookies', 'credential', 'credentials',
121
+ 'password', 'passwd', 'secret', 'session', 'token',
122
+ ].includes(word))
123
+ || ['apikey', 'accesskey', 'privatekey'].some(phrase => joined.includes(phrase))
124
+ || words.some((word, index) => ['api', 'access', 'private'].includes(word) && words[index + 1] === 'key');
125
+ }
100
126
  /**
101
- * Convert one durable session fact into a deliberately narrow, user-visible progress update.
102
- * Raw reasoning chunks, tool arguments, result content, and error details never cross this boundary.
127
+ * Text credentials have no trustworthy terminator: quotes may be malformed or multiply escaped,
128
+ * while spaces, semicolons, and nested assignments can still belong to the value. Once a key is
129
+ * recognized, fail closed through the logical line instead of exposing an arbitrary suffix.
130
+ */
131
+ function redactProgressAssignments(value) {
132
+ let retainedIndex = 0;
133
+ let redacted = '';
134
+ PROGRESS_ASSIGNMENT_START.lastIndex = 0;
135
+ for (let match = PROGRESS_ASSIGNMENT_START.exec(value); match !== null; match = PROGRESS_ASSIGNMENT_START.exec(value)) {
136
+ const key = match[3];
137
+ if (key === undefined || !isSensitiveProgressKey(key)) {
138
+ // The prefix regex consumes the separator of an innocent outer field. Resume inside that
139
+ // match so nested assignments on the same line remain visible to this scanner.
140
+ PROGRESS_ASSIGNMENT_START.lastIndex = match.index + 1;
141
+ continue;
142
+ }
143
+ const valueStart = match.index + match[0].length;
144
+ const lineEnd = progressRedactionEnd(value, valueStart);
145
+ redacted += value.slice(retainedIndex, valueStart) + REDACTED_PROGRESS_VALUE;
146
+ retainedIndex = lineEnd;
147
+ PROGRESS_ASSIGNMENT_START.lastIndex = lineEnd;
148
+ }
149
+ PROGRESS_ASSIGNMENT_START.lastIndex = 0;
150
+ return redacted + value.slice(retainedIndex);
151
+ }
152
+ /** Escaped JSON keys inside a log string cannot be parsed as the outer text; fail closed by line. */
153
+ function redactEscapedKeyProgressAssignments(value) {
154
+ let retainedIndex = 0;
155
+ let redacted = '';
156
+ ESCAPED_QUOTED_PROGRESS_ASSIGNMENT_START.lastIndex = 0;
157
+ for (let match = ESCAPED_QUOTED_PROGRESS_ASSIGNMENT_START.exec(value); match !== null; match = ESCAPED_QUOTED_PROGRESS_ASSIGNMENT_START.exec(value)) {
158
+ const key = match[3];
159
+ if (key === undefined || !isSensitiveProgressKey(key)) {
160
+ ESCAPED_QUOTED_PROGRESS_ASSIGNMENT_START.lastIndex = match.index + 1;
161
+ continue;
162
+ }
163
+ const valueStart = match.index + match[0].length;
164
+ const lineEnd = progressRedactionEnd(value, valueStart);
165
+ redacted += value.slice(retainedIndex, valueStart) + REDACTED_PROGRESS_VALUE;
166
+ retainedIndex = lineEnd;
167
+ ESCAPED_QUOTED_PROGRESS_ASSIGNMENT_START.lastIndex = lineEnd;
168
+ }
169
+ ESCAPED_QUOTED_PROGRESS_ASSIGNMENT_START.lastIndex = 0;
170
+ return redacted + value.slice(retainedIndex);
171
+ }
172
+ function progressRedactionEnd(value, start) {
173
+ let cursor = start;
174
+ while (cursor < value.length) {
175
+ const carriageReturn = value.indexOf('\r', cursor);
176
+ const lineFeed = value.indexOf('\n', cursor);
177
+ const candidates = [carriageReturn, lineFeed].filter(index => index >= 0);
178
+ if (candidates.length === 0)
179
+ return value.length;
180
+ const boundary = Math.min(...candidates);
181
+ if (value[boundary - 1] !== '\\')
182
+ return boundary;
183
+ cursor = boundary + (value[boundary] === '\r' && value[boundary + 1] === '\n' ? 2 : 1);
184
+ }
185
+ return value.length;
186
+ }
187
+ function redactProgressJson(value, state, depth = 0) {
188
+ state.nodes += 1;
189
+ if (depth > MAX_PROGRESS_JSON_DEPTH || state.nodes > MAX_PROGRESS_JSON_NODES) {
190
+ return TRUNCATED_PROGRESS_VALUE;
191
+ }
192
+ if (Array.isArray(value)) {
193
+ const items = value.slice(0, 100).map(item => redactProgressJson(item, state, depth + 1));
194
+ if (value.length > items.length)
195
+ items.push(TRUNCATED_PROGRESS_VALUE);
196
+ return items;
197
+ }
198
+ if (value !== null && typeof value === 'object') {
199
+ const entries = Object.entries(value).slice(0, 100).map(([key, item]) => [
200
+ key,
201
+ isSensitiveProgressKey(key)
202
+ ? REDACTED_PROGRESS_VALUE
203
+ : redactProgressJson(item, state, depth + 1),
204
+ ]);
205
+ if (Object.keys(value).length > entries.length)
206
+ entries.push(['…', TRUNCATED_PROGRESS_VALUE]);
207
+ return Object.fromEntries(entries);
208
+ }
209
+ return typeof value === 'string' ? redactProgressText(value) : value;
210
+ }
211
+ function progressArgumentsPlaceholder(value) {
212
+ const characters = [...redactProgressText(value.slice(0, MAX_PROGRESS_JSON_INPUT_CHARS))];
213
+ let retained = Math.min(characters.length, MAX_PROGRESS_TOOL_PREVIEW_CHARS);
214
+ while (retained >= 0) {
215
+ const encoded = JSON.stringify({
216
+ truncated: true,
217
+ preview: characters.slice(0, retained).join(''),
218
+ });
219
+ if ([...encoded].length <= MAX_PROGRESS_TOOL_PREVIEW_CHARS)
220
+ return encoded;
221
+ retained = retained === 0 ? -1 : Math.floor(retained * 0.75);
222
+ }
223
+ return '{"truncated":true}';
224
+ }
225
+ function progressArgumentsPreview(value) {
226
+ if (value.trim() === '')
227
+ return '{}';
228
+ // Never publish raw malformed or oversized model output. Normal tool calls are valid JSON; when
229
+ // that invariant fails, a status marker is more useful than a best-effort secret scrubber.
230
+ if (value.length > MAX_PROGRESS_JSON_INPUT_CHARS)
231
+ return '{"truncated":true}';
232
+ try {
233
+ const encoded = JSON.stringify(redactProgressJson(JSON.parse(value), { nodes: 0 }), null, 2);
234
+ if (encoded === undefined)
235
+ return '{"invalidJson":true}';
236
+ return [...encoded].length <= MAX_PROGRESS_TOOL_PREVIEW_CHARS
237
+ ? encoded
238
+ : progressArgumentsPlaceholder(encoded);
239
+ }
240
+ catch {
241
+ return '{"invalidJson":true}';
242
+ }
243
+ }
244
+ function redactProgressResultText(value) {
245
+ if (value.length > MAX_PROGRESS_JSON_INPUT_CHARS)
246
+ return '{"truncated":true}';
247
+ const trimmed = value.trim();
248
+ if ((trimmed.startsWith('{') && trimmed.endsWith('}'))
249
+ || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
250
+ try {
251
+ const encoded = JSON.stringify(redactProgressJson(JSON.parse(trimmed), { nodes: 0 }), null, 2);
252
+ if (encoded !== undefined)
253
+ return encoded;
254
+ }
255
+ catch {
256
+ // Fall through to conservative line-oriented handling for JSON-like logs and fragments.
257
+ }
258
+ }
259
+ return redactProgressText(value);
260
+ }
261
+ function progressResultPreview(content, depth = 0) {
262
+ if (depth > 4)
263
+ return TRUNCATED_PROGRESS_VALUE;
264
+ const parts = [];
265
+ let approximateLength = 0;
266
+ const append = (value) => {
267
+ if (approximateLength >= MAX_PROGRESS_TOOL_PREVIEW_CHARS * 2 || value === '')
268
+ return;
269
+ const retained = value.slice(0, MAX_PROGRESS_TOOL_PREVIEW_CHARS * 2 - approximateLength);
270
+ parts.push(retained);
271
+ approximateLength += retained.length;
272
+ };
273
+ let blockCount = 0;
274
+ for (const block of content) {
275
+ if (approximateLength >= MAX_PROGRESS_TOOL_PREVIEW_CHARS * 2)
276
+ break;
277
+ if (blockCount >= MAX_PROGRESS_RESULT_BLOCKS) {
278
+ append(TRUNCATED_PROGRESS_VALUE);
279
+ break;
280
+ }
281
+ blockCount += 1;
282
+ if (block.type === 'text')
283
+ append(redactProgressResultText(block.text));
284
+ else if (block.type === 'image')
285
+ append('[图片]');
286
+ else if (block.type === 'tool-call') {
287
+ append(`${block.name}\n${progressArgumentsPreview(block.arguments)}`);
288
+ }
289
+ else if (block.type === 'tool-result') {
290
+ append(progressResultPreview(block.content, depth + 1));
291
+ }
292
+ }
293
+ return boundedProgressToolPreview(redactProgressText(parts.join('\n')));
294
+ }
295
+ /**
296
+ * Convert one durable session fact into a bounded, user-visible progress update.
297
+ * Reasoning/thinking content and provider error messages never cross this boundary. Tool calls/results
298
+ * contribute bounded previews with common credential shapes redacted; the channel still decides
299
+ * whether its current audience is private enough to render those previews.
103
300
  *
104
- * The `reasoning` block of a durable `assistant/message` is the assistant's own settled summary of
105
- * the step, not a streaming fragment, so it is the one reasoning form safe to surface. Only some
106
- * providers emit it at all: subscription CLIs declare an effort capability but still return text
107
- * only, and the ACP thought channel is not mapped to DSH reasoning yet. `step/start` therefore also
108
- * yields a neutral phase label, so a turn reports progress even on providers that never reason
109
- * out loud.
301
+ * A DSH `reasoning` block is explicitly thinking content and is not guaranteed to be a public
302
+ * summary, even after assembly into `assistant/message`. `step/start` therefore provides the only
303
+ * reasoning-independent phase label, so a turn reports progress without exposing hidden thought.
110
304
  */
111
305
  export function deliveryProgressFromSessionEvent(event) {
112
306
  if (event.type === 'step/start') {
@@ -117,31 +311,39 @@ export function deliveryProgressFromSessionEvent(event) {
117
311
  };
118
312
  }
119
313
  if (event.type === 'assistant/message') {
120
- const reasoning = event.data.message.content
121
- .filter(value => value.type === 'reasoning')
122
- .map(value => (value.type === 'reasoning' ? value.text.trim() : ''))
123
- .filter(text => text !== '')
124
- .join('\n');
125
- return reasoning === '' ? undefined : { kind: 'step', text: boundedProgressText(reasoning) };
314
+ return undefined;
126
315
  }
127
316
  if (event.type === 'tool/call') {
317
+ const argumentsPreview = progressArgumentsPreview(event.data.arguments);
128
318
  return {
129
319
  kind: 'tool-started',
130
320
  callId: String(event.data.callId),
131
321
  toolName: boundedProgressText(event.data.name),
322
+ ...(argumentsPreview === '' ? {} : { argumentsPreview }),
132
323
  };
133
324
  }
134
325
  if (event.type === 'tool/result') {
326
+ // Replacement copies belong only to the model-visible surface (for example after compaction).
327
+ // Human progress is an append-origin transcript, so projecting a replacement would make an
328
+ // old tool result look like a tool invocation from the active turn.
329
+ if (!isAppendSurfaceEvent(event))
330
+ return undefined;
135
331
  const block = event.data.message.content.find(value => value.type === 'tool-result');
136
332
  const callId = block?.type === 'tool-result'
137
333
  ? block.toolCallId
138
334
  : event.data.message.source?.callId;
139
335
  if (callId === undefined)
140
336
  return undefined;
337
+ const resultPreview = block?.type === 'tool-result'
338
+ ? progressResultPreview(block.content)
339
+ : '';
340
+ const code = event.data.error?.code;
141
341
  return {
142
342
  kind: 'tool-finished',
143
343
  callId: String(callId),
144
344
  failed: event.data.error !== undefined || (block?.type === 'tool-result' && block.isError === true),
345
+ ...(resultPreview === '' ? {} : { resultPreview }),
346
+ ...(code === undefined ? {} : { code: boundedProgressText(code) }),
145
347
  };
146
348
  }
147
349
  if (event.type === 'todo/write') {
@@ -412,6 +614,17 @@ function agentSelection(route) {
412
614
  : { reasoningEffort: ReasoningEffortId(route.reasoningEffort) }),
413
615
  };
414
616
  }
617
+ function errorHasCode(error, code) {
618
+ let current = error;
619
+ const visited = new Set();
620
+ while (typeof current === 'object' && current !== null && !visited.has(current)) {
621
+ visited.add(current);
622
+ if ('code' in current && current.code === code)
623
+ return true;
624
+ current = current instanceof Error ? current.cause : undefined;
625
+ }
626
+ return false;
627
+ }
415
628
  async function modelCatalog(llm, conversation, current, isDefault, eventId, expiresAt, signal) {
416
629
  const providers = llm.listProviders();
417
630
  const catalogs = await Promise.all(providers.map(async (provider) => {
@@ -810,6 +1023,44 @@ export class DshDeliveryRuntime {
810
1023
  installModelSelection(agentCtx, { current: selected, assembled: undefined });
811
1024
  await agentPresets?.mount(agentCtx, presetId);
812
1025
  }
1026
+ /**
1027
+ * DSH validates an explicit reasoning effort before calling an adapter. That is normally the
1028
+ * right contract, but a persisted external-channel selection can outlive a provider's live
1029
+ * model directory. Clear only that stale persisted effort before a prompt crosses the durable
1030
+ * dispatch boundary; TraeX still rechecks the newly created ACP session afterwards.
1031
+ */
1032
+ async resolveExecutionRoute(conversation, persisted, route, signal) {
1033
+ if (route.reasoningEffort === undefined)
1034
+ return { route };
1035
+ const llm = this.ctx.get('llm');
1036
+ if (llm === undefined)
1037
+ throw new Error('assistant-delivery: llm service is required');
1038
+ try {
1039
+ await llm.resolveCallConfig({
1040
+ provider: route.provider,
1041
+ model: route.model,
1042
+ reasoningEffort: ReasoningEffortId(route.reasoningEffort),
1043
+ }, signal);
1044
+ return { route };
1045
+ }
1046
+ catch (error) {
1047
+ if (signal.aborted || !errorHasCode(error, 'UNSUPPORTED_REASONING_EFFORT'))
1048
+ throw error;
1049
+ const fallback = { provider: route.provider, model: route.model };
1050
+ // Do not turn an invalid model/default into a silent recovery. This is the same core
1051
+ // preflight without the stale explicit effort, still before an Agent turn is dispatched.
1052
+ await llm.resolveCallConfig(fallback, signal);
1053
+ if (persisted === undefined) {
1054
+ this.ctx.logger.warn(`assistant-delivery: using provider default after unsupported reasoning effort for ${route.provider}/${route.model}`);
1055
+ return { route: fallback };
1056
+ }
1057
+ const recovered = this.options.clearStaleModelReasoningEffort(conversation, persisted);
1058
+ if (!recovered.applied)
1059
+ return { retry: true };
1060
+ this.ctx.logger.warn(`assistant-delivery: cleared stale reasoning effort for ${route.provider}/${route.model} before dispatch`);
1061
+ return { route: toModelRoute(recovered.selection) };
1062
+ }
1063
+ }
813
1064
  async reconcileNativeFullPermissionReviewer(session) {
814
1065
  const result = await this.policy.reconcileNativeFullReviewer(session);
815
1066
  if (result === 'unavailable') {
@@ -1707,9 +1958,15 @@ export class DshDeliveryRuntime {
1707
1958
  if (agents === undefined || sessions === undefined) {
1708
1959
  return { outcome: 'not-processed', failureCode: 'agent-runtime-unavailable', retryable: true };
1709
1960
  }
1710
- const selectedRoute = toModelRoute(prepared?.modelRoute
1711
- ?? this.options.getModelSelection(envelope.conversation)
1961
+ const persistedSelection = this.options.getModelSelection(envelope.conversation);
1962
+ const requestedRoute = toModelRoute(persistedSelection
1963
+ ?? prepared?.modelRoute
1712
1964
  ?? { provider: this.options.provider, model: this.options.model });
1965
+ const resolvedRoute = await this.resolveExecutionRoute(envelope.conversation, persistedSelection, requestedRoute, signal);
1966
+ if ('retry' in resolvedRoute) {
1967
+ return { outcome: 'not-processed', failureCode: 'model-selection-changed-before-dispatch', retryable: true };
1968
+ }
1969
+ const selectedRoute = resolvedRoute.route;
1713
1970
  const selected = agentSelection(selectedRoute);
1714
1971
  let handle;
1715
1972
  let dispatched = false;
@@ -1743,7 +2000,7 @@ export class DshDeliveryRuntime {
1743
2000
  const llm = this.ctx.get('llm');
1744
2001
  if (llm === undefined)
1745
2002
  throw new Error('assistant-delivery: llm service is required');
1746
- requireToolCapability(llm, selected.provider, selected.model, presetId, agentCtx.tools.schemas(agentCtx.agent).length, this.options.toolCapableProviders, this.options.unknownRouteToolCalls);
2003
+ requireAdapterToolCallProtocol(llm, selected.provider, selected.model, presetId, agentCtx.tools.schemas(agentCtx.agent).length);
1747
2004
  } });
1748
2005
  const agent = handle.agent;
1749
2006
  await this.reconcileNativeFullPermissionReviewer(agent.session);
@@ -1843,13 +2100,13 @@ export class DshDeliveryRuntime {
1843
2100
  return { outcome: 'not-processed', failureCode: 'image-capability-notice-failed', retryable: true };
1844
2101
  }
1845
2102
  }
1846
- if (error instanceof ToolCapabilityAdmissionError) {
2103
+ if (error instanceof AdapterToolCallProtocolError) {
1847
2104
  try {
1848
- this.options.replyCommand(binding, envelope.eventId, toolCapabilityReply(error));
2105
+ this.options.replyCommand(binding, envelope.eventId, adapterToolCallProtocolReply(error));
1849
2106
  return { outcome: 'processed' };
1850
2107
  }
1851
2108
  catch {
1852
- return { outcome: 'not-processed', failureCode: 'tool-capability-notice-failed', retryable: true };
2109
+ return { outcome: 'not-processed', failureCode: 'adapter-tool-protocol-notice-failed', retryable: true };
1853
2110
  }
1854
2111
  }
1855
2112
  this.ctx.logger.warn(`assistant-delivery: ${sessionResumeFailureCode(error)} for session ${sessionFingerprint(binding.sessionId)}`);