@sema-agent/client-core 0.8.0 → 0.9.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 +18 -6
- package/dist/adapt.js +12 -48
- package/dist/compensations.d.ts +2 -1
- package/dist/compensations.js +71 -1
- package/dist/hitl/approvalsFeed.d.ts +102 -0
- package/dist/hitl/approvalsFeed.js +185 -0
- package/dist/hitl/askGateWire.d.ts +155 -0
- package/dist/hitl/askGateWire.js +530 -0
- package/dist/hitl/hitlBridge.d.ts +245 -0
- package/dist/hitl/hitlBridge.js +267 -0
- package/dist/hitl/planReviewWire.d.ts +17 -0
- package/dist/hitl/planReviewWire.js +142 -0
- package/dist/hitl/toolApprovalWire.d.ts +126 -0
- package/dist/hitl/toolApprovalWire.js +263 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +36 -0
- package/dist/notifications.d.ts +67 -0
- package/dist/notifications.js +94 -0
- package/dist/subagent/engineTaskHandleWire.d.ts +0 -1
- package/dist/subagent/engineTaskHandleWire.js +4 -3
- package/package.json +2 -2
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B7 批搬迁(2026-07-27,设计稿 §3 B7):cli `src/sema/planReviewWire.ts`(142 行)**整条进包**。
|
|
3
|
+
*
|
|
4
|
+
* 🔴 **`wireConfig()` 第七份收编闭合**:B6 的 `engineWireTarget.ts` 头注记着「本批搬入的是前六份;
|
|
5
|
+
* 第七份 `planReviewWire` 不在 B6 范围,仍留壳内 —— 它是下一批的收编候选,别以为已经收干净了」。
|
|
6
|
+
* 本批就是那一批:这里改用 `engineWireTarget()`,壳树 `grep -rn 'function wireConfig' src/sema/`
|
|
7
|
+
* 的七份手抄至此**归零**(pure 门 B7 段有断言:壳树在场时命中数 = 0;缺席 ⇒ DEGRADED)。
|
|
8
|
+
*
|
|
9
|
+
* 🔴 其余搬迁差分(零行为变化):
|
|
10
|
+
* 1. `logForDebugging` → `hostLog`。
|
|
11
|
+
* 2. `await import('../utils/messageQueueManager.js').enqueuePendingNotification({...})`
|
|
12
|
+
* → `NotificationQueuePort.enqueueMetaPrompt`(B7 新增的**可选**方法 —— 既有宿主实现不破)。
|
|
13
|
+
* 🔴 缺席后果不是「少个装饰」:模型永远不知道 plan 被批准/驳回后引擎跑出了什么结果,
|
|
14
|
+
* 于是它在 approve 之后**原地不动**。所以缺席计 miss(`notificationQueuePortMisses()`),
|
|
15
|
+
* 并保留壳原文的 try/catch fail-soft(投递失败绝不炸流)。
|
|
16
|
+
* 3. `registerLocalQuestionResponder`/`publishQuestionFrame` → 包内 `liveQuestionStore`
|
|
17
|
+
* (B1 已搬,**同一个 module 台账**;两份实例 ⇒ 卡弹出来没人收答)。
|
|
18
|
+
*
|
|
19
|
+
* ── 以下为原文件的领域说明(逐字保留)────────────────────────────────────────────────────────
|
|
20
|
+
*
|
|
21
|
+
* planReviewWire — plan_review park 的壳侧审批闭环(core [508]③ 形状拍板 + [511]② 实证)。
|
|
22
|
+
*
|
|
23
|
+
* WIRE 全链(server 1.138 × core 1.257.4 探针逐字实证,probe-plan-final-frame.mjs):
|
|
24
|
+
* 1. plan-mode turn 里模型调 ExitPlanMode → 引擎 park:done 帧 `status:"needs_review"` +
|
|
25
|
+
* `checkpointGate:{kind:"plan_review",reason:"ExitPlanMode"}` + `errorCode:"review.pending"`;
|
|
26
|
+
* plan 文本已由 ExitPlanMode tool_end 渲染在转录(fence 块),checkpoint 落 FileCheckpointStore。
|
|
27
|
+
* 2. 壳(本模块,runStream done 分支调用)合成一个 QuestionFrame 借 AskUserQuestion overlay 弹
|
|
28
|
+
* 审批卡(core 拍板:不新造 live question 帧,壳自绘)——合成 id `plan-review:<taskId>` 经
|
|
29
|
+
* liveQuestionStore 的 LOCAL responder 拦截,绝不会打到引擎的 /v1/questions(那里 404)。
|
|
30
|
+
* 3. 决断 → POST /v1/assistant/tasks/:taskId/plan_review `{decision:"approve"|"reject"}`——
|
|
31
|
+
* resume 是 SYNC 驱动到终态的(实证 200 `{taskId,sessionId,status:"completed"}`,可能分钟级),
|
|
32
|
+
* 所以 fire 后卡片即关,完成后经 enqueuePendingNotification 入队一个 isMeta turn:模型收到
|
|
33
|
+
* resume 终态,自然接续汇报(approve 后"继续干活"的 CC 体验由引擎侧 resume 已完成,这里是
|
|
34
|
+
* 结果呈现)。reject 同构(引擎丢 plan 保持 plan 语境)。
|
|
35
|
+
*
|
|
36
|
+
* 挂账(诚实边界):approve 后壳本地的 plan-mode footer 徽章不自动退(CC 会退)——退 mode 的
|
|
37
|
+
* app-state seam 不在本模块可达面,记 rc.47 接 REPL 层;engine 侧 handsReadOnly 已由 resume 处理。
|
|
38
|
+
*/
|
|
39
|
+
import { publishQuestionFrame, registerLocalQuestionResponder, } from '../liveQuestionStore.js';
|
|
40
|
+
import { hostLog } from '../host.js';
|
|
41
|
+
import { makeEngineWireClient } from '../engineWireSdk.js';
|
|
42
|
+
import { engineWireTarget } from '../engineWireTarget.js';
|
|
43
|
+
import { enqueuePlanReviewOutcome } from '../notifications.js';
|
|
44
|
+
/** resume 是 SYNC 驱动到终态的(可能分钟级)——原 raw fetch 无超时;SDK Transport 恒有每请求
|
|
45
|
+
* 超时,取 6h 宽上界(诚实预算:超过它基本是引擎僵死,超时文案与网络失败同走 outcome 通知)。 */
|
|
46
|
+
const PLAN_REVIEW_RESUME_TIMEOUT_MS = 6 * 60 * 60_000;
|
|
47
|
+
const APPROVE_LABEL = 'Yes, approve and run the plan';
|
|
48
|
+
const REJECT_LABEL = 'No, reject it (keep planning)';
|
|
49
|
+
/** done 帧的 plan_review park 形状(结构性读,别的终态一律 false)。 */
|
|
50
|
+
export function isPlanReviewPark(result) {
|
|
51
|
+
const r = result;
|
|
52
|
+
return (!!r &&
|
|
53
|
+
r.status === 'needs_review' &&
|
|
54
|
+
r.checkpointGate?.kind === 'plan_review' &&
|
|
55
|
+
typeof r.taskId === 'string' &&
|
|
56
|
+
r.taskId.length > 0);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Arm the approval card for a parked plan_review (call with the RAW done.result). Returns true when
|
|
60
|
+
* armed (live path + shape matched); false = caller renders the terminal as usual. Fail-soft: any
|
|
61
|
+
* error inside must never break the stream drain.
|
|
62
|
+
*/
|
|
63
|
+
export function armPlanReviewApproval(result) {
|
|
64
|
+
try {
|
|
65
|
+
if (!isPlanReviewPark(result))
|
|
66
|
+
return false;
|
|
67
|
+
if (!engineWireTarget())
|
|
68
|
+
return false;
|
|
69
|
+
const { taskId } = result;
|
|
70
|
+
const questionId = `plan-review:${taskId}`;
|
|
71
|
+
registerLocalQuestionResponder(questionId, async (_id, answer) => {
|
|
72
|
+
const selected = answer?.answers?.[0]?.selected?.[0];
|
|
73
|
+
const decision = selected === APPROVE_LABEL ? 'approve' : 'reject';
|
|
74
|
+
// fire-and-forget:resume 同步驱动到终态可能分钟级,不能挂住 overlay;结果经 queue 通知回来
|
|
75
|
+
void decidePlanReview(taskId, decision);
|
|
76
|
+
return { ok: true };
|
|
77
|
+
});
|
|
78
|
+
publishQuestionFrame({
|
|
79
|
+
type: 'question',
|
|
80
|
+
questionId,
|
|
81
|
+
questions: [
|
|
82
|
+
{
|
|
83
|
+
header: 'Plan review',
|
|
84
|
+
question: 'The plan is ready for review. Approve it and start the implementation?',
|
|
85
|
+
options: [
|
|
86
|
+
{ label: APPROVE_LABEL, description: 'Resume the task now and execute the plan (runs to completion engine-side)' },
|
|
87
|
+
{ label: REJECT_LABEL, description: 'Discard this plan and stay in plan mode to refine it' },
|
|
88
|
+
],
|
|
89
|
+
multiSelect: false,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
hostLog('debug', `planReviewWire: approval card armed for parked plan (task ${taskId})`);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
hostLog('debug', `planReviewWire: arm failed (fail-soft): ${String(e)}`);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** POST the decision; on settle enqueue an isMeta turn so the model reports the resume outcome.
|
|
102
|
+
* SDK 宪法迁移批:SDK 0.0.52 assistant.planReview verb(同 wire `POST /v1/assistant/tasks/:id/
|
|
103
|
+
* plan_review` 同 body {decision})——raw fetch 退役。错误模型:非 2xx 抛 APIError(message =
|
|
104
|
+
* server body.error 原文)→ 同款「HTTP <status> <error>」outcome 文案;网络失败走原「could not
|
|
105
|
+
* reach the engine」臂。 */
|
|
106
|
+
export async function decidePlanReview(taskId, decision) {
|
|
107
|
+
const cfg = engineWireTarget();
|
|
108
|
+
if (!cfg)
|
|
109
|
+
return;
|
|
110
|
+
const client = makeEngineWireClient({
|
|
111
|
+
baseUrl: cfg.baseUrl,
|
|
112
|
+
...(cfg.token ? { token: cfg.token } : {}),
|
|
113
|
+
principal: cfg.principal,
|
|
114
|
+
timeoutMs: PLAN_REVIEW_RESUME_TIMEOUT_MS,
|
|
115
|
+
});
|
|
116
|
+
if (!client)
|
|
117
|
+
return;
|
|
118
|
+
let outcome = '';
|
|
119
|
+
try {
|
|
120
|
+
const body = await client.assistant.planReview(taskId, { decision });
|
|
121
|
+
hostLog('debug', `planReviewWire: ${decision} → ok ${JSON.stringify(body).slice(0, 200)}`);
|
|
122
|
+
outcome = `The plan was ${decision === 'approve' ? 'approved and the parked task resumed to completion' : 'rejected (plan discarded)'} — final status: ${body?.status ?? 'unknown'}.`;
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
// APIError 判型走 status duck-check(sseIdleTriage 同款纪律:双包时 instanceof 会分叉)。
|
|
126
|
+
const status = e?.status;
|
|
127
|
+
if (typeof status === 'number') {
|
|
128
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
129
|
+
hostLog('debug', `planReviewWire: ${decision} → ${status} ${msg.slice(0, 200)}`);
|
|
130
|
+
outcome = `The plan_review decision failed: HTTP ${status} ${msg}`.trim();
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
outcome = `The plan_review decision could not reach the engine: ${String(e)}`;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
enqueuePlanReviewOutcome(`<plan-review-outcome>\n${outcome}${decision === 'approve' ? `\nUse TaskOutput("${taskId}") if you need the resumed task's full output, then give the user a brief report of what was done.` : '\nBriefly acknowledge to the user that the plan was rejected and continue planning.'}\n</plan-review-outcome>`);
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
hostLog('debug', `planReviewWire: outcome enqueue failed: ${String(e)}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { type HitlClientLike } from './hitlBridge.js';
|
|
2
|
+
/** fs 写权限 gate 判定:未来的一等 kind(tool_approval)或按 toolName(server 桥首批=fs 写三件,
|
|
3
|
+
* [820] 表)。AskUserQuestion 永不进这里(ask 桥先判)。 */
|
|
4
|
+
export declare function isFsApprovalGate(gate: {
|
|
5
|
+
kind?: unknown;
|
|
6
|
+
toolName?: unknown;
|
|
7
|
+
} | undefined | null): boolean;
|
|
8
|
+
export declare function toolNameIsFsWrite(name: string): boolean;
|
|
9
|
+
export type FsApprovalOutcome = {
|
|
10
|
+
kind: 'decided';
|
|
11
|
+
gatedCallId?: string;
|
|
12
|
+
denied?: boolean;
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'aborted';
|
|
15
|
+
gatedCallId?: string;
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'failed';
|
|
18
|
+
gatedCallId?: string;
|
|
19
|
+
reason: string;
|
|
20
|
+
};
|
|
21
|
+
/** 本桥消费的 wire 面(liveHitlAskWire 的 AskGateWireDeps 同形切片,mock 可注入)。 */
|
|
22
|
+
export interface FsApprovalWireDeps {
|
|
23
|
+
client: HitlClientLike;
|
|
24
|
+
}
|
|
25
|
+
/** 结构等值(键序无关深比较)——updatedInput「真编辑过」判定用。zod parse 会产新引用与重排,
|
|
26
|
+
* 引用比较/JSON.stringify 串比较都会假报「编辑过」。 */
|
|
27
|
+
export declare function structurallyEqual(a: unknown, b: unknown): boolean;
|
|
28
|
+
/** 三选卡的原始决断(vendor 卡的三个选项 + abort + 表面失败)。allow_session=第 2 项
|
|
29
|
+
* (vendor 产出非空 permissionUpdates:setMode acceptEdits / cwd 外 addDirectories)。 */
|
|
30
|
+
export type ApprovalCardDecision = {
|
|
31
|
+
kind: 'allow';
|
|
32
|
+
allowSession: boolean;
|
|
33
|
+
updatedInput?: unknown;
|
|
34
|
+
} | {
|
|
35
|
+
kind: 'deny';
|
|
36
|
+
} | {
|
|
37
|
+
kind: 'aborted';
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'failed';
|
|
40
|
+
reason: string;
|
|
41
|
+
};
|
|
42
|
+
/** 卡口的入参 —— cli `surfaceApprovalCard` 的 `req` 逐字。 */
|
|
43
|
+
export interface ApprovalCardRequest {
|
|
44
|
+
toolName: string;
|
|
45
|
+
args: object;
|
|
46
|
+
callKey: string;
|
|
47
|
+
signal?: AbortSignal;
|
|
48
|
+
/** 后台子代 ask 的归属徽章(卡头 `· @name`,CC swarm workerBadge 原生位;宿主 ask 恒缺席)。 */
|
|
49
|
+
workerBadge?: {
|
|
50
|
+
name: string;
|
|
51
|
+
color: string;
|
|
52
|
+
};
|
|
53
|
+
/** wire 层降级说明(卡 content 上方 dim 行,ToolUseConfirm.wireNote 超集位)——argsOmitted 等
|
|
54
|
+
* 不可注入 args 的提示走这里([1543]③ 记账的 note 位,2026-07-23 落位)。 */
|
|
55
|
+
wireNote?: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 🔴 **拆缝口** —— 弹「三选卡」并等人的决断。壳 = vendored CC `PermissionRequest`;
|
|
59
|
+
* 桌面/web = 各自的模态。缺席 ⇒ 两条决断腿都 `failed`(= 壳原文 print/non-REPL 的同一条路)。
|
|
60
|
+
*/
|
|
61
|
+
export type ApprovalCardPort = (req: ApprovalCardRequest) => Promise<ApprovalCardDecision>;
|
|
62
|
+
/** 装卡口(传 null 卸)。返回还原函数。 */
|
|
63
|
+
export declare function installApprovalCardPort(port: ApprovalCardPort | null): () => void;
|
|
64
|
+
/** 🔴 宿主自检:恒应为 0。非 0 = 有 gate 到达时卡口不在,那次审批走了 fail-closed 路径。 */
|
|
65
|
+
export declare function approvalCardPortMisses(): number;
|
|
66
|
+
/** 卡口在不在(壳原文 `getLeaderToolUseConfirmQueue()` 那个存在性判据的等价物)。 */
|
|
67
|
+
export declare function hasApprovalCardPort(): boolean;
|
|
68
|
+
/** 测试钩:卸口 + 清计数(只卸口不清计数会让「未装配时 misses 为 0」的断言恒假 —— host.ts 同款教训)。 */
|
|
69
|
+
export declare function _resetApprovalCardPortForTest(): void;
|
|
70
|
+
/**
|
|
71
|
+
* 共享卡面:弹 vendored CC 三选卡(PermissionRequest)→ 等用户决断。两条腿共用:
|
|
72
|
+
* - 旧 durable park 腿(surfaceFsApprovalAndDecide → HitlBridge.decideTool)
|
|
73
|
+
* - 新同步帧腿(server 1.191 `tool_approval` 帧 → POST /v1/tool-approvals/:id/respond)
|
|
74
|
+
* 决断落地(decide/respond)由调用方做;卡的渲染/键行为/本地 permissionUpdates
|
|
75
|
+
* (accept-session 升 intent,[816] 壳侧承诺② —— 与决断通道无关,两腿都要)由**宿主卡口**做。
|
|
76
|
+
*/
|
|
77
|
+
export declare function surfaceApprovalCard(req: ApprovalCardRequest): Promise<ApprovalCardDecision>;
|
|
78
|
+
/**
|
|
79
|
+
* 弹 CC 三选卡 → 等决断 → 经 HitlBridge decide(durable park 腿,server 桥前的 checkpointGate 面;
|
|
80
|
+
* 合成单测/旧引擎兜底保留)。decide 成功即引擎驱到下一状态(sync);调用方随后 attach runs.events
|
|
81
|
+
* 续流(与 ask 桥完全同 loop)。
|
|
82
|
+
*
|
|
83
|
+
* @param argsByCall tool_start.args(toolCallId → args)——题干/diff 的一手源;缺则 pending.input。
|
|
84
|
+
*/
|
|
85
|
+
export declare function surfaceFsApprovalAndDecide(deps: FsApprovalWireDeps, taskId: string, argsByCall: Map<string, unknown>, signal?: AbortSignal): Promise<FsApprovalOutcome>;
|
|
86
|
+
/** server 包 src/tool-approval.ts 头注的帧契约(redactDeep + 16KiB 帽;超帽 argsOmitted 仍出帧)。 */
|
|
87
|
+
export interface ToolApprovalFrame {
|
|
88
|
+
type: 'tool_approval' | 'tool_approval_complete';
|
|
89
|
+
approvalId: string;
|
|
90
|
+
toolName?: string;
|
|
91
|
+
message?: string;
|
|
92
|
+
args?: unknown;
|
|
93
|
+
argsOmitted?: boolean;
|
|
94
|
+
outcome?: 'allowed' | 'denied' | 'expired';
|
|
95
|
+
/** server 1.258([1535] 审批链,[1549]①3 判别式定谳):**在场即子代**——server 只对后台子代
|
|
96
|
+
* lane 的 ask 出此键(宿主自身 ask 的 sourceTaskId=sessionId 已在 server 侧收口不上帧)。
|
|
97
|
+
* ⚠️ 值=子代 sessionId 非 a… handle(fence 实测 2026-07-23)——不是 fleet 台账键。 */
|
|
98
|
+
sourceTaskId?: string;
|
|
99
|
+
/** core 1.378 RB-39②([1550]):Runner 填充的只读显式判别键——委派子代 gate 的 ask 恒带,
|
|
100
|
+
* 受信 internals 事实不可伪造。在场即子代(优先于 sourceTaskId 权宜式);候 server 帧透传班车。 */
|
|
101
|
+
fromSubagent?: boolean;
|
|
102
|
+
/** core 1.378 RB-39②:展示身份(UNTRUSTED-for-display,server redact 后上帧)——徽章名一手源。 */
|
|
103
|
+
sourceAgentName?: string;
|
|
104
|
+
}
|
|
105
|
+
/** 子代帧判别:显式键 fromSubagent(core 1.378 RB-39②)优先;缺席退 sourceTaskId 在场性权宜式
|
|
106
|
+
* (server 1.258 [1549]①3,旧代际兼容)。 */
|
|
107
|
+
export declare function isFromSubagent(frame: ToolApprovalFrame): boolean;
|
|
108
|
+
/** 三选卡决断 → respond 端点的 wire 枚举(server parseToolApprovalDecision)。 */
|
|
109
|
+
export type ToolApprovalRespondDecision = 'allow' | 'allow_session' | 'deny';
|
|
110
|
+
/** POST /v1/tool-approvals/:id/respond 的注入面(liveClient 提供;SDK 0.0.46 无此资源,raw fetch)。 */
|
|
111
|
+
export type RespondToolApprovalFn = (approvalId: string, decision: ToolApprovalRespondDecision, opts?: {
|
|
112
|
+
signal?: AbortSignal;
|
|
113
|
+
updatedInput?: unknown;
|
|
114
|
+
}) => Promise<void>;
|
|
115
|
+
/** 结构性识别流上的 tool_approval 帧(named SSE frame,payload.type === 帧名;非 AgentEvent arm)。 */
|
|
116
|
+
export declare function isToolApprovalFrame(ev: unknown): ev is ToolApprovalFrame;
|
|
117
|
+
/**
|
|
118
|
+
* 消费一张 `tool_approval` 帧:弹三选卡 → respond。引擎此刻同步阻塞在本帧上(fail-closed 全腿:
|
|
119
|
+
* TTL 5min/abort/emit 失败⇒deny),所以本函数 await 卡决断是安全的(流上不会再来别的内容帧)。
|
|
120
|
+
*
|
|
121
|
+
* fail-closed:卡面不可用(print 模式无卡口/enqueue 失败)⇒ 立刻 respond deny,不让引擎
|
|
122
|
+
* 干等 TTL。respond 自身失败(404=已过期/settled)⇒ 'unresolved'(诚实放行后续帧,引擎已自决)。
|
|
123
|
+
*
|
|
124
|
+
* @returns 用户(或 fail-closed)落定的决断;'unresolved' = respond 没送达。
|
|
125
|
+
*/
|
|
126
|
+
export declare function surfaceToolApprovalFrameAndRespond(frame: ToolApprovalFrame, respond: RespondToolApprovalFn, streamArgs: unknown | undefined, signal?: AbortSignal): Promise<ToolApprovalRespondDecision | 'unresolved'>;
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { HitlBridge } from './hitlBridge.js';
|
|
2
|
+
import { hostLog } from '../host.js';
|
|
3
|
+
import { readEngineActiveBgTasks } from '../fleet/fleetLedger.js';
|
|
4
|
+
/** fs 写权限 gate 判定:未来的一等 kind(tool_approval)或按 toolName(server 桥首批=fs 写三件,
|
|
5
|
+
* [820] 表)。AskUserQuestion 永不进这里(ask 桥先判)。 */
|
|
6
|
+
export function isFsApprovalGate(gate) {
|
|
7
|
+
if (!gate)
|
|
8
|
+
return false;
|
|
9
|
+
if (gate.kind === 'tool_approval')
|
|
10
|
+
return true;
|
|
11
|
+
return typeof gate.toolName === 'string' && toolNameIsFsWrite(gate.toolName);
|
|
12
|
+
}
|
|
13
|
+
export function toolNameIsFsWrite(name) {
|
|
14
|
+
const n = name.replace(/[\s_-]+/g, '').toLowerCase();
|
|
15
|
+
return (n === 'write' || n === 'filewrite' || n === 'edit' || n === 'fileedit' || n === 'multiedit' || n === 'notebookedit');
|
|
16
|
+
}
|
|
17
|
+
/** 结构等值(键序无关深比较)——updatedInput「真编辑过」判定用。zod parse 会产新引用与重排,
|
|
18
|
+
* 引用比较/JSON.stringify 串比较都会假报「编辑过」。 */
|
|
19
|
+
export function structurallyEqual(a, b) {
|
|
20
|
+
if (a === b)
|
|
21
|
+
return true;
|
|
22
|
+
if (typeof a !== typeof b)
|
|
23
|
+
return false;
|
|
24
|
+
if (a === null || b === null || typeof a !== 'object')
|
|
25
|
+
return false;
|
|
26
|
+
if (Array.isArray(a) !== Array.isArray(b))
|
|
27
|
+
return false;
|
|
28
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
29
|
+
if (a.length !== b.length)
|
|
30
|
+
return false;
|
|
31
|
+
return a.every((v, i) => structurallyEqual(v, b[i]));
|
|
32
|
+
}
|
|
33
|
+
const ka = Object.keys(a);
|
|
34
|
+
const kb = Object.keys(b);
|
|
35
|
+
if (ka.length !== kb.length)
|
|
36
|
+
return false;
|
|
37
|
+
return ka.every(k => structurallyEqual(a[k], b[k]));
|
|
38
|
+
}
|
|
39
|
+
let cardPort = null;
|
|
40
|
+
let cardPortMisses = 0;
|
|
41
|
+
/** 装卡口(传 null 卸)。返回还原函数。 */
|
|
42
|
+
export function installApprovalCardPort(port) {
|
|
43
|
+
const prev = cardPort;
|
|
44
|
+
cardPort = port;
|
|
45
|
+
return () => {
|
|
46
|
+
cardPort = prev;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** 🔴 宿主自检:恒应为 0。非 0 = 有 gate 到达时卡口不在,那次审批走了 fail-closed 路径。 */
|
|
50
|
+
export function approvalCardPortMisses() {
|
|
51
|
+
return cardPortMisses;
|
|
52
|
+
}
|
|
53
|
+
/** 卡口在不在(壳原文 `getLeaderToolUseConfirmQueue()` 那个存在性判据的等价物)。 */
|
|
54
|
+
export function hasApprovalCardPort() {
|
|
55
|
+
return cardPort !== null;
|
|
56
|
+
}
|
|
57
|
+
/** 测试钩:卸口 + 清计数(只卸口不清计数会让「未装配时 misses 为 0」的断言恒假 —— host.ts 同款教训)。 */
|
|
58
|
+
export function _resetApprovalCardPortForTest() {
|
|
59
|
+
cardPort = null;
|
|
60
|
+
cardPortMisses = 0;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 共享卡面:弹 vendored CC 三选卡(PermissionRequest)→ 等用户决断。两条腿共用:
|
|
64
|
+
* - 旧 durable park 腿(surfaceFsApprovalAndDecide → HitlBridge.decideTool)
|
|
65
|
+
* - 新同步帧腿(server 1.191 `tool_approval` 帧 → POST /v1/tool-approvals/:id/respond)
|
|
66
|
+
* 决断落地(decide/respond)由调用方做;卡的渲染/键行为/本地 permissionUpdates
|
|
67
|
+
* (accept-session 升 intent,[816] 壳侧承诺② —— 与决断通道无关,两腿都要)由**宿主卡口**做。
|
|
68
|
+
*/
|
|
69
|
+
export function surfaceApprovalCard(req) {
|
|
70
|
+
if (cardPort === null) {
|
|
71
|
+
cardPortMisses++;
|
|
72
|
+
return Promise.resolve({
|
|
73
|
+
kind: 'failed',
|
|
74
|
+
reason: 'no approval card port installed (print/non-REPL mode, or host forgot installApprovalCardPort)',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return cardPort(req);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 弹 CC 三选卡 → 等决断 → 经 HitlBridge decide(durable park 腿,server 桥前的 checkpointGate 面;
|
|
81
|
+
* 合成单测/旧引擎兜底保留)。decide 成功即引擎驱到下一状态(sync);调用方随后 attach runs.events
|
|
82
|
+
* 续流(与 ask 桥完全同 loop)。
|
|
83
|
+
*
|
|
84
|
+
* @param argsByCall tool_start.args(toolCallId → args)——题干/diff 的一手源;缺则 pending.input。
|
|
85
|
+
*/
|
|
86
|
+
export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signal) {
|
|
87
|
+
if (!hasApprovalCardPort()) {
|
|
88
|
+
cardPortMisses++;
|
|
89
|
+
return { kind: 'failed', reason: 'no approval card port installed (print/non-REPL mode)' };
|
|
90
|
+
}
|
|
91
|
+
// pending 行 = gate 的权威源 + D-1 绑定 surface(与 ask 桥同姿势)。
|
|
92
|
+
let pending;
|
|
93
|
+
try {
|
|
94
|
+
const rows = await deps.client.approvals.list(signal ? { signal } : undefined);
|
|
95
|
+
pending =
|
|
96
|
+
rows.find(r => r.taskId === taskId && typeof r.toolName === 'string' && toolNameIsFsWrite(r.toolName)) ??
|
|
97
|
+
rows.find(r => r.taskId === taskId);
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
return { kind: 'failed', reason: `approvals.list failed: ${String(e)}` };
|
|
101
|
+
}
|
|
102
|
+
if (!pending)
|
|
103
|
+
return { kind: 'failed', reason: 'no pending checkpoint for this run (resolved/expired?)' };
|
|
104
|
+
const gatedCallId = pending.toolCallId ?? pending.boundCallId ?? undefined;
|
|
105
|
+
const toolName = typeof pending.toolName === 'string' ? pending.toolName : '';
|
|
106
|
+
// 工具入参:优先流上 tool_start.args(完整、零额外语义);缺则 pending.input(service 已 redact+限长
|
|
107
|
+
// ——大 Write 的 content 可能被截,对话框 diff 退化但仍可决断)。
|
|
108
|
+
const args = (gatedCallId !== undefined ? argsByCall.get(gatedCallId) : undefined) ??
|
|
109
|
+
pending.input ??
|
|
110
|
+
{};
|
|
111
|
+
if (typeof args !== 'object' || args === null) {
|
|
112
|
+
return { kind: 'failed', gatedCallId, reason: 'gate has no tool input payload' };
|
|
113
|
+
}
|
|
114
|
+
const bridge = new HitlBridge(deps.client, taskId);
|
|
115
|
+
const callKey = gatedCallId ?? taskId;
|
|
116
|
+
const card = await surfaceApprovalCard({
|
|
117
|
+
toolName,
|
|
118
|
+
args,
|
|
119
|
+
callKey,
|
|
120
|
+
...(signal ? { signal } : {}),
|
|
121
|
+
});
|
|
122
|
+
switch (card.kind) {
|
|
123
|
+
case 'failed':
|
|
124
|
+
return { kind: 'failed', gatedCallId, reason: card.reason };
|
|
125
|
+
case 'aborted':
|
|
126
|
+
// turn 被中断(Esc/Ctrl+C):cancel-by-deny(contract/04 §2.4),best-effort fire-and-forget。
|
|
127
|
+
void bridge.decideTool({ decision: 'deny', reason: 'Interrupted by user' }, gatedCallId).catch(() => { });
|
|
128
|
+
return { kind: 'aborted', gatedCallId };
|
|
129
|
+
case 'allow':
|
|
130
|
+
try {
|
|
131
|
+
// accept-session(三选第 2 项)在 durable-park 腿的引擎侧半场:decide 带 remember:"session"
|
|
132
|
+
// (server 1.191 approvalExemptionStore.grant)→ 同 session 该工具后续 ask 免 park 免卡
|
|
133
|
+
// (frame 腿的 allow_session 对偶)。老 server 不识别 remember ⇒ 400 —— 回退纯 approve
|
|
134
|
+
// (本地 acceptEdits intent 已升,下一 turn 起 permissionMode 就位,同 turn 内可能再弹卡=
|
|
135
|
+
// 修复前行为,fail-soft 不更糟)。
|
|
136
|
+
if (card.allowSession) {
|
|
137
|
+
try {
|
|
138
|
+
await bridge.decideTool({ decision: 'approve', remember: 'session', ...(card.updatedInput !== undefined ? { updatedInput: card.updatedInput } : {}) }, gatedCallId, signal ? { signal } : undefined);
|
|
139
|
+
return { kind: 'decided', gatedCallId };
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
hostLog('debug', `liveToolApprovalWire: decide(approve+remember) failed (${String(e)}) — falling back to plain approve`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
await bridge.decideTool({ decision: 'approve', ...(card.updatedInput !== undefined ? { updatedInput: card.updatedInput } : {}) }, gatedCallId, signal ? { signal } : undefined);
|
|
146
|
+
return { kind: 'decided', gatedCallId };
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
return { kind: 'failed', gatedCallId, reason: `decide(approve) failed: ${String(e)}` };
|
|
150
|
+
}
|
|
151
|
+
case 'deny':
|
|
152
|
+
try {
|
|
153
|
+
await bridge.decideTool({ decision: 'deny', reason: 'The user rejected this tool use' }, gatedCallId, signal ? { signal } : undefined);
|
|
154
|
+
return { kind: 'decided', gatedCallId, denied: true };
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
return { kind: 'failed', gatedCallId, reason: `decide(deny) failed: ${String(e)}` };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** 子代帧判别:显式键 fromSubagent(core 1.378 RB-39②)优先;缺席退 sourceTaskId 在场性权宜式
|
|
162
|
+
* (server 1.258 [1549]①3,旧代际兼容)。 */
|
|
163
|
+
export function isFromSubagent(frame) {
|
|
164
|
+
if (frame.fromSubagent === true)
|
|
165
|
+
return true;
|
|
166
|
+
return typeof frame.sourceTaskId === 'string' && frame.sourceTaskId.length > 0;
|
|
167
|
+
}
|
|
168
|
+
/** 结构性识别流上的 tool_approval 帧(named SSE frame,payload.type === 帧名;非 AgentEvent arm)。 */
|
|
169
|
+
export function isToolApprovalFrame(ev) {
|
|
170
|
+
const e = ev;
|
|
171
|
+
return (!!e &&
|
|
172
|
+
(e.type === 'tool_approval' || e.type === 'tool_approval_complete') &&
|
|
173
|
+
typeof e.approvalId === 'string' &&
|
|
174
|
+
e.approvalId.length > 0);
|
|
175
|
+
}
|
|
176
|
+
/** 从 gate 的 ask 文案(core fs-write-gate-policy `approve write to "<path>"?`)兜出 path——argsOmitted
|
|
177
|
+
* 且流上无 tool_start.args 时的最后退化(卡仍可弹,diff 退化为空写)。 */
|
|
178
|
+
function pathFromGateMessage(message) {
|
|
179
|
+
const m = typeof message === 'string' ? /"([^"]+)"/.exec(message) : null;
|
|
180
|
+
return m?.[1];
|
|
181
|
+
}
|
|
182
|
+
/** 子代审批帧的归属徽章([1535] cli 半场):名一手源=帧上 sourceAgentName(core 1.378 RB-39②
|
|
183
|
+
* 显式展示身份,server redact 后透传,候 server 班车到货即亮);缺席退 fleet 在飞台账行名
|
|
184
|
+
* (`${agentType}: ${description}` 合成形;⚠️ sourceTaskId 实测=子代 sessionId 非 a… handle,
|
|
185
|
+
* 台账按 handle 键常 miss);再退 sourceTaskId 短形——徽章永在场,归属永不静默。截 32c 防破行。
|
|
186
|
+
* 🔴 搬迁差分:cli 用延迟 require 拉 `fleetClient.js`(理由=纯 wire 模块不背 fleet 图冷启动);
|
|
187
|
+
* 在包内 fleet 台账就是同一个包的模块,改静态 import。try/catch 保留(台账不可用的兜底那一支)。 */
|
|
188
|
+
function subagentBadgeFor(frame) {
|
|
189
|
+
let name = typeof frame.sourceAgentName === 'string' && frame.sourceAgentName.length > 0 ? frame.sourceAgentName : '';
|
|
190
|
+
if (!name && typeof frame.sourceTaskId === 'string' && frame.sourceTaskId.length > 0) {
|
|
191
|
+
name = frame.sourceTaskId;
|
|
192
|
+
try {
|
|
193
|
+
const row = readEngineActiveBgTasks().tasks.find(t => t.id === frame.sourceTaskId);
|
|
194
|
+
if (typeof row?.name === 'string' && row.name.length > 0)
|
|
195
|
+
name = row.name;
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
/* 台账不可用 → taskId 短形兜底 */
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (!name)
|
|
202
|
+
name = 'background agent';
|
|
203
|
+
if (name.length > 32)
|
|
204
|
+
name = `${name.slice(0, 31)}…`;
|
|
205
|
+
return { name, color: 'cyan' };
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* 消费一张 `tool_approval` 帧:弹三选卡 → respond。引擎此刻同步阻塞在本帧上(fail-closed 全腿:
|
|
209
|
+
* TTL 5min/abort/emit 失败⇒deny),所以本函数 await 卡决断是安全的(流上不会再来别的内容帧)。
|
|
210
|
+
*
|
|
211
|
+
* fail-closed:卡面不可用(print 模式无卡口/enqueue 失败)⇒ 立刻 respond deny,不让引擎
|
|
212
|
+
* 干等 TTL。respond 自身失败(404=已过期/settled)⇒ 'unresolved'(诚实放行后续帧,引擎已自决)。
|
|
213
|
+
*
|
|
214
|
+
* @returns 用户(或 fail-closed)落定的决断;'unresolved' = respond 没送达。
|
|
215
|
+
*/
|
|
216
|
+
export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamArgs, signal) {
|
|
217
|
+
const toolName = typeof frame.toolName === 'string' ? frame.toolName : 'Write';
|
|
218
|
+
// 工具入参:流上 tool_start.args(完整一手源)> 帧 args(redacted+16KiB 帽)> message 兜 path。
|
|
219
|
+
let args = streamArgs !== undefined && streamArgs !== null
|
|
220
|
+
? streamArgs
|
|
221
|
+
: frame.args !== undefined && frame.args !== null
|
|
222
|
+
? frame.args
|
|
223
|
+
: undefined;
|
|
224
|
+
let wireNote;
|
|
225
|
+
if (typeof args !== 'object' || args === null) {
|
|
226
|
+
// [1543]③:args>16KiB 超帽时 server 出帧带 argsOmitted:true 而无 args。卡面提示走
|
|
227
|
+
// ToolUseConfirm.wireNote 超集位(2026-07-23 落位,清 [1543]③ 记账)——dim 行不进 args,
|
|
228
|
+
// 不会随「编辑并批准」的 updatedInput 转发污染工具入参。
|
|
229
|
+
if (frame.argsOmitted === true) {
|
|
230
|
+
wireNote = 'tool arguments exceeded the wire cap and were omitted — the diff below is reconstructed from the gate message, not the full payload';
|
|
231
|
+
hostLog('debug', `liveToolApprovalWire: frame ${frame.approvalId} args omitted (>16KiB wire cap) — card falls back to message-derived path`);
|
|
232
|
+
}
|
|
233
|
+
const p = pathFromGateMessage(frame.message);
|
|
234
|
+
args = p !== undefined ? { file_path: p } : {};
|
|
235
|
+
}
|
|
236
|
+
// [1535]:子代的写审批浮到宿主卡(server 1.258 broker 腿)——卡头渲归属徽章(`· @name`),
|
|
237
|
+
// 用户知道在替谁批。判别=fromSubagent 显式键(core 1.378)优先、sourceTaskId 在场性权宜式兜底;
|
|
238
|
+
// 宿主自身 ask 两键恒缺席=徽章缺席,卡形不变。
|
|
239
|
+
const card = await surfaceApprovalCard({
|
|
240
|
+
toolName,
|
|
241
|
+
args: args,
|
|
242
|
+
callKey: `hitl-frame:${frame.approvalId}`,
|
|
243
|
+
...(signal ? { signal } : {}),
|
|
244
|
+
...(isFromSubagent(frame) ? { workerBadge: subagentBadgeFor(frame) } : {}),
|
|
245
|
+
...(wireNote !== undefined ? { wireNote } : {}),
|
|
246
|
+
});
|
|
247
|
+
const decision = card.kind === 'allow' ? (card.allowSession ? 'allow_session' : 'allow') : 'deny';
|
|
248
|
+
if (card.kind === 'failed') {
|
|
249
|
+
hostLog('debug', `liveToolApprovalWire: approval card unavailable (${card.reason}) — fail-closed deny for ${frame.approvalId}`);
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
// abort 后的 deny 仍要送达(引擎侧同款 cancel-by-deny)——不带已 aborted 的 signal。
|
|
253
|
+
await respond(frame.approvalId, decision, {
|
|
254
|
+
...(signal && !signal.aborted ? { signal } : {}),
|
|
255
|
+
...(card.kind === 'allow' && card.updatedInput !== undefined ? { updatedInput: card.updatedInput } : {}),
|
|
256
|
+
});
|
|
257
|
+
return decision;
|
|
258
|
+
}
|
|
259
|
+
catch (e) {
|
|
260
|
+
hostLog('debug', `liveToolApprovalWire: respond(${decision}) failed for ${frame.approvalId}: ${String(e)} — engine self-settles (TTL/abort)`);
|
|
261
|
+
return 'unresolved';
|
|
262
|
+
}
|
|
263
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,20 @@
|
|
|
5
5
|
* 沿革:@sema-agent/wire-cc-adapter 0.1.0 = seam 类型 + id 确定性派生 + 首批踩坑纯函数;
|
|
6
6
|
* 0.1.2(#52a)= adapt() 管线首批(纯投影臂全落 + 壳态耦合臂投影成 ChromeEvent + 差分守卫);
|
|
7
7
|
* 0.2.0 = 迁入 sema-client-core 独立仓并改名(旧 npm 名 deprecate 指本包);
|
|
8
|
+
* 0.9.0 = **B7 批**(设计稿 §3 B7「完成事件单一权威 + HITL」):
|
|
9
|
+
* ① **HITL 族四件**(新目录 `hitl/`):`hitlBridge`(整搬,🔴 **D-1 两元组 verbatim 回显**
|
|
10
|
+
* 与「409 绝不自动重试」两段**逐字节**搬,pure 门 B7 段用冻结字面量 + 壳树双向对拍 +
|
|
11
|
+
* 毒化行为口径三重钉)· `toolApprovalWire`(**拆**:判定/编排进包,三选卡本体经
|
|
12
|
+
* `ApprovalCardPort` 留端)· `askGateWire`(整搬;footer 通知与 Recent Denials 记账经
|
|
13
|
+
* `HitlHostSurface` 留端)· `planReviewWire`(整搬,**第七份 `wireConfig()` 收编闭合**)。
|
|
14
|
+
* ② **完成事件族 T2/T3**:`taskNotificationDedupKeyFromWire`(裸 wire 载荷键形)+
|
|
15
|
+
* `parseTranscriptNotificationSeeds` / `seedTaskNotificationDedupFromTranscript`
|
|
16
|
+
* (#63 resume 回植的 XML 反解)搬入 `notifications.ts`。**P1-3 未到货 ⇒ 只搬不删**,
|
|
17
|
+
* 四道去重原样;退役条件登记进 `compensations.ts`(T1/T2/T3/T25/T53 五条)。
|
|
18
|
+
* ③ **`approvals.stream()` 换轮询**(census G20):`hitl/approvalsFeed.ts` —— SDK 原生推送优先、
|
|
19
|
+
* 断流/无端点**回落轮询**的韧性臂保留;两条腿都收敛到「非 heartbeat 事件 ⇒ 重取权威
|
|
20
|
+
* `list()`」(SDK 自述 payload 是 FORWARD-DRAFT,绝不信未确认的 delta 形)。
|
|
21
|
+
* 🆕 队列口新增**可选**动词 `enqueueMetaPrompt`(plan_review 结局回植;既有宿主不破)。
|
|
8
22
|
* 0.8.0 = **B6 批**(设计稿 §3 B6,分两轮落):
|
|
9
23
|
* ——【前半,fleet/子代身份族】fleetClient 沿 §2.5 拆两半(帧体归库、连接归端):
|
|
10
24
|
* `fleet/fleetProjection` + `fleet/fleetLedger` · 子代 wire 族六件(`subagent/*`)·
|
|
@@ -80,6 +94,12 @@
|
|
|
80
94
|
* · liveInitToolFace.ts(`cached`)—— 两份只是多探一次 `/v1/capabilities/scenarios/:name`,
|
|
81
95
|
* **不是**静默失效;仍登记,免得下次读表的人以为它没状态。
|
|
82
96
|
* · model/providerPresets.ts(`presetIndexCache`)—— 由不可变 JSON 派生的纯 memo,两份只费内存。
|
|
97
|
+
* —— B7 新增(两条都是**装在一份、读另一份 = 面直接不工作**,比台账分裂更响):
|
|
98
|
+
* · **hitl/toolApprovalWire.ts**(`cardPort` + `cardPortMisses`)—— 写方 = 宿主启动装配
|
|
99
|
+
* `installApprovalCardPort()`,读方 = 两条决断腿。两份 ⇒ `hasApprovalCardPort()` 恒 false ⇒
|
|
100
|
+
* **每一张写权限 gate 都走 fail-closed deny(卡根本不弹)**,而宿主那边"我装了啊"。
|
|
101
|
+
* · **hitl/askGateWire.ts**(`hostSurface` + `hostSurfaceMisses`)—— 写方 = 宿主装配,
|
|
102
|
+
* 读方 = cancel-deny warn 与分类器 deny 记账。两份 ⇒ 那一行 warn 与 Recent Denials 全丢。
|
|
83
103
|
* 🔴 **宿主装配**——B4 起总入口是 `installHost({log,probe,queue,timers,settings,fs,session})`
|
|
84
104
|
* (`installNotificationQueuePort()` 仍是队列的真源出口,`installHost({queue})` 直通它,两者不分裂);
|
|
85
105
|
* 自检口 `hostPortMisses()` 恒应为空对象,非空 = 有口漏装、对应的面已在静默失效。
|
|
@@ -174,4 +194,9 @@ export * from './workflowClient.js';
|
|
|
174
194
|
export * from './hooksWireCaps.js';
|
|
175
195
|
export * from './liveInitToolFace.js';
|
|
176
196
|
export * from './model/providerPresets.js';
|
|
197
|
+
export * from './hitl/hitlBridge.js';
|
|
198
|
+
export * from './hitl/toolApprovalWire.js';
|
|
199
|
+
export * from './hitl/askGateWire.js';
|
|
200
|
+
export * from './hitl/planReviewWire.js';
|
|
201
|
+
export * from './hitl/approvalsFeed.js';
|
|
177
202
|
export * from './compensations.js';
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,20 @@
|
|
|
5
5
|
* 沿革:@sema-agent/wire-cc-adapter 0.1.0 = seam 类型 + id 确定性派生 + 首批踩坑纯函数;
|
|
6
6
|
* 0.1.2(#52a)= adapt() 管线首批(纯投影臂全落 + 壳态耦合臂投影成 ChromeEvent + 差分守卫);
|
|
7
7
|
* 0.2.0 = 迁入 sema-client-core 独立仓并改名(旧 npm 名 deprecate 指本包);
|
|
8
|
+
* 0.9.0 = **B7 批**(设计稿 §3 B7「完成事件单一权威 + HITL」):
|
|
9
|
+
* ① **HITL 族四件**(新目录 `hitl/`):`hitlBridge`(整搬,🔴 **D-1 两元组 verbatim 回显**
|
|
10
|
+
* 与「409 绝不自动重试」两段**逐字节**搬,pure 门 B7 段用冻结字面量 + 壳树双向对拍 +
|
|
11
|
+
* 毒化行为口径三重钉)· `toolApprovalWire`(**拆**:判定/编排进包,三选卡本体经
|
|
12
|
+
* `ApprovalCardPort` 留端)· `askGateWire`(整搬;footer 通知与 Recent Denials 记账经
|
|
13
|
+
* `HitlHostSurface` 留端)· `planReviewWire`(整搬,**第七份 `wireConfig()` 收编闭合**)。
|
|
14
|
+
* ② **完成事件族 T2/T3**:`taskNotificationDedupKeyFromWire`(裸 wire 载荷键形)+
|
|
15
|
+
* `parseTranscriptNotificationSeeds` / `seedTaskNotificationDedupFromTranscript`
|
|
16
|
+
* (#63 resume 回植的 XML 反解)搬入 `notifications.ts`。**P1-3 未到货 ⇒ 只搬不删**,
|
|
17
|
+
* 四道去重原样;退役条件登记进 `compensations.ts`(T1/T2/T3/T25/T53 五条)。
|
|
18
|
+
* ③ **`approvals.stream()` 换轮询**(census G20):`hitl/approvalsFeed.ts` —— SDK 原生推送优先、
|
|
19
|
+
* 断流/无端点**回落轮询**的韧性臂保留;两条腿都收敛到「非 heartbeat 事件 ⇒ 重取权威
|
|
20
|
+
* `list()`」(SDK 自述 payload 是 FORWARD-DRAFT,绝不信未确认的 delta 形)。
|
|
21
|
+
* 🆕 队列口新增**可选**动词 `enqueueMetaPrompt`(plan_review 结局回植;既有宿主不破)。
|
|
8
22
|
* 0.8.0 = **B6 批**(设计稿 §3 B6,分两轮落):
|
|
9
23
|
* ——【前半,fleet/子代身份族】fleetClient 沿 §2.5 拆两半(帧体归库、连接归端):
|
|
10
24
|
* `fleet/fleetProjection` + `fleet/fleetLedger` · 子代 wire 族六件(`subagent/*`)·
|
|
@@ -80,6 +94,12 @@
|
|
|
80
94
|
* · liveInitToolFace.ts(`cached`)—— 两份只是多探一次 `/v1/capabilities/scenarios/:name`,
|
|
81
95
|
* **不是**静默失效;仍登记,免得下次读表的人以为它没状态。
|
|
82
96
|
* · model/providerPresets.ts(`presetIndexCache`)—— 由不可变 JSON 派生的纯 memo,两份只费内存。
|
|
97
|
+
* —— B7 新增(两条都是**装在一份、读另一份 = 面直接不工作**,比台账分裂更响):
|
|
98
|
+
* · **hitl/toolApprovalWire.ts**(`cardPort` + `cardPortMisses`)—— 写方 = 宿主启动装配
|
|
99
|
+
* `installApprovalCardPort()`,读方 = 两条决断腿。两份 ⇒ `hasApprovalCardPort()` 恒 false ⇒
|
|
100
|
+
* **每一张写权限 gate 都走 fail-closed deny(卡根本不弹)**,而宿主那边"我装了啊"。
|
|
101
|
+
* · **hitl/askGateWire.ts**(`hostSurface` + `hostSurfaceMisses`)—— 写方 = 宿主装配,
|
|
102
|
+
* 读方 = cancel-deny warn 与分类器 deny 记账。两份 ⇒ 那一行 warn 与 Recent Denials 全丢。
|
|
83
103
|
* 🔴 **宿主装配**——B4 起总入口是 `installHost({log,probe,queue,timers,settings,fs,session})`
|
|
84
104
|
* (`installNotificationQueuePort()` 仍是队列的真源出口,`installHost({queue})` 直通它,两者不分裂);
|
|
85
105
|
* 自检口 `hostPortMisses()` 恒应为空对象,非空 = 有口漏装、对应的面已在静默失效。
|
|
@@ -217,6 +237,22 @@ export * from './liveInitToolFace.js';
|
|
|
217
237
|
// 来源。表不在包里,那条中央规则在 web/桌面上恒拿不到 cap ⇒ 对 deepseek-chat(8192)这类模型
|
|
218
238
|
// 超发 ⇒ 网关 400。搬迁期与壳树两份 JSON 由 pure 门的 byte-identity 断言锁住。
|
|
219
239
|
export * from './model/providerPresets.js';
|
|
240
|
+
// ── B7 批:HITL 族(2026-07-27,设计稿 §3 B7)────────────────────────────────────────────────
|
|
241
|
+
// 🔴 `hitl/hitlBridge.ts` 里的 **D-1 两元组 verbatim 回显**是安全不变量(`bindingOf` 绝不重算
|
|
242
|
+
// hash;409 ⇒ `HitlSafetyError('binding_mismatch')` 绝不自动重试)—— pure 门 B7 段做**字节级**
|
|
243
|
+
// 断言,改一个字符就红。
|
|
244
|
+
// 🔴 两个新拆缝口(缺席都计 miss,宿主自检恒应为 0):
|
|
245
|
+
// · `installApprovalCardPort` —— 三选卡本体(vendored CC `PermissionRequest` / 桌面模态);
|
|
246
|
+
// · `installHitlHostSurface` —— footer 通知与 /permissions Recent Denials 记账(端状态形状)。
|
|
247
|
+
// 🔴 单实例:`toolApprovalWire` 的卡口槽 + `askGateWire` 的宿主面槽是 module 级变量;两份实例 ⇒
|
|
248
|
+
// 装在一份、读另一份 = 每张 gate 都走 fail-closed deny(卡根本不弹)。
|
|
249
|
+
export * from './hitl/hitlBridge.js';
|
|
250
|
+
export * from './hitl/toolApprovalWire.js';
|
|
251
|
+
export * from './hitl/askGateWire.js';
|
|
252
|
+
export * from './hitl/planReviewWire.js';
|
|
253
|
+
// B7 ③(census G20,**行为改动**不是搬迁):pending-approvals 推送 feed(stream 优先 / 断流回落
|
|
254
|
+
// 轮询 / 定期再试)。🔴 它**不替换** D-1 的取件 —— 那三处必须继续走权威 `list()`(见文件头)。
|
|
255
|
+
export * from './hitl/approvalsFeed.js';
|
|
220
256
|
// ── B6 余项③(P5):补偿层登记表 —— 把「哪条补偿拆了、拆缝对面是谁、什么时候能退休」做成数据。
|
|
221
257
|
// 🔴 它**不是** `ADAPTER_DIVERGENCES`(那张表说的是 adapt 与 cli 行为不同的地方;本表里的东西
|
|
222
258
|
// 两侧行为相同)。自检口 `compensationSplitViolations()` 恒应为空。
|