@sema-agent/client-core 0.72.13 → 0.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +2 -1
- package/dist/engineAgentPanelStore.js +10 -2
- package/dist/fleet/fleetLedger.d.ts +0 -4
- package/dist/fleet/fleetLedger.js +12 -4
- package/dist/fleet/fleetProjection.d.ts +20 -8
- package/dist/fleet/fleetProjection.js +20 -8
- package/dist/fleetAgentPanelProjection.js +26 -11
- package/dist/hitl/approvalsFeed.d.ts +28 -1
- package/dist/hitl/approvalsFeed.js +88 -5
- package/dist/hitl/crashConverged.d.ts +2 -1
- package/dist/hitl/hitlHostSurface.d.ts +4 -0
- package/dist/hitl/hitlHostSurface.js +8 -0
- package/dist/hitl/livePendingAsk.d.ts +107 -0
- package/dist/hitl/livePendingAsk.js +198 -0
- package/dist/hitl/toolApprovalWire.d.ts +16 -0
- package/dist/hitl/toolApprovalWire.js +16 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/workflowClient.js +23 -6
- package/dist/workflowMonitor.d.ts +2 -1
- package/docs/INTEGRATION-CLIENTS.md +91 -6
- package/package.json +1 -1
|
@@ -163,6 +163,14 @@ export function surfaceRememberNotApplied() {
|
|
|
163
163
|
export function surfaceEditNotForwarded() {
|
|
164
164
|
surfaceSelfClearingWarn(EDIT_NOT_FORWARDED_KEY, EDIT_NOT_FORWARDED_WARN_TEXT, EDIT_NOT_FORWARDED_TIMEOUT_MS);
|
|
165
165
|
}
|
|
166
|
+
/** 0.72.14:入参不可得的卡上收到了「编辑后批准」——什么都没发,ask 仍挂着(文案测试锁字面)。 */
|
|
167
|
+
export const EDIT_REFUSED_ON_BLIND_ASK_WARN_TEXT = 'That approval was NOT sent: this request does not expose its tool arguments here, so an edited approval cannot be applied. ' +
|
|
168
|
+
'The request is still waiting — approve or deny it as it is.';
|
|
169
|
+
const EDIT_REFUSED_ON_BLIND_ASK_KEY = 'hitl-edit-refused-blind-ask';
|
|
170
|
+
/** 见 {@link EDIT_REFUSED_ON_BLIND_ASK_WARN_TEXT};无宿主口 ⇒ 静默(与同族通知同语义)。 */
|
|
171
|
+
export function surfaceEditRefusedOnBlindAsk() {
|
|
172
|
+
surfaceSelfClearingWarn(EDIT_REFUSED_ON_BLIND_ASK_KEY, EDIT_REFUSED_ON_BLIND_ASK_WARN_TEXT, EDIT_NOT_FORWARDED_TIMEOUT_MS);
|
|
173
|
+
}
|
|
166
174
|
/**
|
|
167
175
|
* 文案(测试锁字面)。措辞刻意描述**后果**,并且**只说证得出的话**(异源对抗复审四轮 [medium] 修):
|
|
168
176
|
* · 不写「这台引擎不支持」——能力位缺席有三种同形成因(宿主没接这个字段 / 探测还没回来 / 探测失败),
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* livePendingAsk — 「悬挂的流内 ask」的三端共用读面与决断口(0.72.14)。
|
|
3
|
+
*
|
|
4
|
+
* ── 这个模块补的是哪一个缺口 ────────────────────────────────────────────────────────────────
|
|
5
|
+
* 流内 ask 在铸造那一刻若没有活着的宿主流可投,引擎把它转成**悬挂**(窗约一小时,不补发、不重算),此后只在
|
|
6
|
+
* `GET /v1/approvals` 回体的第二顶层键 `livePending` 里可发现。后台 workflow 的子代恒落这一形:宿主 turn 先结束,
|
|
7
|
+
* 子代稍后才撞上审批门 ⇒ 用户面零帧零卡,代理静默卡到窗尽。本模块让端从 {@link startApprovalsFeed} 的快照里
|
|
8
|
+
* 读到这些行、出带子代身份的卡、并经**与流内帧腿同一条链**决断。
|
|
9
|
+
*
|
|
10
|
+
* ── 🔴 四条读法 ────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
* ① **键缺席 ≠ 没有悬挂 ask**:老引擎不发这个键 ⇒ `not_reported`;快照上键缺席,计数读 `null`(不知道),绝不铸 `[]` / `0`。
|
|
12
|
+
* ② **数组名即路由判据**:`livePending` 行的决议口是 `POST /v1/tool-approvals/{approvalId}/respond`(流内审批那个口),
|
|
13
|
+
* **不是** durable 行的 decide 口;两数组不混编。幂等键 = `approvalId`(与流内帧同一个 id)。
|
|
14
|
+
* ③ **可选旗标 only-if-true**:`fromSubagent` / `governanceForced` / `requiresRealApproval` 缺席绝不编码成 `false`。
|
|
15
|
+
* ④ **行上没有工具入参**(引擎刻意不在这个读面开第二道脱敏面)⇒ 卡必须明说「看不到入参」,不许渲一个空命令冒充全貌。
|
|
16
|
+
*
|
|
17
|
+
* sdk 的 barrel 没有导出这一行的类型 ⇒ 按**结构**窄读(与 `capabilities.executionLane` 同一姿势),多余成员不过境。
|
|
18
|
+
*/
|
|
19
|
+
import type { ApprovalsFeedSnapshot } from './approvalsFeed.js';
|
|
20
|
+
import { type RespondToolApprovalFn, type ToolApprovalFrame, type ToolApprovalFrameLaneOpts, type ToolApprovalFrameOutcome } from './toolApprovalWire.js';
|
|
21
|
+
/** 一条悬挂的流内 ask(窄读后的形;键集即全部)。 */
|
|
22
|
+
export interface LivePendingAskView {
|
|
23
|
+
/** 与 respond 口收的同一个 id。 */
|
|
24
|
+
approvalId: string;
|
|
25
|
+
toolName: string;
|
|
26
|
+
/** 登记时刻(epoch ms)。 */
|
|
27
|
+
ts: number;
|
|
28
|
+
/** 窗死线(绝对 epoch ms;引擎铸定,本包不重算)。 */
|
|
29
|
+
expiresAtMs: number;
|
|
30
|
+
sessionId?: string;
|
|
31
|
+
requiresRealApproval?: true;
|
|
32
|
+
governanceForced?: true;
|
|
33
|
+
/** 出自委派子代。 */
|
|
34
|
+
fromSubagent?: true;
|
|
35
|
+
/** 子代 taskId(与 `fromSubagent` 同生同缺)。 */
|
|
36
|
+
originTaskId?: string;
|
|
37
|
+
}
|
|
38
|
+
/** `livePending` 段的三态读数。`malformed` = 键在场但读不懂(**不**折成 `not_reported`,也不折成空)。 */
|
|
39
|
+
export type LivePendingReading = {
|
|
40
|
+
kind: 'not_reported';
|
|
41
|
+
} | {
|
|
42
|
+
kind: 'malformed';
|
|
43
|
+
} | {
|
|
44
|
+
kind: 'present';
|
|
45
|
+
rows: LivePendingAskView[];
|
|
46
|
+
dropped: number;
|
|
47
|
+
};
|
|
48
|
+
/** `GET /v1/approvals` 回体 → `livePending` 段读数。四必填位坏形的行丢弃并计数;同 `approvalId` 重复行留第一条。 */
|
|
49
|
+
export declare function readLivePendingRows(body: unknown): LivePendingReading;
|
|
50
|
+
/** 视图过滤:给了 `sessionId` ⇒ 只留该会话的行;行上没有 `sessionId` = 归不了属 ⇒ 不留(不替别的会话出卡)。 */
|
|
51
|
+
export interface SuspendedAskFilter {
|
|
52
|
+
sessionId?: string;
|
|
53
|
+
}
|
|
54
|
+
/** 快照里**出自子代**的悬挂 ask(宿主自己的流内 ask 走流内帧腿,不在这里出卡)。未报 / 没有快照 ⇒ `[]`。 */
|
|
55
|
+
export declare function suspendedSubagentAsks(snapshot: ApprovalsFeedSnapshot | null | undefined, filter?: SuspendedAskFilter): LivePendingAskView[];
|
|
56
|
+
/** 「在等人决定」的计数。`null` = 不知道(没有快照 / 引擎没报这一段),**不是** 0。 */
|
|
57
|
+
export interface ApprovalsAwaitingDecisionCount {
|
|
58
|
+
/** durable 停驻行数。 */
|
|
59
|
+
durable: number | null;
|
|
60
|
+
/** 流内 ask 行数(含宿主自己的与子代的)。 */
|
|
61
|
+
live: number | null;
|
|
62
|
+
/** 其中出自子代的行数。 */
|
|
63
|
+
suspendedSubagent: number | null;
|
|
64
|
+
}
|
|
65
|
+
export declare function countApprovalsAwaitingDecision(snapshot: ApprovalsFeedSnapshot | null | undefined, filter?: SuspendedAskFilter): ApprovalsAwaitingDecisionCount;
|
|
66
|
+
/**
|
|
67
|
+
* 悬挂行 → 流内帧腿的输入。只带行上**真有**的位:窗三键不带(行上只有绝对死线,没有出帧时刻,凑不出可校偏的窗),
|
|
68
|
+
* `args` 不带(行上没有)。`sourceTaskId` = `originTaskId`(卡头子代身份徽章的来源)。
|
|
69
|
+
*/
|
|
70
|
+
export declare function livePendingRowToApprovalFrame(row: LivePendingAskView): ToolApprovalFrame;
|
|
71
|
+
/**
|
|
72
|
+
* 出卡并决断一条悬挂 ask —— **与流内帧腿同一条链**({@link surfaceToolApprovalFrameAndRespond}):同一个卡口、
|
|
73
|
+
* 同一个身份键铸口(`approvalId`)、同一套回执 / 拒绝读法。差别只有一处:行上没有入参 ⇒ 卡上明说
|
|
74
|
+
* (`argsUnavailable`),且这张卡上收到「编辑后批准」时**整次不发**(结局 `editRefused`;没有原文可编,丢掉改写再批准 =
|
|
75
|
+
* 批准了人没看到的原始入参)。
|
|
76
|
+
*/
|
|
77
|
+
export declare function surfaceSuspendedAskAndRespond(row: LivePendingAskView, respond: RespondToolApprovalFn, signal?: AbortSignal, lane?: ToolApprovalFrameLaneOpts): Promise<ToolApprovalFrameOutcome>;
|
|
78
|
+
/** {@link createSuspendedAskTracker} 每次吞一张快照的产出。 */
|
|
79
|
+
export interface SuspendedAskDelta {
|
|
80
|
+
/** 首次见到、该出卡的行。 */
|
|
81
|
+
appeared: LivePendingAskView[];
|
|
82
|
+
/** 此前 appeared 过、现在不在列表里的 approvalId(已被别处决断 / 过期 / 引擎换代)⇒ 撤卡。 */
|
|
83
|
+
gone: string[];
|
|
84
|
+
}
|
|
85
|
+
export interface SuspendedAskTracker {
|
|
86
|
+
ingest(snapshot: ApprovalsFeedSnapshot | null | undefined): SuspendedAskDelta;
|
|
87
|
+
/**
|
|
88
|
+
* 流内帧腿出卡**之前**调:认领这只 ask。返回 `true` = 帧腿可以出卡(此后快照不再为它出第二张);
|
|
89
|
+
* 返回 `false` = 它已经从快照出过卡 / 已被认领 / 已决断 ⇒ 帧腿**跳过出卡**(两条通道同一只 ask 只有一张卡、一次决断)。
|
|
90
|
+
*/
|
|
91
|
+
noteSurfacedByStream(approvalId: string): boolean;
|
|
92
|
+
/** 让一只已出过卡的 ask 在下一张快照里重新 `appeared`(卡被关掉 / 改写被拒 `editRefused` 之后)。 */
|
|
93
|
+
requeue(approvalId: string): void;
|
|
94
|
+
/** 这只 ask 已在本端决断 ⇒ 引擎结算前它还会在列表里待一会儿,不重新出卡;之后消失也不再报 gone。 */
|
|
95
|
+
noteDecided(approvalId: string): void;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 快照 → 出卡 / 撤卡的去重账(每个宿主会话一只;不是模块级单例)。同一 `approvalId` 只 `appeared` 一次;
|
|
99
|
+
* `livePending` 键缺席按「现在一条都看不到」处理已出的卡(引擎换代后旧卡没有决断口可用)。
|
|
100
|
+
*
|
|
101
|
+
* 两本账,**都不按条数淘汰活跃条目**(异源对抗复审:无差别的「满了删最旧」会让仍挂着的卡重出、漏撤):
|
|
102
|
+
* · `shown` = 从快照出过卡、且仍在列表里的 ask。条目随「从列表消失」离账 ⇒ 大小恒 ≤ 列表长度,不设上界。
|
|
103
|
+
* · `claimed` = 被流内帧腿认领 / 已在本端决断的 ask,值 = 是否已在某张快照里见过。见过之后从列表消失 ⇒ 这只 ask
|
|
104
|
+
* 的一生结束,离账(同 id 日后再出现是新的一只)。**从未见过**的认领才受上界管(帧先到、快照一直没列到它的那一小段),
|
|
105
|
+
* 满了只淘汰这一类里最旧的。
|
|
106
|
+
*/
|
|
107
|
+
export declare function createSuspendedAskTracker(filter?: SuspendedAskFilter): SuspendedAskTracker;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* livePendingAsk — 「悬挂的流内 ask」的三端共用读面与决断口(0.72.14)。
|
|
3
|
+
*
|
|
4
|
+
* ── 这个模块补的是哪一个缺口 ────────────────────────────────────────────────────────────────
|
|
5
|
+
* 流内 ask 在铸造那一刻若没有活着的宿主流可投,引擎把它转成**悬挂**(窗约一小时,不补发、不重算),此后只在
|
|
6
|
+
* `GET /v1/approvals` 回体的第二顶层键 `livePending` 里可发现。后台 workflow 的子代恒落这一形:宿主 turn 先结束,
|
|
7
|
+
* 子代稍后才撞上审批门 ⇒ 用户面零帧零卡,代理静默卡到窗尽。本模块让端从 {@link startApprovalsFeed} 的快照里
|
|
8
|
+
* 读到这些行、出带子代身份的卡、并经**与流内帧腿同一条链**决断。
|
|
9
|
+
*
|
|
10
|
+
* ── 🔴 四条读法 ────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
* ① **键缺席 ≠ 没有悬挂 ask**:老引擎不发这个键 ⇒ `not_reported`;快照上键缺席,计数读 `null`(不知道),绝不铸 `[]` / `0`。
|
|
12
|
+
* ② **数组名即路由判据**:`livePending` 行的决议口是 `POST /v1/tool-approvals/{approvalId}/respond`(流内审批那个口),
|
|
13
|
+
* **不是** durable 行的 decide 口;两数组不混编。幂等键 = `approvalId`(与流内帧同一个 id)。
|
|
14
|
+
* ③ **可选旗标 only-if-true**:`fromSubagent` / `governanceForced` / `requiresRealApproval` 缺席绝不编码成 `false`。
|
|
15
|
+
* ④ **行上没有工具入参**(引擎刻意不在这个读面开第二道脱敏面)⇒ 卡必须明说「看不到入参」,不许渲一个空命令冒充全貌。
|
|
16
|
+
*
|
|
17
|
+
* sdk 的 barrel 没有导出这一行的类型 ⇒ 按**结构**窄读(与 `capabilities.executionLane` 同一姿势),多余成员不过境。
|
|
18
|
+
*/
|
|
19
|
+
import { surfaceToolApprovalFrameAndRespond, } from './toolApprovalWire.js';
|
|
20
|
+
function nonEmptyString(v) {
|
|
21
|
+
return typeof v === 'string' && v !== '';
|
|
22
|
+
}
|
|
23
|
+
function nonNegativeFinite(v) {
|
|
24
|
+
return typeof v === 'number' && Number.isFinite(v) && v >= 0;
|
|
25
|
+
}
|
|
26
|
+
/** `GET /v1/approvals` 回体 → `livePending` 段读数。四必填位坏形的行丢弃并计数;同 `approvalId` 重复行留第一条。 */
|
|
27
|
+
export function readLivePendingRows(body) {
|
|
28
|
+
if (body === null || typeof body !== 'object' || Array.isArray(body))
|
|
29
|
+
return { kind: 'malformed' };
|
|
30
|
+
if (!('livePending' in body))
|
|
31
|
+
return { kind: 'not_reported' };
|
|
32
|
+
const raw = body.livePending;
|
|
33
|
+
if (raw === undefined)
|
|
34
|
+
return { kind: 'not_reported' };
|
|
35
|
+
if (!Array.isArray(raw))
|
|
36
|
+
return { kind: 'malformed' };
|
|
37
|
+
const rows = [];
|
|
38
|
+
const seen = new Set();
|
|
39
|
+
let dropped = 0;
|
|
40
|
+
for (const item of raw) {
|
|
41
|
+
if (item === null || typeof item !== 'object' || Array.isArray(item)) {
|
|
42
|
+
dropped++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const o = item;
|
|
46
|
+
if (!nonEmptyString(o.approvalId) || !nonEmptyString(o.toolName) || !nonNegativeFinite(o.ts) || !nonNegativeFinite(o.expiresAtMs)) {
|
|
47
|
+
dropped++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (seen.has(o.approvalId)) {
|
|
51
|
+
dropped++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
seen.add(o.approvalId);
|
|
55
|
+
rows.push({
|
|
56
|
+
approvalId: o.approvalId,
|
|
57
|
+
toolName: o.toolName,
|
|
58
|
+
ts: o.ts,
|
|
59
|
+
expiresAtMs: o.expiresAtMs,
|
|
60
|
+
...(nonEmptyString(o.sessionId) ? { sessionId: o.sessionId } : {}),
|
|
61
|
+
...(o.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
62
|
+
...(o.governanceForced === true ? { governanceForced: true } : {}),
|
|
63
|
+
...(o.fromSubagent === true ? { fromSubagent: true } : {}),
|
|
64
|
+
...(nonEmptyString(o.originTaskId) ? { originTaskId: o.originTaskId } : {}),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return { kind: 'present', rows, dropped };
|
|
68
|
+
}
|
|
69
|
+
/** 快照里**出自子代**的悬挂 ask(宿主自己的流内 ask 走流内帧腿,不在这里出卡)。未报 / 没有快照 ⇒ `[]`。 */
|
|
70
|
+
export function suspendedSubagentAsks(snapshot, filter) {
|
|
71
|
+
const rows = snapshot?.livePending;
|
|
72
|
+
if (!Array.isArray(rows))
|
|
73
|
+
return [];
|
|
74
|
+
return rows.filter(r => r.fromSubagent === true && (filter?.sessionId === undefined || r.sessionId === filter.sessionId));
|
|
75
|
+
}
|
|
76
|
+
export function countApprovalsAwaitingDecision(snapshot, filter) {
|
|
77
|
+
if (snapshot === null || snapshot === undefined)
|
|
78
|
+
return { durable: null, live: null, suspendedSubagent: null };
|
|
79
|
+
const bySession = (rows) => filter?.sessionId === undefined ? [...rows] : rows.filter(r => r.sessionId === filter.sessionId);
|
|
80
|
+
const durable = Array.isArray(snapshot.pending) ? bySession(snapshot.pending).length : null;
|
|
81
|
+
if (!Array.isArray(snapshot.livePending))
|
|
82
|
+
return { durable, live: null, suspendedSubagent: null };
|
|
83
|
+
const live = bySession(snapshot.livePending);
|
|
84
|
+
return { durable, live: live.length, suspendedSubagent: live.filter(r => r.fromSubagent === true).length };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 悬挂行 → 流内帧腿的输入。只带行上**真有**的位:窗三键不带(行上只有绝对死线,没有出帧时刻,凑不出可校偏的窗),
|
|
88
|
+
* `args` 不带(行上没有)。`sourceTaskId` = `originTaskId`(卡头子代身份徽章的来源)。
|
|
89
|
+
*/
|
|
90
|
+
export function livePendingRowToApprovalFrame(row) {
|
|
91
|
+
return {
|
|
92
|
+
type: 'tool_approval',
|
|
93
|
+
approvalId: row.approvalId,
|
|
94
|
+
toolName: row.toolName,
|
|
95
|
+
...(row.fromSubagent === true ? { fromSubagent: true } : {}),
|
|
96
|
+
...(row.originTaskId !== undefined ? { sourceTaskId: row.originTaskId } : {}),
|
|
97
|
+
...(row.governanceForced === true ? { governanceForced: true } : {}),
|
|
98
|
+
...(row.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 出卡并决断一条悬挂 ask —— **与流内帧腿同一条链**({@link surfaceToolApprovalFrameAndRespond}):同一个卡口、
|
|
103
|
+
* 同一个身份键铸口(`approvalId`)、同一套回执 / 拒绝读法。差别只有一处:行上没有入参 ⇒ 卡上明说
|
|
104
|
+
* (`argsUnavailable`),且这张卡上收到「编辑后批准」时**整次不发**(结局 `editRefused`;没有原文可编,丢掉改写再批准 =
|
|
105
|
+
* 批准了人没看到的原始入参)。
|
|
106
|
+
*/
|
|
107
|
+
export function surfaceSuspendedAskAndRespond(row, respond, signal, lane) {
|
|
108
|
+
return surfaceToolApprovalFrameAndRespond(livePendingRowToApprovalFrame(row), respond, undefined, signal, {
|
|
109
|
+
...lane,
|
|
110
|
+
argsUnavailable: true,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** 「认领了但还没在任何一张快照里见过」的条目上界(见 {@link createSuspendedAskTracker} 的账目说明)。 */
|
|
114
|
+
const TRACKER_UNSEEN_CLAIMS_MAX = 512;
|
|
115
|
+
/**
|
|
116
|
+
* 快照 → 出卡 / 撤卡的去重账(每个宿主会话一只;不是模块级单例)。同一 `approvalId` 只 `appeared` 一次;
|
|
117
|
+
* `livePending` 键缺席按「现在一条都看不到」处理已出的卡(引擎换代后旧卡没有决断口可用)。
|
|
118
|
+
*
|
|
119
|
+
* 两本账,**都不按条数淘汰活跃条目**(异源对抗复审:无差别的「满了删最旧」会让仍挂着的卡重出、漏撤):
|
|
120
|
+
* · `shown` = 从快照出过卡、且仍在列表里的 ask。条目随「从列表消失」离账 ⇒ 大小恒 ≤ 列表长度,不设上界。
|
|
121
|
+
* · `claimed` = 被流内帧腿认领 / 已在本端决断的 ask,值 = 是否已在某张快照里见过。见过之后从列表消失 ⇒ 这只 ask
|
|
122
|
+
* 的一生结束,离账(同 id 日后再出现是新的一只)。**从未见过**的认领才受上界管(帧先到、快照一直没列到它的那一小段),
|
|
123
|
+
* 满了只淘汰这一类里最旧的。
|
|
124
|
+
*/
|
|
125
|
+
export function createSuspendedAskTracker(filter) {
|
|
126
|
+
const shown = new Set();
|
|
127
|
+
const claimed = new Map();
|
|
128
|
+
const claim = (id) => {
|
|
129
|
+
if (claimed.has(id))
|
|
130
|
+
return;
|
|
131
|
+
claimed.set(id, false);
|
|
132
|
+
let unseen = 0;
|
|
133
|
+
for (const seen of claimed.values())
|
|
134
|
+
if (!seen)
|
|
135
|
+
unseen++;
|
|
136
|
+
if (unseen <= TRACKER_UNSEEN_CLAIMS_MAX)
|
|
137
|
+
return;
|
|
138
|
+
for (const [oldId, seen] of claimed) {
|
|
139
|
+
if (seen)
|
|
140
|
+
continue;
|
|
141
|
+
claimed.delete(oldId);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
return {
|
|
146
|
+
ingest(snapshot) {
|
|
147
|
+
const rows = suspendedSubagentAsks(snapshot, filter);
|
|
148
|
+
const present = new Set(rows.map(r => r.approvalId));
|
|
149
|
+
const appeared = [];
|
|
150
|
+
for (const r of rows) {
|
|
151
|
+
if (claimed.has(r.approvalId)) {
|
|
152
|
+
claimed.set(r.approvalId, true);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (shown.has(r.approvalId))
|
|
156
|
+
continue;
|
|
157
|
+
shown.add(r.approvalId);
|
|
158
|
+
appeared.push(r);
|
|
159
|
+
}
|
|
160
|
+
const gone = [];
|
|
161
|
+
for (const id of [...shown]) {
|
|
162
|
+
if (present.has(id))
|
|
163
|
+
continue;
|
|
164
|
+
shown.delete(id);
|
|
165
|
+
gone.push(id);
|
|
166
|
+
}
|
|
167
|
+
// 见过、现在不在列表里的认领 = 这只 ask 已结算 ⇒ 离账
|
|
168
|
+
for (const [id, seen] of [...claimed])
|
|
169
|
+
if (seen && !present.has(id))
|
|
170
|
+
claimed.delete(id);
|
|
171
|
+
return { appeared, gone };
|
|
172
|
+
},
|
|
173
|
+
noteSurfacedByStream(approvalId) {
|
|
174
|
+
if (typeof approvalId !== 'string' || approvalId === '')
|
|
175
|
+
return false;
|
|
176
|
+
if (shown.has(approvalId) || claimed.has(approvalId))
|
|
177
|
+
return false;
|
|
178
|
+
claim(approvalId);
|
|
179
|
+
return true;
|
|
180
|
+
},
|
|
181
|
+
noteDecided(approvalId) {
|
|
182
|
+
if (typeof approvalId !== 'string' || approvalId === '')
|
|
183
|
+
return;
|
|
184
|
+
// 从快照出过卡的 ask 已在列表里见过 ⇒ 认领直接记「见过」,结算后随列表消失离账
|
|
185
|
+
const wasShown = shown.delete(approvalId);
|
|
186
|
+
if (wasShown)
|
|
187
|
+
claimed.set(approvalId, true);
|
|
188
|
+
else
|
|
189
|
+
claim(approvalId);
|
|
190
|
+
},
|
|
191
|
+
requeue(approvalId) {
|
|
192
|
+
if (typeof approvalId !== 'string' || approvalId === '')
|
|
193
|
+
return;
|
|
194
|
+
shown.delete(approvalId);
|
|
195
|
+
claimed.delete(approvalId);
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
@@ -486,6 +486,11 @@ export interface ApprovalCardRequest {
|
|
|
486
486
|
/** wire 层降级说明(卡 content 上方 dim 行,ToolUseConfirm.wireNote 超集位)——argsOmitted 等
|
|
487
487
|
* 不可注入 args 的提示走这里([1543]③ 记账的 note 位,2026-07-23 落位)。 */
|
|
488
488
|
wireNote?: string;
|
|
489
|
+
/**
|
|
490
|
+
* 0.72.14:这只 ask 的工具入参在本读面上**不可得**(悬挂的流内 ask)。为真 ⇒ 卡上渲的 `args` 不是工具的真实入参,
|
|
491
|
+
* 端**不要**给「编辑后批准」;包侧收到带改写的批准会拒发(见 `ToolApprovalFrameOutcome.editRefused`)。缺席 ≠ false。
|
|
492
|
+
*/
|
|
493
|
+
argsUnavailable?: true;
|
|
489
494
|
/**
|
|
490
495
|
* 治理强制位(server ≥7.5.0,2026-08-08 补透传)。**缺席 ≠ false**:只在为真时在场,缺席 = 无治理
|
|
491
496
|
* 来源的证据。壳应据此把门呈成**表态掀不掉**(而不是引导用户去改 `permissionMode`);呈现形是壳
|
|
@@ -1284,6 +1289,11 @@ export interface ToolApprovalFrameOutcome {
|
|
|
1284
1289
|
* (传输层失败)都是真实形。
|
|
1285
1290
|
*/
|
|
1286
1291
|
respondRefusal?: ToolApprovalRespondRefusal;
|
|
1292
|
+
/**
|
|
1293
|
+
* 0.72.14:入参不可得的卡(悬挂的流内 ask)上收到了「编辑后批准」⇒ 包侧**一个字节都没发**,`decision` 为 `'unresolved'`,
|
|
1294
|
+
* 这只 ask 在引擎上仍然挂着。端应重新出卡(跟踪器 `requeue`)。缺席 = 没发生这件事。
|
|
1295
|
+
*/
|
|
1296
|
+
editRefused?: true;
|
|
1287
1297
|
}
|
|
1288
1298
|
/**
|
|
1289
1299
|
* #225 件5:respond 抛错的**结构化原文**(不是新的决断态,见
|
|
@@ -1484,5 +1494,11 @@ export interface ToolApprovalFrameLaneOpts {
|
|
|
1484
1494
|
* 老宿主不传本位时行为 = 0.38.1,零回归)。live 腿的宿主接线随各端提货批补 `true`。
|
|
1485
1495
|
*/
|
|
1486
1496
|
windowIsCurrent?: boolean;
|
|
1497
|
+
/**
|
|
1498
|
+
* 0.72.14:这只 ask 的**工具入参在本读面上不可得**(悬挂的流内 ask 从 `GET /v1/approvals` 的 `livePending` 段读出,
|
|
1499
|
+
* 该段刻意不带入参)。为真 ⇒ 卡上明说「看不到入参」,且「编辑后批准」的改写不转发(没有原文可编,转发一个空对象
|
|
1500
|
+
* 会把工具的真实入参换掉)—— 收到带改写的批准时**整次不发**,结局标 `editRefused`。只由 `surfaceSuspendedAskAndRespond` 置;流内帧腿不传。
|
|
1501
|
+
*/
|
|
1502
|
+
argsUnavailable?: true;
|
|
1487
1503
|
}
|
|
1488
1504
|
export declare function surfaceToolApprovalFrameAndRespond(frame: ToolApprovalFrame, respond: RespondToolApprovalFn, streamArgs: unknown | undefined, signal?: AbortSignal, lane?: ToolApprovalFrameLaneOpts): Promise<ToolApprovalFrameOutcome>;
|
|
@@ -87,7 +87,7 @@ import { askParkRowArm } from './askParkRowRouting.js';
|
|
|
87
87
|
import { hostLog } from '../host.js';
|
|
88
88
|
import { createSessionSlot, DEFAULT_SESSION_KEY } from '../sessionSlot.js';
|
|
89
89
|
import { readEngineActiveBgTasks } from '../fleet/fleetLedger.js';
|
|
90
|
-
import { observeCancelByDeny, surfaceRememberNotApplied, surfaceEditNotForwarded, surfaceRuleArmNotSent, surfaceRuleArmRejected } from './hitlHostSurface.js';
|
|
90
|
+
import { observeCancelByDeny, surfaceRememberNotApplied, surfaceEditNotForwarded, surfaceEditRefusedOnBlindAsk, surfaceRuleArmNotSent, surfaceRuleArmRejected } from './hitlHostSurface.js';
|
|
91
91
|
import { approvalCallKey, liveFrameCallKey } from './gateIdentity.js';
|
|
92
92
|
// 0.67.0(core 7.14.0 #688 C3):`ruleStoreUnreadable` 的**闭二词判据**。词表属主 = core
|
|
93
93
|
// (`RULE_STORE_UNREADABLE_KINDS`),镜像与措辞都在 `gateVocabulary.ts` 那个唯一铸点上 ——
|
|
@@ -1353,6 +1353,12 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
|
|
|
1353
1353
|
wireNote = 'tool arguments exceeded the wire cap and were omitted — the diff below is reconstructed from the gate message, not the full payload';
|
|
1354
1354
|
hostLog('debug', `liveToolApprovalWire: frame ${frame.approvalId} args omitted (>16KiB wire cap) — card falls back to message-derived path`);
|
|
1355
1355
|
}
|
|
1356
|
+
if (lane?.argsUnavailable === true) {
|
|
1357
|
+
wireNote =
|
|
1358
|
+
"this request's tool arguments are not available on this surface — it was raised while no client was connected, " +
|
|
1359
|
+
'so you are deciding without seeing them; deny it if you are not sure what it will do. ' +
|
|
1360
|
+
'Edits are not accepted on this card: there is no original input to edit';
|
|
1361
|
+
}
|
|
1356
1362
|
const p = pathFromGateMessage(frame.message);
|
|
1357
1363
|
args = p !== undefined ? { file_path: p } : {};
|
|
1358
1364
|
}
|
|
@@ -1382,6 +1388,7 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
|
|
|
1382
1388
|
...(signal ? { signal } : {}),
|
|
1383
1389
|
...(isFromSubagent(frame) ? { workerBadge: subagentBadgeFor(frame) } : {}),
|
|
1384
1390
|
...(wireNote !== undefined ? { wireNote } : {}),
|
|
1391
|
+
...(lane?.argsUnavailable === true ? { argsUnavailable: true } : {}),
|
|
1385
1392
|
...(frame.governanceForced === true ? { governanceForced: true } : {}),
|
|
1386
1393
|
// [4851]:窗三键透传,三闸合取(异源对抗复审 P1/P2 收编):
|
|
1387
1394
|
// ① `lane.windowIsCurrent === true` —— durable 账本重放的历史帧带的是铸帧时刻的旧余量,
|
|
@@ -1468,6 +1475,14 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
|
|
|
1468
1475
|
// `"policy"`**:那是一个正面事实(出自部署 ToolPolicy),把「老引擎没报」折进去 = 替引擎编话。
|
|
1469
1476
|
...(typeof frame.origin === 'string' && frame.origin !== '' ? { origin: frame.origin } : {}),
|
|
1470
1477
|
});
|
|
1478
|
+
// 0.72.14:入参不可得的卡上收到「编辑后批准」⇒ **什么都不发**。丢掉改写再发 allow = 批准了人没看到、也不是他改成的那份
|
|
1479
|
+
// 原始入参;把改写转发出去 = 拿卡上那个空对象替换工具的真实入参。两条都不是人按下的那个决定 ⇒ 这只 ask 保持悬挂,
|
|
1480
|
+
// 结局如实标 `editRefused`,由端重新出卡(异源对抗复审 [high])。
|
|
1481
|
+
if (lane?.argsUnavailable === true && card.kind === 'allow' && card.updatedInput !== undefined) {
|
|
1482
|
+
hostLog('error', `liveToolApprovalWire: ${frame.approvalId} card returned an edited approval on an args-unavailable ask — nothing sent (the ask stays pending)`);
|
|
1483
|
+
surfaceEditRefusedOnBlindAsk();
|
|
1484
|
+
return { decision: 'unresolved', editRefused: true };
|
|
1485
|
+
}
|
|
1471
1486
|
const decision = card.kind === 'allow' ? (card.allowSession ? 'allow_session' : 'allow') : 'deny';
|
|
1472
1487
|
if (card.kind === 'failed') {
|
|
1473
1488
|
hostLog('debug', `liveToolApprovalWire: approval card unavailable (${card.reason}) — fail-closed deny for ${frame.approvalId}`);
|
package/dist/index.d.ts
CHANGED
|
@@ -267,6 +267,7 @@ export * from './hitl/resumeRunningCard.js';
|
|
|
267
267
|
export * from './hitl/persistedRulesWire.js';
|
|
268
268
|
export * from './hitl/localAllowRule.js';
|
|
269
269
|
export * from './hitl/approvalsFeed.js';
|
|
270
|
+
export * from './hitl/livePendingAsk.js';
|
|
270
271
|
export * from './hitl/crashConverged.js';
|
|
271
272
|
export * from './interactiveHalt.js';
|
|
272
273
|
export * from './compensations.js';
|
package/dist/index.js
CHANGED
|
@@ -492,6 +492,8 @@ export * from './hitl/localAllowRule.js';
|
|
|
492
492
|
// B7 ③(census G20,**行为改动**不是搬迁):pending-approvals 推送 feed(stream 优先 / 断流回落
|
|
493
493
|
// 轮询 / 定期再试)。🔴 它**不替换** D-1 的取件 —— 那三处必须继续走权威 `list()`(见文件头)。
|
|
494
494
|
export * from './hitl/approvalsFeed.js';
|
|
495
|
+
// 0.72.14:悬挂的流内 ask(`GET /v1/approvals` 的 `livePending` 段)的读面、视图、去重账与决断口。
|
|
496
|
+
export * from './hitl/livePendingAsk.js';
|
|
495
497
|
// ── L-38:`/v1/approvals` additive 键 `crashConverged` 的读面 + 纯投影 ─────────────────────────
|
|
496
498
|
// local 引擎崩在审批门上时,那些孤儿 ask 被 server 重启后收敛成 DENIED 同码;这一键把「上一条命
|
|
497
499
|
// 留下了什么」交到端手上。收在库里的理由是**两处判定**三端各写一遍必然各错一遍:① **缺席 vs
|
package/dist/workflowClient.js
CHANGED
|
@@ -385,11 +385,28 @@ export function projectWorkflowRun(run) {
|
|
|
385
385
|
const parks = readWorkflowParks(run);
|
|
386
386
|
const agents = (run.agents ?? []).map((r, i) => projectAgent(r, i));
|
|
387
387
|
const phases = projectPhases(run, agents);
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
388
|
+
// 0.73.0:总用量三态 —— 有键 ⇒ 一个**完整**的总数(含真 0);缺键 ⇒ 没有完整总数。判据(每个分量都先验非负整数):
|
|
389
|
+
// ① 引擎聚合 `stats.tokens` 在场且合法:无 nested 载体 ⇒ 就是它;nested 载体在场 ⇒ 其 tokens 也得合法才相加,否则缺席
|
|
390
|
+
// (残缺的聚合不按 0 补齐 —— 那会发布一个偏小的「总数」);
|
|
391
|
+
// ② `stats.tokens` 缺席但 nested 载体在场 ⇒ 聚合残缺 ⇒ 缺席(不退到腿上求和:腿只覆盖本 run,会把嵌套的那部分漏掉);
|
|
392
|
+
// ③ 两者都没有 ⇒ 退到腿上求和,但只在**每条腿都报过合法用量**且至少一条腿时成立;有腿未报 / 非法 / 零条腿 ⇒ 缺席。
|
|
393
|
+
const validTokens = (v) => typeof v === 'number' && Number.isInteger(v) && v >= 0 ? v : undefined;
|
|
394
|
+
const ownTokens = validTokens(run.stats?.tokens);
|
|
395
|
+
const nestedCarrier = run.stats?.nested;
|
|
396
|
+
const hasNestedCarrier = nestedCarrier !== undefined && nestedCarrier !== null;
|
|
397
|
+
const nestedTokens = hasNestedCarrier ? validTokens(nestedCarrier.tokens) : undefined;
|
|
398
|
+
const legTokens = agents.map(a => validTokens(a.tokens));
|
|
399
|
+
const totalTokens = ownTokens !== undefined
|
|
400
|
+
? !hasNestedCarrier
|
|
401
|
+
? ownTokens
|
|
402
|
+
: nestedTokens !== undefined
|
|
403
|
+
? ownTokens + nestedTokens
|
|
404
|
+
: undefined
|
|
405
|
+
: hasNestedCarrier
|
|
406
|
+
? undefined
|
|
407
|
+
: legTokens.length > 0 && legTokens.every(t => t !== undefined)
|
|
408
|
+
? legTokens.reduce((s, t) => s + t, 0)
|
|
409
|
+
: undefined;
|
|
393
410
|
const state = {
|
|
394
411
|
workflowRunId: run.id,
|
|
395
412
|
// 0.72.12 CC-51(异源对抗复审 R2 [medium]):run 状态词认不出 / 缺席时先看腿上的**活跃证据** —— 有腿在跑 / 排队 ⇒ running
|
|
@@ -404,7 +421,7 @@ export function projectWorkflowRun(run) {
|
|
|
404
421
|
// a script we do not have. scriptPath is likewise absent until the service projects it.
|
|
405
422
|
script: '',
|
|
406
423
|
agentCount: agents.length,
|
|
407
|
-
totalTokens,
|
|
424
|
+
...(totalTokens !== undefined ? { totalTokens } : {}),
|
|
408
425
|
phases,
|
|
409
426
|
};
|
|
410
427
|
if (run.name)
|
|
@@ -90,7 +90,8 @@ export interface WorkflowRunState {
|
|
|
90
90
|
status: 'running' | 'done' | 'failed' | 'stopped' | 'paused' | 'parked' | 'unknown';
|
|
91
91
|
cloud?: boolean;
|
|
92
92
|
agentCount: number;
|
|
93
|
-
|
|
93
|
+
/** 🔴 0.73.0(BREAKING):可选。有键 ⇒ 总用量(含真 0);缺键 ⇒ 没有权威总数且有腿未报(或零条腿)—— 渲「—」,不渲 0。 */
|
|
94
|
+
totalTokens?: number;
|
|
94
95
|
/** phases as authored; the component derives the active/clamped windows itself. */
|
|
95
96
|
phases: WorkflowPhase[];
|
|
96
97
|
/**
|