@sema-agent/client-core 0.12.1 → 0.13.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 +15 -2
- package/dist/abortableSleep.d.ts +29 -0
- package/dist/abortableSleep.js +43 -0
- package/dist/adapt/arms.d.ts +58 -0
- package/dist/adapt/arms.js +613 -0
- package/dist/adapt/ids.d.ts +22 -0
- package/dist/adapt/ids.js +34 -0
- package/dist/adapt/instanceLedger.d.ts +36 -0
- package/dist/adapt/instanceLedger.js +50 -0
- package/dist/adapt/panelTasks.d.ts +54 -0
- package/dist/adapt/panelTasks.js +193 -0
- package/dist/adapt/textStream.d.ts +49 -0
- package/dist/adapt/textStream.js +141 -0
- package/dist/adapt/toolCards.d.ts +65 -0
- package/dist/adapt/toolCards.js +100 -0
- package/dist/adapt/turnFlags.d.ts +33 -0
- package/dist/adapt/turnFlags.js +55 -0
- package/dist/adapt/wireShapes.d.ts +93 -0
- package/dist/adapt/wireShapes.js +167 -0
- package/dist/adapt.d.ts +32 -59
- package/dist/adapt.js +78 -1206
- package/dist/adapter/downstream/eventToSdkMessage.d.ts +55 -13
- package/dist/adapter/downstream/eventToSdkMessage.js +166 -106
- package/dist/adapter/downstream/terminalToSdkResult.js +149 -160
- package/dist/adapter/downstream/turnUsageToModelUsage.d.ts +36 -0
- package/dist/adapter/downstream/turnUsageToModelUsage.js +34 -6
- package/dist/adapter/runStream.d.ts +29 -4
- package/dist/adapter/runStream.js +129 -13
- package/dist/adapter/types.d.ts +2 -2
- package/dist/agentSession/backgroundView.js +4 -15
- package/dist/agentsWireCaps.d.ts +10 -6
- package/dist/agentsWireCaps.js +21 -7
- package/dist/argvFlagValue.d.ts +41 -0
- package/dist/argvFlagValue.js +69 -0
- package/dist/attachmentsWireCaps.d.ts +3 -2
- package/dist/attachmentsWireCaps.js +5 -3
- package/dist/classifierVerdictWire.d.ts +8 -0
- package/dist/classifierVerdictWire.js +8 -0
- package/dist/cloudConfigWireCaps.d.ts +28 -1
- package/dist/cloudConfigWireCaps.js +51 -11
- package/dist/controlRouter.d.ts +14 -8
- package/dist/controlRouter.js +15 -21
- package/dist/detachWire.d.ts +15 -5
- package/dist/detachWire.js +17 -7
- package/dist/effortWire.d.ts +0 -21
- package/dist/effortWire.js +6 -20
- package/dist/engineInlineTaskStats.d.ts +12 -6
- package/dist/engineWireSdk.d.ts +12 -0
- package/dist/env/localeGeo.js +2 -1
- package/dist/envFlag.d.ts +39 -0
- package/dist/envFlag.js +51 -0
- package/dist/finalVerifyWire.d.ts +7 -5
- package/dist/finalVerifyWire.js +6 -5
- package/dist/fleet/fleetLedger.d.ts +32 -9
- package/dist/fleet/fleetLedger.js +119 -34
- package/dist/fleet/fleetProjection.d.ts +44 -6
- package/dist/fleet/fleetProjection.js +52 -10
- package/dist/fleetAgentPanelProjection.d.ts +18 -1
- package/dist/fleetAgentPanelProjection.js +61 -14
- package/dist/forkWireCaps.d.ts +2 -1
- package/dist/forkWireCaps.js +5 -11
- package/dist/goalStopHook.d.ts +142 -0
- package/dist/goalStopHook.js +258 -0
- package/dist/headlessPermissionModeWire.d.ts +10 -0
- package/dist/headlessPermissionModeWire.js +24 -21
- package/dist/headlessReconnectWire.d.ts +7 -1
- package/dist/headlessReconnectWire.js +20 -2
- package/dist/hitl/approvalsFeed.js +31 -8
- package/dist/hitl/askGateWire.d.ts +27 -96
- package/dist/hitl/askGateWire.js +69 -546
- package/dist/hitl/frameRouter.d.ts +86 -0
- package/dist/hitl/frameRouter.js +342 -0
- package/dist/hitl/gateLedger.d.ts +107 -0
- package/dist/hitl/gateLedger.js +113 -0
- package/dist/hitl/hitlBridge.d.ts +75 -15
- package/dist/hitl/hitlBridge.js +94 -22
- package/dist/hitl/hitlHostSurface.d.ts +49 -0
- package/dist/hitl/hitlHostSurface.js +155 -0
- package/dist/hitl/parkResolver.d.ts +74 -0
- package/dist/hitl/parkResolver.js +241 -0
- package/dist/hitl/planReviewWire.d.ts +60 -2
- package/dist/hitl/planReviewWire.js +152 -74
- package/dist/hitl/toolApprovalWire.d.ts +67 -4
- package/dist/hitl/toolApprovalWire.js +119 -31
- package/dist/hooksWireCaps.d.ts +1 -82
- package/dist/hooksWireCaps.js +34 -235
- package/dist/host.d.ts +16 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.js +15 -1
- package/dist/interactiveToolsWire.d.ts +15 -4
- package/dist/interactiveToolsWire.js +24 -26
- package/dist/limitsWire.js +7 -40
- package/dist/liveInitToolFace.d.ts +51 -6
- package/dist/liveQuestionStore.d.ts +5 -6
- package/dist/model/providerPresets.js +11 -1
- package/dist/notifications.d.ts +48 -2
- package/dist/notifications.js +223 -46
- package/dist/retainBackgroundWireCaps.d.ts +3 -2
- package/dist/retainBackgroundWireCaps.js +5 -9
- package/dist/sandboxWire.d.ts +9 -31
- package/dist/sandboxWire.js +51 -50
- package/dist/scenarioWire.d.ts +1 -1
- package/dist/scenarioWire.js +25 -36
- package/dist/seam.d.ts +23 -6
- package/dist/seam.js +40 -30
- package/dist/seatContract.d.ts +369 -83
- package/dist/seatContract.js +585 -198
- package/dist/selfOrchestrationWireCaps.d.ts +6 -5
- package/dist/selfOrchestrationWireCaps.js +8 -12
- package/dist/sessionSlot.d.ts +8 -0
- package/dist/sessionSlot.js +1 -0
- package/dist/steering.js +2 -2
- package/dist/subagent/engineTaskHandleWire.d.ts +3 -0
- package/dist/subagent/engineTaskHandleWire.js +17 -2
- package/dist/subagentContentStore.d.ts +5 -5
- package/dist/toolResult.d.ts +89 -8
- package/dist/toolResult.js +99 -30
- package/dist/typePins.d.ts +17 -0
- package/dist/typePins.js +1 -0
- package/dist/ultracodeWireCaps.js +5 -6
- package/dist/unrefTimer.d.ts +19 -0
- package/dist/unrefTimer.js +5 -0
- package/dist/workflow.d.ts +3 -2
- package/dist/workflow.js +3 -2
- package/dist/workflowClient.d.ts +7 -0
- package/dist/workflowClient.js +47 -12
- package/package.json +3 -3
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* frameRouter — HITL gate 桥**内环**的帧路由(REF-CC-032 / SPLIT-12 第二刀,2026-08-02)。
|
|
3
|
+
*
|
|
4
|
+
* 一帧进、一个 `FrameAction` 出:路由本身不 yield、不读流、不决断,只把「这一帧该怎么处置」表达成
|
|
5
|
+
* 数据。驱动(`askGateWire.bridgeAskUserQuestionGates`)照 action 吐帧 / 结束 / park,外环决断在
|
|
6
|
+
* `parkResolver.ts`。这样内外环对台账的写全部经 `GateLedger` 的动词,不再各自直写同一批容器。
|
|
7
|
+
*
|
|
8
|
+
* 🔴 本文件同时是两把刀的**共享词汇层**:`AskGateWireDeps` / `isAskTool` / 两个 CC 逐字常量 /
|
|
9
|
+
* park 形状谓词都落在这里 —— 它是「内环路由」与「外环决断」都够得着的最低模块(`parkResolver.ts`
|
|
10
|
+
* import 本文件,反向没有边,不成环)。`askGateWire.ts` 原样 re-export 其中的公开面,对外零变化。
|
|
11
|
+
*
|
|
12
|
+
* 🔴 tool_end 的**有序**臂(拆分硬约束):原文 5 条分支的顺序是承重的 ——
|
|
13
|
+
* 分类器 deny(结构性机器签名,比「fs 写报错」更特定)必须先于 fs 的 HOLD/REJECT 分支判。
|
|
14
|
+
* 注册表化之后顺序不再由代码位置显式表达,所以 `TOOL_END_ARM_ORDER` 把顺序写成**数据**,
|
|
15
|
+
* 模块加载期一条断言把两者钉死:重排 `TOOL_END_ARMS` 而不同批改 `TOOL_END_ARM_ORDER` ⇒ 当场抛。
|
|
16
|
+
*/
|
|
17
|
+
import type { AgentEvent } from '@sema-agent/sdk';
|
|
18
|
+
import type { HitlClientLike } from './hitlBridge.js';
|
|
19
|
+
import { type RespondToolApprovalFn } from './toolApprovalWire.js';
|
|
20
|
+
import type { GateLedger } from './gateLedger.js';
|
|
21
|
+
/**
|
|
22
|
+
* CC `utils/messages.ts` 的 `REJECT_MESSAGE` **逐字**(B7 搬迁差分 2)。deny 后重放 tool_end 的
|
|
23
|
+
* render 面 stamp 用 —— vendored `renderToolUseRejectedMessage` 渲 `User rejected <op> to <path>`。
|
|
24
|
+
* 🔴 pure 门 B7 段:包内冻结字面量(无条件)+ 壳树全树扫描「每一处声明都逐字节相同」(壳树缺席=跳过)。
|
|
25
|
+
*/
|
|
26
|
+
export declare const HITL_REJECT_MESSAGE = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.";
|
|
27
|
+
/** core 对被 gate/连坐 abort 的 call 铸的 tool_end 载体(逐字;desktop session-host 真引擎实测
|
|
28
|
+
* 同款)——HOLD 谓词锚它做**精确等值**,普通工具错的输出是各自错误文案,永不进 HOLD。 */
|
|
29
|
+
export declare const ENGINE_ABORT_TOOL_RESULT = "Operation aborted";
|
|
30
|
+
/** 本桥消费的 wire 面(@sema-agent/sdk AgentClient 的结构切片,mock 可注入)。 */
|
|
31
|
+
export interface AskGateWireDeps {
|
|
32
|
+
/** approvals.list/decide + assistant(HitlBridge 的 client 切片)。 */
|
|
33
|
+
client: HitlClientLike;
|
|
34
|
+
/** GET /v1/runs/:id/events — decide 后的续流 attach(lastEventId 续传)。 */
|
|
35
|
+
runsEvents: (taskId: string, opts?: {
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
lastEventId?: string;
|
|
38
|
+
}) => AsyncGenerator<AgentEvent>;
|
|
39
|
+
/** POST /v1/tool-approvals/:id/respond(server 1.191 同步帧腿,[830]①)。缺省=不消费
|
|
40
|
+
* tool_approval 帧(帧被吞、引擎按自身 fail-closed TTL 自决)——mock/旧引擎路径零影响。 */
|
|
41
|
+
respondToolApproval?: RespondToolApprovalFn;
|
|
42
|
+
}
|
|
43
|
+
/** 两族 gate:AskUserQuestion 走问答 overlay(原路);其余(fs 写三件 / Bash / 一等
|
|
44
|
+
* `kind==='tool_approval'`)走 CC 三选卡。 */
|
|
45
|
+
export type GateKind = 'ask' | 'fs';
|
|
46
|
+
/** park 请求:内环遇 gate 时带出去给外环决断。 */
|
|
47
|
+
export interface GatePark {
|
|
48
|
+
gate: GateKind;
|
|
49
|
+
/** sync leg 的 `done{suspended}` 终帧 —— fail-soft 回退时原样吐回去(durable leg 的 park 无此帧)。 */
|
|
50
|
+
pendingDone?: AgentEvent | undefined;
|
|
51
|
+
}
|
|
52
|
+
/** 帧路由的产物:驱动只认这四种,自己不再判帧型。 */
|
|
53
|
+
export type FrameAction = {
|
|
54
|
+
kind: 'yield';
|
|
55
|
+
events: readonly AgentEvent[];
|
|
56
|
+
} | {
|
|
57
|
+
kind: 'skip';
|
|
58
|
+
} | {
|
|
59
|
+
kind: 'park';
|
|
60
|
+
gate: GateKind;
|
|
61
|
+
pendingDone?: AgentEvent | undefined;
|
|
62
|
+
} | {
|
|
63
|
+
kind: 'end';
|
|
64
|
+
events: readonly AgentEvent[];
|
|
65
|
+
};
|
|
66
|
+
/** 跨刀共享的可变标量(`realTaskId`):sync leg 从 `done{suspended}` 捕获、外环 attach 时读。
|
|
67
|
+
* 显式 ref 传参而不是让两把刀各存一份 —— 两份会在 durable 腿上分叉。 */
|
|
68
|
+
export interface TaskIdRef {
|
|
69
|
+
current: string | undefined;
|
|
70
|
+
}
|
|
71
|
+
export interface FrameRouterCtx {
|
|
72
|
+
led: GateLedger;
|
|
73
|
+
deps: AskGateWireDeps;
|
|
74
|
+
taskId: TaskIdRef;
|
|
75
|
+
signal?: AbortSignal | undefined;
|
|
76
|
+
}
|
|
77
|
+
/** 上者的**窄口**(`findPendingForTask` 的 gate 过滤谓词签名)—— 出境的那一版不吃 `unknown`:
|
|
78
|
+
* 边界层的 unknown 在本模块里窄化完再跨模块(typeshape 门 B2 的纪律)。 */
|
|
79
|
+
export declare function isAskTool(toolName: string | undefined): boolean;
|
|
80
|
+
/** 这一帧是不是「host lane 的模型推进」= 扣留帧的放行信号。 */
|
|
81
|
+
export declare function isHostProgressFrame(ev: AgentEvent): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* 一帧 → 一个处置。手柄的**先后**与原文逐条同序:tool_approval 帧 → tool_start → tool_end →
|
|
84
|
+
* suspended → done → failed → 透传。
|
|
85
|
+
*/
|
|
86
|
+
export declare function routeFrame(ev: AgentEvent, ctx: FrameRouterCtx): Promise<FrameAction>;
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { hostLog } from '../host.js';
|
|
2
|
+
import { isToolApprovalGate, toolNameIsFsWrite, toolNameIsShellExec, isToolApprovalFrame, isFromSubagent, surfaceToolApprovalFrameAndRespond, } from './toolApprovalWire.js';
|
|
3
|
+
import { classifierDenyFromToolEnd } from '../classifierVerdictWire.js';
|
|
4
|
+
import { surfaceForCurrentSession } from './hitlHostSurface.js';
|
|
5
|
+
/**
|
|
6
|
+
* CC `utils/messages.ts` 的 `REJECT_MESSAGE` **逐字**(B7 搬迁差分 2)。deny 后重放 tool_end 的
|
|
7
|
+
* render 面 stamp 用 —— vendored `renderToolUseRejectedMessage` 渲 `User rejected <op> to <path>`。
|
|
8
|
+
* 🔴 pure 门 B7 段:包内冻结字面量(无条件)+ 壳树全树扫描「每一处声明都逐字节相同」(壳树缺席=跳过)。
|
|
9
|
+
*/
|
|
10
|
+
export const HITL_REJECT_MESSAGE = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.";
|
|
11
|
+
function rejectMessageForRender() {
|
|
12
|
+
return HITL_REJECT_MESSAGE;
|
|
13
|
+
}
|
|
14
|
+
/** core 对被 gate/连坐 abort 的 call 铸的 tool_end 载体(逐字;desktop session-host 真引擎实测
|
|
15
|
+
* 同款)——HOLD 谓词锚它做**精确等值**,普通工具错的输出是各自错误文案,永不进 HOLD。 */
|
|
16
|
+
export const ENGINE_ABORT_TOOL_RESULT = 'Operation aborted';
|
|
17
|
+
// ── gate 词汇(两把刀共享)──────────────────────────────────────────────────────────────────────
|
|
18
|
+
/** 工具名是不是 AskUserQuestion(空白/下划线/连字符与大小写都不敏感)。
|
|
19
|
+
* **宽口**:帧上的 `toolName` 位是 UNTRUSTED 的任意值,判据自己吃 `unknown`,本模块内部用。 */
|
|
20
|
+
function isAskToolLoose(name) {
|
|
21
|
+
return typeof name === 'string' && name.replace(/[\s_-]+/g, '').toLowerCase() === 'askuserquestion';
|
|
22
|
+
}
|
|
23
|
+
/** 上者的**窄口**(`findPendingForTask` 的 gate 过滤谓词签名)—— 出境的那一版不吃 `unknown`:
|
|
24
|
+
* 边界层的 unknown 在本模块里窄化完再跨模块(typeshape 门 B2 的纪律)。 */
|
|
25
|
+
export function isAskTool(toolName) {
|
|
26
|
+
return isAskToolLoose(toolName);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 「这个工具名归 gate 管」的**统一**判据(#110 修,2026-08-02)。
|
|
30
|
+
*
|
|
31
|
+
* 🔴 它必须与 park 判定(`isAskTool` / `isToolApprovalGate`)覆盖**同一个集合** ——
|
|
32
|
+
* 两边不同集就是 [paired-mechanisms-must-share-premise] 那种「park 认得出、HOLD 认不出」的
|
|
33
|
+
* 半场病:[2150] S1 给 park 判定加了 `toolNameIsShellExec`(Bash/shell 类进 gate 了),但
|
|
34
|
+
* tool_end 手柄的 HOLD/REJECT 谓词还停在 `isAskTool || toolNameIsFsWrite` ——
|
|
35
|
+
* 于是 Bash 的 durable gate 走到这里就两件事同时出错:
|
|
36
|
+
* ① park 期的毒化帧(`tool_end{isError:true, output:"Operation aborted"}`)没被 HOLD,
|
|
37
|
+
* **当场上屏**;用户按了 Yes、命令也真跑了,屏幕上却永远停在 `Error: Operation aborted`;
|
|
38
|
+
* ② 更狠的是紧接着的 `markEnded(toolCallId)` —— resume 后引擎重放的**真** tool_end
|
|
39
|
+
* 被手柄开头的 `isEnded` 早退静默去重,真结果一帧都上不了屏。
|
|
40
|
+
* 附带第三件:deny 腿的 `User rejected …` 文案对 Bash 同样进不去,用户看到的是引擎原文。
|
|
41
|
+
* 证据链见 cli 仓 design/cli-164 T4-A(硬 oracle=磁盘状态:按 Yes 后 printf +504ms 真落盘)。
|
|
42
|
+
*/
|
|
43
|
+
function isGatedToolName(name) {
|
|
44
|
+
if (isAskToolLoose(name))
|
|
45
|
+
return true;
|
|
46
|
+
if (typeof name !== 'string')
|
|
47
|
+
return false;
|
|
48
|
+
return toolNameIsFsWrite(name) || toolNameIsShellExec(name);
|
|
49
|
+
}
|
|
50
|
+
/** done 帧的 AskUserQuestion park 形状(结构性读;别的终态一律 false)。 */
|
|
51
|
+
function isAskGatePark(result) {
|
|
52
|
+
const r = result;
|
|
53
|
+
return (!!r &&
|
|
54
|
+
r.status === 'suspended' &&
|
|
55
|
+
isAskToolLoose(r.checkpointGate?.toolName) &&
|
|
56
|
+
typeof r.taskId === 'string' &&
|
|
57
|
+
r.taskId.length > 0);
|
|
58
|
+
}
|
|
59
|
+
/** done 帧的工具审批 park 形状([816] 放宽腿 + [2150] Bash 半场):status suspended +
|
|
60
|
+
* checkpointGate 指 fs 写三件 / Bash / 一等 kind==='tool_approval'(超集谓词 isToolApprovalGate,
|
|
61
|
+
* decide 腿对两族同构、卡形归壳分发)。Ask gate 恒先判(问答 overlay 原路)。 */
|
|
62
|
+
function isFsApprovalPark(result) {
|
|
63
|
+
const r = result;
|
|
64
|
+
return (!!r &&
|
|
65
|
+
r.status === 'suspended' &&
|
|
66
|
+
typeof r.taskId === 'string' &&
|
|
67
|
+
r.taskId.length > 0 &&
|
|
68
|
+
!isAskToolLoose(r.checkpointGate?.toolName) &&
|
|
69
|
+
isToolApprovalGate(r.checkpointGate));
|
|
70
|
+
}
|
|
71
|
+
/** 台账里那份 gated `tool_start` 当拍的 args 快照(UNTRUSTED,只搬运不解释)。 */
|
|
72
|
+
function argsOfGatedStart(led, callId) {
|
|
73
|
+
if (callId === undefined)
|
|
74
|
+
return undefined;
|
|
75
|
+
return led.gatedStartArgs().get(callId);
|
|
76
|
+
}
|
|
77
|
+
// ── [2084]① 扣留帧「模型推进即放行」(2026-07-30;同日 web 复审纠偏收窄触发集)────────────────
|
|
78
|
+
//
|
|
79
|
+
// HOLD 只护 park 窗口。放行触发集 = host-lane **模型推进**帧:text_delta/reasoning_delta/
|
|
80
|
+
// tool_start —— park 路径上这三种在 done{suspended} 之前不会出现,真错路径上下一轮必来其一。
|
|
81
|
+
// 🔴 turn_end 明确**不在**触发集(初版在,web 复审真机轨迹逮红):core agent-loop 的 turn_end
|
|
82
|
+
// emit 在 executeToolCalls 之后**无 abort/suspend 守卫**(邻臂都有,唯此一处没有)⇒ 真 park
|
|
83
|
+
// 的帧序是 tool_end(毒化)→ message_committed → turn_end → done{suspended},turn_end 若放行
|
|
84
|
+
// 会把毒化帧漏上屏,且 flushHeld 的 ended 记账让 decide 后重放的解答帧被当已收口丢弃
|
|
85
|
+
// (真实答案卡永不出现)。代价 = turn 末尾无后续推进帧的 abort 形帧等到终帧 flush,与修前同。
|
|
86
|
+
// parentToolCallId 在场 = sub-agent 行帧,park 期间孙代照常说话,不算 host 推进(负控①C)。
|
|
87
|
+
// (普通工具错自 [2084]①-b 谓词收窄后根本不进 HOLD,零滞后不依赖本触发集。)
|
|
88
|
+
/** 这一帧是不是「host lane 的模型推进」= 扣留帧的放行信号。 */
|
|
89
|
+
export function isHostProgressFrame(ev) {
|
|
90
|
+
return ((ev.type === 'text_delta' || ev.type === 'reasoning_delta' || ev.type === 'tool_start') &&
|
|
91
|
+
ev.parentToolCallId === undefined);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 🔴 **有序**(承重):分类器批3([907])的注释原文 = 「结构性识别(core 机器签名)优先于 fs
|
|
95
|
+
* HOLD/REJECT 分支(签名比"fs 写报错"更特定;分类器 deny 从不产 tool_approval 帧,denied /
|
|
96
|
+
* denyStamp 与之互斥)」。顺序作为数据写在这里,下方加载期断言把它与 `TOOL_END_ARMS` 钉死。
|
|
97
|
+
*/
|
|
98
|
+
const TOOL_END_ARM_ORDER = [
|
|
99
|
+
'classifier-deny',
|
|
100
|
+
'deny-stamp-next',
|
|
101
|
+
'denied-call',
|
|
102
|
+
'hold-poison',
|
|
103
|
+
'generic-close',
|
|
104
|
+
];
|
|
105
|
+
/**
|
|
106
|
+
* tool_end 的**有序**臂表:自上而下第一个返回动作的臂胜出。顺序 = 原文 5 条分支的书写顺序,
|
|
107
|
+
* 由 `TOOL_END_ARM_ORDER` + 下方加载期断言锁死(改这张表的顺序必须同批改那张表)。
|
|
108
|
+
* 🔴 表尾 `generic-close` 是**无条件**兜底臂 —— 给它加早退条件 = tool_end 会被整帧静默丢弃。
|
|
109
|
+
*/
|
|
110
|
+
const TOOL_END_ARMS = [
|
|
111
|
+
{
|
|
112
|
+
// ① 分类器 deny 裁决帧:副作用 = CC 207 通知行 + Recent Denials 记账;render 面 stamp 去
|
|
113
|
+
// system-reminder 包裹渲裁决原文(render-only,模型面原文不动)。
|
|
114
|
+
id: 'classifier-deny',
|
|
115
|
+
run(ev, callId, led) {
|
|
116
|
+
if (ev.isError !== true || led.isDenied(callId))
|
|
117
|
+
return undefined;
|
|
118
|
+
const verdict = classifierDenyFromToolEnd(ev);
|
|
119
|
+
if (!verdict)
|
|
120
|
+
return undefined;
|
|
121
|
+
led.markEnded(callId);
|
|
122
|
+
led.dropHeld(callId);
|
|
123
|
+
surfaceForCurrentSession()?.surfaceClassifierDeny(typeof ev.toolName === 'string' ? ev.toolName : 'tool', verdict);
|
|
124
|
+
hostLog('debug', `liveHitlAskWire: classifier deny verdict on ${callId} (${String(ev.toolName)}) — labeled render + notice`);
|
|
125
|
+
return { kind: 'yield', events: [{ ...ev, output: verdict.message, structured: undefined }] };
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
// ② 同步帧腿 deny 且 callId 没关联上:下一张 gated 报错帧即该 gate 的收口帧,stamp REJECT 文案。
|
|
130
|
+
// #110:stamp 也要盖 shell —— 否则按 No 时用户看到的是引擎原文 `Operation aborted`。
|
|
131
|
+
id: 'deny-stamp-next',
|
|
132
|
+
run(ev, callId, led) {
|
|
133
|
+
if (ev.isError !== true || !isGatedToolName(ev.toolName) || led.isDenied(callId))
|
|
134
|
+
return undefined;
|
|
135
|
+
if (!led.takeDenyStamp())
|
|
136
|
+
return undefined;
|
|
137
|
+
led.markEnded(callId);
|
|
138
|
+
led.dropHeld(callId);
|
|
139
|
+
return { kind: 'yield', events: [{ ...ev, output: rejectMessageForRender(), structured: undefined }] };
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
// ③ 三选卡 No:重放的报错帧 stamp REJECT 文案 → vendored 卡渲 `User rejected <op> to <path>`。
|
|
144
|
+
id: 'denied-call',
|
|
145
|
+
run(ev, callId, led) {
|
|
146
|
+
if (ev.isError !== true || !isGatedToolName(ev.toolName))
|
|
147
|
+
return undefined;
|
|
148
|
+
if (!led.takeDenied(callId))
|
|
149
|
+
return undefined;
|
|
150
|
+
led.markEnded(callId);
|
|
151
|
+
led.dropHeld(callId);
|
|
152
|
+
return { kind: 'yield', events: [{ ...ev, output: rejectMessageForRender(), structured: undefined }] };
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
// ④ [2084]①-b HOLD 谓词收窄(2026-07-30,desktop 复审建议采纳):park 毒化帧的载体是**确切的
|
|
157
|
+
// 引擎 abort 标记**(core 对被 gate/连坐 abort 的 call 铸 `tool_end{isError:true,
|
|
158
|
+
// output:"Operation aborted"}`;desktop session-host 2026-07-29 对真引擎实测同款)。普通
|
|
159
|
+
// 工具错(old_string 不命中/read-gate 拒绝…)输出是各自的错误文案,**根本不该进 HOLD**
|
|
160
|
+
// —— 收窄后它们零滞后直达转录(落到 ⑤ 通用收口),不再依赖「推进信号放行」。
|
|
161
|
+
id: 'hold-poison',
|
|
162
|
+
run(ev, callId, led) {
|
|
163
|
+
if (ev.isError !== true || !isGatedToolName(ev.toolName))
|
|
164
|
+
return undefined;
|
|
165
|
+
if (ev.output !== ENGINE_ABORT_TOOL_RESULT)
|
|
166
|
+
return undefined;
|
|
167
|
+
led.hold(callId, ev); // park 毒化帧,先 HOLD(gate 定性前不渲 Error 卡)
|
|
168
|
+
return { kind: 'skip' };
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
// ⑤ 通用收口(**无条件命中**,兜底臂):正常收口(重放的 isError:false 解答帧)赢;已作答的
|
|
173
|
+
// gate 在这里把真实答案 stamp 回 `structured`,否则卡片渲成结果不可用。
|
|
174
|
+
id: 'generic-close',
|
|
175
|
+
run(ev, callId, led) {
|
|
176
|
+
led.markEnded(callId);
|
|
177
|
+
led.dropHeld(callId);
|
|
178
|
+
const answered = led.answerFor(callId);
|
|
179
|
+
if (answered && ev.isError !== true && ev.structured === undefined) {
|
|
180
|
+
led.dropAnswer(callId);
|
|
181
|
+
return { kind: 'yield', events: [{ ...ev, structured: answered }] }; // 真实答案卡
|
|
182
|
+
}
|
|
183
|
+
return { kind: 'yield', events: [ev] };
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
];
|
|
187
|
+
// 🔴 顺序锁(SPLIT-12 硬约束):注册表化把「顺序」从代码位置搬成了数据,于是顺序本身需要一条断言。
|
|
188
|
+
// 加载期求值 —— 重排 `TOOL_END_ARMS` 而没同批改 `TOOL_END_ARM_ORDER` 的那次 import 当场抛,
|
|
189
|
+
// 而不是等到某个 Bash gate 的毒化帧在用户屏上停住才发现。
|
|
190
|
+
{
|
|
191
|
+
const actual = TOOL_END_ARMS.map(a => a.id).join(' > ');
|
|
192
|
+
const expected = TOOL_END_ARM_ORDER.join(' > ');
|
|
193
|
+
if (actual !== expected) {
|
|
194
|
+
throw new Error(`frameRouter: tool_end 有序臂被重排(实际 ${actual} ≠ 登记 ${expected})—— ` +
|
|
195
|
+
'这五条分支的先后是承重的(分类器 deny 的结构性签名必须先于 fs 的 HOLD/REJECT 判),' +
|
|
196
|
+
'改顺序必须同批改 TOOL_END_ARM_ORDER 并说明为什么新顺序仍然对。');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// ── 帧手柄 ────────────────────────────────────────────────────────────────────────────────────
|
|
200
|
+
async function routeToolApprovalFrame(ev, ctx) {
|
|
201
|
+
// ── server 1.191 同步帧腿([830]①):`tool_approval` 帧 → 三选卡 → respond ────────────────
|
|
202
|
+
// 引擎此刻同步阻塞在帧上(fail-closed:无 respond/TTL 5min/abort ⇒ deny),await 卡决断安全。
|
|
203
|
+
// 帧是 HITL 词汇非 AgentEvent arm——两种帧都不外泄到渲染管道。
|
|
204
|
+
const { led, deps } = ctx;
|
|
205
|
+
if (ev.type === 'tool_approval_complete')
|
|
206
|
+
return { kind: 'skip' }; // 收口帧:卡已由决断路径撤下,吞掉
|
|
207
|
+
if (!deps.respondToolApproval) {
|
|
208
|
+
hostLog('debug', `liveHitlAskWire: tool_approval frame ${ev.approvalId} but no respond wire — engine self-settles (TTL deny)`);
|
|
209
|
+
return { kind: 'skip' };
|
|
210
|
+
}
|
|
211
|
+
// 补验批 askq 案定谳(2026-07-23,黑板 [1534],引擎 dist 逐坐标读定):core 的 durable
|
|
212
|
+
// question policy 对 AskUserQuestion 出 action:"ask",本意让 durable gate park(suspended
|
|
213
|
+
// → 下方 bridge 弹卡收答);但 core suspendAsk 的退位条件(safety 缺席 + onAsk 在场)
|
|
214
|
+
// 把问题门也抢进了同步审批帧腿(壳为 fs 写权限三选卡开的 TOOL_APPROVAL_ENABLED)——同步帧
|
|
215
|
+
// 二值 respond 装不下「答案」,链路死锁:deny=模型误信问题已展示原地等;allow=执行工具
|
|
216
|
+
// 本体炸 durable 装配的 onQuestion 占位(QUESTION_AWAITS_RESUME)。此处取两害之轻恒 allow:
|
|
217
|
+
// 模型收到明确错误后退化成正文提问,用户尚可下一条消息作答(deny 形连这条退化路都没有)。
|
|
218
|
+
// 承重域收敛史([1543]§一 cli 注意):core 1.375 修 question gate 恒 park(健康路),
|
|
219
|
+
// 1.376 又断了降级路(checkpoint pre-commit/put 失败 fallback 改 typed refusal)与继承链
|
|
220
|
+
// 两臂——1.376+ 下 ask 工具在任何路径都不再出 tool_approval 帧,本分支唯一剩余承重
|
|
221
|
+
// = 旧引擎(<1.375)兼容,askq pty fence @1.375+ 已四门全绿实证([1542])。
|
|
222
|
+
if (isAskToolLoose(ev.toolName)) {
|
|
223
|
+
try {
|
|
224
|
+
await deps.respondToolApproval(ev.approvalId, 'allow', {
|
|
225
|
+
...(ctx.signal && !ctx.signal.aborted ? { signal: ctx.signal } : {}),
|
|
226
|
+
});
|
|
227
|
+
hostLog('debug', `liveHitlAskWire: ask-tool approval frame ${ev.approvalId} → allow(问题门放行,候 park 弹卡)`);
|
|
228
|
+
}
|
|
229
|
+
catch (e) {
|
|
230
|
+
hostLog('debug', `liveHitlAskWire: ask-tool approval allow failed for ${ev.approvalId}: ${String(e)} — engine self-settles (TTL)`);
|
|
231
|
+
}
|
|
232
|
+
return { kind: 'skip' };
|
|
233
|
+
}
|
|
234
|
+
// [1535]/server 1.258:子代的审批帧(broker 腿浮到宿主流,判别=fromSubagent 显式键
|
|
235
|
+
// core 1.378 优先/sourceTaskId 在场性兜底)——子代的 tool_start 不在宿主行流上,绝不把
|
|
236
|
+
// 宿主自己的 pending fs call 关联给它(args 会错配成宿主的 diff、deny 会误标宿主 call 的
|
|
237
|
+
// REJECT 收口)。args 从帧上取,deny 收口归子代 lane 自理。
|
|
238
|
+
const fromSubagent = isFromSubagent(ev);
|
|
239
|
+
hostLog('debug', `liveHitlAskWire: tool_approval frame ${ev.approvalId} tool=${String(ev.toolName)}${fromSubagent ? ` from-subagent=${String(ev.sourceTaskId ?? ev.sourceAgentName ?? 'explicit')}` : ''}`);
|
|
240
|
+
const gatedCallId = !fromSubagent ? led.lastPendingFsCall() : undefined;
|
|
241
|
+
const decision = await surfaceToolApprovalFrameAndRespond(ev, deps.respondToolApproval, argsOfGatedStart(led, gatedCallId), ctx.signal);
|
|
242
|
+
if (decision === 'deny' && !fromSubagent) {
|
|
243
|
+
// 该 call 的报错收口帧 stamp REJECT 文案(vendored `User rejected <op> to <path>` 卡)。
|
|
244
|
+
if (gatedCallId !== undefined)
|
|
245
|
+
led.markDenied(gatedCallId);
|
|
246
|
+
else
|
|
247
|
+
led.armDenyStamp();
|
|
248
|
+
}
|
|
249
|
+
return { kind: 'skip' };
|
|
250
|
+
}
|
|
251
|
+
function routeToolStart(ev, callId, led) {
|
|
252
|
+
if (!led.markStarted(callId))
|
|
253
|
+
return { kind: 'skip' }; // durable 重放的已渲 call
|
|
254
|
+
if (isGatedToolName(ev.toolName)) {
|
|
255
|
+
// #110:Bash 也要记 args —— 卡片渲染要拿到 `command`,不记则 shell gate 的卡面空着。
|
|
256
|
+
led.noteGatedStart(callId, ev);
|
|
257
|
+
if (typeof ev.toolName === 'string' && toolNameIsFsWrite(ev.toolName))
|
|
258
|
+
led.pushFsCall(callId);
|
|
259
|
+
// REF-CC-034:`park.gate==='fs'` 分支覆盖 fs 写 + shell 执行两族(isToolApprovalGate
|
|
260
|
+
// 超集谓词)——`lastFsOrShellGatedCallId` 两族都要跟,否则 Bash gate 的已决断身份
|
|
261
|
+
// 候选恒 undefined。与 `pushFsCall` 不同,它不在 tool_end 到达时清空(见台账声明处注)。
|
|
262
|
+
if (typeof ev.toolName === 'string' && (toolNameIsFsWrite(ev.toolName) || toolNameIsShellExec(ev.toolName))) {
|
|
263
|
+
led.noteFsOrShellGate(callId);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return { kind: 'yield', events: [ev] };
|
|
267
|
+
}
|
|
268
|
+
function routeToolEnd(ev, callId, led) {
|
|
269
|
+
if (led.isEnded(callId))
|
|
270
|
+
return { kind: 'skip' }; // durable 重放/park 复写的已收口 call
|
|
271
|
+
led.dropFsCall(callId);
|
|
272
|
+
for (const arm of TOOL_END_ARMS) {
|
|
273
|
+
const action = arm.run(ev, callId, led);
|
|
274
|
+
if (action)
|
|
275
|
+
return action;
|
|
276
|
+
}
|
|
277
|
+
// 不可达:兜底臂 generic-close 无条件命中。真走到这里 = 有人给它加了早退条件,
|
|
278
|
+
// 那样 tool_end 会被整帧静默丢弃(用户看不到任何收口),必须当场喊出来而不是 `skip`。
|
|
279
|
+
throw new Error('frameRouter: tool_end 有序臂全部落空 —— generic-close 不再是无条件兜底臂');
|
|
280
|
+
}
|
|
281
|
+
function routeSuspended(ev, ctx) {
|
|
282
|
+
// durable leg 的 park arm(续流中模型再次提问/再次撞写权限)。gate 只带 kind/toolName,
|
|
283
|
+
// payload 走 approvals。
|
|
284
|
+
if (ev.gate && isAskToolLoose(ev.gate.toolName) && ctx.taskId.current)
|
|
285
|
+
return { kind: 'park', gate: 'ask' };
|
|
286
|
+
if (ev.gate &&
|
|
287
|
+
isToolApprovalGate(ev.gate) &&
|
|
288
|
+
ctx.taskId.current) {
|
|
289
|
+
return { kind: 'park', gate: 'fs' };
|
|
290
|
+
}
|
|
291
|
+
return { kind: 'yield', events: [ev] }; // 其余 gate:透传(现状,eventToSdkMessage 出 null)
|
|
292
|
+
}
|
|
293
|
+
function routeDone(ev, ctx) {
|
|
294
|
+
const { led } = ctx;
|
|
295
|
+
if (isAskGatePark(ev.result)) {
|
|
296
|
+
ctx.taskId.current = ctx.taskId.current ?? ev.result.taskId;
|
|
297
|
+
return { kind: 'park', gate: 'ask', pendingDone: ev };
|
|
298
|
+
}
|
|
299
|
+
if (isFsApprovalPark(ev.result)) {
|
|
300
|
+
ctx.taskId.current = ctx.taskId.current ?? ev.result.taskId;
|
|
301
|
+
return { kind: 'park', gate: 'fs', pendingDone: ev };
|
|
302
|
+
}
|
|
303
|
+
const events = [...led.flushHeld()]; // 真错(非 gate)的 Ask tool_end 此刻诚实渲染
|
|
304
|
+
// durable 终帧的 result.taskId === sessionId(引擎 durable quirk)会让 captureSessionId 跳过
|
|
305
|
+
// rewind 绑定 —— 用 sync leg 捕的真 handle 修正回去。
|
|
306
|
+
const r = ev.result;
|
|
307
|
+
const realTaskId = ctx.taskId.current;
|
|
308
|
+
if (realTaskId && r && r.taskId === r.sessionId && r.taskId !== realTaskId) {
|
|
309
|
+
events.push({ ...ev, result: { ...ev.result, taskId: realTaskId } });
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
events.push(ev);
|
|
313
|
+
}
|
|
314
|
+
return { kind: 'end', events };
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* 一帧 → 一个处置。手柄的**先后**与原文逐条同序:tool_approval 帧 → tool_start → tool_end →
|
|
318
|
+
* suspended → done → failed → 透传。
|
|
319
|
+
*/
|
|
320
|
+
export async function routeFrame(ev, ctx) {
|
|
321
|
+
const { led } = ctx;
|
|
322
|
+
if (isToolApprovalFrame(ev)) {
|
|
323
|
+
// #51: `ToolApprovalFrame` frames are documented as NOT part of the
|
|
324
|
+
// `AgentEvent` union (a separate SSE wire vocabulary riding the same
|
|
325
|
+
// stream) — intersecting the type-predicate's asserted type with the
|
|
326
|
+
// unrelated `AgentEvent` union collapses every arm to `never`. Local
|
|
327
|
+
// rebind restores the real (already-checked) shape.
|
|
328
|
+
const frame = ev;
|
|
329
|
+
return await routeToolApprovalFrame(frame, ctx);
|
|
330
|
+
}
|
|
331
|
+
if (ev.type === 'tool_start' && typeof ev.toolCallId === 'string')
|
|
332
|
+
return routeToolStart(ev, ev.toolCallId, led);
|
|
333
|
+
if (ev.type === 'tool_end' && typeof ev.toolCallId === 'string')
|
|
334
|
+
return routeToolEnd(ev, ev.toolCallId, led);
|
|
335
|
+
if (ev.type === 'suspended')
|
|
336
|
+
return routeSuspended(ev, ctx);
|
|
337
|
+
if (ev.type === 'done')
|
|
338
|
+
return routeDone(ev, ctx);
|
|
339
|
+
if (ev.type === 'failed')
|
|
340
|
+
return { kind: 'end', events: [...led.flushHeld(), ev] };
|
|
341
|
+
return { kind: 'yield', events: [ev] };
|
|
342
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gateLedger — HITL gate 桥的**单一台账**(REF-CC-032 / SPLIT-12 第一刀,2026-08-02)。
|
|
3
|
+
*
|
|
4
|
+
* 拆分动机(P1 lens-giant-split SPLIT-12 的 evidence 段逐条):`bridgeAskUserQuestionGates`
|
|
5
|
+
* 里的 8 个集合 + 若干标量,**内环帧路由**与**外环 park 决断**各自直写同一批容器 ——
|
|
6
|
+
* · `heldAskEnds` 写于内环 tool_end 手柄、删于内环四处 **与外环 decide 成功那一处**、drain 于 flushHeld;
|
|
7
|
+
* · `deniedCalls` 写于内环 tool_approval 手柄 **与外环 fs decide 的 denied 臂**、消费于内环 tool_end;
|
|
8
|
+
* · `resolvedAnswers` 写于**外环**、消费于**内环**;
|
|
9
|
+
* · `endedCalls` 写于内环四处 **与 flushHeld**。
|
|
10
|
+
* 「同一个容器有两个写者、写法各自散着」正是这段代码没法拆的真约束。本文件把容器收成一个对象并
|
|
11
|
+
* 给出**动词**:内外环从此都只经动词写,容器本身只有一个写者(本模块),两把刀之间靠显式传参共享
|
|
12
|
+
* 这一个台账对象。
|
|
13
|
+
*
|
|
14
|
+
* 🔴 本模块**没有**模块级状态:`createGateLedger()` 每个 turn 造一份(与原来那些函数体内 `new Map()`
|
|
15
|
+
* 的生命周期逐字相同)。它不是单例,也不该变成单例 —— 两个并发 turn 共用一份台账 =
|
|
16
|
+
* A turn 的 HOLD 帧被 B turn 的 flush 吐出去。
|
|
17
|
+
*
|
|
18
|
+
* 🔴 `AskAnsweredOutput` 落在本文件而不是留在 `askGateWire.ts`:它是 `resolvedAnswers` 的值类型,
|
|
19
|
+
* 台账要能自描述,值类型必须与台账同层或更低。`askGateWire.ts` 原样 re-export 它,对外类型面不变。
|
|
20
|
+
*/
|
|
21
|
+
import type { AgentEvent } from '@sema-agent/sdk';
|
|
22
|
+
/** CC AskUserQuestion outputSchema 形状(答过的问题卡):toolResult 的 `ask-user-question` arm
|
|
23
|
+
* 消费它渲真实答案卡(否则「诚实缺席」路径会把这张卡渲成结果不可用)。 */
|
|
24
|
+
export interface AskAnsweredOutput {
|
|
25
|
+
type: 'ask-user-question';
|
|
26
|
+
questions: unknown[];
|
|
27
|
+
answers: Record<string, string>;
|
|
28
|
+
annotations?: Record<string, {
|
|
29
|
+
notes?: string;
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 一个 turn 的 gate 台账。动词按「谁写、谁读」分组,每组的注释说明它承的是哪条不变量。
|
|
34
|
+
*
|
|
35
|
+
* 🔴 命名纪律:`mark*` = 只写;`is*` / `*For` / `last*` = 只读;`take*` = 读且消费(一次性);
|
|
36
|
+
* `drop*` = 只删。读写混合的动词只有 `take*` 与 `markStarted`,两者的一次性语义都写在各自注释里 ——
|
|
37
|
+
* 因为原文里正是「`has` 完接着 `delete`」「`has` 完接着 `add`」这两种成对写法散在多处,
|
|
38
|
+
* 收成一个动词才谈得上「写者唯一」。
|
|
39
|
+
*/
|
|
40
|
+
export interface GateLedger {
|
|
41
|
+
/** #110 缺陷② a:`lastSeq` 要跟得上**已消费的最大 seq**。durable `runs.events` 流上并非每帧
|
|
42
|
+
* 都带 `id`(数字 `seq` 位的帧同样在场),旧式只吃 `ev.id` ⇒ 停在最后一个带 id 的帧上 ⇒
|
|
43
|
+
* re-attach 从一个陈旧位置重放,把已经决断的 park 又送一遍(实测:消费到 16 却 attach from 8)。 */
|
|
44
|
+
noteSeq(ev: AgentEvent): void;
|
|
45
|
+
lastSeq(): string | undefined;
|
|
46
|
+
/** 首见 ⇒ true(并记账);durable 重放的已渲 call ⇒ false。 */
|
|
47
|
+
markStarted(callId: string): boolean;
|
|
48
|
+
/** 🔴 存 **tool_start 当拍的 `args` 快照**而非帧引用(与拆分前 `askArgsByCall` 逐字同语义;
|
|
49
|
+
* codex 复审确证差异 #1 的收货修)——存帧引用意味着「上游在 yield 后改写 `ev.args`」会漂进
|
|
50
|
+
* 审批 payload,且台账持帧延长了整帧生命周期。载荷仍是 UNTRUSTED unknown,台账只搬运不解释。 */
|
|
51
|
+
noteGatedStart(callId: string, ev: AgentEvent): void;
|
|
52
|
+
gatedStartArgs(): ReadonlyMap<string, unknown>;
|
|
53
|
+
isEnded(callId: string): boolean;
|
|
54
|
+
markEnded(callId: string): void;
|
|
55
|
+
/** park 毒化的 gated tool_end(isError:true "Operation aborted")——HOLD 到 gate 定性
|
|
56
|
+
* (AskUserQuestion + [816] 放宽后的 fs 写工具 + [2150] Bash)。 */
|
|
57
|
+
hold(callId: string, ev: AgentEvent): void;
|
|
58
|
+
dropHeld(callId: string): void;
|
|
59
|
+
heldCount(): number;
|
|
60
|
+
/** drain:逐帧吐出并**同批记 ended**(fail-soft 回退路径与终帧路径共用的唯一出口)。 */
|
|
61
|
+
flushHeld(): Generator<AgentEvent>;
|
|
62
|
+
/** 三选卡 deny 过的 call:重放 tool_end 的 render 面 stamp REJECT_MESSAGE(vendored
|
|
63
|
+
* renderToolUseRejectedMessage 渲 `User rejected <op> to <path>`,CC 逐字)。 */
|
|
64
|
+
markDenied(callId: string): void;
|
|
65
|
+
isDenied(callId: string): boolean;
|
|
66
|
+
/** 一次性消费(原文的 `has` + `delete` 成对写法)。 */
|
|
67
|
+
takeDenied(callId: string): boolean;
|
|
68
|
+
/** 同步帧腿 deny 但 callId 关联不上(引擎在 tool_start 前出帧的形状):stamp 下一张 gated 报错帧。 */
|
|
69
|
+
armDenyStamp(): void;
|
|
70
|
+
/** 一次性消费:武装中 ⇒ 解除并返回 true。 */
|
|
71
|
+
takeDenyStamp(): boolean;
|
|
72
|
+
/** started-未-ended 的 fs 写 call(顺序保留)——帧契约不带 toolCallId;引擎阻塞在帧上,
|
|
73
|
+
* 最后一张未收口 fs 写 call 即被 gate 的 call。 */
|
|
74
|
+
pushFsCall(callId: string): void;
|
|
75
|
+
dropFsCall(callId: string): void;
|
|
76
|
+
lastPendingFsCall(): string | undefined;
|
|
77
|
+
/** REF-CC-034:最近一次见到的 fs 写 / shell 执行 gated tool_start 的 callId(`park.gate==='fs'`
|
|
78
|
+
* 分支的两族 toolName)——**不**随 tool_end 清空(与 `pushFsCall/dropFsCall` 的关键差异:
|
|
79
|
+
* 那一组在 tool_end 到达就 drop 掉,而 durable re-attach 重放的 `suspended` park 没有配套的新
|
|
80
|
+
* tool_start/tool_end,那时它早已空了,取不到候选 callId)。只在**新的** gated tool_start 到来
|
|
81
|
+
* 时前进,天然把「这次 park 到底是哪个 call」的身份带过 decide→重放两轮。 */
|
|
82
|
+
noteFsOrShellGate(callId: string): void;
|
|
83
|
+
lastFsOrShellGatedCallId(): string | undefined;
|
|
84
|
+
/** 续流重放的解答 tool_end 无 output,不补就会渲成结果不可用;这里记下真实答案供该帧 stamp
|
|
85
|
+
* `structured` 让卡片渲真实选择。 */
|
|
86
|
+
rememberAnswer(callId: string, answered: AskAnsweredOutput): void;
|
|
87
|
+
answerFor(callId: string): AskAnsweredOutput | undefined;
|
|
88
|
+
dropAnswer(callId: string): void;
|
|
89
|
+
/**
|
|
90
|
+
* #110 缺陷② 的**承重**修 / REF-CC-034:durable re-attach 会把「已经决断过的 park」重放一遍
|
|
91
|
+
* (边界上必然发生:`lastEventId` 再准也只能精确到帧,park 帧本身就在续流的重放窗口里)。重放的
|
|
92
|
+
* park 走到决断会去取 pending approval —— 那一行早已 resolved ⇒ 拿不到 ⇒ `failed` ⇒ 合成
|
|
93
|
+
* `hitl_unanswered` 终帧。而引擎侧 run 其实已经 `completed`:壳谎报失败,连带 CC 转录 0 行写出、
|
|
94
|
+
* 同 session `--resume` 报 `No conversation found` —— **会话废掉**。
|
|
95
|
+
*
|
|
96
|
+
* 首选判据是文案/code 双臂(`parkResolver.isAlreadyResolvedFailure`);本集合是第二条、
|
|
97
|
+
* **身份匹配**(非文案)的独立判据:拿 `lastFsOrShellGatedCallId()` 算出这次 park 的候选 callId,
|
|
98
|
+
* 若它已在本集合里,不论失败文案长什么样都按「已解决」处置。方向偏宽符合不对称原则(误判成
|
|
99
|
+
* 「已解决」最多多读一轮、被 hop 预算兜底;误判成「真失败」烧会话)。
|
|
100
|
+
*/
|
|
101
|
+
markDecided(callId: string): void;
|
|
102
|
+
isDecided(callId: string): boolean;
|
|
103
|
+
/** 只给 debug 串用的规模位(`[decided so far: N]`)。 */
|
|
104
|
+
decidedCount(): number;
|
|
105
|
+
}
|
|
106
|
+
/** 造一份 turn 级 gate 台账(**不是单例**,见文件头注)。 */
|
|
107
|
+
export declare function createGateLedger(): GateLedger;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/** 造一份 turn 级 gate 台账(**不是单例**,见文件头注)。 */
|
|
2
|
+
export function createGateLedger() {
|
|
3
|
+
const startedCalls = new Set();
|
|
4
|
+
const endedCalls = new Set();
|
|
5
|
+
const heldAskEnds = new Map();
|
|
6
|
+
const gatedStartArgsByCall = new Map();
|
|
7
|
+
const deniedCalls = new Set();
|
|
8
|
+
const pendingFsCalls = [];
|
|
9
|
+
const resolvedAnswers = new Map();
|
|
10
|
+
const decidedGates = new Set();
|
|
11
|
+
let lastFsOrShellGatedCall;
|
|
12
|
+
let denyStampNextFsEnd = false;
|
|
13
|
+
let seq;
|
|
14
|
+
return {
|
|
15
|
+
noteSeq(ev) {
|
|
16
|
+
const rawSeq = ev.id ?? ev.seq;
|
|
17
|
+
if (typeof rawSeq === 'string' && rawSeq.length > 0)
|
|
18
|
+
seq = rawSeq;
|
|
19
|
+
else if (typeof rawSeq === 'number' && Number.isFinite(rawSeq))
|
|
20
|
+
seq = String(rawSeq);
|
|
21
|
+
},
|
|
22
|
+
lastSeq() {
|
|
23
|
+
return seq;
|
|
24
|
+
},
|
|
25
|
+
markStarted(callId) {
|
|
26
|
+
if (startedCalls.has(callId))
|
|
27
|
+
return false;
|
|
28
|
+
startedCalls.add(callId);
|
|
29
|
+
return true;
|
|
30
|
+
},
|
|
31
|
+
noteGatedStart(callId, ev) {
|
|
32
|
+
gatedStartArgsByCall.set(callId, ev.args);
|
|
33
|
+
},
|
|
34
|
+
gatedStartArgs() {
|
|
35
|
+
return gatedStartArgsByCall;
|
|
36
|
+
},
|
|
37
|
+
isEnded(callId) {
|
|
38
|
+
return endedCalls.has(callId);
|
|
39
|
+
},
|
|
40
|
+
markEnded(callId) {
|
|
41
|
+
endedCalls.add(callId);
|
|
42
|
+
},
|
|
43
|
+
hold(callId, ev) {
|
|
44
|
+
heldAskEnds.set(callId, ev);
|
|
45
|
+
},
|
|
46
|
+
dropHeld(callId) {
|
|
47
|
+
heldAskEnds.delete(callId);
|
|
48
|
+
},
|
|
49
|
+
heldCount() {
|
|
50
|
+
return heldAskEnds.size;
|
|
51
|
+
},
|
|
52
|
+
*flushHeld() {
|
|
53
|
+
for (const [callId, held] of heldAskEnds) {
|
|
54
|
+
endedCalls.add(callId);
|
|
55
|
+
yield held;
|
|
56
|
+
}
|
|
57
|
+
heldAskEnds.clear();
|
|
58
|
+
},
|
|
59
|
+
markDenied(callId) {
|
|
60
|
+
deniedCalls.add(callId);
|
|
61
|
+
},
|
|
62
|
+
isDenied(callId) {
|
|
63
|
+
return deniedCalls.has(callId);
|
|
64
|
+
},
|
|
65
|
+
takeDenied(callId) {
|
|
66
|
+
return deniedCalls.delete(callId);
|
|
67
|
+
},
|
|
68
|
+
armDenyStamp() {
|
|
69
|
+
denyStampNextFsEnd = true;
|
|
70
|
+
},
|
|
71
|
+
takeDenyStamp() {
|
|
72
|
+
if (!denyStampNextFsEnd)
|
|
73
|
+
return false;
|
|
74
|
+
denyStampNextFsEnd = false;
|
|
75
|
+
return true;
|
|
76
|
+
},
|
|
77
|
+
pushFsCall(callId) {
|
|
78
|
+
pendingFsCalls.push(callId);
|
|
79
|
+
},
|
|
80
|
+
dropFsCall(callId) {
|
|
81
|
+
const at = pendingFsCalls.indexOf(callId);
|
|
82
|
+
if (at !== -1)
|
|
83
|
+
pendingFsCalls.splice(at, 1);
|
|
84
|
+
},
|
|
85
|
+
lastPendingFsCall() {
|
|
86
|
+
return pendingFsCalls.length > 0 ? pendingFsCalls[pendingFsCalls.length - 1] : undefined;
|
|
87
|
+
},
|
|
88
|
+
noteFsOrShellGate(callId) {
|
|
89
|
+
lastFsOrShellGatedCall = callId;
|
|
90
|
+
},
|
|
91
|
+
lastFsOrShellGatedCallId() {
|
|
92
|
+
return lastFsOrShellGatedCall;
|
|
93
|
+
},
|
|
94
|
+
rememberAnswer(callId, answered) {
|
|
95
|
+
resolvedAnswers.set(callId, answered);
|
|
96
|
+
},
|
|
97
|
+
answerFor(callId) {
|
|
98
|
+
return resolvedAnswers.get(callId);
|
|
99
|
+
},
|
|
100
|
+
dropAnswer(callId) {
|
|
101
|
+
resolvedAnswers.delete(callId);
|
|
102
|
+
},
|
|
103
|
+
markDecided(callId) {
|
|
104
|
+
decidedGates.add(callId);
|
|
105
|
+
},
|
|
106
|
+
isDecided(callId) {
|
|
107
|
+
return decidedGates.has(callId);
|
|
108
|
+
},
|
|
109
|
+
decidedCount() {
|
|
110
|
+
return decidedGates.size;
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|