@sema-agent/client-core 0.27.0 → 0.29.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/README.md +4 -3
- package/dist/adapter/activeRunSelfHeal.d.ts +191 -0
- package/dist/adapter/activeRunSelfHeal.js +287 -0
- package/dist/adapter/runStream.d.ts +23 -6
- package/dist/adapter/runStream.js +6 -2
- package/dist/hitl/armedGateRegistry.d.ts +19 -0
- package/dist/hitl/armedGateRegistry.js +92 -0
- package/dist/hitl/frameRouter.d.ts +5 -1
- package/dist/hitl/frameRouter.js +1 -1
- package/dist/hitl/gateIdentity.d.ts +42 -0
- package/dist/hitl/gateIdentity.js +56 -0
- package/dist/hitl/hitlBridge.d.ts +12 -0
- package/dist/hitl/hitlBridge.js +43 -3
- package/dist/hitl/parkOwnership.d.ts +65 -0
- package/dist/hitl/parkOwnership.js +49 -0
- package/dist/hitl/parkResolver.js +3 -1
- package/dist/hitl/planReviewWire.d.ts +51 -0
- package/dist/hitl/planReviewWire.js +172 -17
- package/dist/hitl/toolApprovalWire.d.ts +92 -13
- package/dist/hitl/toolApprovalWire.js +82 -5
- package/dist/index.d.ts +4 -0
- package/dist/index.js +20 -0
- package/dist/liveQuestionStore.d.ts +11 -0
- package/dist/liveQuestionStore.js +13 -0
- package/package.json +3 -3
|
@@ -36,11 +36,14 @@
|
|
|
36
36
|
* 挂账(诚实边界):approve 后壳本地的 plan-mode footer 徽章不自动退(CC 会退)——退 mode 的
|
|
37
37
|
* app-state seam 不在本模块可达面,记 rc.47 接 REPL 层;engine 侧 handsReadOnly 已由 resume 处理。
|
|
38
38
|
*/
|
|
39
|
-
import { publishQuestionFrame, registerLocalQuestionResponder, hasLocalQuestionResponder, } from '../liveQuestionStore.js';
|
|
39
|
+
import { publishQuestionFrame, publishQuestionFrameFor, registerLocalQuestionResponder, unregisterLocalQuestionResponder, hasLocalQuestionResponder, hasQuestionOverlayFor, } from '../liveQuestionStore.js';
|
|
40
40
|
import { hostLog } from '../host.js';
|
|
41
41
|
import { makeEngineWireClient } from '../engineWireSdk.js';
|
|
42
42
|
import { engineWireTarget } from '../engineWireTarget.js';
|
|
43
43
|
import { enqueuePlanReviewOutcome } from '../notifications.js';
|
|
44
|
+
import { planReviewQuestionId, REOPEN_ID_TAIL } from './gateIdentity.js';
|
|
45
|
+
import { clearArmedGate, clearArmedGateFor, registerArmedGateFor, wasGateArmedFor } from './armedGateRegistry.js';
|
|
46
|
+
import { DEFAULT_SESSION_KEY } from '../sessionSlot.js';
|
|
44
47
|
/** resume 是 SYNC 驱动到终态的(可能分钟级)——原 raw fetch 无超时;SDK Transport 恒有每请求
|
|
45
48
|
* 超时,取 6h 宽上界(诚实预算:超过它基本是引擎僵死,超时文案与网络失败同走 outcome 通知)。 */
|
|
46
49
|
const PLAN_REVIEW_RESUME_TIMEOUT_MS = 6 * 60 * 60_000;
|
|
@@ -92,26 +95,32 @@ const armedPlanReviewTaskIds = new Set();
|
|
|
92
95
|
export function _resetArmedPlanReviewsForTest() {
|
|
93
96
|
armedPlanReviewTaskIds.clear();
|
|
94
97
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
/* 合成 questionId 的唯一铸口:A-028.3 起收编进 `gateIdentity.planReviewQuestionId`(公面导出,
|
|
99
|
+
* 壳侧重开腿从此 import 同一铸口,不再手抄字面)。本文件只 import,不再自铸。 */
|
|
100
|
+
/** 首次 arm 的题面正句。 */
|
|
101
|
+
const ARM_QUESTION = 'The plan is ready for review. Approve it and start the implementation?';
|
|
102
|
+
/** 重开腿的题面正句(点明「这张卡就是占住会话的那件事」;标签/选项与首呈同源)。 */
|
|
103
|
+
const REOPEN_QUESTION = 'This plan is still waiting for your review — it is what is holding this session. Approve it and start the implementation?';
|
|
104
|
+
/** 题面唯一构造点 —— 首次 arm、重放重呈、重开三处共用,标签/选项描述不许各写一份。 */
|
|
105
|
+
function planReviewQuestions(question) {
|
|
106
|
+
return [
|
|
107
|
+
{
|
|
108
|
+
header: 'Plan review',
|
|
109
|
+
question,
|
|
110
|
+
options: [
|
|
111
|
+
{ label: APPROVE_LABEL, description: 'Resume the task now and execute the plan (runs to completion engine-side)' },
|
|
112
|
+
{ label: REJECT_LABEL, description: 'Discard this plan and stay in plan mode to refine it' },
|
|
113
|
+
],
|
|
114
|
+
multiSelect: false,
|
|
115
|
+
},
|
|
116
|
+
];
|
|
98
117
|
}
|
|
99
|
-
/**
|
|
118
|
+
/** 审批卡的发布口 —— 首次 arm 与「重放 arm 重新弹卡」共用同一份(canonical 身份)。 */
|
|
100
119
|
function publishPlanReviewCard(taskId) {
|
|
101
120
|
publishQuestionFrame({
|
|
102
121
|
type: 'question',
|
|
103
122
|
questionId: planReviewQuestionId(taskId),
|
|
104
|
-
questions:
|
|
105
|
-
{
|
|
106
|
-
header: 'Plan review',
|
|
107
|
-
question: 'The plan is ready for review. Approve it and start the implementation?',
|
|
108
|
-
options: [
|
|
109
|
-
{ label: APPROVE_LABEL, description: 'Resume the task now and execute the plan (runs to completion engine-side)' },
|
|
110
|
-
{ label: REJECT_LABEL, description: 'Discard this plan and stay in plan mode to refine it' },
|
|
111
|
-
],
|
|
112
|
-
multiSelect: false,
|
|
113
|
-
},
|
|
114
|
-
],
|
|
123
|
+
questions: planReviewQuestions(ARM_QUESTION),
|
|
115
124
|
});
|
|
116
125
|
}
|
|
117
126
|
/**
|
|
@@ -128,6 +137,11 @@ export function armPlanReviewApproval(result) {
|
|
|
128
137
|
if (!engineWireTarget())
|
|
129
138
|
return false;
|
|
130
139
|
const { taskId } = result;
|
|
140
|
+
// 单活纪律(复审二轮,与 reopen 侧对偶):arm/重呈要发的 canonical 卡从此是这个 gate 唯一的
|
|
141
|
+
// 作答入口 —— 名下还在场的重开卡先退役(注销 responder + 撤帧)。不退役的话,durable 重放的
|
|
142
|
+
// arm 会在 fresh 重开卡还开着时再立一张 canonical 卡,双活双投递面从 arm 侧复发。
|
|
143
|
+
// (arm 臂是默认会话装配,退役也按默认键;多会话宿主的 arm 路径归后续工单。)
|
|
144
|
+
retireActiveReopen(DEFAULT_SESSION_KEY, planReviewQuestionId(taskId));
|
|
131
145
|
if (armedPlanReviewTaskIds.has(taskId)) {
|
|
132
146
|
// REF-CC-027:同一张卡还没消解就被重放的 arm 撞上——幂等,**不二次注册** responder。
|
|
133
147
|
//
|
|
@@ -165,9 +179,15 @@ export function armPlanReviewApproval(result) {
|
|
|
165
179
|
const decided = planReviewDecisionFromAnswer(answer);
|
|
166
180
|
if (decided === 'dismissed') {
|
|
167
181
|
// dismiss/无法判读:诚实缺席优先于编造默认值(REF-CC-026)——不静默驱动 decidePlanReview,
|
|
168
|
-
//
|
|
182
|
+
// 卡的重开路径把决定权还给用户。呈现台账的键**不清**:同一张卡再被重开就是真「reopened」。
|
|
169
183
|
return { ok: true };
|
|
170
184
|
}
|
|
185
|
+
// A-024.4(键粒度收口):plan 键 = `plan-review:<taskId>`,粒度是 taskId 不是 gate ——
|
|
186
|
+
// 决断一经递交,这个门的呈现史就消费掉;同 run 的**下一个** plan gate(approve 推进后
|
|
187
|
+
// 引擎可再 park 一个新 plan)必须读回首见,否则文案对一张从未呈现过的新卡说「reopened」。
|
|
188
|
+
// 递交后决定未生效的形(RB-471 族)读回首见只损失「reopened」一词 —— 首见文案零历史断言,
|
|
189
|
+
// 诚实方向安全。
|
|
190
|
+
clearArmedGate(planReviewQuestionId(taskId));
|
|
171
191
|
// fire-and-forget:resume 同步驱动到终态可能分钟级,不能挂住 overlay;结果经 queue 通知回来
|
|
172
192
|
void decidePlanReview(taskId, decided);
|
|
173
193
|
return { ok: true };
|
|
@@ -287,3 +307,138 @@ export async function decidePlanReview(taskId, decision) {
|
|
|
287
307
|
hostLog('debug', `planReviewWire: outcome enqueue threw: ${String(e)}`);
|
|
288
308
|
}
|
|
289
309
|
}
|
|
310
|
+
/** 重开身份的进程内唯一尾缀(时基 + 随机段:两份包实例同时铸也不撞键 —— 撞键会被宿主 overlay
|
|
311
|
+
* 的去重集吃掉帧,重开静默变 no-op;所以刻意不用模块级计数器)。 */
|
|
312
|
+
function reopenIdSuffix() {
|
|
313
|
+
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* 每个 `(sessionKey, canonicalId)` **至多一个在场的重开 responder**(复审 [high]:此前每次重开
|
|
317
|
+
* 都新注册一个 responder 而不退役上一个 —— 重试/并发 409 会留下两张都能投递决断的活卡,
|
|
318
|
+
* 一张 approve 一张 reject 就是两次相互冲突的状态变更请求)。铸新卡前:退役上一张的 responder +
|
|
319
|
+
* 对它的帧 id 发 `question_complete` 撤卡;作答/退役后清账。
|
|
320
|
+
* 🔴 模块级单例(singleton-manifest 登记):两份实例 ⇒ 各退各的,跨份的旧卡退役不掉 ——
|
|
321
|
+
* 退化回修复前的重复活卡形。
|
|
322
|
+
*/
|
|
323
|
+
const activeReopenResponders = new Map();
|
|
324
|
+
/** 退役 `(sessionKey, canonicalId)` 名下还在场的重开 responder(撤帧 + 注销 + 清账)。 */
|
|
325
|
+
function retireActiveReopen(sessionKey, canonicalId) {
|
|
326
|
+
const key = `${sessionKey}\u0000${canonicalId}`;
|
|
327
|
+
const prev = activeReopenResponders.get(key);
|
|
328
|
+
if (!prev)
|
|
329
|
+
return;
|
|
330
|
+
activeReopenResponders.delete(key);
|
|
331
|
+
try {
|
|
332
|
+
prev.unregister();
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
// 注销钩自身的意外 throw 不许挡住新卡铸造(退役面 fail-soft,新卡才是用户的把手)
|
|
336
|
+
}
|
|
337
|
+
publishQuestionFrameFor(sessionKey, { type: 'question_complete', questionId: prev.questionId });
|
|
338
|
+
}
|
|
339
|
+
/** 测试钩:清空重开 responder 台账(跨用例状态)。 */
|
|
340
|
+
export function _resetActiveReopenRespondersForTest() {
|
|
341
|
+
activeReopenResponders.clear();
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* 重开某个 parked plan_review 的审批卡(409 自愈分诊树 `attemptActiveRunSelfHeal` 的
|
|
345
|
+
* `reopenPlanReview` 注入口的包内生产实现)。判决形/成文语义见 {@link ReopenCardVerdict}
|
|
346
|
+
* (`adapter/activeRunSelfHeal.ts`);本函数不产 `presented` 位 —— 包看不到像素,回执机制归
|
|
347
|
+
* 宿主端包装。绝不抛(turn 收尾路径)。
|
|
348
|
+
*
|
|
349
|
+
* 与 arm 臂的合成关系(A-028.4):首呈(done 帧)= `armPlanReviewApproval`;重开(409 撞锁)=
|
|
350
|
+
* 本函数。两臂共用同一份题面构造点/标签单源/决断三态判决与同一条 `decidePlanReview` 投递管道;
|
|
351
|
+
* 「铸新 questionId 还是复用同 id」由宿主去重语义作参数(见 {@link ReopenPlanReviewOpts})。
|
|
352
|
+
* firstSight 判据 = 呈现台账(`armedGateRegistry`,键 = canonical `plan-review:<taskId>`);
|
|
353
|
+
* 先查后记 —— 顺序决定判决正确性。
|
|
354
|
+
*/
|
|
355
|
+
export function reopenPlanReviewCard(taskId, opts) {
|
|
356
|
+
try {
|
|
357
|
+
if (typeof taskId !== 'string' || taskId.length === 0)
|
|
358
|
+
return { reopened: false };
|
|
359
|
+
const sessionKey = opts?.sessionKey ?? DEFAULT_SESSION_KEY;
|
|
360
|
+
// 诚实边界:请求方会话没有 overlay 订阅者(headless / 尚未挂载)⇒ 返回 false,调用方如实说
|
|
361
|
+
// 「没能重开」。overlay 在场答的是「有没有一个 UI 会收到这一帧」;检查按 sessionKey 走 ——
|
|
362
|
+
// 别的会话挂着 overlay 不等于本会话的卡有地方渲(复审 [high]:默认键硬走会把帧投进别人的面)。
|
|
363
|
+
if (!hasQuestionOverlayFor(sessionKey))
|
|
364
|
+
return { reopened: false };
|
|
365
|
+
const canonicalId = planReviewQuestionId(taskId);
|
|
366
|
+
// 先查(firstSight)后记(铸卡即 arm)—— 顺序决定判决正确性;台账按 sessionKey 分账。
|
|
367
|
+
const firstSight = !wasGateArmedFor(sessionKey, canonicalId);
|
|
368
|
+
const mintFresh = opts?.mintFreshQuestionId !== false;
|
|
369
|
+
if (!mintFresh && hasLocalQuestionResponder(canonicalId)) {
|
|
370
|
+
// 重呈短路(与 arm 臂的重放短路同形):同一个 canonical 身份、同一份题面、同一个 responder。
|
|
371
|
+
// 卡还开着的宿主只是收到一帧同形重绘,卡已被 dismiss 的宿主拿回重开路径。
|
|
372
|
+
// 0.29.0 发包扫描门:本臂沿用 arm responder ⇒ opts.deliverDecision 不生效(JSDoc 成文例外),
|
|
373
|
+
// 传了就留痕 —— 静默吞注入口是契约不一致的形。
|
|
374
|
+
if (opts?.deliverDecision !== undefined) {
|
|
375
|
+
hostLog('debug', `planReviewWire: reopen canonical-reuse branch for task ${taskId} ignores opts.deliverDecision (answers route through the existing arm responder — documented exception)`);
|
|
376
|
+
}
|
|
377
|
+
// 先退役本会话名下还在场的**重开**卡(如此前走过 mintFresh 臂)—— 同一 gate 不留两张活卡。
|
|
378
|
+
retireActiveReopen(sessionKey, canonicalId);
|
|
379
|
+
publishQuestionFrameFor(sessionKey, {
|
|
380
|
+
type: 'question',
|
|
381
|
+
questionId: canonicalId,
|
|
382
|
+
questions: planReviewQuestions(ARM_QUESTION),
|
|
383
|
+
});
|
|
384
|
+
registerArmedGateFor(sessionKey, canonicalId);
|
|
385
|
+
return { reopened: true, firstSight };
|
|
386
|
+
}
|
|
387
|
+
// 复审 [high] 单活纪律(两轮收紧):铸新卡前退役**全部**旧作答入口 —— 同一个 gate 至多一张
|
|
388
|
+
// 能投递决断的卡,两张都活着时一张 approve 一张 reject = 两次相互冲突的状态变更请求。
|
|
389
|
+
// · 上一张重开卡:注销 responder + question_complete 撤帧(单活台账);
|
|
390
|
+
// · canonical(arm)卡:question_complete 撤帧**且按 id 注销 responder**(二轮 [high]:
|
|
391
|
+
// 撤帧只是 UI 动作,表里的 responder 不死,在途的迟到作答仍能经它投递)。canonical
|
|
392
|
+
// responder 死后,arm 重放走它自己的「陈旧武装态 ⇒ 整条重新 arm」自愈臂([2393] hitl-F3),
|
|
393
|
+
// 不会留下弹不出卡的死短路。
|
|
394
|
+
retireActiveReopen(sessionKey, canonicalId);
|
|
395
|
+
if (unregisterLocalQuestionResponder(canonicalId)) {
|
|
396
|
+
hostLog('debug', `planReviewWire: reopen retired the canonical responder for task ${taskId} (single-active discipline)`);
|
|
397
|
+
}
|
|
398
|
+
publishQuestionFrameFor(sessionKey, { type: 'question_complete', questionId: canonicalId });
|
|
399
|
+
const questionId = `${canonicalId}${REOPEN_ID_TAIL}${reopenIdSuffix()}`;
|
|
400
|
+
const deliver = opts?.deliverDecision ??
|
|
401
|
+
((tid, decision) => {
|
|
402
|
+
// fire-and-forget:resume 同步驱动到终态可能分钟级,不能挂住 overlay(arm 臂同姿势);
|
|
403
|
+
// 失败处置/结果回植由 decidePlanReview 自带的 outcome 管道承担。
|
|
404
|
+
void decidePlanReview(tid, decision);
|
|
405
|
+
});
|
|
406
|
+
const activeKey = `${sessionKey}\u0000${canonicalId}`;
|
|
407
|
+
const unregister = registerLocalQuestionResponder(questionId, async (_id, answer) => {
|
|
408
|
+
unregister(); // 一次性(arm 臂同款):respond 侧 delete-before-invoke 之外的第二道钉
|
|
409
|
+
// 单活台账清账(只清**自己**的条目 —— 自己被更新的重开取代后,旧 responder 的迟到作答
|
|
410
|
+
// 不许把新卡的条目清掉)。
|
|
411
|
+
if (activeReopenResponders.get(activeKey)?.questionId === questionId) {
|
|
412
|
+
activeReopenResponders.delete(activeKey);
|
|
413
|
+
}
|
|
414
|
+
const decided = planReviewDecisionFromAnswer(answer);
|
|
415
|
+
if (decided === 'dismissed') {
|
|
416
|
+
// 三态(REF-CC-026):dismiss/读不出不驱动 decidePlanReview;呈现台账不清 ——
|
|
417
|
+
// 这张卡再被重开就是真「reopened」。
|
|
418
|
+
return { ok: true };
|
|
419
|
+
}
|
|
420
|
+
// A-024.4:决断递交即消费呈现史(与 arm 臂 responder 同点,理由见那里)。
|
|
421
|
+
clearArmedGateFor(sessionKey, canonicalId);
|
|
422
|
+
try {
|
|
423
|
+
await deliver(taskId, decided);
|
|
424
|
+
}
|
|
425
|
+
catch (e) {
|
|
426
|
+
// 投递编排是注入方的职责面(缺省臂自带 outcome 管道不会走到这);这里只留痕,
|
|
427
|
+
// 绝不把宿主投递臂的 throw 冒进 overlay 的 respond 链。
|
|
428
|
+
hostLog('error', `planReviewWire: reopen decision delivery threw for task ${taskId}: ${String(e)}`);
|
|
429
|
+
}
|
|
430
|
+
return { ok: true };
|
|
431
|
+
});
|
|
432
|
+
activeReopenResponders.set(activeKey, { questionId, unregister });
|
|
433
|
+
registerArmedGateFor(sessionKey, canonicalId);
|
|
434
|
+
publishQuestionFrameFor(sessionKey, {
|
|
435
|
+
type: 'question',
|
|
436
|
+
questionId,
|
|
437
|
+
questions: planReviewQuestions(REOPEN_QUESTION),
|
|
438
|
+
});
|
|
439
|
+
return { reopened: true, firstSight };
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
return { reopened: false }; // 重开失败绝不炸 turn —— 调用方会如实说「没能重开」
|
|
443
|
+
}
|
|
444
|
+
}
|
|
@@ -60,6 +60,22 @@
|
|
|
60
60
|
* 🔴 fail-soft 铁律(与 ask 桥同款):leader 队列未注册(print/非 REPL)、pending 蒸发、decide 409/404
|
|
61
61
|
* 都返回 failed —— 调用方回退「flush + 原样终帧」的诚实红,绝不更糟。
|
|
62
62
|
* 🔴 UNTRUSTED:gate args 为模型作文(service 已 redact),只渲染绝不回喂;决断只带 decision 枚举。
|
|
63
|
+
*
|
|
64
|
+
* ── #144 半场(client-core 0.28.0)────────────────────────────────────────────────────────────
|
|
65
|
+
* ① **透传** `persistedRuleShadowed`(server ≥7.13.0,来源锚 = engine fixture
|
|
66
|
+
* `@sema-agent/server/dist/tool-approval.d.ts` 同名键;ADDITIVE、`tool_approval` only)——
|
|
67
|
+
* 帧 → {@link ApprovalCardRequest.persistedRuleShadowed},壳据此渲「你的规则仍在,只是这次被
|
|
68
|
+
* 要求逐次确认」。🔴 它是 **display 值,不是身份通道**:脱敏后的展示文本,不保证与规则店里那条
|
|
69
|
+
* 规则逐字节相同、更不是它的 id;下游「删除/撤销规则」面拿它当身份用 = 删错或删不掉。
|
|
70
|
+
* ② **deny 归因通道**({@link ApprovalCardDenyDecision.reason}):两腿不同形 ——
|
|
71
|
+
* durable `/decide` **有** reason 位 ⇒ 原文上 wire(包内按 server 4096 上限截断);
|
|
72
|
+
* live 帧腿 `/v1/tool-approvals/:id/respond`:✅ **#229 已兑现**(0.29.0 respond-note 批,
|
|
73
|
+
* server ≥7.15.0)—— 端点收 **`note`**(不是 reason;与 durable `AskDecisionBody.note`
|
|
74
|
+
* 同词同源同一列 `decision_note`),deny reason 经 {@link RespondToolApprovalOpts.note} 上 wire。
|
|
75
|
+
* 发送前置两道门:能力位 `approvalDecisionNote`(SDK 6.16 成文「位缺席就别发」)+ 2048 上限
|
|
76
|
+
* (server 超限响亮 400 且**连决断一起拒**,所以超限 ⇒ 整条不发 + 留痕,诚实缺席优先于
|
|
77
|
+
* 静默截断);真落行与否看 ack 的 `noteRecorded`(缺席 ≠ false)。
|
|
78
|
+
* 🔴 方向纪律:reason/note 只做归因,绝不参与裁决;缺席 ⇒ 现状字节不变。
|
|
63
79
|
*/
|
|
64
80
|
import { type HitlClientLike } from './hitlBridge.js';
|
|
65
81
|
import type { RuleSuggestion, ToolApprovalRespondAck } from '@sema-agent/sdk';
|
|
@@ -111,9 +127,29 @@ export interface ApprovalCardAllowDecision {
|
|
|
111
127
|
* 缺席/空串 = 本次不兑付。表外文本会在编排层被丢键留痕(server 亦拒 rule_not_offered)。 */
|
|
112
128
|
persistRule?: string;
|
|
113
129
|
}
|
|
114
|
-
|
|
130
|
+
/**
|
|
131
|
+
* deny 决断臂(命名形,typeshape B4 口径;0.28.0 因 `reason` 位抽名,与 0.26.0
|
|
132
|
+
* {@link ApprovalCardAllowDecision} 抽名同姿势)。
|
|
133
|
+
*/
|
|
134
|
+
export interface ApprovalCardDenyDecision {
|
|
115
135
|
kind: 'deny';
|
|
116
|
-
|
|
136
|
+
/**
|
|
137
|
+
* #144 半场(0.28.0):这次拒绝的**归因原文**——人手打的拒绝说明,或壳侧权限规则自动 deny 的
|
|
138
|
+
* 机器判据(rule-deny / dontAsk / headless fail-closed 三条路)。缺席/空白 = 不带归因,调用方
|
|
139
|
+
* 落回既有缺省拒因,**现状字节不变**。
|
|
140
|
+
*
|
|
141
|
+
* 🔴 **方向纪律**:reason 只进归因通道,**绝不参与裁决** —— 带不带它、内容是什么,都不改变
|
|
142
|
+
* 这次决断仍是 deny(两条腿上的 `decision` 位逐字节不动)。
|
|
143
|
+
* 🔴 **两腿不同形**(见 {@link surfaceFsApprovalAndDecide} / {@link surfaceToolApprovalFrameAndRespond}):
|
|
144
|
+
* · durable park 腿(`/decide`)wire **有** reason 位 ⇒ 原文上 wire(包内按 server 上限截断);
|
|
145
|
+
* · live 帧腿(`/v1/tool-approvals/:id/respond`)wire 是三词闭集、**无** reason 位 ⇒ 只留痕不发,
|
|
146
|
+
* 绝不自造侧信道(候 server 给 respond 补位)。
|
|
147
|
+
* 与 `{kind:'failed'}` 的 `reason` **不同义**:那一位是「卡面本身不可用」的诊断,不是人的拒因,
|
|
148
|
+
* 两者绝不互相顶替。
|
|
149
|
+
*/
|
|
150
|
+
reason?: string;
|
|
151
|
+
}
|
|
152
|
+
export type ApprovalCardDecision = ApprovalCardAllowDecision | ApprovalCardDenyDecision | {
|
|
117
153
|
kind: 'aborted';
|
|
118
154
|
} | {
|
|
119
155
|
kind: 'failed';
|
|
@@ -150,6 +186,19 @@ export interface ApprovalCardRequest {
|
|
|
150
186
|
* 出身裁剪(MANDATED ask 不提供本档)候 core 5.25.0 出身键 wire 过境,见 #144/[3438]/[3442]。
|
|
151
187
|
*/
|
|
152
188
|
ruleSuggestions?: RuleSuggestion[];
|
|
189
|
+
/**
|
|
190
|
+
* 被越级的持久 allow 规则**原文**(#144,原样来自 {@link ToolApprovalFrame.persistedRuleShadowed}
|
|
191
|
+
* 的合形值)——壳据此渲「你的规则仍在,只是这次调用被要求逐次确认」;缺席 = 卡形与 0.27.0
|
|
192
|
+
* 字节不变(不渲该行)。
|
|
193
|
+
*
|
|
194
|
+
* 🔴 **display 值,不是身份通道**:它是一段**已过脱敏**的展示文本(core `inlineUntrusted` +
|
|
195
|
+
* server `redactSecrets`),不保证与规则店里存着的那条规则逐字节相同,更不是规则的 id/句柄。
|
|
196
|
+
* 下游的「删除/撤销这条规则」面**不许**把它当身份用(拿它去 match 规则店 = 删错规则或删不掉),
|
|
197
|
+
* 也不许把它回喂给模型/工具入参;要操作规则必须走规则店自己的枚举与标识。
|
|
198
|
+
* 🔴 **缺席 ≠「你没有规则」**:键只在「有规则命中 ∧ 规则清不掉这只 ask」时在场,绝大多数 ask
|
|
199
|
+
* 压根没有规则命中(缺席),而命中且清掉了的那些根本不会变成卡。
|
|
200
|
+
*/
|
|
201
|
+
persistedRuleShadowed?: string;
|
|
153
202
|
}
|
|
154
203
|
/**
|
|
155
204
|
* 🔴 **拆缝口** —— 弹「三选卡」并等人的决断。壳 = vendored CC `PermissionRequest`;
|
|
@@ -239,6 +288,30 @@ export interface ToolApprovalFrame {
|
|
|
239
288
|
* (`respond` 体 `persistRule.rule`),报表外文本 = server 拒 `rule_not_offered`。
|
|
240
289
|
*/
|
|
241
290
|
ruleSuggestions?: RuleSuggestion[];
|
|
291
|
+
/**
|
|
292
|
+
* server ≥7.13.0(#144 / core 5.25.0,[3438] 接力契约 / [3443] 主件;**ADDITIVE**,
|
|
293
|
+
* `"tool_approval"` only。来源锚 = engine fixture `@sema-agent/server/dist/tool-approval.d.ts`
|
|
294
|
+
* 的同名键)——这只 ask **命中了**调用方的一条持久 allow 规则,而那条规则**没能清掉它**。
|
|
295
|
+
* 值 = 被越级的那条规则**原文**(core `AskRequest.persistedRuleShadowed`,core 侧已过
|
|
296
|
+
* `inlineUntrusted`;server 侧与 `message`/`sourceAgentName` 同待遇 `redactSecrets` 后上帧)。
|
|
297
|
+
*
|
|
298
|
+
* 🔴 它存在的理由是一次真实的用户面回归:core 5.25.0 收窄消音边界后(「Allow rules silence the
|
|
299
|
+
* CLASSIFIER's questions, never a MANDATED one」),此前被规则消掉的 ask 重新出现 —— 没有这个键,
|
|
300
|
+
* 人看到的是「我明明点过『不再询问』,它怎么又问」,唯一合理的结论是「我的规则坏了/没存上」。
|
|
301
|
+
*
|
|
302
|
+
* 🔴 **缺席 ≠「你没有规则」**:键只在「有规则命中 ∧ 规则清不掉这只 ask」时在场。绝大多数 ask
|
|
303
|
+
* 压根没有规则命中(缺席),而**命中且清掉了**的那些根本不会变成 ask(被消音了,没有卡)。
|
|
304
|
+
* 🔴 与 {@link governanceForced} **刻意分列不合并**:那个键答「门是谁下的」(运维治理层),本键
|
|
305
|
+
* 答「你那条规则怎么了」。不可消音的来源有三类(governance / doctrine `always` / 工具自带 mark),
|
|
306
|
+
* 合并会让后两类要么谎报治理出身、要么丢掉规则解释。
|
|
307
|
+
* 🔴 **UNTRUSTED-for-display,且不是身份通道** —— 详见
|
|
308
|
+
* {@link ApprovalCardRequest.persistedRuleShadowed} 的同名警示(下游删除规则面不许拿它当规则身份)。
|
|
309
|
+
*
|
|
310
|
+
* 耐久路(park 行)的对偶是 `gate.riskDescriptor.shadowedRule`(server 对 riskDescriptor 整体透传);
|
|
311
|
+
* 本包的 durable 腿今天不 stamp 任何展示键(见 {@link surfaceFsApprovalAndDecide} 的卡入参),
|
|
312
|
+
* 那一路的消费是独立的一件,不在本键的施工面内。
|
|
313
|
+
*/
|
|
314
|
+
persistedRuleShadowed?: string;
|
|
242
315
|
}
|
|
243
316
|
/** {@link ToolApprovalFrame.delegation} 的形(命名形,不用内联匿名 —— typeshape 门 B4 棘轮口径)。 */
|
|
244
317
|
export interface ToolApprovalDelegation {
|
|
@@ -254,7 +327,7 @@ export interface ToolApprovalDelegation {
|
|
|
254
327
|
* `TOOL_APPROVAL_FRAME_KEYS` 比对——SDK additive 增键时对账当天红,不再人肉追平。
|
|
255
328
|
* 下面两个类型钉保证镜像与 interface 本身不可能漂移(少键/多键都是编译错)。
|
|
256
329
|
*/
|
|
257
|
-
export declare const TOOL_APPROVAL_FRAME_KEYS_MIRROR: readonly ["type", "approvalId", "toolCallId", "toolName", "sourceTaskId", "fromSubagent", "sourceAgentName", "message", "args", "argsOmitted", "governanceForced", "ruleSuggestions", "delegation", "outcome"];
|
|
330
|
+
export declare const TOOL_APPROVAL_FRAME_KEYS_MIRROR: readonly ["type", "approvalId", "toolCallId", "toolName", "sourceTaskId", "fromSubagent", "sourceAgentName", "message", "args", "argsOmitted", "governanceForced", "ruleSuggestions", "persistedRuleShadowed", "delegation", "outcome"];
|
|
258
331
|
/** 子代帧判别:显式键 fromSubagent(core 1.378 RB-39②)优先;缺席退 sourceTaskId 在场性权宜式
|
|
259
332
|
* (server 1.258 [1549]①3,旧代际兼容)。 */
|
|
260
333
|
export declare function isFromSubagent(frame: ToolApprovalFrame): boolean;
|
|
@@ -280,6 +353,11 @@ export interface RespondToolApprovalOpts {
|
|
|
280
353
|
updatedInput?: unknown;
|
|
281
354
|
/** #225 件1:兑付键 —— 帧候选之一的**原文**(编排层已做表内核对与 deny 剥除)。 */
|
|
282
355
|
persistRule?: string;
|
|
356
|
+
/** #229(server ≥7.15.0):回决备注 —— 与 durable 腿 `AskDecisionBody.note` **同词同源同一列**
|
|
357
|
+
* (`decision_note`,≤2048)。任何 decision 都可带(deny 的「为什么拒」正是审计面上最值钱的
|
|
358
|
+
* 一条);真落行与否看 ack 的 {@link surfaceToolApprovalFrameAndRespond} 消费的 `noteRecorded`。
|
|
359
|
+
* 🔴 编排层(本文件)只在能力位确认 + 上限内才填本位 —— 注入面原样透传,别自己再判。 */
|
|
360
|
+
note?: string;
|
|
283
361
|
}
|
|
284
362
|
export type RespondToolApprovalFn = (approvalId: string, decision: ToolApprovalRespondDecision, opts?: RespondToolApprovalOpts) => Promise<ToolApprovalRespondAck | void>;
|
|
285
363
|
/**
|
|
@@ -310,13 +388,14 @@ export interface ToolApprovalFrameOutcome {
|
|
|
310
388
|
* `AgentEvent` 的臂了(durable 腿也回放),所以结构识别与 union 收窄两条路都成立;本函数仍按
|
|
311
389
|
* 结构读(不依赖类型收窄),因为它同时服务 raw SSE 与 durable 回放两条入口。 */
|
|
312
390
|
export declare function isToolApprovalFrame(ev: unknown): ev is ToolApprovalFrame;
|
|
313
|
-
/**
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
391
|
+
/** 帧腿的宿主车道参数(#229 respond-note 批,0.29.0)。 */
|
|
392
|
+
export interface ToolApprovalFrameLaneOpts {
|
|
393
|
+
/**
|
|
394
|
+
* server 能力位 `capabilities.approvalDecisionNote` 的读数(宿主从自己的 caps 缓存供给)。
|
|
395
|
+
* 🔴 SDK 6.16 成文:**位缺席就别发 `note`** —— 老 server 对未知请求键静默忽略且照回 200,
|
|
396
|
+
* 「这台不认识 note」与「记上了」在响应上不可分,发了只造「已留档」的错觉。
|
|
397
|
+
* 缺席/false ⇒ 不发(fail-closed 到「不发」侧;决断本身照常送达,现状字节不变)。
|
|
398
|
+
*/
|
|
399
|
+
approvalDecisionNoteCapable?: boolean;
|
|
400
|
+
}
|
|
401
|
+
export declare function surfaceToolApprovalFrameAndRespond(frame: ToolApprovalFrame, respond: RespondToolApprovalFn, streamArgs: unknown | undefined, signal?: AbortSignal, lane?: ToolApprovalFrameLaneOpts): Promise<ToolApprovalFrameOutcome>;
|
|
@@ -60,12 +60,29 @@
|
|
|
60
60
|
* 🔴 fail-soft 铁律(与 ask 桥同款):leader 队列未注册(print/非 REPL)、pending 蒸发、decide 409/404
|
|
61
61
|
* 都返回 failed —— 调用方回退「flush + 原样终帧」的诚实红,绝不更糟。
|
|
62
62
|
* 🔴 UNTRUSTED:gate args 为模型作文(service 已 redact),只渲染绝不回喂;决断只带 decision 枚举。
|
|
63
|
+
*
|
|
64
|
+
* ── #144 半场(client-core 0.28.0)────────────────────────────────────────────────────────────
|
|
65
|
+
* ① **透传** `persistedRuleShadowed`(server ≥7.13.0,来源锚 = engine fixture
|
|
66
|
+
* `@sema-agent/server/dist/tool-approval.d.ts` 同名键;ADDITIVE、`tool_approval` only)——
|
|
67
|
+
* 帧 → {@link ApprovalCardRequest.persistedRuleShadowed},壳据此渲「你的规则仍在,只是这次被
|
|
68
|
+
* 要求逐次确认」。🔴 它是 **display 值,不是身份通道**:脱敏后的展示文本,不保证与规则店里那条
|
|
69
|
+
* 规则逐字节相同、更不是它的 id;下游「删除/撤销规则」面拿它当身份用 = 删错或删不掉。
|
|
70
|
+
* ② **deny 归因通道**({@link ApprovalCardDenyDecision.reason}):两腿不同形 ——
|
|
71
|
+
* durable `/decide` **有** reason 位 ⇒ 原文上 wire(包内按 server 4096 上限截断);
|
|
72
|
+
* live 帧腿 `/v1/tool-approvals/:id/respond`:✅ **#229 已兑现**(0.29.0 respond-note 批,
|
|
73
|
+
* server ≥7.15.0)—— 端点收 **`note`**(不是 reason;与 durable `AskDecisionBody.note`
|
|
74
|
+
* 同词同源同一列 `decision_note`),deny reason 经 {@link RespondToolApprovalOpts.note} 上 wire。
|
|
75
|
+
* 发送前置两道门:能力位 `approvalDecisionNote`(SDK 6.16 成文「位缺席就别发」)+ 2048 上限
|
|
76
|
+
* (server 超限响亮 400 且**连决断一起拒**,所以超限 ⇒ 整条不发 + 留痕,诚实缺席优先于
|
|
77
|
+
* 静默截断);真落行与否看 ack 的 `noteRecorded`(缺席 ≠ false)。
|
|
78
|
+
* 🔴 方向纪律:reason/note 只做归因,绝不参与裁决;缺席 ⇒ 现状字节不变。
|
|
63
79
|
*/
|
|
64
|
-
import { HitlBridge, HitlSafetyError, findPendingForTask } from './hitlBridge.js';
|
|
80
|
+
import { DEFAULT_DENY_REASON, HitlBridge, HitlSafetyError, denyReasonForWire, findPendingForTask, } from './hitlBridge.js';
|
|
65
81
|
import { hostLog } from '../host.js';
|
|
66
82
|
import { createSessionSlot, DEFAULT_SESSION_KEY } from '../sessionSlot.js';
|
|
67
83
|
import { readEngineActiveBgTasks } from '../fleet/fleetLedger.js';
|
|
68
84
|
import { observeCancelByDeny, surfaceRememberNotApplied, surfaceEditNotForwarded } from './hitlHostSurface.js';
|
|
85
|
+
import { approvalCallKey, liveFrameCallKey } from './gateIdentity.js';
|
|
69
86
|
/** fs 写权限 gate 判定:未来的一等 kind(tool_approval)或按 toolName(server 桥首批=fs 写三件,
|
|
70
87
|
* [820] 表)。AskUserQuestion 永不进这里(ask 桥先判)。 */
|
|
71
88
|
export function isFsApprovalGate(gate) {
|
|
@@ -195,6 +212,8 @@ function cardPortMissReason() {
|
|
|
195
212
|
}
|
|
196
213
|
return 'no approval card port installed (print/non-REPL mode, or host forgot installApprovalCardPort)';
|
|
197
214
|
}
|
|
215
|
+
// deny 归因窄化(0.28.0 发版扫描 F1 收编):单源下沉 hitlBridge —— 三条 reason 出口(卡 deny /
|
|
216
|
+
// canUseTool deny / plan-review)共用一把,坐标与理由见 hitlBridge.ts 的导出处顶注。
|
|
198
217
|
/**
|
|
199
218
|
* 弹 CC 三选卡 → 等决断 → 经 HitlBridge decide(durable park 腿,server 桥前的 checkpointGate 面;
|
|
200
219
|
* 合成单测/旧引擎兜底保留)。decide 成功即引擎驱到下一状态(sync);调用方随后 attach runs.events
|
|
@@ -227,7 +246,8 @@ export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signa
|
|
|
227
246
|
return { kind: 'failed', gatedCallId, reason: 'gate has no tool input payload' };
|
|
228
247
|
}
|
|
229
248
|
const bridge = new HitlBridge(deps.client, taskId);
|
|
230
|
-
|
|
249
|
+
// A-028.3:卡身份键经 gateIdentity 唯一铸口(呈现台账/壳侧重开腿同源,字面不再各铸各的)。
|
|
250
|
+
const callKey = approvalCallKey(gatedCallId, taskId);
|
|
231
251
|
const card = await surfaceApprovalCard({
|
|
232
252
|
toolName,
|
|
233
253
|
args,
|
|
@@ -277,7 +297,11 @@ export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signa
|
|
|
277
297
|
}
|
|
278
298
|
case 'deny':
|
|
279
299
|
try {
|
|
280
|
-
|
|
300
|
+
// #144 半场(0.28.0):卡给的拒因原样上 wire(`/decide` 本就有 reason 位)—— 壳侧三处权限
|
|
301
|
+
// 规则自动 deny(rule-deny / dontAsk / headless fail-closed)的真实拒因此前只能落 debug 日志,
|
|
302
|
+
// 引擎与运维队列上看到的都是那句无差别的缺省文案。缺席/空白 ⇒ 缺省,现状字节不变。
|
|
303
|
+
// 🔴 归因不改裁决:无论 reason 是什么,这一支恒 `decision:'deny'`。
|
|
304
|
+
await bridge.decideTool({ decision: 'deny', reason: denyReasonForWire(card.reason, `task ${taskId}`) ?? DEFAULT_DENY_REASON }, gatedCallId, signal ? { signal } : undefined, pending);
|
|
281
305
|
return { kind: 'decided', gatedCallId, denied: true };
|
|
282
306
|
}
|
|
283
307
|
catch (e) {
|
|
@@ -309,6 +333,11 @@ export const TOOL_APPROVAL_FRAME_KEYS_MIRROR = [
|
|
|
309
333
|
// sdk 6.14.0 追平(#225 件1,client-core 0.26.0):server 7.12.0 起真发规则候选 —— 门
|
|
310
334
|
// (run-approval-frame-keys-test.mjs)在 bump SDK 当天恰红抓获本键缺席,本次补齐。
|
|
311
335
|
'ruleSuggestions',
|
|
336
|
+
// #144(client-core 0.28.0):server 7.13.0 起真发 `persistedRuleShadowed`(证据=engine fixture
|
|
337
|
+
// 的 `@sema-agent/server/dist/tool-approval.d.ts`)。⚠️ 本键**领先** SDK 运行期锚一代
|
|
338
|
+
// (sdk 6.14.0/6.15.0 的 TOOL_APPROVAL_FRAME_KEYS 都还没有它)⇒ 对账门里有一条**带退出条件**的
|
|
339
|
+
// 领先登记(AHEAD_OF_ANCHOR):SDK 锚一旦补上,那条登记当场红,逼人删登记而不是让豁免长住。
|
|
340
|
+
'persistedRuleShadowed',
|
|
312
341
|
'delegation',
|
|
313
342
|
'outcome',
|
|
314
343
|
];
|
|
@@ -384,6 +413,9 @@ export function readToolApprovalRespondAck(v) {
|
|
|
384
413
|
...(typeof o.ruleRefusal === 'string' && o.ruleRefusal !== ''
|
|
385
414
|
? { ruleRefusal: o.ruleRefusal }
|
|
386
415
|
: {}),
|
|
416
|
+
// #229(sdk 6.16.0):这次回决的 note 有没有**真落进持久行**。缺席 ≠ false(未带 note 的回决/
|
|
417
|
+
// 老 server 字段省略);非布尔降缺席 —— 它决定「审计理由记没记上」的诚实告知。
|
|
418
|
+
...(typeof o.noteRecorded === 'boolean' ? { noteRecorded: o.noteRecorded } : {}),
|
|
387
419
|
};
|
|
388
420
|
}
|
|
389
421
|
/** 结构性识别流上的 tool_approval 帧(named SSE frame,payload.type === 帧名)。
|
|
@@ -473,7 +505,10 @@ function subagentBadgeFor(frame) {
|
|
|
473
505
|
*
|
|
474
506
|
* @returns {@link ToolApprovalFrameOutcome} —— 决断 + server ack(ack 缺席 = 未知,不是 false)。
|
|
475
507
|
*/
|
|
476
|
-
|
|
508
|
+
/** respond `note` 位的 server 上限(server `MAX_DECISION_NOTE_CHARS` = 2048,与 durable
|
|
509
|
+
* `AskDecisionBody.note` 同一列;超限 server **响亮 400 且连决断一起拒**,所以本包发送前有界)。 */
|
|
510
|
+
const MAX_RESPOND_NOTE_CHARS = 2048;
|
|
511
|
+
export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamArgs, signal, lane) {
|
|
477
512
|
const toolName = typeof frame.toolName === 'string' ? frame.toolName : 'Write';
|
|
478
513
|
// 工具入参:流上 tool_start.args(完整一手源)> 帧 args(redacted+16KiB 帽)> message 兜 path。
|
|
479
514
|
let args = streamArgs !== undefined && streamArgs !== null
|
|
@@ -503,7 +538,8 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
|
|
|
503
538
|
const card = await surfaceApprovalCard({
|
|
504
539
|
toolName,
|
|
505
540
|
args: args,
|
|
506
|
-
|
|
541
|
+
// A-028.3:live 帧腿的卡身份键经 gateIdentity 唯一铸口(帧上无 gatedCallId,以 approvalId 铸)。
|
|
542
|
+
callKey: liveFrameCallKey(frame.approvalId),
|
|
507
543
|
...(signal ? { signal } : {}),
|
|
508
544
|
...(isFromSubagent(frame) ? { workerBadge: subagentBadgeFor(frame) } : {}),
|
|
509
545
|
...(wireNote !== undefined ? { wireNote } : {}),
|
|
@@ -518,11 +554,44 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
|
|
|
518
554
|
: {}),
|
|
519
555
|
// #225 件1:规则候选透传(合形项;缺席/坏形 ⇒ 键不 stamp,卡形与 0.25.0 字节不变)。
|
|
520
556
|
...(ruleSuggestions !== undefined ? { ruleSuggestions } : {}),
|
|
557
|
+
// #144:被越级的持久规则原文(UNTRUSTED-for-display)。窄化=**非空白串才 stamp**,坏形降缺席
|
|
558
|
+
// (server 明说空串不铸键:「空串是坏值不是『空规则』」)—— 一格空白的规则解释比没有解释更坏。
|
|
559
|
+
// 🔴 stamp 的是**原文字节**(不 trim、不截、不改写):展示值一改写就不再是「你那条规则」的原文;
|
|
560
|
+
// 渲染面的长度夹取归壳,与 `message`/`sourceAgentName` 同族口径(本模块对展示文本零加工)。
|
|
561
|
+
...(typeof frame.persistedRuleShadowed === 'string' && frame.persistedRuleShadowed.trim() !== ''
|
|
562
|
+
? { persistedRuleShadowed: frame.persistedRuleShadowed }
|
|
563
|
+
: {}),
|
|
521
564
|
});
|
|
522
565
|
const decision = card.kind === 'allow' ? (card.allowSession ? 'allow_session' : 'allow') : 'deny';
|
|
523
566
|
if (card.kind === 'failed') {
|
|
524
567
|
hostLog('debug', `liveToolApprovalWire: approval card unavailable (${card.reason}) — fail-closed deny for ${frame.approvalId}`);
|
|
525
568
|
}
|
|
569
|
+
// #229 兑现(0.29.0 respond-note 批;此前 0.28.0 记账「候 server」,server 7.15.0 已到货):
|
|
570
|
+
// respond 端点收 **`note`**(不是 reason —— 与 durable `AskDecisionBody.note` 同词同源同一列
|
|
571
|
+
// `decision_note`),任何 decision 都可带。deny reason 经它上 wire,发送前置两道门:
|
|
572
|
+
// · 能力位(`lane.approvalDecisionNoteCapable === true`)—— SDK 6.16 成文「位缺席就别发」:
|
|
573
|
+
// 老 server 静默忽略未知键照回 200,「没听懂」与「记上了」不可分,发了只造「已留档」错觉;
|
|
574
|
+
// · 上限(≤{@link MAX_RESPOND_NOTE_CHARS}=2048)—— server 超限**响亮 400 且连决断一起拒**,
|
|
575
|
+
// 而备注是决断的补充面,绝不许它把决断本身打失败。超限 ⇒ **整条不发** + 留痕
|
|
576
|
+
// (诚实缺席优先于静默截断 —— 「一条被悄悄砍半的审计理由比没有理由更坏」,server 同款立场)。
|
|
577
|
+
// 🔴 留痕纪律不变:只写**元数据**(在场 + 长度 + 没发的原因),绝不把正文拷进日志 ——
|
|
578
|
+
// 正文是宿主自己卡口返回的自由文本,回喂宿主 sink 零诊断增量,还是外溢/注入面。
|
|
579
|
+
let note;
|
|
580
|
+
if (card.kind === 'deny' && typeof card.reason === 'string' && card.reason.trim() !== '') {
|
|
581
|
+
if (lane?.approvalDecisionNoteCapable !== true) {
|
|
582
|
+
hostLog('debug', `liveToolApprovalWire: ${frame.approvalId} card supplied a deny reason (len=${card.reason.length}) but the ` +
|
|
583
|
+
'approvalDecisionNote capability is not confirmed for this engine — note not sent (an unknown key would be ' +
|
|
584
|
+
'silently swallowed by an older server while still acking 200)');
|
|
585
|
+
}
|
|
586
|
+
else if (card.reason.length > MAX_RESPOND_NOTE_CHARS) {
|
|
587
|
+
hostLog('debug', `liveToolApprovalWire: ${frame.approvalId} deny reason exceeds the decision_note cap ` +
|
|
588
|
+
`(len=${card.reason.length} > ${MAX_RESPOND_NOTE_CHARS}) — note not sent at all (the server rejects the WHOLE ` +
|
|
589
|
+
'respond with 400 over an oversize note, and a silently halved audit reason is worse than none)');
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
note = card.reason;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
526
595
|
try {
|
|
527
596
|
// abort 后的 deny 仍要送达(引擎侧同款 cancel-by-deny)——不带已 aborted 的 signal。
|
|
528
597
|
// #225 件1:卡决断携带的 persistRule 只在「确是帧候选之一的原文」时上 wire —— server 本就
|
|
@@ -542,6 +611,7 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
|
|
|
542
611
|
...(signal && !signal.aborted ? { signal } : {}),
|
|
543
612
|
...(card.kind === 'allow' && card.updatedInput !== undefined ? { updatedInput: card.updatedInput } : {}),
|
|
544
613
|
...(persistRule !== undefined ? { persistRule } : {}),
|
|
614
|
+
...(note !== undefined ? { note } : {}),
|
|
545
615
|
});
|
|
546
616
|
// 🔴 相关性门(对抗复审二轮):ack 必须是**这一次**审批的回执 —— id 与决断词都要对上。
|
|
547
617
|
// 对不上的 ack(注入面串了别人的响应 / 坏 mock / raw-fetch 包装器复用了连接)如果照样被
|
|
@@ -569,6 +639,13 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
|
|
|
569
639
|
hostLog('debug', `liveToolApprovalWire: ${frame.approvalId} edited args were NOT forwarded (ack.updatedInputForwarded=false) — the tool runs on the ORIGINAL input`);
|
|
570
640
|
surfaceEditNotForwarded();
|
|
571
641
|
}
|
|
642
|
+
// #229:note 发了而 ack 说没落行(noteRecorded=false:纯活卡无行可落 / 店抖但裁决照常生效 /
|
|
643
|
+
// 并发同决议先落行)或位缺席(异常形:能力位为真却没回执)—— 审计面事实,decision 本身不受
|
|
644
|
+
// 影响,debug 留痕即可(不惊动用户:决断没丢,丢的只是理由的持久档)。
|
|
645
|
+
if (note !== undefined && ack !== undefined && ack.noteRecorded !== true) {
|
|
646
|
+
hostLog('debug', `liveToolApprovalWire: ${frame.approvalId} decision note was sent but not persisted ` +
|
|
647
|
+
`(ack.noteRecorded=${String(ack.noteRecorded)}) — decision stood; the audit note did not land on the ask row`);
|
|
648
|
+
}
|
|
572
649
|
return ack !== undefined ? { decision, ack } : { decision };
|
|
573
650
|
}
|
|
574
651
|
catch (e) {
|
package/dist/index.d.ts
CHANGED
|
@@ -182,6 +182,7 @@ export * from './adapter/downstream/turnUsageToModelUsage.js';
|
|
|
182
182
|
export * from './adapter/downstream/eventToSdkMessage.js';
|
|
183
183
|
export * from './adapter/downstream/terminalToSdkResult.js';
|
|
184
184
|
export * from './adapter/runStream.js';
|
|
185
|
+
export * from './adapter/activeRunSelfHeal.js';
|
|
185
186
|
export * from './request/taskRequest.js';
|
|
186
187
|
export * from './clientSlice.js';
|
|
187
188
|
export * from './host.js';
|
|
@@ -220,6 +221,9 @@ export * from './hitl/hitlHostSurface.js';
|
|
|
220
221
|
export * from './hitl/toolApprovalWire.js';
|
|
221
222
|
export * from './hitl/askGateWire.js';
|
|
222
223
|
export * from './hitl/planReviewWire.js';
|
|
224
|
+
export * from './hitl/gateIdentity.js';
|
|
225
|
+
export * from './hitl/armedGateRegistry.js';
|
|
226
|
+
export * from './hitl/parkOwnership.js';
|
|
223
227
|
export * from './hitl/approvalsFeed.js';
|
|
224
228
|
export * from './compensations.js';
|
|
225
229
|
export * from './request/printNotification.js';
|
package/dist/index.js
CHANGED
|
@@ -208,6 +208,14 @@ export * from './adapter/downstream/turnUsageToModelUsage.js';
|
|
|
208
208
|
export * from './adapter/downstream/eventToSdkMessage.js';
|
|
209
209
|
export * from './adapter/downstream/terminalToSdkResult.js';
|
|
210
210
|
export * from './adapter/runStream.js';
|
|
211
|
+
// ── A-028.1(#244 族A,2026-08-12):409 active-run 自愈的分诊树 + 结局文案层 ————————————
|
|
212
|
+
// 判别半场(activeRunBusySignal)在 runStream.js 早已出面;本件是它的消费侧:四路分诊
|
|
213
|
+
// (pendingGate.kind 优先 / status 表回退)+ 八种结局的可覆盖默认文案。归通用半场的理由:
|
|
214
|
+
// 三端面对同一把会话锁 —— cli 交互/headless 两车道换包导入删本地抄件(提货批);desktop
|
|
215
|
+
// session-host 已自写同族「parked run 不能 cancel」分诊,收敛到此;web 读 activeTaskId 的
|
|
216
|
+
// busy 面同源。文案默认串 = cli 产品面逐字,端经 ActiveRunSelfHealCopy 覆盖出路句。
|
|
217
|
+
// 🔴 零值级 import 纯函数叶(类型 type-only 借 runStream),不进内核/A 层闭包。
|
|
218
|
+
export * from './adapter/activeRunSelfHeal.js';
|
|
211
219
|
// ── B4 批:A 层帧分派收官 + 请求面合一 + 构造点收编(2026-07-27)────────────────────────────────
|
|
212
220
|
// 请求面:三个构造器(seamQuery.toTaskRequest / seamQueryEngine / liveClient.toLiveRequest)
|
|
213
221
|
// 的**字段集与合并语义**收成一份;车道差异做成 REQUEST_FIELD_MATRIX 数据表(gap:true = 判为漏)。
|
|
@@ -306,6 +314,18 @@ export * from './hitl/hitlHostSurface.js';
|
|
|
306
314
|
export * from './hitl/toolApprovalWire.js';
|
|
307
315
|
export * from './hitl/askGateWire.js';
|
|
308
316
|
export * from './hitl/planReviewWire.js';
|
|
317
|
+
// ── A-028.2/.3(#244 族A,2026-08-12):决断卡链的键词汇/呈现台账/归属判据三件 ————————————
|
|
318
|
+
// · gateIdentity:三条身份键字面(hitl-ask:/callKey/plan-review:,外加 hitl-frame: 同族)的唯一
|
|
319
|
+
// 铸口 —— 包内三处铸口与呈现台账的键推导同源;壳侧 armedGateRegistry/askParkReopen 的手抄字面
|
|
320
|
+
// 换 import 删本地(提货批);desktop session-host 的 plan:/durable: 前缀族收敛候后续对齐。
|
|
321
|
+
// · armedGateRegistry:per-session 呈现台账(firstSight 文案分形的唯一判据);sessionSlot 键支持
|
|
322
|
+
// 多会话宿主(desktop session-host 多引擎会话),cli 走 DEFAULT_SESSION_KEY 零参兼容层。
|
|
323
|
+
// · parkOwnership:ask park 重开腿的纯判据三件(fail-closed 归属证明/questions 结构判定/
|
|
324
|
+
// HitlSafetyError duck 判型)—— cli 换包导入删抄件;desktop refreshPendingApprovals 的自写
|
|
325
|
+
// 归属判定收敛到同一判据。approvals.list 编排与 stderr 留各端。
|
|
326
|
+
export * from './hitl/gateIdentity.js';
|
|
327
|
+
export * from './hitl/armedGateRegistry.js';
|
|
328
|
+
export * from './hitl/parkOwnership.js';
|
|
309
329
|
// B7 ③(census G20,**行为改动**不是搬迁):pending-approvals 推送 feed(stream 优先 / 断流回落
|
|
310
330
|
// 轮询 / 定期再试)。🔴 它**不替换** D-1 的取件 —— 那三处必须继续走权威 `list()`(见文件头)。
|
|
311
331
|
export * from './hitl/approvalsFeed.js';
|
|
@@ -68,6 +68,17 @@ type RespondFn = (id: string, answer: QuestionAnswer, opts?: {
|
|
|
68
68
|
/** Register a one-shot local responder for a SYNTHETIC question frame (e.g. `plan-review:<taskId>`).
|
|
69
69
|
* Returns an unregister fn (dialog dismissed without answering ⇒ caller cleans up). */
|
|
70
70
|
export declare function registerLocalQuestionResponder(id: string, fn: RespondFn): () => void;
|
|
71
|
+
/**
|
|
72
|
+
* Unregister a local responder **by id**(A-028.4 复审二轮收紧,2026-08-12)。
|
|
73
|
+
*
|
|
74
|
+
* 为什么需要按 id 注销:`registerLocalQuestionResponder` 的注销钩只回到注册方手里,而重开腿
|
|
75
|
+
* (`planReviewWire.reopenPlanReviewCard`)要退役的是**别的注册方**(arm 臂)登记的 canonical
|
|
76
|
+
* responder —— 只发 `question_complete` 撤帧撤不掉表里的 responder,一个在途的迟到作答仍能经
|
|
77
|
+
* 它投递决断,与新卡的投递构成同一 gate 的双决断面。返回是否真删了一条(调用方留痕用)。
|
|
78
|
+
* 🔴 只删表项,不通知原注册方 —— 原注册方的自愈臂(如 arm 的陈旧武装态分支)按
|
|
79
|
+
* `hasLocalQuestionResponder` 探询真相,不依赖回调。
|
|
80
|
+
*/
|
|
81
|
+
export declare function unregisterLocalQuestionResponder(id: string): boolean;
|
|
71
82
|
/**
|
|
72
83
|
* Is a local responder for this synthetic questionId still bound?([2393] hitl-F3,2026-08-02)
|
|
73
84
|
*
|