@sema-agent/client-core 0.12.2 → 0.14.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 +67 -0
- package/dist/adapt/arms.js +623 -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 +59 -0
- package/dist/adapt/panelTasks.js +193 -0
- package/dist/adapt/textStream.d.ts +63 -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 +37 -0
- package/dist/adapt/turnFlags.js +55 -0
- package/dist/adapt/wireShapes.d.ts +96 -0
- package/dist/adapt/wireShapes.js +167 -0
- package/dist/adapt.d.ts +33 -60
- package/dist/adapt.js +84 -1207
- package/dist/adapter/downstream/eventToSdkMessage.d.ts +57 -13
- package/dist/adapter/downstream/eventToSdkMessage.js +174 -106
- package/dist/adapter/downstream/terminalToSdkResult.js +151 -160
- package/dist/adapter/downstream/turnUsageToModelUsage.d.ts +36 -0
- package/dist/adapter/downstream/turnUsageToModelUsage.js +34 -6
- package/dist/adapter/runStream.d.ts +36 -4
- package/dist/adapter/runStream.js +195 -13
- package/dist/adapter/types.d.ts +28 -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.d.ts +22 -1
- package/dist/hitl/approvalsFeed.js +103 -9
- package/dist/hitl/askGateWire.d.ts +27 -96
- package/dist/hitl/askGateWire.js +69 -618
- package/dist/hitl/frameRouter.d.ts +86 -0
- package/dist/hitl/frameRouter.js +383 -0
- package/dist/hitl/gateLedger.d.ts +119 -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 +250 -0
- package/dist/hitl/planReviewWire.d.ts +60 -2
- package/dist/hitl/planReviewWire.js +197 -91
- package/dist/hitl/toolApprovalWire.d.ts +67 -4
- package/dist/hitl/toolApprovalWire.js +129 -31
- package/dist/hooksWireCaps.d.ts +1 -82
- package/dist/hooksWireCaps.js +44 -237
- 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 +18 -6
- package/dist/liveQuestionStore.js +15 -0
- package/dist/model/providerPresets.js +11 -1
- package/dist/notifications.d.ts +62 -2
- package/dist/notifications.js +306 -49
- package/dist/printToolResultFrame.d.ts +12 -4
- package/dist/printToolResultFrame.js +11 -21
- 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 +30 -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
|
@@ -28,11 +28,19 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import { permissionModeField } from './permissionWireCaps.js';
|
|
30
30
|
import { hostEnv } from './hostEnv.js';
|
|
31
|
+
import { lastFlagValue } from './argvFlagValue.js';
|
|
31
32
|
/** 部署旋钮 env 键名(settings.json env 块 → 1a-envseed → process.env 同车道)。 */
|
|
32
33
|
export const HEADLESS_PERMISSION_MODE_ENV = 'SEMA_HEADLESS_PERMISSION_MODE';
|
|
33
34
|
/** wire 词表(permissionWireCaps.PermissionMode 的运行时镜像;大小写敏感——与 CC flag choices 一致)。
|
|
34
35
|
* 'auto'(分类器批3,[907]):server ≥1.209 五模式表认;老 server coerce 未知词 → "default" 只紧不松。 */
|
|
35
36
|
const HEADLESS_PERMISSION_WIRE_MODE_TUPLE = ['default', 'plan', 'acceptEdits', 'bypassPermissions', 'auto'];
|
|
37
|
+
/**
|
|
38
|
+
* 🔴 [2006] B-CC-2(2026-07-29):`readonly PermissionMode[]` 的注解只保证「元素合法」,**从不保证
|
|
39
|
+
* 穷举** —— server 五模式表加第六个词、`PermissionMode` 跟了而这张运行时镜像没跟 ⇒ 编译期全绿,
|
|
40
|
+
* 而那个词从 env 旋钮走进来会被判非法、一次性 warn 后**静默回落**(部署以为设了、实际没设)。
|
|
41
|
+
* 补「类型 → 常量」那半边;之前的看守只有 `length > 0 && includes('plan')`(对这个方向零判别力)。
|
|
42
|
+
* `Covers`(REF-CC-145/dup-12,P3 wave1 卡C-argv)改吃共用叶 typePins.ts——四份逐字重复钉之一。
|
|
43
|
+
*/
|
|
36
44
|
const _headlessPermissionModePins = [true, true];
|
|
37
45
|
void _headlessPermissionModePins;
|
|
38
46
|
export const HEADLESS_PERMISSION_WIRE_MODES = HEADLESS_PERMISSION_WIRE_MODE_TUPLE;
|
|
@@ -40,33 +48,28 @@ export const HEADLESS_PERMISSION_WIRE_MODES = HEADLESS_PERMISSION_WIRE_MODE_TUPL
|
|
|
40
48
|
* argv 里的 `--permission-mode <v>` / `--permission-mode=<v>`(后者/后出现者赢,commander 同语义)与
|
|
41
49
|
* `--dangerously-skip-permissions`(= bypassPermissions;`--permission-mode` 在场时让位)。
|
|
42
50
|
* 返回 undefined = flag 车道未显式表态。
|
|
51
|
+
*
|
|
52
|
+
* REF-CC-143(dup-07)收编:`--permission-mode` 的取值改吃共用 lastFlagValue(argvFlagValue.ts,
|
|
53
|
+
* `onMissingValue:'latch-previous'`)——旧手写循环对「某一次 occurrence 缺值」的处置是跳过这一次、
|
|
54
|
+
* 锁存(latch)住之前已经取到的合法值,不是把整个旗塌缩成「从没写过」(`--permission-mode plan
|
|
55
|
+
* --permission-mode` 的旧行为是 `mode==='plan'`,不是 `undefined`)。共用叶的缺省策略
|
|
56
|
+
* (`'invalidate'`,sandboxWire/scenarioWire 用)会把这个情形塌缩成「有旗无值」,与本函数原有语义
|
|
57
|
+
* 不符,所以本调用点显式传 `'latch-previous'` 保住旧行为——这才是任务书说的「各调用方保持自己现有
|
|
58
|
+
* 对缺值的处置」(wave1 回炉修复,blocker:此前误用了共用叶的 invalidate 缺省,详见 commit 记账)。
|
|
59
|
+
* `--dangerously-skip-permissions` 的存在性判别不属于「取某旗最后值」这个不变量,单独一个小循环即
|
|
60
|
+
* 可,零重复。
|
|
43
61
|
*/
|
|
44
62
|
export function permissionModeFromArgv(argv) {
|
|
45
|
-
|
|
46
|
-
|
|
63
|
+
const flag = lastFlagValue(argv, '--permission-mode', 'latch-previous');
|
|
64
|
+
if (flag.present && flag.raw !== undefined)
|
|
65
|
+
return flag.raw;
|
|
47
66
|
for (let i = 0; i < argv.length; i++) {
|
|
48
67
|
const a = argv[i];
|
|
49
|
-
if (a === '--
|
|
50
|
-
const v = argv[i + 1];
|
|
51
|
-
if (typeof v === 'string' && !v.startsWith('-')) {
|
|
52
|
-
mode = v;
|
|
53
|
-
i++;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
else if (a?.startsWith('--permission-mode=')) {
|
|
57
|
-
mode = a.slice('--permission-mode='.length);
|
|
58
|
-
}
|
|
59
|
-
else if (a === '--dangerously-skip-permissions') {
|
|
60
|
-
dsp = true;
|
|
61
|
-
}
|
|
62
|
-
else if (a === '--') {
|
|
68
|
+
if (a === '--')
|
|
63
69
|
break; // POSIX 约定:-- 之后不是 flag
|
|
64
|
-
|
|
70
|
+
if (a === '--dangerously-skip-permissions')
|
|
71
|
+
return 'bypassPermissions';
|
|
65
72
|
}
|
|
66
|
-
if (mode !== undefined)
|
|
67
|
-
return mode;
|
|
68
|
-
if (dsp)
|
|
69
|
-
return 'bypassPermissions';
|
|
70
73
|
return undefined;
|
|
71
74
|
}
|
|
72
75
|
// 一次性告警 latch(-p 是 one-shot 进程;测试钩子可重置)
|
|
@@ -56,7 +56,13 @@
|
|
|
56
56
|
* 消费面兜住。
|
|
57
57
|
*/
|
|
58
58
|
import type { AgentEvent } from '@sema-agent/sdk';
|
|
59
|
-
/** 每 run 壳级重连预算(design 铁律 5)。cap/416 的 SDK 内部续传不计入(同一次 openEvents)。
|
|
59
|
+
/** 每 run 壳级重连预算(design 铁律 5)。cap/416 的 SDK 内部续传不计入(同一次 openEvents)。
|
|
60
|
+
* 选值:design/HEADLESS-RECONNECT-DESIGN.md §2 铁律 5 只钉了「有界」与这个数,未留更细的推导
|
|
61
|
+
* —— 诚实记账,不倒推一个当时没写下来的公式。可核实的取值依据是本环自身的形状(REF-CC-148
|
|
62
|
+
* 普查记账):每次尝试是两次真网络往返(重试环里的 `probe(deps.baseUrl)` + `deps.openEvents(…)`)
|
|
63
|
+
* 且两次尝试之间**零退避**,预算越大,在一个真死引擎上耗尽这个环、落到诚实判死文案之前的墙钟代价就越高
|
|
64
|
+
* ——2 是「给瞬时抖动一次重试余地」与「死引擎上不过度拖延判死」之间的保守取值,不是精算结果;
|
|
65
|
+
* 若上游把撕裂形态改成更常见的可重连态,这个数可能需要上调(域词表-19 已留此账)。 */
|
|
60
66
|
export declare const HEADLESS_RECONNECT_MAX = 2;
|
|
61
67
|
/** GET /v1/runs/:id 的 run row 终局子集(SDK RunRecord;字段防御性读)。 */
|
|
62
68
|
export interface RunRowLike {
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { isSseIdleError, probeEngineAlive } from './sseIdleTriage.js';
|
|
2
2
|
import { hostEnv } from './hostEnv.js';
|
|
3
|
-
/** 每 run 壳级重连预算(design 铁律 5)。cap/416 的 SDK 内部续传不计入(同一次 openEvents)。
|
|
3
|
+
/** 每 run 壳级重连预算(design 铁律 5)。cap/416 的 SDK 内部续传不计入(同一次 openEvents)。
|
|
4
|
+
* 选值:design/HEADLESS-RECONNECT-DESIGN.md §2 铁律 5 只钉了「有界」与这个数,未留更细的推导
|
|
5
|
+
* —— 诚实记账,不倒推一个当时没写下来的公式。可核实的取值依据是本环自身的形状(REF-CC-148
|
|
6
|
+
* 普查记账):每次尝试是两次真网络往返(重试环里的 `probe(deps.baseUrl)` + `deps.openEvents(…)`)
|
|
7
|
+
* 且两次尝试之间**零退避**,预算越大,在一个真死引擎上耗尽这个环、落到诚实判死文案之前的墙钟代价就越高
|
|
8
|
+
* ——2 是「给瞬时抖动一次重试余地」与「死引擎上不过度拖延判死」之间的保守取值,不是精算结果;
|
|
9
|
+
* 若上游把撕裂形态改成更常见的可重连态,这个数可能需要上调(域词表-19 已留此账)。 */
|
|
4
10
|
export const HEADLESS_RECONNECT_MAX = 2;
|
|
5
11
|
/** SEMA_DEBUG 探针(headless `-p` 车道 Ink 未挂载,stderr 可用;绝不带凭证/帧内容)。 */
|
|
6
12
|
function debugLog(msg) {
|
|
@@ -72,10 +78,22 @@ export async function* withHeadlessR1Reconnect(live, deps) {
|
|
|
72
78
|
let attempts = 0;
|
|
73
79
|
let lastSeenSeq; // R1 去重锚(runs.events SSE id = 账本 seq;见模块头选型)
|
|
74
80
|
const probe = deps.probeAlive ?? probeEngineAlive;
|
|
81
|
+
// REF-CC-148(域词表-19):originalTearErr 在环内从不被重写(它是进入本重试环前唯一捕获的
|
|
82
|
+
// 错误对象),所以这个判型是**环不变式**——算一次即可,不必逐轮重算。
|
|
83
|
+
// wave1 回炉复审(codex blocker):求值时机必须排在铁律 3(用户中止恒先行)**之后**才对 main
|
|
84
|
+
// 逐字——main 是「每轮入口先查 aborted,再判型」,若把判型提到循环顶(环外、抢在第一轮的
|
|
85
|
+
// aborted 检查之前),当 originalTearErr 是一个 `.name` getter 会抛的畸形错误对象、且入环时
|
|
86
|
+
// signal 已经 aborted 时,新顺序会先让 isSseIdleError 抛出**那个畸形对象的二次异常**,盖过
|
|
87
|
+
// 本该抛出的 originalTearErr,违反铁律 3。改法:惰性 memo——用 `undefined` 起始态占位「尚未算
|
|
88
|
+
// 过」(合法返回值只有 true/false),真正求值仍只发生一次,但**排在每轮的 aborted 检查之后**,
|
|
89
|
+
// 与 main 的求值顺序逐字一致,「只判一次」的不变量原样保留。
|
|
90
|
+
let isRecoverableTear;
|
|
75
91
|
for (;;) {
|
|
76
92
|
if (deps.signal?.aborted)
|
|
77
93
|
throw originalTearErr; // 铁律 3:用户中止恒先行,绝不进重连分支
|
|
78
|
-
if (
|
|
94
|
+
if (isRecoverableTear === undefined)
|
|
95
|
+
isRecoverableTear = isSseIdleError(originalTearErr);
|
|
96
|
+
if (!isRecoverableTear)
|
|
79
97
|
throw originalTearErr; // 只救 engine-alive-idle 的静默撕裂形
|
|
80
98
|
const taskId = deps.getTaskId();
|
|
81
99
|
if (!taskId)
|
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
* 2. **poll**:stream 抛(404 无端点 / SDK 连 5 次开不起来 / 连 5 次空转)⇒ 落到定时 `list()`。
|
|
34
34
|
* 并按 `streamRetryMs` 定期**再试一次 stream** —— 回落是降级不是永久放弃
|
|
35
35
|
* (服务端升级上线后不该还要重启客户端)。
|
|
36
|
-
*
|
|
36
|
+
* 🆕 [C77]② 这条腿可由宿主按拍闸门:`opts.shouldPoll`(desktop 的「仅活跃 run 时轮询」)。
|
|
37
|
+
* 跳拍 ≠ 停摆(mode 仍 'poll'、定时器照排、不计 polls、不算 F5 逃逸),契约见该选项的 doc。
|
|
38
|
+
* 3. **idle**:stop() 之后 / signal abort 之后 / F5 判定 poll 腿真挂了。
|
|
37
39
|
*
|
|
38
40
|
* ## 诚实面([probe-must-prove-it-speaks])
|
|
39
41
|
*
|
|
@@ -86,6 +88,25 @@ export interface ApprovalsFeedOptions {
|
|
|
86
88
|
pollIntervalMs?: number;
|
|
87
89
|
/** 降级后多久再试一次 stream;缺省 60_000ms。0 = 不再试(永久轮询)。 */
|
|
88
90
|
streamRetryMs?: number;
|
|
91
|
+
/**
|
|
92
|
+
* [C77]② 轮询条件旋钮(小黑板 desktop 请托:那一端的现行纪律是**仅活跃 run 时轮询**)。
|
|
93
|
+
* 每一拍**触发时**查询一次:`false` ⇒ 跳过本拍取件,但照常排下一拍。缺省(不传)= 恒 true,
|
|
94
|
+
* 行为与加旋钮之前一字不差。
|
|
95
|
+
*
|
|
96
|
+
* 🔴 语义与 `mode()==='idle'` 严格分开(别混同,端的自检读的就是这两个量):
|
|
97
|
+
* · `shouldPoll()===false` = **消费方说现在不用查** —— 腿活着、定时器照排、`mode` 仍是
|
|
98
|
+
* `'poll'`、`stats.polls` 不增(跳掉的那一拍没取件,计成 poll 就是谎报流量)、
|
|
99
|
+
* **不计入** F5 的 `consecutivePollEscapes`(跳拍不是失败,连着跳一百拍也不该转 idle);
|
|
100
|
+
* · `mode()==='idle'` = 这条腿**真挂了**(F5:回调体连续逃逸到上限)或已 `stop()`。
|
|
101
|
+
*
|
|
102
|
+
* 管辖边界(只管轮询腿,三处**不**受它约束,都是有理由的):
|
|
103
|
+
* · **构造期那一次取件**不受管 —— 它是「首帧之前也要有内容」的一次性快照(否则端分不清
|
|
104
|
+
* 「空白」与「没有 pending」);要连这一次都不要,就晚一点再 `startApprovalsFeed`;
|
|
105
|
+
* · **push 腿**(`approvals.stream` 事件驱动的重取)不受管 —— 那是服务端说「变了」,
|
|
106
|
+
* 省掉它 = 明知有变化还不去看,与本旋钮要省的「空转流量」不是一回事;
|
|
107
|
+
* · **`refresh()`** 不受管 —— 那是端自己按下的显式动作。
|
|
108
|
+
*/
|
|
109
|
+
shouldPoll?: () => boolean;
|
|
89
110
|
signal?: AbortSignal;
|
|
90
111
|
}
|
|
91
112
|
export interface ApprovalsFeedHandle {
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { hostLog } from '../host.js';
|
|
2
|
+
import { unrefTimer } from '../unrefTimer.js';
|
|
3
|
+
/** [2393] hitl-F5:poll 回调体**连续**逃逸多少次之后判定「这条腿真挂了」并把 `mode` 打成 idle。
|
|
4
|
+
* 取 5 是为了让「偶发一拍畸形 payload」不误判(单次逃逸下一拍就归零),同时不让一条永远抛的腿
|
|
5
|
+
* 无限期地对着端的自检假装自己还活着。 */
|
|
6
|
+
const MAX_CONSECUTIVE_POLL_ESCAPES = 5;
|
|
2
7
|
/**
|
|
3
8
|
* pending 列表的稳定摘要 —— 只在**内容变了**的时候发快照。
|
|
4
9
|
* 键里带 `boundInputHash`:同一条 pending 的绑定被服务端换掉(TOCTOU 场景)也算变化,
|
|
@@ -33,13 +38,40 @@ export function startApprovalsFeed(client, onSnapshot, opts) {
|
|
|
33
38
|
let last = null;
|
|
34
39
|
let pollTimer;
|
|
35
40
|
let retryTimer;
|
|
41
|
+
/** [2393] hitl-F5:连续「回调体抛出」的次数(一次跑完就归零)——见 `schedulePoll` 的 catch 臂。 */
|
|
42
|
+
let consecutivePollEscapes = 0;
|
|
36
43
|
const stats = {
|
|
37
44
|
pushEvents: 0, heartbeats: 0, polls: 0, listErrors: 0, streamFailures: 0, snapshots: 0,
|
|
38
45
|
};
|
|
46
|
+
/** 域词表-14 收编:「怎么调 unref」的单一实现见 unrefTimer.ts,本函数只是就地起个短名。 */
|
|
39
47
|
function arm(t) {
|
|
40
|
-
;
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
return unrefTimer(t);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* [C77]② 这一拍要不要真去取件。缺省(宿主没装旋钮)恒 true —— 缺省路径上连一个函数调用都不多。
|
|
52
|
+
*
|
|
53
|
+
* 两个不确定方向都倒向**「查」**(fail-safe 方向定在这里,理由是代价不对称):多查一次 =
|
|
54
|
+
* 一个 `list()` 的流量;少查一次 = 一条待审批在端上迟迟不出现,而这条腿服务的正是 HITL 面。
|
|
55
|
+
* · 谓词抛错 ⇒ 查(并记一行:宿主的谓词坏了它自己得看得见,不是静默吞);
|
|
56
|
+
* · 谓词返回非布尔(宿主 bug / 忘了 return)⇒ 查,同样记一行。
|
|
57
|
+
*/
|
|
58
|
+
function pollGateOpen() {
|
|
59
|
+
const gate = opts?.shouldPoll;
|
|
60
|
+
if (!gate)
|
|
61
|
+
return true;
|
|
62
|
+
let verdict;
|
|
63
|
+
try {
|
|
64
|
+
verdict = gate();
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
hostLog('debug', `approvalsFeed: shouldPoll() threw — treating this tick as "poll" (fail-safe): ${String(e)}`);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
if (typeof verdict !== 'boolean') {
|
|
71
|
+
hostLog('debug', `approvalsFeed: shouldPoll() returned ${typeof verdict} (not boolean) — treating this tick as "poll"`);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return verdict;
|
|
43
75
|
}
|
|
44
76
|
async function take(via) {
|
|
45
77
|
if (stopped)
|
|
@@ -86,30 +118,80 @@ export function startApprovalsFeed(client, onSnapshot, opts) {
|
|
|
86
118
|
return;
|
|
87
119
|
pollTimer = arm(setTimeout(() => {
|
|
88
120
|
pollTimer = undefined;
|
|
121
|
+
// [C77]② 跳拍:消费方说这一拍不用查 ⇒ 不取件、不计 `stats.polls`(没取件却计数 = 谎报流量)、
|
|
122
|
+
// **不动** `consecutivePollEscapes`(跳拍不是失败,连着跳一万拍也不该被 F5 判成腿挂了),
|
|
123
|
+
// 但下一拍照排 —— 这是「腿活着但闲着」,不是停摆。
|
|
124
|
+
if (!pollGateOpen()) {
|
|
125
|
+
if (mode === 'poll')
|
|
126
|
+
schedulePoll();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
89
129
|
void (async () => {
|
|
90
130
|
stats.polls++;
|
|
91
131
|
await take('poll');
|
|
132
|
+
consecutivePollEscapes = 0; // 跑完一整拍 ⇒ 之前的逃逸不再连续
|
|
133
|
+
if (mode === 'poll')
|
|
134
|
+
schedulePoll();
|
|
135
|
+
})().catch(e => {
|
|
136
|
+
// REF-CC-024 ①:take() 理论上自己兜底(见 take() 内的 try/catch),但这是它逃逸时的最后一道
|
|
137
|
+
// 接盘 —— 缺了它,轮询定时器的回调体抛出会变成 unhandled rejection(Node 默认致命)。
|
|
138
|
+
//
|
|
139
|
+
// 🔴 [2393] hitl-F5(2026-08-02):光接盘不够,**接盘之后还要接着排下一拍**。旧码在这里只记
|
|
140
|
+
// 一行 debug 就结束:那一拍的 `schedulePoll()` 在 `await take()` 之后、永远轮不到执行 ⇒
|
|
141
|
+
// 轮询腿当场死透。而 `mode()` 照旧报 'poll'、`stats()` 照旧有数 —— 文件头 38-42 卖的正是
|
|
142
|
+
// 「跑了 N 秒之后 mode 不是 idle 且 polls+pushEvents > 0」这条端自检,于是自检看着一个
|
|
143
|
+
// 已经死掉的 feed 说它活着(而「一条 pending 都没有」与「腿挂了」本来就长得一样,这恰恰是
|
|
144
|
+
// 那段诚实面存在的理由)。逃逸不是理论形:`take()` 的 try/catch 只护 `list()` 本身,其后的
|
|
145
|
+
// `digestOf(rows)` 在 wire 出畸形 payload(`pending` 不是数组)时就会从这里出去。
|
|
146
|
+
//
|
|
147
|
+
// 两条腿(不是二选一):① **续排** —— 一次抛出不该判 feed 死刑,下一拍照跑(下一拍在
|
|
148
|
+
// `pollIntervalMs` 之后,天然不忙转;不新开定时器,单例卫兵仍是 `pollTimer`);
|
|
149
|
+
// ② **连续失败上限转 idle** —— 一直抛就是真的挂了,那时 `mode` 必须说真话,让端的自检
|
|
150
|
+
// 看得见([honest-absence-not-fabricated-zero]:诚实缺席优先于一个还在报活的假象)。
|
|
151
|
+
hostLog('debug', `approvalsFeed: poll leg threw unexpectedly (take() should have self-caught): ${String(e)}`);
|
|
152
|
+
consecutivePollEscapes++;
|
|
153
|
+
if (consecutivePollEscapes >= MAX_CONSECUTIVE_POLL_ESCAPES) {
|
|
154
|
+
mode = 'idle';
|
|
155
|
+
hostLog('error', `approvalsFeed: poll leg gave up after ${consecutivePollEscapes} consecutive escapes — mode → idle ` +
|
|
156
|
+
'(停摆就说停摆:端的自检从此看得见,不再拿一个死掉的 feed 当活的)');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
92
159
|
if (mode === 'poll')
|
|
93
160
|
schedulePoll();
|
|
94
|
-
})
|
|
161
|
+
});
|
|
95
162
|
}, pollIntervalMs));
|
|
96
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* 与 schedulePoll 对称(域词表-18):**幂等卫兵**(retryTimer 已在飞则不再排一个)+ **回调首行
|
|
166
|
+
* 自清**(否则 retryTimer 触发后仍持有已死句柄,degradeToPoll 若被再次调用会直接覆盖它而不清掉
|
|
167
|
+
* 旧的——旧的照样触发一次 `void runStream()`,两条 push 腿并行 = 双倍取件)。
|
|
168
|
+
*/
|
|
97
169
|
function scheduleStreamRetry() {
|
|
98
170
|
if (stopped || ac.signal.aborted || streamRetryMs <= 0)
|
|
99
171
|
return;
|
|
100
172
|
if (!client.approvals.stream)
|
|
101
173
|
return;
|
|
174
|
+
if (retryTimer !== undefined)
|
|
175
|
+
return;
|
|
102
176
|
retryTimer = arm(setTimeout(() => {
|
|
177
|
+
retryTimer = undefined;
|
|
103
178
|
if (stopped || ac.signal.aborted)
|
|
104
179
|
return;
|
|
105
180
|
hostLog('debug', 'approvalsFeed: retrying push leg (降级是暂时的,服务端升级后不该还要重启客户端)');
|
|
106
181
|
void runStream();
|
|
107
182
|
}, streamRetryMs));
|
|
108
183
|
}
|
|
109
|
-
|
|
184
|
+
/**
|
|
185
|
+
* REF-CC-024 ③:`real`(缺省 true)只计**真正的** stream 故障(抛出 / 空转结束)进
|
|
186
|
+
* `stats.streamFailures`。「SDK 根本没有 `approvals.stream` 这个动词」不是失败,是能力缺席
|
|
187
|
+
* (老 SDK / mock)——此前无条件 `++` 会让这类宿主在 t=0 就报 streamFailures=1,而文件头
|
|
188
|
+
* 38-42 正是拿这个数卖「区分『连着但没事发生』与『断了』」,能力缺席被计成故障就说反了。
|
|
189
|
+
*/
|
|
190
|
+
function degradeToPoll(why, real = true) {
|
|
110
191
|
if (stopped)
|
|
111
192
|
return;
|
|
112
|
-
|
|
193
|
+
if (real)
|
|
194
|
+
stats.streamFailures++;
|
|
113
195
|
if (mode !== 'poll') {
|
|
114
196
|
hostLog('debug', `approvalsFeed: push leg unavailable (${why}) — falling back to polling every ${pollIntervalMs}ms`);
|
|
115
197
|
}
|
|
@@ -120,7 +202,7 @@ export function startApprovalsFeed(client, onSnapshot, opts) {
|
|
|
120
202
|
async function runStream() {
|
|
121
203
|
const stream = client.approvals.stream;
|
|
122
204
|
if (!stream) {
|
|
123
|
-
degradeToPoll('SDK has no approvals.stream (old SDK / mock)');
|
|
205
|
+
degradeToPoll('SDK has no approvals.stream (old SDK / mock)', false);
|
|
124
206
|
return;
|
|
125
207
|
}
|
|
126
208
|
try {
|
|
@@ -156,13 +238,25 @@ export function startApprovalsFeed(client, onSnapshot, opts) {
|
|
|
156
238
|
}
|
|
157
239
|
// 首帧之前也要有内容:立刻取一次权威快照(否则端在第一个事件到达前是空白的,
|
|
158
240
|
// 而「空白」与「没有 pending」看起来一样)。
|
|
159
|
-
|
|
241
|
+
//
|
|
242
|
+
// 🔴 [2393] hitl-F5(同一条缺陷的**第二个**站点,2026-08-02):catch 必须排在 then **之前**。
|
|
243
|
+
// 旧写法是 `take().then(起两条腿).catch(记一行)` —— 首帧 take 逃逸时 `.then` 整个被跳过,
|
|
244
|
+
// 于是 `runStream()` / `degradeToPoll()` 一条都没起:feed 在 t=0 就是死的,而 `mode()` 还报着
|
|
245
|
+
// 构造期算出来的 'push'/'poll'。把接盘挪到前面,「起腿」这件事就从「首帧成功」这个前提上解耦了
|
|
246
|
+
// ——它本来也不该依赖首帧成不成功([paired-mechanisms-must-share-premise])。
|
|
247
|
+
void take(mode === 'push' ? 'push' : 'poll')
|
|
248
|
+
.catch(e => {
|
|
249
|
+
// REF-CC-024 ①:同款接盘(见 schedulePoll 里那份同款注释)——首帧取件是唯一没被 try/catch
|
|
250
|
+
// 包住的调用点之一,这里补上不让它成为 unhandled rejection。
|
|
251
|
+
hostLog('debug', `approvalsFeed: initial take() threw unexpectedly (take() should have self-caught): ${String(e)}`);
|
|
252
|
+
})
|
|
253
|
+
.then(() => {
|
|
160
254
|
if (stopped)
|
|
161
255
|
return;
|
|
162
256
|
if (client.approvals.stream)
|
|
163
257
|
void runStream();
|
|
164
258
|
else
|
|
165
|
-
degradeToPoll('SDK has no approvals.stream (old SDK / mock)');
|
|
259
|
+
degradeToPoll('SDK has no approvals.stream (old SDK / mock)', false);
|
|
166
260
|
});
|
|
167
261
|
return {
|
|
168
262
|
stop() {
|
|
@@ -22,6 +22,21 @@
|
|
|
22
22
|
* 同时进 bundle ⇒ overlay responder 注册在一份、`publishQuestionFrame` 发到另一份 = 对话框
|
|
23
23
|
* 弹出来但没人收答(静默挂死)。
|
|
24
24
|
*
|
|
25
|
+
* ── REF-CC-032(2026-08-02,P3 A 族压轴卡 askgate-split;设计输入 = P1 lens-giant-split SPLIT-12)──
|
|
26
|
+
*
|
|
27
|
+
* 本文件从 369 行的单函数塌成**驱动 + 公开面**。三刀落在同目录三个新文件,分工与各自头注一致:
|
|
28
|
+
* ① `gateLedger.ts` —— 8 个集合 + 3 个记账标量收成一个台账对象并给出动词。拆分的真约束就是
|
|
29
|
+
* 「同一个容器有内环、外环两个写者」(`heldAskEnds` / `deniedCalls` / `resolvedAnswers` /
|
|
30
|
+
* `endedCalls` 四个各自都有两处写点),收口后容器只剩一个写者。
|
|
31
|
+
* ② `frameRouter.ts` —— 内环帧路由:一帧进、一个 `FrameAction` 出。tool_end 的 5 条分支是
|
|
32
|
+
* **有序**的(分类器 deny 的结构性签名必须先于 fs HOLD/REJECT 判),注册表化后顺序写成数据
|
|
33
|
+
* 并配一条加载期断言钉死。共享词汇(`AskGateWireDeps` / `isAskTool` / 两个 CC 逐字常量 /
|
|
34
|
+
* park 形状谓词)也落在那里 —— 它是内外环都够得着的最低模块。
|
|
35
|
+
* ③ `parkResolver.ts` —— 外环 park 决断:`reattach` 还是 `failsoft`,两条 hostLog 与 hop 预算
|
|
36
|
+
* 全在那里。
|
|
37
|
+
* 本文件只剩:双层驱动循环(内环读流吐帧、外环决断后换流)+ 对外 re-export。**公开面一个名字
|
|
38
|
+
* 都没动**(`export *` 经本文件出去的四个值 + 四个类型全部原位可达)。
|
|
39
|
+
*
|
|
25
40
|
* ── 以下为原文件的领域说明(逐字保留)────────────────────────────────────────────────────────
|
|
26
41
|
*
|
|
27
42
|
* liveHitlAskWire — §4④ AskUserQuestion 的 suspended↔overlay 桥(engine 1.163 HITL 语义,2026-07-12)。
|
|
@@ -54,107 +69,23 @@
|
|
|
54
69
|
* (`selected ⊆ options`,off-list 进 note)。
|
|
55
70
|
*/
|
|
56
71
|
import type { AgentEvent } from '@sema-agent/sdk';
|
|
57
|
-
import { type
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
export
|
|
63
|
-
key: string;
|
|
64
|
-
text: string;
|
|
65
|
-
color: 'warning';
|
|
66
|
-
priority: 'immediate';
|
|
67
|
-
timeoutMs: number;
|
|
68
|
-
}
|
|
69
|
-
/** HITL 的宿主副作用面 —— 通知上屏 + 分类器 deny 的端侧记账。 */
|
|
70
|
-
export interface HitlHostSurface {
|
|
71
|
-
/** 立即顶到 current(壳:`notifications.current = notice`,queue 不动)。 */
|
|
72
|
-
showNotice(notice: HitlNotice): void;
|
|
73
|
-
/** **仅当** current 仍是这个 key 时清掉(壳原文的 if-still-mine 语义 —— 否则会误清别人的通知)。 */
|
|
74
|
-
clearNoticeIfCurrent(key: string): void;
|
|
75
|
-
/** 分类器 deny 裁决的宿主副作用:footer 通知 + /permissions Recent Denials 记账(壳资产)。 */
|
|
76
|
-
surfaceClassifierDeny(toolName: string, verdict: ClassifierDenyVerdict): void;
|
|
77
|
-
}
|
|
78
|
-
/** 装 HITL 宿主面(传 null 卸)。返回还原函数。 */
|
|
79
|
-
export declare function installHitlHostSurface(surface: HitlHostSurface | null): () => void;
|
|
80
|
-
/** W1 带 key 变体:装/卸某会话的 HITL 宿主面(多会话宿主每会话一键,互不顶盖)。 */
|
|
81
|
-
export declare function installHitlHostSurfaceFor(sessionKey: string, surface: HitlHostSurface | null): () => void;
|
|
82
|
-
/** 🔴 宿主自检:恒应为 0。非 0 = 有 HITL 副作用发生时口不在,那一行 warn / 那条记账丢了。 */
|
|
83
|
-
export declare function hitlHostSurfaceMisses(): number;
|
|
84
|
-
/** W1 带 key 变体(miss 计数逐键)。 */
|
|
85
|
-
export declare function hitlHostSurfaceMissesFor(sessionKey: string): number;
|
|
86
|
-
/** W1 读端:某会话的 HITL 宿主面(未装 = null;**不计 miss** —— miss 只在副作用真发生时计)。 */
|
|
87
|
-
export declare function hitlHostSurfaceFor(sessionKey: string): HitlHostSurface | null;
|
|
88
|
-
/** 测试钩:卸口 + 清计数。 */
|
|
89
|
-
export declare function _resetHitlHostSurfaceForTest(): void;
|
|
90
|
-
/**
|
|
91
|
-
* CC `utils/messages.ts` 的 `REJECT_MESSAGE` **逐字**(搬迁差分 2)。deny 后重放 tool_end 的
|
|
92
|
-
* render 面 stamp 用 —— vendored `renderToolUseRejectedMessage` 渲 `User rejected <op> to <path>`。
|
|
93
|
-
* 🔴 pure 门 B7 段:包内冻结字面量(无条件)+ 壳树全树扫描「每一处声明都逐字节相同」(壳树缺席=跳过)。
|
|
94
|
-
*/
|
|
95
|
-
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.";
|
|
96
|
-
/** 本桥消费的 wire 面(@sema-ai/sdk AgentClient 的结构切片,mock 可注入)。 */
|
|
97
|
-
export interface AskGateWireDeps {
|
|
98
|
-
/** approvals.list/decide + assistant(HitlBridge 的 client 切片)。 */
|
|
99
|
-
client: HitlClientLike;
|
|
100
|
-
/** GET /v1/runs/:id/events — decide 后的续流 attach(lastEventId 续传)。 */
|
|
101
|
-
runsEvents: (taskId: string, opts?: {
|
|
102
|
-
signal?: AbortSignal;
|
|
103
|
-
lastEventId?: string;
|
|
104
|
-
}) => AsyncGenerator<AgentEvent>;
|
|
105
|
-
/** POST /v1/tool-approvals/:id/respond(server 1.191 同步帧腿,[830]①)。缺省=不消费
|
|
106
|
-
* tool_approval 帧(帧被吞、引擎按自身 fail-closed TTL 自决)——mock/旧引擎路径零影响。 */
|
|
107
|
-
respondToolApproval?: RespondToolApprovalFn;
|
|
108
|
-
}
|
|
109
|
-
/** core 对被 gate/连坐 abort 的 call 铸的 tool_end 载体(逐字;desktop session-host 真引擎实测
|
|
110
|
-
* 同款)——HOLD 谓词锚它做**精确等值**,普通工具错的输出是各自错误文案,永不进 HOLD。 */
|
|
111
|
-
export declare const ENGINE_ABORT_TOOL_RESULT = "Operation aborted";
|
|
112
|
-
/** CC AskUserQuestion outputSchema 形状(答过的问题卡):toolResult 的 `ask-user-question` arm
|
|
113
|
-
* 消费它渲真实答案卡(否则「诚实缺席」路径会把这张卡渲成结果不可用)。 */
|
|
114
|
-
export interface AskAnsweredOutput {
|
|
115
|
-
type: 'ask-user-question';
|
|
116
|
-
questions: unknown[];
|
|
117
|
-
answers: Record<string, string>;
|
|
118
|
-
annotations?: Record<string, {
|
|
119
|
-
notes?: string;
|
|
120
|
-
}>;
|
|
121
|
-
}
|
|
122
|
-
export type GateOutcome = {
|
|
123
|
-
kind: 'decided';
|
|
124
|
-
gatedCallId?: string;
|
|
125
|
-
answered?: AskAnsweredOutput;
|
|
126
|
-
} | {
|
|
127
|
-
kind: 'aborted';
|
|
128
|
-
gatedCallId?: string;
|
|
129
|
-
} | {
|
|
130
|
-
kind: 'failed';
|
|
131
|
-
gatedCallId?: string;
|
|
132
|
-
reason: string;
|
|
133
|
-
};
|
|
134
|
-
/** 把 wire 答案({answers:[{header,selected,note?}]})折回 CC 卡片的 Record<question,string> 形状
|
|
135
|
-
* (multiSelect 与对话框同款 ", " lossy join;note → annotations.notes)。 */
|
|
136
|
-
export declare function toAnsweredOutput(questions: unknown[], answer: QuestionAnswer): AskAnsweredOutput;
|
|
137
|
-
/** cancel-by-deny 的后台 settle 预算。decide 是 SYNC 驱动的(引擎跑到下一 park/终态才返,实测
|
|
138
|
-
* 4-5s+),但 DENY-abort 语义上引擎收到即终结 run;2s 内连收都没收到 ⇒ 按丢失警示(晚到成功
|
|
139
|
-
* 只是多一行良性 warn,比锁死无线索诚实)。 */
|
|
140
|
-
export declare const CANCEL_DENY_BUDGET_MS = 2000;
|
|
141
|
-
/** warn 行文案(测试锁字面)。 */
|
|
142
|
-
export declare const CANCEL_DENY_WARN_TEXT = "could not cancel the pending question \u2014 the session may stay locked; the run may need engine-side recovery";
|
|
143
|
-
/**
|
|
144
|
-
* 中断 deny 的有界观察(壳侧单测 `hitlCancelDeny.test.ts` 的被测面)。
|
|
145
|
-
* 铁律:不 await 进 abort 返回路径(用户立即拿回控制);这里只管后台 settle 的«观察»:
|
|
146
|
-
* - 2s 内 settle 成功 ⇒ 零上屏(SEMA_DEBUG 记成功);
|
|
147
|
-
* - 失败/超时 ⇒ 上屏一行 warn + SEMA_DEBUG 记原因(deny 丢失 = run 卡 suspended,下一条消息
|
|
148
|
-
* 撞 409;配合件2b 的专属文案,用户知道现场 + 出路);
|
|
149
|
-
* - HitlSafetyError code==='no_pending' ⇒ 良性静默(pending 已被别处消解/过期 —— run 没锁;
|
|
150
|
-
* hitlBridge decideTool 的同款语义,那里的静默维持不动)。
|
|
151
|
-
*/
|
|
152
|
-
export declare function observeCancelByDeny(settle: Promise<unknown>, taskId: string): void;
|
|
72
|
+
import { type AskGateWireDeps } from './frameRouter.js';
|
|
73
|
+
export { HITL_REJECT_MESSAGE, ENGINE_ABORT_TOOL_RESULT } from './frameRouter.js';
|
|
74
|
+
export type { AskGateWireDeps } from './frameRouter.js';
|
|
75
|
+
export type { AskAnsweredOutput } from './gateLedger.js';
|
|
76
|
+
export { toAnsweredOutput, isAlreadyResolvedGateReason } from './parkResolver.js';
|
|
77
|
+
export type { GateOutcome, GateFailureCode } from './parkResolver.js';
|
|
153
78
|
/**
|
|
154
79
|
* 包一层 AgentEvent 流:把 AskUserQuestion 的 suspended park 变成「对话框 → decide → 续流」闭环。
|
|
155
80
|
* 其它事件原样透传;非 AskUserQuestion 的 gate 保持现状。fail-soft:任何桥内失败回退为
|
|
156
81
|
* 「flush 毒化 tool_end + 原样终帧」(= 修复前行为)。
|
|
157
82
|
*
|
|
83
|
+
* 双层循环(REF-CC-032 后只剩驱动骨架):
|
|
84
|
+
* · 内环 `for await` 读流 —— 每帧先记 seq、再判扣留帧放行、再交 `routeFrame` 定处置;
|
|
85
|
+
* `park` 动作 break 出内环。
|
|
86
|
+
* · 外环 `while` —— `resolvePark` 决断:`reattach` 就换一条 `runs.events` 流接着读
|
|
87
|
+
* (hop 预算在 resolver 内按 `hops` 判),`failsoft` 就吐完 events 收场。
|
|
88
|
+
*
|
|
158
89
|
* @param source 上游 AgentEvent 流(tasks.stream 或 runs.events,已过 demuxQuestionFrames)。
|
|
159
90
|
* @param opts.taskId runs.events 消费时已知的 run handle(sync leg 从 suspended done 捕获)。
|
|
160
91
|
*/
|