@sema-agent/client-core 0.41.0 → 0.42.0
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/CHANGELOG.md +238 -0
- package/README.md +1 -1
- package/dist/adapter/downstream/eventToSdkMessage.js +24 -0
- package/dist/attachmentsWireCaps.d.ts +43 -8
- package/dist/attachmentsWireCaps.js +64 -14
- package/dist/hitl/askGateWire.d.ts +4 -2
- package/dist/hitl/editedRuleTextPrecheck.d.ts +102 -0
- package/dist/hitl/editedRuleTextPrecheck.js +91 -0
- package/dist/hitl/hitlBridge.d.ts +17 -3
- package/dist/hitl/hitlBridge.js +20 -4
- package/dist/hitl/hitlHostSurface.d.ts +33 -3
- package/dist/hitl/hitlHostSurface.js +33 -3
- package/dist/hitl/parkResolver.js +15 -3
- package/dist/hitl/toolApprovalWire.d.ts +118 -2
- package/dist/hitl/toolApprovalWire.js +123 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -0
- package/dist/model/catalogLoader.js +151 -45
- package/dist/printToolResultFrame.d.ts +19 -0
- package/dist/seatContract.d.ts +25 -1
- package/dist/seatContract.js +29 -2
- package/docs/INTEGRATION-CLIENTS.md +123 -7
- package/package.json +2 -2
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* editedRuleTextPrecheck.ts — 编辑臂的「边打字边校验」判官**转出口**(#225 / [5076] 自领件,0.42.0)。
|
|
3
|
+
*
|
|
4
|
+
* ## 这一格要解决的问题
|
|
5
|
+
* 卡上的规则编辑框想在人还在打字的时候就说「这条能不能提交」。唯一合法的判官是引擎自己那只 ——
|
|
6
|
+
* core 5.57.0 导出的纯函数 `precheckEditedRuleText(text, command)`,它与 `confirmRuleApproval` 的
|
|
7
|
+
* fresh-edit 臂**跑同一个函数体**(`checkEditedRuleText`),所以预检面与真裁判**结构性不可分歧**。
|
|
8
|
+
* 在边界上复读一遍 `parseAllowRuleText` 是**装第二个更严的判官**:`Bash(adb *)` 这类肌肉记忆形
|
|
9
|
+
* 会被它当场误杀,而编辑面本来先跑拼写归一(`normalizeEditedSpelling`)再接受
|
|
10
|
+
* ——[5071] 定界②的原话,core [5075] 复核确认。
|
|
11
|
+
*
|
|
12
|
+
* ## 🔴 为什么是**端口注入**而不是 `export { precheckEditedRuleText } from '@sema-agent/core'`
|
|
13
|
+
* [5076] 的自领件原话是「re-export + 类型面」。**逐字照做在本包里是不可能的**,而且不是口味问题,
|
|
14
|
+
* 是本仓门族的硬约束(0.42.0 施工时实测,证据两条):
|
|
15
|
+
* · `scripts/run-client-core-portability-test.mjs` 的 `EXPECTED_PACKAGES_INDEX` 是**等值门**
|
|
16
|
+
* ——包总入口闭包的外部包集合恒等于 `{diff, @sema-agent/sdk}`,多一个当场红;
|
|
17
|
+
* · 同门 ③ 段拿 esbuild `--platform=browser` **真打一次包**。`@sema-agent/core` 的 barrel 值级
|
|
18
|
+
* 拉进 `node:crypto`(`engine/session/log-digest.js`)、`node:fs`/`node:path`
|
|
19
|
+
* (`core/skills-directory.js`)…… 浏览器腿当场打不成。
|
|
20
|
+
* ⇒ 一个 value 级 re-export 会把**整台引擎**焊进每一个装 client-core 的端(web/desktop 首当其冲),
|
|
21
|
+
* 换来一只纯函数。本包的存在理由正是「三端共用的**客户端**运行时」,这条边不能连。
|
|
22
|
+
*
|
|
23
|
+
* 于是转出口取**同等效力的第二形**:
|
|
24
|
+
* ① **类型面**逐形转出({@link EditedRuleTextPrecheck} / {@link EditedRuleTextPrechecker}),
|
|
25
|
+
* 三端从此对着同一份形写代码,谁都不用自己抄一份返回型;
|
|
26
|
+
* ② **注入口**({@link installEditedRuleTextPrechecker}):Node 宿主(TUI / desktop 主进程 /
|
|
27
|
+
* server 侧渲染)在启动时把 core 那只函数原样装进来 —— **原样装,不许包一层判断**;
|
|
28
|
+
* ③ **读口**({@link precheckEditedRuleText}):没装 ⇒ 返 `undefined`(**诚实缺席**),
|
|
29
|
+
* 绝不返一个编出来的 `{ok:true}`。缺席时呈现面的正解是退「提交后才知道」的往返形
|
|
30
|
+
* ——[5076] 施工要点③ 已经把这两段写成互不阻塞的两腿。
|
|
31
|
+
* 🔴 浏览器 lane(web)结构上装不了这只函数(引擎不在那一侧),它**本就该**留缺席走往返形;
|
|
32
|
+
* 本口的缺席语义因此不是「宿主忘了装」的同义词,别拿它去判部署形态。
|
|
33
|
+
*
|
|
34
|
+
* ## 🔴 两点语义预披露(core [5075],消费侧写文案前必读;[5076] 已落账为 #225 施工要点)
|
|
35
|
+
* · `ok` = **可提交**,不是「confirm 必成」。记录级闸(binding 回声 / 记录 owner 与 state /
|
|
36
|
+
* scope 继承 / 部署开关)**不在**预检覆盖面,由 `confirmRuleApproval` 对着记录重判。
|
|
37
|
+
* ⇒ UI 文案只许写「可提交」,**绝不**写「将被批准」。
|
|
38
|
+
* · `canonicalRule` 可能与输入**字节不同**(拼写归一)。它是真正会落库的那一形,所以内联反馈
|
|
39
|
+
* 应当显示 **canonical 形**,而不是把人的输入原样回显。
|
|
40
|
+
*
|
|
41
|
+
* ## 调用面契约(core 逐字,注入口不许替它软化)
|
|
42
|
+
* · `text` 是**人**的输入 ⇒ 任何畸形拼写都是一个**答案**(refusal),永不抛;
|
|
43
|
+
* · `command` 是**调用方**的上下文(卡所依据的被裁决命令)⇒ 缺席/非串是**调用方 bug**,
|
|
44
|
+
* core 响亮抛(`config.invalid_argument`)。本包的读口**不吞这个抛**(见 {@link precheckEditedRuleText}
|
|
45
|
+
* 的 `@throws`):把它折成一个 refusal 会让人读到「你的规则不接受空命令」——把调用方的错
|
|
46
|
+
* 误归到人头上,正是 core 那段头注点名要避免的事。
|
|
47
|
+
* · 卡上**压根没有被裁决命令**(老记录 / 店丢了这一列)⇒ 那是**记录级**拒绝,不该问本函数;
|
|
48
|
+
* 宿主此时的正解是**根本不给编辑框**。
|
|
49
|
+
*/
|
|
50
|
+
/**
|
|
51
|
+
* `text × command` 三步门(拼写归一 → 语法门 → coverage 闸)的判决 —— core 5.57.0
|
|
52
|
+
* `EditedRuleTextPrecheck` 的**逐形镜像**(结构等价,故 core 那只函数可直接装进
|
|
53
|
+
* {@link EditedRuleTextPrechecker} 而无需任何适配)。
|
|
54
|
+
*
|
|
55
|
+
* 🔴 `code` 的**在场规则**与提交面的 `edit_rejected.detail` 同源:验证器自己拒时**在场**,
|
|
56
|
+
* coverage 闸拒时**缺席** —— 同一具函数体出来的同一批值。缺席不许被读成「没有理由」。
|
|
57
|
+
* 🔴 `code` 是**开集串**:core 的 `RuleRejectCode` 词表属主是引擎,本包**刻意不镜像那张枚举**
|
|
58
|
+
* (镜像 = 引擎加员当天把一个合法值判没,#157 词表纪律的反面)。要分支就按串比,未知值原样呈现。
|
|
59
|
+
*/
|
|
60
|
+
export type EditedRuleTextPrecheck = {
|
|
61
|
+
ok: true;
|
|
62
|
+
/** 真正会落库的那一形(可能与输入字节不同 —— 拼写归一)。内联反馈显示**这一形**。 */
|
|
63
|
+
canonicalRule: string;
|
|
64
|
+
} | {
|
|
65
|
+
ok: false;
|
|
66
|
+
/** 验证器自己拒时在场;coverage 闸拒时缺席(开集串,见类型注)。 */
|
|
67
|
+
code?: string;
|
|
68
|
+
/** 拒绝措辞(与 `edit_rejected.detail` 同词汇)。原样呈现,包边界零加工。 */
|
|
69
|
+
message: string;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* 判官的形:`(text, command) => 判决`。**唯一合法的实参** = core 5.57.0 导出的
|
|
73
|
+
* `precheckEditedRuleText`,原样装,不许在外面包一层自己的判断
|
|
74
|
+
* ——包一层就是把「同一函数体」这条唯一的抗漂移保证亲手拆掉。
|
|
75
|
+
*/
|
|
76
|
+
export type EditedRuleTextPrechecker = (text: string, command: string) => EditedRuleTextPrecheck;
|
|
77
|
+
/**
|
|
78
|
+
* 装/卸判官。返回**还原函数**(装口族统一姿势:还原到装之前那一只,不是无脑清空 —— 嵌套装载
|
|
79
|
+
* 时无脑清空会把外层那只一起抹掉)。
|
|
80
|
+
*
|
|
81
|
+
* Node 宿主的装法(**逐字**,别加工):
|
|
82
|
+
* ```ts
|
|
83
|
+
* import { precheckEditedRuleText } from '@sema-agent/core'
|
|
84
|
+
* installEditedRuleTextPrechecker(precheckEditedRuleText)
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export declare function installEditedRuleTextPrechecker(fn: EditedRuleTextPrechecker | null): () => void;
|
|
88
|
+
/**
|
|
89
|
+
* 装没装(存在性读口哨兵:`boolean` 形 —— 见 `docs/INTEGRATION-CLIENTS.md` §5a 的三形分野)。
|
|
90
|
+
* 呈现面据它决定**给不给编辑框的内联反馈**;`false` 时编辑框本身仍然可用(走往返形)。
|
|
91
|
+
*/
|
|
92
|
+
export declare function hasEditedRuleTextPrechecker(): boolean;
|
|
93
|
+
/**
|
|
94
|
+
* 预检一段编辑过的规则文本。**没装判官 ⇒ 返 `undefined`**(诚实缺席,不是 `{ok:true}`
|
|
95
|
+
* 也不是 `{ok:false}` —— 两者都是在替一只不在场的判官发言)。
|
|
96
|
+
*
|
|
97
|
+
* @throws core 判官对**调用方 bug**(`command` 缺席/非串/不可渲染)的响亮拒**原样上抛**
|
|
98
|
+
* ——本包不吞它:吞掉会让一个调用方错误变成一句对人的判决(见模块头注「调用面契约」)。
|
|
99
|
+
*/
|
|
100
|
+
export declare function precheckEditedRuleText(text: string, command: string): EditedRuleTextPrecheck | undefined;
|
|
101
|
+
/** 测试钩:卸口。 */
|
|
102
|
+
export declare function _resetEditedRuleTextPrecheckerForTest(): void;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* editedRuleTextPrecheck.ts — 编辑臂的「边打字边校验」判官**转出口**(#225 / [5076] 自领件,0.42.0)。
|
|
3
|
+
*
|
|
4
|
+
* ## 这一格要解决的问题
|
|
5
|
+
* 卡上的规则编辑框想在人还在打字的时候就说「这条能不能提交」。唯一合法的判官是引擎自己那只 ——
|
|
6
|
+
* core 5.57.0 导出的纯函数 `precheckEditedRuleText(text, command)`,它与 `confirmRuleApproval` 的
|
|
7
|
+
* fresh-edit 臂**跑同一个函数体**(`checkEditedRuleText`),所以预检面与真裁判**结构性不可分歧**。
|
|
8
|
+
* 在边界上复读一遍 `parseAllowRuleText` 是**装第二个更严的判官**:`Bash(adb *)` 这类肌肉记忆形
|
|
9
|
+
* 会被它当场误杀,而编辑面本来先跑拼写归一(`normalizeEditedSpelling`)再接受
|
|
10
|
+
* ——[5071] 定界②的原话,core [5075] 复核确认。
|
|
11
|
+
*
|
|
12
|
+
* ## 🔴 为什么是**端口注入**而不是 `export { precheckEditedRuleText } from '@sema-agent/core'`
|
|
13
|
+
* [5076] 的自领件原话是「re-export + 类型面」。**逐字照做在本包里是不可能的**,而且不是口味问题,
|
|
14
|
+
* 是本仓门族的硬约束(0.42.0 施工时实测,证据两条):
|
|
15
|
+
* · `scripts/run-client-core-portability-test.mjs` 的 `EXPECTED_PACKAGES_INDEX` 是**等值门**
|
|
16
|
+
* ——包总入口闭包的外部包集合恒等于 `{diff, @sema-agent/sdk}`,多一个当场红;
|
|
17
|
+
* · 同门 ③ 段拿 esbuild `--platform=browser` **真打一次包**。`@sema-agent/core` 的 barrel 值级
|
|
18
|
+
* 拉进 `node:crypto`(`engine/session/log-digest.js`)、`node:fs`/`node:path`
|
|
19
|
+
* (`core/skills-directory.js`)…… 浏览器腿当场打不成。
|
|
20
|
+
* ⇒ 一个 value 级 re-export 会把**整台引擎**焊进每一个装 client-core 的端(web/desktop 首当其冲),
|
|
21
|
+
* 换来一只纯函数。本包的存在理由正是「三端共用的**客户端**运行时」,这条边不能连。
|
|
22
|
+
*
|
|
23
|
+
* 于是转出口取**同等效力的第二形**:
|
|
24
|
+
* ① **类型面**逐形转出({@link EditedRuleTextPrecheck} / {@link EditedRuleTextPrechecker}),
|
|
25
|
+
* 三端从此对着同一份形写代码,谁都不用自己抄一份返回型;
|
|
26
|
+
* ② **注入口**({@link installEditedRuleTextPrechecker}):Node 宿主(TUI / desktop 主进程 /
|
|
27
|
+
* server 侧渲染)在启动时把 core 那只函数原样装进来 —— **原样装,不许包一层判断**;
|
|
28
|
+
* ③ **读口**({@link precheckEditedRuleText}):没装 ⇒ 返 `undefined`(**诚实缺席**),
|
|
29
|
+
* 绝不返一个编出来的 `{ok:true}`。缺席时呈现面的正解是退「提交后才知道」的往返形
|
|
30
|
+
* ——[5076] 施工要点③ 已经把这两段写成互不阻塞的两腿。
|
|
31
|
+
* 🔴 浏览器 lane(web)结构上装不了这只函数(引擎不在那一侧),它**本就该**留缺席走往返形;
|
|
32
|
+
* 本口的缺席语义因此不是「宿主忘了装」的同义词,别拿它去判部署形态。
|
|
33
|
+
*
|
|
34
|
+
* ## 🔴 两点语义预披露(core [5075],消费侧写文案前必读;[5076] 已落账为 #225 施工要点)
|
|
35
|
+
* · `ok` = **可提交**,不是「confirm 必成」。记录级闸(binding 回声 / 记录 owner 与 state /
|
|
36
|
+
* scope 继承 / 部署开关)**不在**预检覆盖面,由 `confirmRuleApproval` 对着记录重判。
|
|
37
|
+
* ⇒ UI 文案只许写「可提交」,**绝不**写「将被批准」。
|
|
38
|
+
* · `canonicalRule` 可能与输入**字节不同**(拼写归一)。它是真正会落库的那一形,所以内联反馈
|
|
39
|
+
* 应当显示 **canonical 形**,而不是把人的输入原样回显。
|
|
40
|
+
*
|
|
41
|
+
* ## 调用面契约(core 逐字,注入口不许替它软化)
|
|
42
|
+
* · `text` 是**人**的输入 ⇒ 任何畸形拼写都是一个**答案**(refusal),永不抛;
|
|
43
|
+
* · `command` 是**调用方**的上下文(卡所依据的被裁决命令)⇒ 缺席/非串是**调用方 bug**,
|
|
44
|
+
* core 响亮抛(`config.invalid_argument`)。本包的读口**不吞这个抛**(见 {@link precheckEditedRuleText}
|
|
45
|
+
* 的 `@throws`):把它折成一个 refusal 会让人读到「你的规则不接受空命令」——把调用方的错
|
|
46
|
+
* 误归到人头上,正是 core 那段头注点名要避免的事。
|
|
47
|
+
* · 卡上**压根没有被裁决命令**(老记录 / 店丢了这一列)⇒ 那是**记录级**拒绝,不该问本函数;
|
|
48
|
+
* 宿主此时的正解是**根本不给编辑框**。
|
|
49
|
+
*/
|
|
50
|
+
let installed = null;
|
|
51
|
+
/**
|
|
52
|
+
* 装/卸判官。返回**还原函数**(装口族统一姿势:还原到装之前那一只,不是无脑清空 —— 嵌套装载
|
|
53
|
+
* 时无脑清空会把外层那只一起抹掉)。
|
|
54
|
+
*
|
|
55
|
+
* Node 宿主的装法(**逐字**,别加工):
|
|
56
|
+
* ```ts
|
|
57
|
+
* import { precheckEditedRuleText } from '@sema-agent/core'
|
|
58
|
+
* installEditedRuleTextPrechecker(precheckEditedRuleText)
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function installEditedRuleTextPrechecker(fn) {
|
|
62
|
+
const prev = installed;
|
|
63
|
+
installed = fn;
|
|
64
|
+
return () => {
|
|
65
|
+
installed = prev;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 装没装(存在性读口哨兵:`boolean` 形 —— 见 `docs/INTEGRATION-CLIENTS.md` §5a 的三形分野)。
|
|
70
|
+
* 呈现面据它决定**给不给编辑框的内联反馈**;`false` 时编辑框本身仍然可用(走往返形)。
|
|
71
|
+
*/
|
|
72
|
+
export function hasEditedRuleTextPrechecker() {
|
|
73
|
+
return installed !== null;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 预检一段编辑过的规则文本。**没装判官 ⇒ 返 `undefined`**(诚实缺席,不是 `{ok:true}`
|
|
77
|
+
* 也不是 `{ok:false}` —— 两者都是在替一只不在场的判官发言)。
|
|
78
|
+
*
|
|
79
|
+
* @throws core 判官对**调用方 bug**(`command` 缺席/非串/不可渲染)的响亮拒**原样上抛**
|
|
80
|
+
* ——本包不吞它:吞掉会让一个调用方错误变成一句对人的判决(见模块头注「调用面契约」)。
|
|
81
|
+
*/
|
|
82
|
+
export function precheckEditedRuleText(text, command) {
|
|
83
|
+
const fn = installed;
|
|
84
|
+
if (fn === null)
|
|
85
|
+
return undefined;
|
|
86
|
+
return fn(text, command);
|
|
87
|
+
}
|
|
88
|
+
/** 测试钩:卸口。 */
|
|
89
|
+
export function _resetEditedRuleTextPrecheckerForTest() {
|
|
90
|
+
installed = null;
|
|
91
|
+
}
|
|
@@ -308,9 +308,23 @@ export declare class HitlBridge {
|
|
|
308
308
|
*
|
|
309
309
|
* - `approve` → `{decision:"approve", boundCallId, boundInputHash}`; `updatedInput` rides along for
|
|
310
310
|
* approve-with-edit (applied AFTER the binding check — the hash still binds the ORIGINAL input).
|
|
311
|
-
* - `deny` → `{decision:"deny", reason}`.
|
|
312
|
-
*
|
|
313
|
-
*
|
|
311
|
+
* - `deny` → `{decision:"deny", reason}`.
|
|
312
|
+
*
|
|
313
|
+
* 🔴 **§2.4「DENY-abort」撤稿(0.42.0;server [4833] 明请,契约成文 `4631a0f` 随 7.39 出)**。
|
|
314
|
+
* 本段原文写的是:「CANCEL a suspended run by DENYING, never by `runs.cancel` (**which 409s on a
|
|
315
|
+
* suspended run** — contract/04 §2.4); the deny-and-abort `interrupt:true` EFFECT is *the run ends
|
|
316
|
+
* after the deny*」。**三句话里有两句已被上游证伪,逐条**:
|
|
317
|
+
* · **「deny 用来 cancel 一条 run」** —— `ASSISTANT-WIRE-CONTRACT.md` §4a 逐字反过来说:
|
|
318
|
+
* **DENY is a TOOL-level answer, NEVER a run kill**;客户端不得把用户的拒绝译成 cancel。
|
|
319
|
+
* 两个动词的 wire 判别式是 `cancelled`(真取消)vs `gate.batch_halted`(裸拒的兄弟结算)。
|
|
320
|
+
* · **「`runs.cancel` 对 suspended run 回 409」** —— 自 server [868] 起**就地取消**:
|
|
321
|
+
* `runs.js` 的 cancel 腿对 SUSPENDED/needs_review 先结算 pending checkpoint(CAS expire)再
|
|
322
|
+
* 终态化,`cancelSuspended` 有实体,409 只剩 `conflict.approval_settled` 一条
|
|
323
|
+
* (本仓 `docs/fresh-scan-client-core-2026-08-08.md` B型-5 已按真字节证伪,当时未跟修注释)。
|
|
324
|
+
* · **仍然成立的那一句**:deny 之后 run 是否结束由**后端编排**决定,不是 wire 上的旗标
|
|
325
|
+
* (裸拒 ⇒ `gate.batch_halted` + `haltedOnUserRejection`;带留言拒 ⇒ run 续跑)。
|
|
326
|
+
* ⇒ 本方法的**行为一字未改**(它本来走的就是 §4a 说的那条 TOOL 级 decide 通路);改的是这段
|
|
327
|
+
* 引用错权威、并把一条早已失效的 409 断言当理由的散文。
|
|
314
328
|
*
|
|
315
329
|
* The resumed run continues its SAME durable stream. A binding mismatch (409) is re-raised as a
|
|
316
330
|
* `HitlSafetyError('binding_mismatch')` — the caller re-presents, NEVER auto-retries.
|
package/dist/hitl/hitlBridge.js
CHANGED
|
@@ -415,9 +415,23 @@ export class HitlBridge {
|
|
|
415
415
|
*
|
|
416
416
|
* - `approve` → `{decision:"approve", boundCallId, boundInputHash}`; `updatedInput` rides along for
|
|
417
417
|
* approve-with-edit (applied AFTER the binding check — the hash still binds the ORIGINAL input).
|
|
418
|
-
* - `deny` → `{decision:"deny", reason}`.
|
|
419
|
-
*
|
|
420
|
-
*
|
|
418
|
+
* - `deny` → `{decision:"deny", reason}`.
|
|
419
|
+
*
|
|
420
|
+
* 🔴 **§2.4「DENY-abort」撤稿(0.42.0;server [4833] 明请,契约成文 `4631a0f` 随 7.39 出)**。
|
|
421
|
+
* 本段原文写的是:「CANCEL a suspended run by DENYING, never by `runs.cancel` (**which 409s on a
|
|
422
|
+
* suspended run** — contract/04 §2.4); the deny-and-abort `interrupt:true` EFFECT is *the run ends
|
|
423
|
+
* after the deny*」。**三句话里有两句已被上游证伪,逐条**:
|
|
424
|
+
* · **「deny 用来 cancel 一条 run」** —— `ASSISTANT-WIRE-CONTRACT.md` §4a 逐字反过来说:
|
|
425
|
+
* **DENY is a TOOL-level answer, NEVER a run kill**;客户端不得把用户的拒绝译成 cancel。
|
|
426
|
+
* 两个动词的 wire 判别式是 `cancelled`(真取消)vs `gate.batch_halted`(裸拒的兄弟结算)。
|
|
427
|
+
* · **「`runs.cancel` 对 suspended run 回 409」** —— 自 server [868] 起**就地取消**:
|
|
428
|
+
* `runs.js` 的 cancel 腿对 SUSPENDED/needs_review 先结算 pending checkpoint(CAS expire)再
|
|
429
|
+
* 终态化,`cancelSuspended` 有实体,409 只剩 `conflict.approval_settled` 一条
|
|
430
|
+
* (本仓 `docs/fresh-scan-client-core-2026-08-08.md` B型-5 已按真字节证伪,当时未跟修注释)。
|
|
431
|
+
* · **仍然成立的那一句**:deny 之后 run 是否结束由**后端编排**决定,不是 wire 上的旗标
|
|
432
|
+
* (裸拒 ⇒ `gate.batch_halted` + `haltedOnUserRejection`;带留言拒 ⇒ run 续跑)。
|
|
433
|
+
* ⇒ 本方法的**行为一字未改**(它本来走的就是 §4a 说的那条 TOOL 级 decide 通路);改的是这段
|
|
434
|
+
* 引用错权威、并把一条早已失效的 409 断言当理由的散文。
|
|
421
435
|
*
|
|
422
436
|
* The resumed run continues its SAME durable stream. A binding mismatch (409) is re-raised as a
|
|
423
437
|
* `HitlSafetyError('binding_mismatch')` — the caller re-presents, NEVER auto-retries.
|
|
@@ -621,7 +635,9 @@ export function makeHitlCanUseTool(bridge, prompt) {
|
|
|
621
635
|
const decision = forceDecision ??
|
|
622
636
|
(await prompt({ toolName: tool.name, input, toolUseID, gate }));
|
|
623
637
|
if (decision.behavior === 'deny') {
|
|
624
|
-
// Deny →
|
|
638
|
+
// Deny → 一次 **TOOL 级**的 deny 应答(server `ASSISTANT-WIRE-CONTRACT.md` §4a;0.42.0 撤稿:
|
|
639
|
+
// 原文写的是「cancel-by-deny (contract/04 §2.4)」,而 §4a 逐字反对把拒绝读成 run kill ——
|
|
640
|
+
// 详见 `decideTool` 头注的撤稿段)。The deny message rides `reason`.
|
|
625
641
|
// 0.28.0 发版扫描 F1(P2):message 逐字嵌原始命令(壳侧 bashPermissions 无上限)——必须与
|
|
626
642
|
// 卡腿同门经窄化器截到 4096,否则 server 413 丢的是整次 deny(run 留 suspended)。
|
|
627
643
|
await bridge.decideTool({ decision: 'deny', reason: denyReasonForWire(decision.message, `canUseTool ${toolUseID}`) ?? DEFAULT_DENY_REASON }, toolUseID);
|
|
@@ -31,9 +31,32 @@ export declare function _resetHitlHostSurfaceForTest(): void;
|
|
|
31
31
|
/** 内部读点(计 miss)。`askGateWire.ts` 的 `surfaceClassifierDeny` 路径与本文件的
|
|
32
32
|
* `surfaceCancelDenyWarn` 共用它。 */
|
|
33
33
|
export declare function surfaceForCurrentSession(): HitlHostSurface | null;
|
|
34
|
-
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
/**
|
|
35
|
+
* 中断-deny 的后台 settle 观察预算。
|
|
36
|
+
*
|
|
37
|
+
* ══ 🔴 §2.4「DENY-abort」撤稿 + 本常量的论证前提重审(0.42.0;server [4833] 明请)═══════════
|
|
38
|
+
*
|
|
39
|
+
* **本段 0.28.0 原文写的是**:「decide 是 SYNC 驱动的(引擎跑到下一 park/终态才返,实测 4-5s+),
|
|
40
|
+
* 但 **DENY-abort 语义上引擎收到即终结 run**;2s 内连收都没收到 ⇒ 按丢失警示」。
|
|
41
|
+
* 那句加粗的前提**已被上游撤稿**,来源是 server 自己的契约成文(`4631a0f`,随 7.39 出;
|
|
42
|
+
* `ASSISTANT-WIRE-CONTRACT.md` §4a):**DENY 是 TOOL 级的应答,永远不是 run kill**;客户端不得
|
|
43
|
+
* 把「拒绝」译成「取消」;两个动词的 wire 判别式是 `cancelled` vs `gate.batch_halted`。
|
|
44
|
+
* ⇒ 「引擎收到 deny 即终结 run」这条**不成立**:deny 只结算**这一只 ask**,run 按自己的编排继续
|
|
45
|
+
* (裸拒 ⇒ `gate.batch_halted` 兄弟 coded 结算 + `haltedOnUserRejection`;带留言拒 ⇒ run 续跑)。
|
|
46
|
+
*
|
|
47
|
+
* **重审结论(本批只改论证,不改数值 —— 理由写全)**:
|
|
48
|
+
* · 旧论证「2s 没结算 = deny 大概率丢了,因为收到就该终结」**作废**;
|
|
49
|
+
* · 但本常量守的那件事**换一个理由仍然成立**:它是一个**观察上限**,给「decide 永不返回」这种
|
|
50
|
+
* 形态一条出声的路 —— 没有它,一次真的丢失就只剩静默;
|
|
51
|
+
* · **数值不在本批动**:动它是行为面改动,而判据(多久算「没回来」)只有拿真实 decide 往返分布
|
|
52
|
+
* 说了算,那份实测在**消费端**(壳中断路径)而不在包里;且下游 `sema-cli` 的
|
|
53
|
+
* `src/sema/hitlCancelDeny.test.ts` 按现值锁着行为,单边改会当场把消费端打红。
|
|
54
|
+
* · **如实登记的残余**(接入档 §6e/§7 同批记):预算到点就发的那行 warn 措辞是
|
|
55
|
+
* {@link CANCEL_DENY_WARN_TEXT}(「the session may stay locked」),而在新契约下「decide 慢」
|
|
56
|
+
* 与「deny 丢了」这两件事在 2s 这个刻度上**不可分** —— 晚到的成功不会撤回那行 warn(只有 10s
|
|
57
|
+
* 自清)。要根治得做成两档(软档只记 debug、硬档才上屏),那是**跨仓一批**:包侧改时序、壳侧
|
|
58
|
+
* 同批换判据与用例。本批不做单边改动。
|
|
59
|
+
*/
|
|
37
60
|
export declare const CANCEL_DENY_BUDGET_MS = 2000;
|
|
38
61
|
/** warn 行文案(测试锁字面)。 */
|
|
39
62
|
export declare const CANCEL_DENY_WARN_TEXT = "could not cancel the pending question \u2014 the session may stay locked; the run may need engine-side recovery";
|
|
@@ -66,6 +89,13 @@ export declare function surfaceRememberNotApplied(): void;
|
|
|
66
89
|
export declare function surfaceEditNotForwarded(): void;
|
|
67
90
|
/**
|
|
68
91
|
* 中断 deny 的有界观察(壳侧单测 `hitlCancelDeny.test.ts` 的被测面;REF-CC-023 起两条决断腿共用)。
|
|
92
|
+
*
|
|
93
|
+
* 🔴 **命名撤稿(0.42.0)**:函数名与日志里的「cancel-by-deny」是**历史词**,保留只为不打断下游
|
|
94
|
+
* 按名锚的用例。它描述的动作在现行契约(server `ASSISTANT-WIRE-CONTRACT.md` §4a)下的准确说法是
|
|
95
|
+
* 「**中断时把这只挂着的 ask 用一个 TOOL 级 deny 结算掉**,好让 run 不停在 suspended 上」——
|
|
96
|
+
* **不是**「用 deny 去 cancel 一条 run」。方向也别读反:这里是把**用户的中断**结算成一次 deny,
|
|
97
|
+
* 而 §4a 禁的是反向的那件事(把**用户的拒绝**译成 cancel),两者不是同一件事。
|
|
98
|
+
*
|
|
69
99
|
* 铁律:不 await 进 abort 返回路径(用户立即拿回控制);这里只管后台 settle 的«观察»:
|
|
70
100
|
* - 2s 内 settle 成功 ⇒ 零上屏(SEMA_DEBUG 记成功);
|
|
71
101
|
* - 失败/超时 ⇒ 上屏一行 warn + SEMA_DEBUG 记原因(deny 丢失 = run 卡 suspended,下一条消息
|
|
@@ -61,9 +61,32 @@ export function surfaceForCurrentSession() {
|
|
|
61
61
|
return hostSurface;
|
|
62
62
|
}
|
|
63
63
|
// ── 件3(中断事故修复批 G,2026-07-15)—— 中断 deny 的有界观察 ─────────────────────────────────
|
|
64
|
-
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
64
|
+
/**
|
|
65
|
+
* 中断-deny 的后台 settle 观察预算。
|
|
66
|
+
*
|
|
67
|
+
* ══ 🔴 §2.4「DENY-abort」撤稿 + 本常量的论证前提重审(0.42.0;server [4833] 明请)═══════════
|
|
68
|
+
*
|
|
69
|
+
* **本段 0.28.0 原文写的是**:「decide 是 SYNC 驱动的(引擎跑到下一 park/终态才返,实测 4-5s+),
|
|
70
|
+
* 但 **DENY-abort 语义上引擎收到即终结 run**;2s 内连收都没收到 ⇒ 按丢失警示」。
|
|
71
|
+
* 那句加粗的前提**已被上游撤稿**,来源是 server 自己的契约成文(`4631a0f`,随 7.39 出;
|
|
72
|
+
* `ASSISTANT-WIRE-CONTRACT.md` §4a):**DENY 是 TOOL 级的应答,永远不是 run kill**;客户端不得
|
|
73
|
+
* 把「拒绝」译成「取消」;两个动词的 wire 判别式是 `cancelled` vs `gate.batch_halted`。
|
|
74
|
+
* ⇒ 「引擎收到 deny 即终结 run」这条**不成立**:deny 只结算**这一只 ask**,run 按自己的编排继续
|
|
75
|
+
* (裸拒 ⇒ `gate.batch_halted` 兄弟 coded 结算 + `haltedOnUserRejection`;带留言拒 ⇒ run 续跑)。
|
|
76
|
+
*
|
|
77
|
+
* **重审结论(本批只改论证,不改数值 —— 理由写全)**:
|
|
78
|
+
* · 旧论证「2s 没结算 = deny 大概率丢了,因为收到就该终结」**作废**;
|
|
79
|
+
* · 但本常量守的那件事**换一个理由仍然成立**:它是一个**观察上限**,给「decide 永不返回」这种
|
|
80
|
+
* 形态一条出声的路 —— 没有它,一次真的丢失就只剩静默;
|
|
81
|
+
* · **数值不在本批动**:动它是行为面改动,而判据(多久算「没回来」)只有拿真实 decide 往返分布
|
|
82
|
+
* 说了算,那份实测在**消费端**(壳中断路径)而不在包里;且下游 `sema-cli` 的
|
|
83
|
+
* `src/sema/hitlCancelDeny.test.ts` 按现值锁着行为,单边改会当场把消费端打红。
|
|
84
|
+
* · **如实登记的残余**(接入档 §6e/§7 同批记):预算到点就发的那行 warn 措辞是
|
|
85
|
+
* {@link CANCEL_DENY_WARN_TEXT}(「the session may stay locked」),而在新契约下「decide 慢」
|
|
86
|
+
* 与「deny 丢了」这两件事在 2s 这个刻度上**不可分** —— 晚到的成功不会撤回那行 warn(只有 10s
|
|
87
|
+
* 自清)。要根治得做成两档(软档只记 debug、硬档才上屏),那是**跨仓一批**:包侧改时序、壳侧
|
|
88
|
+
* 同批换判据与用例。本批不做单边改动。
|
|
89
|
+
*/
|
|
67
90
|
export const CANCEL_DENY_BUDGET_MS = 2000;
|
|
68
91
|
/** warn 行文案(测试锁字面)。 */
|
|
69
92
|
export const CANCEL_DENY_WARN_TEXT = 'could not cancel the pending question — the session may stay locked; the run may need engine-side recovery';
|
|
@@ -161,6 +184,13 @@ function isNoPendingError(e) {
|
|
|
161
184
|
}
|
|
162
185
|
/**
|
|
163
186
|
* 中断 deny 的有界观察(壳侧单测 `hitlCancelDeny.test.ts` 的被测面;REF-CC-023 起两条决断腿共用)。
|
|
187
|
+
*
|
|
188
|
+
* 🔴 **命名撤稿(0.42.0)**:函数名与日志里的「cancel-by-deny」是**历史词**,保留只为不打断下游
|
|
189
|
+
* 按名锚的用例。它描述的动作在现行契约(server `ASSISTANT-WIRE-CONTRACT.md` §4a)下的准确说法是
|
|
190
|
+
* 「**中断时把这只挂着的 ask 用一个 TOOL 级 deny 结算掉**,好让 run 不停在 suspended 上」——
|
|
191
|
+
* **不是**「用 deny 去 cancel 一条 run」。方向也别读反:这里是把**用户的中断**结算成一次 deny,
|
|
192
|
+
* 而 §4a 禁的是反向的那件事(把**用户的拒绝**译成 cancel),两者不是同一件事。
|
|
193
|
+
*
|
|
164
194
|
* 铁律:不 await 进 abort 返回路径(用户立即拿回控制);这里只管后台 settle 的«观察»:
|
|
165
195
|
* - 2s 内 settle 成功 ⇒ 零上屏(SEMA_DEBUG 记成功);
|
|
166
196
|
* - 失败/超时 ⇒ 上屏一行 warn + SEMA_DEBUG 记原因(deny 丢失 = run 卡 suspended,下一条消息
|
|
@@ -154,12 +154,24 @@ parkGatedCallId) {
|
|
|
154
154
|
});
|
|
155
155
|
const bridge = new HitlBridge(deps.client, taskId);
|
|
156
156
|
if (answer === null) {
|
|
157
|
-
// turn 被中断(Esc/Ctrl+C)
|
|
157
|
+
// turn 被中断(Esc/Ctrl+C):把这只挂着的 ask 用一次 **TOOL 级 deny** 结算掉,好让 run 不停在
|
|
158
|
+
// suspended 上(server `ASSISTANT-WIRE-CONTRACT.md` §4a)。
|
|
159
|
+
// 🔴 **0.42.0 撤稿**:原文写的是「cancel-by-deny(contract/04 §2.4 —— suspended run 不
|
|
160
|
+
// runs.cancel)」。那条引用的两个前提都已作废 —— §4a 逐字说 DENY 是 tool 级应答**永远不是**
|
|
161
|
+
// run kill;而「suspended run 不能 runs.cancel」自 server [868] 起就不成立(就地取消已实装,
|
|
162
|
+
// 本仓 fresh-scan B型-5 按真字节证伪)。这里选 deny 不是因为 cancel 不可用,是因为**这一刻
|
|
163
|
+
// 要处理的就是一只挂着的 ask**:先把它结算掉,run 才走得下去。下方那句「引擎侧解锁腿到货前」
|
|
164
|
+
// 同批订正:那条腿早就到货了,本臂保留的理由变成「结算 ask 是这一步的正解」,不再是权宜。
|
|
158
165
|
// 件3(中断事故修复批 G,2026-07-15,症状1 壳侧配套):此前 .catch(()=>{}) 全吞 = deny 丢失时
|
|
159
166
|
// run 永卡 suspended,session 锁死,用户下一条消息撞 409「active run」还全无线索。改为有界观察
|
|
160
167
|
// (observeCancelByDeny,2s 预算):abort 仍立即返回用户控制(不 await,交互时序不变),后台
|
|
161
|
-
// settle 失败/超时上屏一行 warn + SEMA_DEBUG
|
|
162
|
-
// suspended 改语义 +
|
|
168
|
+
// settle 失败/超时上屏一行 warn + SEMA_DEBUG 记失败原因。
|
|
169
|
+
// 🔴 **0.42.0 订正**:本段原文以「引擎侧解锁腿([866] server:cancel suspended 改语义 +
|
|
170
|
+
// reapSuspended TTL)**到货前**,这是壳能做的最诚实半场」收尾 —— 那条腿早已到货
|
|
171
|
+
// (server [868] 起 suspended run 就地取消 + `reapSuspended` 实体在,fresh-scan B型-5 已按
|
|
172
|
+
// 真字节证伪)。本臂**不是**在等一条不存在的上游腿:它保留的理由是「这一刻要处理的就是一只
|
|
173
|
+
// 挂着的 ask,先结算它 run 才走得下去」。观察器的预算论证前提见 `CANCEL_DENY_BUDGET_MS` 头注
|
|
174
|
+
// 的重审段(同批 §2.4 撤稿件)。
|
|
163
175
|
observeCancelByDeny(bridge.decideTool({ decision: 'deny', reason: 'Interrupted by user' }, gatedCallId, undefined, pending), taskId);
|
|
164
176
|
return { kind: 'aborted', gatedCallId };
|
|
165
177
|
}
|
|
@@ -130,8 +130,27 @@ export interface ApprovalCardAllowDecision {
|
|
|
130
130
|
allowSession: boolean;
|
|
131
131
|
updatedInput?: unknown;
|
|
132
132
|
/** #225 件1:卡上选中的持久规则候选**原文**({@link ApprovalCardRequest.ruleSuggestions} 之一);
|
|
133
|
-
* 缺席/空串 = 本次不兑付。表外文本会在编排层被丢键留痕(server 亦拒 rule_not_offered)。
|
|
133
|
+
* 缺席/空串 = 本次不兑付。表外文本会在编排层被丢键留痕(server 亦拒 rule_not_offered)。
|
|
134
|
+
* ⚠️ 0.42.0 起本位有一个**兄弟位** {@link persistRuleEdited} —— 带上它就是「这段文本是人手改的
|
|
135
|
+
* 自由文本」,此时本位**不再**要求是候选之一(见该位注)。本位自身的类型与字节一字未动。 */
|
|
134
136
|
persistRule?: string;
|
|
137
|
+
/**
|
|
138
|
+
* #225 编辑臂(0.42.0;server #340 `respondFreeFormRules`,[5071] wire 形):{@link persistRule}
|
|
139
|
+
* 里那段文本是**人在卡上手打/改过的自由文本**,不是帧候选表里的原文。
|
|
140
|
+
*
|
|
141
|
+
* 🔴 **缺席 ≠ false**:缺席 = 既有候选臂(表内核对照旧、语义与 0.41.0 逐字节相同);
|
|
142
|
+
* `true` = 编辑臂。刻意只收 `true` 一个值(机读位是二值的,「在场但不是 true」没有语义)。
|
|
143
|
+
* 🔴 **它不是放行凭据**,是**出身声明**:带上它只会让编排层放弃「必须是候选之一」那道表核
|
|
144
|
+
* (见 {@link surfaceToolApprovalFrameAndRespond} 的兑付段),真正的判官仍在引擎侧
|
|
145
|
+
* (core `confirmRuleApproval` 同函数体)。客户端**绝不**在这里替引擎预判文本合不合法 ——
|
|
146
|
+
* 在边界复读解析器就是装第二个更严的判官,`Bash(adb *)` 这类肌肉记忆形会被当场误杀
|
|
147
|
+
* ([5071] 定界②,core [5075] 复核确认)。要「边打字边校验」请用
|
|
148
|
+
* {@link import('./editedRuleTextPrecheck.js').precheckEditedRuleText} 的注入口 —— 那是**引擎
|
|
149
|
+
* 自己那只**判官,不是第二份。
|
|
150
|
+
* 🔴 **能力位在场才发**:`true` 而 {@link ToolApprovalFrameLaneOpts.respondFreeFormRulesCapable}
|
|
151
|
+
* 未确认 ⇒ 编排层**整条丢 persistRule**(决断照送),见该位注。
|
|
152
|
+
*/
|
|
153
|
+
persistRuleEdited?: true;
|
|
135
154
|
}
|
|
136
155
|
/**
|
|
137
156
|
* deny 决断臂(命名形,typeshape B4 口径;0.28.0 因 `reason` 位抽名,与 0.26.0
|
|
@@ -598,8 +617,22 @@ export type ToolApprovalRespondDecision = 'allow' | 'allow_session' | 'deny';
|
|
|
598
617
|
export interface RespondToolApprovalOpts {
|
|
599
618
|
signal?: AbortSignal;
|
|
600
619
|
updatedInput?: unknown;
|
|
601
|
-
/** #225 件1:兑付键 —— 帧候选之一的**原文**(编排层已做表内核对与 deny 剥除)。
|
|
620
|
+
/** #225 件1:兑付键 —— 帧候选之一的**原文**(编排层已做表内核对与 deny 剥除)。
|
|
621
|
+
* ⚠️ 带 {@link persistRuleEdited} 时表核已让位,本位是人手改的自由文本(类型不变)。 */
|
|
602
622
|
persistRule?: string;
|
|
623
|
+
/**
|
|
624
|
+
* #225 编辑臂(0.42.0;server #340,[5071]):{@link persistRule} 是**自由文本**而非候选原文。
|
|
625
|
+
*
|
|
626
|
+
* 🔴 **注入面的映射义务**(本包只到这一格,wire 体由宿主/SDK 铸):server 的 respond 体形是
|
|
627
|
+
* `persistRule: { rule, edited: true }`。本包刻意保持**扁平兄弟位**(`persistRule: string`
|
|
628
|
+
* 字节不变 + 一个可选布尔),由注入面把两位合成那个嵌套形:
|
|
629
|
+
* `persistRule !== undefined ? { rule: persistRule, ...(persistRuleEdited === true ? { edited: true } : {}) } : undefined`
|
|
630
|
+
* 改成嵌套形会是既有 `persistRule: string` 消费者的 BREAKING,而这一批的纲领是 additive。
|
|
631
|
+
* 🔴 **缺席 = 候选臂**(server 侧 `rule_not_offered` 语义一字不变);老 server 收到带 `edited`
|
|
632
|
+
* 的体会诚实降级为 `rule_not_offered`,这正是 [5071] 选这个键名而不是顶层新键的理由
|
|
633
|
+
* (顶层新键在老 server 上是静默 200 什么都不落,坏于诚实降级)。
|
|
634
|
+
*/
|
|
635
|
+
persistRuleEdited?: true;
|
|
603
636
|
/** #229(server ≥7.15.0):回决备注 —— 与 durable 腿 `AskDecisionBody.note` **同词同源同一列**
|
|
604
637
|
* (`decision_note`,≤2048)。任何 decision 都可带(deny 的「为什么拒」正是审计面上最值钱的
|
|
605
638
|
* 一条);真落行与否看 ack 的 {@link surfaceToolApprovalFrameAndRespond} 消费的 `noteRecorded`。
|
|
@@ -629,7 +662,79 @@ export interface ToolApprovalFrameOutcome {
|
|
|
629
662
|
decision: ToolApprovalRespondDecision | 'unresolved';
|
|
630
663
|
/** server 的 200 ack。**缺席 = 未知**(注入面回 void / 旧 server / respond 失败),绝不当成 false。 */
|
|
631
664
|
ack?: ToolApprovalRespondAck;
|
|
665
|
+
/**
|
|
666
|
+
* #225 件5(0.42.0):respond **抛错**那一支的原文交还位。
|
|
667
|
+
*
|
|
668
|
+
* 🔴 修的是一条真断链:此前本函数的 catch 只写一行 `debug` 然后返 `{decision:'unresolved'}`,
|
|
669
|
+
* 于是 server 的响亮 400(`persistRule.rule` 空/超长、`edited` 非布尔、顶层 `scope`、
|
|
670
|
+
* `edit-rejected` 的拒句 …… [5071] G4/G5/G6)在**包边界上被吞掉** —— 宿主的错误反馈面
|
|
671
|
+
* 无论怎么写都拿不到那句话,人在卡上改了规则被拒,屏上什么都不会说。
|
|
672
|
+
* 🔴 **不改 `decision` 的语义**:`'unresolved'` 仍是 `'unresolved'`(respond 没落定 = 引擎按
|
|
673
|
+
* TTL/abort 自决,这一位的含义一字未动);本位是**附加**的诊断面,不是新的决断态。
|
|
674
|
+
* 🔴 **缺席 = 没有拒绝原文可交**(respond 成功 / 抛的东西上**三位皆读不出**),绝不造一句。
|
|
675
|
+
* 在场时**三位都可能缺席其二** —— 有 `status` 没文本(应答体为空)、有文本没 `status`
|
|
676
|
+
* (传输层失败)都是真实形。
|
|
677
|
+
*/
|
|
678
|
+
respondRefusal?: ToolApprovalRespondRefusal;
|
|
632
679
|
}
|
|
680
|
+
/**
|
|
681
|
+
* #225 件5:respond 抛错的**结构化原文**(不是新的决断态,见
|
|
682
|
+
* {@link ToolApprovalFrameOutcome.respondRefusal})。
|
|
683
|
+
*
|
|
684
|
+
* 三位**各自独立**防御读、各自缺席不铸:`status` 只认有限数、`errorCode` 只认非空串、
|
|
685
|
+
* `message` 只认真读得出来的文本(取值序与 try 保护见 {@link readToolApprovalRespondRefusal})。
|
|
686
|
+
* 🔴 **三位皆缺席时整只不铸**(异源复审 [medium] 采纳,0.42.0):此前 `message` 是必填、读不出时
|
|
687
|
+
* 无条件退 `String(err)`,于是 `throw {}` 会得到一句 `"[object Object]"` —— 那不是 server 说的话,
|
|
688
|
+
* 是本层编的([honest-absence-not-fabricated-zero])。
|
|
689
|
+
* 🔴 **原样交还,零加工**:不 trim、不截断、不改写、不按识别表过滤。server 的拒句是给人看的
|
|
690
|
+
* 指路文本([5071] 三条 400 拒句各自成文),包边界任何一次改写都会让宿主呈的不再是引擎说的那句;
|
|
691
|
+
* 长度夹取与呈现归端(与 `message`/`approver` 同族口径)。
|
|
692
|
+
* 🔴 **UNTRUSTED-for-display**:它是 HTTP 应答体上的文本,只渲染,绝不回喂模型/工具入参。
|
|
693
|
+
*/
|
|
694
|
+
interface ToolApprovalRespondRefusalFields {
|
|
695
|
+
/** HTTP 状态(在场 = 引擎真应答了;缺席 = 传输层失败/注入面自抛,不许反推成 0)。 */
|
|
696
|
+
status?: number;
|
|
697
|
+
/** 机读码(开集,原样;缺席 = 应答没带码)。 */
|
|
698
|
+
errorCode?: string;
|
|
699
|
+
/**
|
|
700
|
+
* 拒句原文(server 的响亮拒文案)。
|
|
701
|
+
* 🔴 **缺席 = 这个抛出物上读不出任何可用文本**(异源复审 [medium] 采纳,0.42.0)——
|
|
702
|
+
* 此前本位是必填、读不出时无条件铸 `String(err)`,于是 `throw {}` 会得到一句
|
|
703
|
+
* `"[object Object]"`、`throw null` 得到 `"null"`:那**不是** server 说的话,是本层编的,
|
|
704
|
+
* 与本形头注承诺的「读不出原文就缺席」直接冲突([honest-absence-not-fabricated-zero])。
|
|
705
|
+
*/
|
|
706
|
+
message?: string;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* 🔴 **「在场即至少有一位」写进类型**(异源复审 [medium] 采纳,0.42.0):三位全 optional 的
|
|
710
|
+
* interface 在类型上允许 `{}`,而实现明确承诺「三位皆缺席时整只返 `undefined`」——
|
|
711
|
+
* 消费端于是既不能依赖「对象在场 = 至少有一条诊断信息」,也没法穷举安全渲染。
|
|
712
|
+
* 用**三选一联合**把那条运行期不变量抬到编译期:`{}` 从此不可赋值。
|
|
713
|
+
*/
|
|
714
|
+
export type ToolApprovalRespondRefusal = (ToolApprovalRespondRefusalFields & {
|
|
715
|
+
status: number;
|
|
716
|
+
}) | (ToolApprovalRespondRefusalFields & {
|
|
717
|
+
errorCode: string;
|
|
718
|
+
}) | (ToolApprovalRespondRefusalFields & {
|
|
719
|
+
message: string;
|
|
720
|
+
});
|
|
721
|
+
/**
|
|
722
|
+
* 从 respond 抛出来的东西上读 {@link ToolApprovalRespondRefusal}。**永不抛、永不造**。
|
|
723
|
+
*
|
|
724
|
+
* 键位口径与 {@link import('../wireErrorTriage.js').classifyTurnWireError} 同源([2055] 死键
|
|
725
|
+
* 纪律:只认活键 `errorCode`,退役的 `code` 槽不做兼容)。
|
|
726
|
+
*
|
|
727
|
+
* @returns 三位**全缺席**时返回 `undefined` —— 一个三位皆空的 refusal 对象是「有拒句」的假象。
|
|
728
|
+
*
|
|
729
|
+
* 🔴 **文本取值序**(异源复审 [medium] 修):`err.message` 非空串 > 抛出物本身是非空串 >
|
|
730
|
+
* 受保护的 `String(err)`,且**只接受**真有内容的结果 —— `[object Object]` / `null` /
|
|
731
|
+
* `undefined` 三种占位串一律当作「读不出」。
|
|
732
|
+
* 🔴 **`String(err)` 用 try 包住**:抛出物可以自带 `Symbol.toPrimitive` / `toString` 钩子并在里面
|
|
733
|
+
* 抛错。本函数的唯一调用点在 `surfaceToolApprovalFrameAndRespond` 的 catch 里,那里的契约是
|
|
734
|
+
* 「respond 失败 ⇒ 返回 unresolved」;让一个不可信的转换钩子把这条收敛路径变成 reject,
|
|
735
|
+
* 等于给注入面开了一个「让整次审批消费抛出去」的口。
|
|
736
|
+
*/
|
|
737
|
+
export declare function readToolApprovalRespondRefusal(err: unknown): ToolApprovalRespondRefusal | undefined;
|
|
633
738
|
/** 结构性识别流上的 tool_approval 帧(named SSE frame,payload.type === 帧名)。
|
|
634
739
|
* ⚠️ 口径更正(2026-08-08):此处原写「非 AgentEvent arm」—— SDK #185a 起这两个帧**是**
|
|
635
740
|
* `AgentEvent` 的臂了(durable 腿也回放),所以结构识别与 union 收窄两条路都成立;本函数仍按
|
|
@@ -644,6 +749,16 @@ export interface ToolApprovalFrameLaneOpts {
|
|
|
644
749
|
* 缺席/false ⇒ 不发(fail-closed 到「不发」侧;决断本身照常送达,现状字节不变)。
|
|
645
750
|
*/
|
|
646
751
|
approvalDecisionNoteCapable?: boolean;
|
|
752
|
+
/**
|
|
753
|
+
* server 能力位 `capabilities.respondFreeFormRules` 的读数(宿主从自己的 caps 缓存供给;
|
|
754
|
+
* server ≥7.44,#340 [5071] 恒真版本位)。
|
|
755
|
+
* 🔴 形与判据**逐字照** {@link approvalDecisionNoteCapable} 既有形(同一条 SDK 6.16 成文纪律:
|
|
756
|
+
* **位缺席就别发**)—— 老 server 对未知请求键静默忽略且照回 200,「这台不认识自由文本臂」与
|
|
757
|
+
* 「记上了」在响应上不可分,发了只造「规则已存」的错觉。
|
|
758
|
+
* 缺席/false ⇒ 编辑臂的 `persistRule` **整条不发**(fail-closed 到「不发」侧;决断本身照常送达,
|
|
759
|
+
* 现状字节不变,老引擎零受迫)。候选臂**不受本位影响**(它是 0.25.0 起就有的既有通道)。
|
|
760
|
+
*/
|
|
761
|
+
respondFreeFormRulesCapable?: boolean;
|
|
647
762
|
/**
|
|
648
763
|
* 这条帧是不是**当下**从 live 流上收到的(异源对抗复审 P1 采纳;壳孪生帧族同名闸的
|
|
649
764
|
* 本包席位 —— 壳 `approvalStreamWire` 头注逐字:「账本重放的历史帧带的是铸帧时刻的旧余量,
|
|
@@ -656,3 +771,4 @@ export interface ToolApprovalFrameLaneOpts {
|
|
|
656
771
|
windowIsCurrent?: boolean;
|
|
657
772
|
}
|
|
658
773
|
export declare function surfaceToolApprovalFrameAndRespond(frame: ToolApprovalFrame, respond: RespondToolApprovalFn, streamArgs: unknown | undefined, signal?: AbortSignal, lane?: ToolApprovalFrameLaneOpts): Promise<ToolApprovalFrameOutcome>;
|
|
774
|
+
export {};
|