@sema-agent/client-core 0.7.0 → 0.8.1
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 +31 -11
- package/dist/adapt.js +36 -3
- package/dist/compensations.d.ts +63 -0
- package/dist/compensations.js +138 -0
- package/dist/detachWire.d.ts +141 -0
- package/dist/detachWire.js +182 -0
- package/dist/engineSessionParam.d.ts +6 -2
- package/dist/engineSessionParam.js +24 -18
- package/dist/engineWireTarget.d.ts +14 -0
- package/dist/engineWireTarget.js +53 -0
- package/dist/fleet/fleetLedger.d.ts +132 -0
- package/dist/fleet/fleetLedger.js +393 -0
- package/dist/fleet/fleetProjection.d.ts +181 -0
- package/dist/fleet/fleetProjection.js +258 -0
- package/dist/hooksWireCaps.d.ts +91 -0
- package/dist/hooksWireCaps.js +359 -0
- package/dist/host.d.ts +34 -2
- package/dist/host.js +6 -0
- package/dist/index.d.ts +56 -1
- package/dist/index.js +82 -1
- package/dist/liveInitToolFace.d.ts +53 -0
- package/dist/liveInitToolFace.js +251 -0
- package/dist/model/modelFamilies.json +63 -0
- package/dist/model/providerPresets.d.ts +46 -0
- package/dist/model/providerPresets.js +160 -0
- package/dist/model/providerPresets.json +1179 -0
- package/dist/seam.d.ts +19 -3
- package/dist/seam.js +14 -2
- package/dist/subagent/engineCompactWire.d.ts +12 -0
- package/dist/subagent/engineCompactWire.js +176 -0
- package/dist/subagent/engineDelegatedPrompt.d.ts +44 -0
- package/dist/subagent/engineDelegatedPrompt.js +205 -0
- package/dist/subagent/engineRowStopGate.d.ts +18 -0
- package/dist/subagent/engineRowStopGate.js +54 -0
- package/dist/subagent/engineSubagentOutput.d.ts +15 -0
- package/dist/subagent/engineSubagentOutput.js +98 -0
- package/dist/subagent/engineSubagentSteer.d.ts +10 -0
- package/dist/subagent/engineSubagentSteer.js +68 -0
- package/dist/subagent/engineSubagentTail.d.ts +40 -0
- package/dist/subagent/engineSubagentTail.js +241 -0
- package/dist/subagent/engineTaskHandleWire.d.ts +89 -0
- package/dist/subagent/engineTaskHandleWire.js +213 -0
- package/dist/workflowClient.d.ts +42 -0
- package/dist/workflowClient.js +467 -0
- package/dist/workflowMonitor.d.ts +94 -0
- package/dist/workflowMonitor.js +43 -0
- package/package.json +2 -2
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { hostLog } from '../host.js';
|
|
2
|
+
import { engineWireDebugEnabled } from '../engineWireTarget.js';
|
|
3
|
+
import { engineSessionParam } from '../engineSessionParam.js';
|
|
4
|
+
// `isHookNoticeFrame` 不在这里 re-export —— 它已由 index.ts 的 `export * from './notifications.js'`
|
|
5
|
+
// 出口(重复 re-export 只会让同名绑定在包根变含糊)。端要先分诊再喂台账,从包根 import 即可。
|
|
6
|
+
import { enqueueBgChildNotification, isOwnWorkflowRun } from '../notifications.js';
|
|
7
|
+
import { isOwnEngineRun, recordBgParentRun, recordBgTerminalFacts, recordOwnEngineRun, } from '../subagentContentStore.js';
|
|
8
|
+
import { rowIdTail } from '../workflow.js';
|
|
9
|
+
import { recordEngineTranscriptId } from '../subagent/engineDelegatedPrompt.js';
|
|
10
|
+
import { TERMINAL_FLEET_TASK_STATUSES, projectTasks, projectWorkflows, } from './fleetProjection.js';
|
|
11
|
+
/** 终态行留存宽限窗(187 的 agents 页把完成会话留在列表;窗过即净,防长会话堆行)。 */
|
|
12
|
+
export const TERMINAL_RETAIN_MS = 60_000;
|
|
13
|
+
const liveBgViews = new Set();
|
|
14
|
+
/**
|
|
15
|
+
* server 代际证据读面:任一 live fleet 源的 meta 报过 `sessionScoped=true`。
|
|
16
|
+
* true ⇒ 到达本连接的**行帧**已经过 server 会话过滤,端侧行级 own 判别可以让位。
|
|
17
|
+
* 🔴 只覆盖【行帧】:通知帧的 own-run 台账判别不看本标,恒开(1.244 对缺 ownerSessionId 的通知
|
|
18
|
+
* 仍广播,fail-closed 是 1.245+)。
|
|
19
|
+
*/
|
|
20
|
+
export function isFleetSessionScoped() {
|
|
21
|
+
for (const view of liveBgViews) {
|
|
22
|
+
try {
|
|
23
|
+
if (view.sessionScoped() === true)
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
/* 单视图炸不拖累读面 */
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
/** 引擎侧在飞 bg 子代读数(union 所有未 dispose 的源,按 row.id 去重)。无 live 源 ⇒ `{[], false}`。 */
|
|
33
|
+
export function readEngineActiveBgTasks() {
|
|
34
|
+
const byId = new Map();
|
|
35
|
+
let fresh = false;
|
|
36
|
+
for (const view of liveBgViews) {
|
|
37
|
+
try {
|
|
38
|
+
const r = view.read();
|
|
39
|
+
if (r.connected)
|
|
40
|
+
fresh = true;
|
|
41
|
+
for (const row of r.rows)
|
|
42
|
+
byId.set(row.id, row);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
/* 单视图炸不拖累读面 */
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { tasks: [...byId.values()], fresh };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 清空所有 live 源的终态留存池 —— 会话切换(/clear、REPL 内 resume、fork)信号一到就调。
|
|
52
|
+
* R7:留存池是渲染层宽限窗,快照臂刻意不清它(重连不该抽走行);但换了会话继续渲上一会话的
|
|
53
|
+
* 终态行(名字 = objective 派生)就是跨会话残影。
|
|
54
|
+
*/
|
|
55
|
+
export function clearAllRetainedFleetRows() {
|
|
56
|
+
for (const view of liveBgViews) {
|
|
57
|
+
try {
|
|
58
|
+
view.clearRetained();
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
/* 单视图炸不拖累信号 */
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** 测试钩:摘掉所有登记的视图(同进程多组断言之间互不污染)。 */
|
|
66
|
+
export function __resetFleetLedgerRegistryForTests() {
|
|
67
|
+
liveBgViews.clear();
|
|
68
|
+
}
|
|
69
|
+
// ── P1-4:会话过滤参数(取代壳侧 makeSessionScopingFetch)───────────────────────────────────────
|
|
70
|
+
/**
|
|
71
|
+
* `client.fleet.stream(...)` 的会话过滤参数。缺席(端未装 SessionPort / 还没有会话 id)⇒ `{}`
|
|
72
|
+
* = principal 级视图,**与 1.37 之前逐字同行为**(不是错误态)。
|
|
73
|
+
*/
|
|
74
|
+
export function fleetStreamOptions() {
|
|
75
|
+
const s = engineSessionParam();
|
|
76
|
+
return s ? { session: s } : {};
|
|
77
|
+
}
|
|
78
|
+
/** G19:`client.fleet.snapshot(...)` 的参数(同上;首屏/温切探针用)。 */
|
|
79
|
+
export function fleetSnapshotOptions(opts) {
|
|
80
|
+
return {
|
|
81
|
+
...fleetStreamOptions(),
|
|
82
|
+
...(opts?.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 建一个 fleet 帧台账。**不开流、不构造 client** —— 宿主的 consume 循环把解码后的帧喂进来。
|
|
87
|
+
* (这条切缝就是设计稿 §2.5 给 fleetClient 划的那一刀:帧体归库、连接归端。)
|
|
88
|
+
*/
|
|
89
|
+
export function createFleetLedger(hooks = {}) {
|
|
90
|
+
const taskMap = new Map();
|
|
91
|
+
const wfMap = new Map();
|
|
92
|
+
/** [1481]①「elapsed 恒 0s」补偿:记每行最后一帧到达时刻(引擎只在活动边界重发行)。 */
|
|
93
|
+
const rowMeta = new Map();
|
|
94
|
+
/** [1481]②「完成即消失」补偿:终态行进留存池,宽限窗内继续投影。 */
|
|
95
|
+
const retained = new Map();
|
|
96
|
+
let connected = false;
|
|
97
|
+
let version = null;
|
|
98
|
+
let scoped = null;
|
|
99
|
+
let sessionScopedMeta;
|
|
100
|
+
let bgNotifyFailClosedMeta = false;
|
|
101
|
+
let disposed = false;
|
|
102
|
+
const debug = (line) => {
|
|
103
|
+
if (engineWireDebugEnabled())
|
|
104
|
+
hostLog('debug', line);
|
|
105
|
+
};
|
|
106
|
+
const bgView = {
|
|
107
|
+
read: () => ({
|
|
108
|
+
connected,
|
|
109
|
+
rows: [...taskMap.values()]
|
|
110
|
+
.filter(r => !TERMINAL_FLEET_TASK_STATUSES.has(r.status ?? ''))
|
|
111
|
+
.map(r => ({
|
|
112
|
+
id: r.id,
|
|
113
|
+
name: r.name,
|
|
114
|
+
status: r.status ?? 'running',
|
|
115
|
+
...(r.parentId ? { parentId: r.parentId } : {}),
|
|
116
|
+
})),
|
|
117
|
+
}),
|
|
118
|
+
sessionScoped: () => sessionScopedMeta,
|
|
119
|
+
clearRetained: () => {
|
|
120
|
+
retained.clear();
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
liveBgViews.add(bgView);
|
|
124
|
+
function applyFrame(frame) {
|
|
125
|
+
switch (frame.type) {
|
|
126
|
+
case 'meta': {
|
|
127
|
+
version = frame.version;
|
|
128
|
+
scoped = frame.scoped;
|
|
129
|
+
// 🔴 B6 typed 直读(SDK 0.0.117 `FleetFrame.meta` 已 declare 这两键;此前是
|
|
130
|
+
// `(frame as {sessionScoped?: unknown})` 兜读)。server 1.291.0 实发
|
|
131
|
+
// `{version:1, scoped, sessionScoped: callerSession!==null, bgNotifyFailClosed:true}`。
|
|
132
|
+
sessionScopedMeta = typeof frame.sessionScoped === 'boolean' ? frame.sessionScoped : undefined;
|
|
133
|
+
// [1510] 代际标:fail-closed 语义只在 session-bound 连接生效(CHANGELOG 钉「仅 session-bound
|
|
134
|
+
// 连接适用」),所以让位判定必须叠加 sessionScoped —— 亲拆 1.247 dist:meta 恒带 true。
|
|
135
|
+
bgNotifyFailClosedMeta = frame.bgNotifyFailClosed === true;
|
|
136
|
+
// [1521]F1 教训:让位路径此前是死代码而围栏全绿(未断言路径本身)——数据面日志让围栏能锚
|
|
137
|
+
// 「代际标真的到达且被消费」,防中继剥键类回归再度静默。
|
|
138
|
+
debug(`[fleet-frame] meta sessionScoped=${String(sessionScopedMeta)} bgNotifyFailClosed=${String(bgNotifyFailClosedMeta)}`);
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
case 'snapshot': {
|
|
142
|
+
// 新快照 REPLACE 整个活跃集(重连间隙里被删掉的行不该 linger)。
|
|
143
|
+
taskMap.clear();
|
|
144
|
+
wfMap.clear();
|
|
145
|
+
rowMeta.clear();
|
|
146
|
+
const at = frame.ts;
|
|
147
|
+
for (const row of frame.tasks) {
|
|
148
|
+
taskMap.set(row.id, row);
|
|
149
|
+
rowMeta.set(row.id, { receivedAt: at });
|
|
150
|
+
}
|
|
151
|
+
for (const row of frame.workflows)
|
|
152
|
+
wfMap.set(row.id, row);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case 'task': {
|
|
156
|
+
const row = frame.row;
|
|
157
|
+
if (!row?.id)
|
|
158
|
+
break;
|
|
159
|
+
taskMap.set(row.id, row); // POST-MERGE FULL row → replace by id
|
|
160
|
+
rowMeta.set(row.id, { receivedAt: frame.ts });
|
|
161
|
+
debug(`[fleet-row] id=${row.id} parent=${row.parentId ?? '-'} agentType=${row.agentType ?? '-'}` +
|
|
162
|
+
` agentName=${row.agentName ?? '-'} name=${row.name ?? '-'} status=${row.status ?? '-'}` +
|
|
163
|
+
` tokens=${row.tokens ?? 0} parentToolCallId=${row.parentToolCallId ?? '-'}`);
|
|
164
|
+
// [1491] subagentOutput 寻址:子代行出生帧带 parentId(spawn run)——记映射供查看态终报读面。
|
|
165
|
+
// 归属判别与通知臂同锚(R1/F1,2026-07-22):
|
|
166
|
+
// · parent ∈ own-run 台账 → own:登映射 + 把本行 taskId 也登进台账(F1 闭包 —— 嵌套孙代的
|
|
167
|
+
// bg_notification.parentTaskId 是**直接父**,不闭包的话孙代完成通知会被当 foreign 丢);
|
|
168
|
+
// · parent ∉ 台账但 meta.sessionScoped===true → server 已按会话过滤行帧,到达即本会话
|
|
169
|
+
// (或无标行)→ 登映射**不登台账**(台账只收硬证据,防无标外来行污染台账后反过来给
|
|
170
|
+
// 通知臂放行);这也保住「壳重启+resume 接回会话」的终报 hydrate;
|
|
171
|
+
// · 两者都不是(老代际广播窗)→ 不登映射(隔离优先)。
|
|
172
|
+
if (row.parentId) {
|
|
173
|
+
const childTid = rowIdTail(row.id);
|
|
174
|
+
const parentTid = rowIdTail(row.parentId);
|
|
175
|
+
const parentOwned = isOwnEngineRun(parentTid);
|
|
176
|
+
if (parentOwned)
|
|
177
|
+
recordOwnEngineRun(childTid);
|
|
178
|
+
if (parentOwned || sessionScopedMeta === true) {
|
|
179
|
+
recordBgParentRun(childTid, parentTid);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
debug(`[fleet-frame] task row parent-map SKIPPED foreign/unowned(row=${row.id} parent=${parentTid})`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// [1839]① 终态行帧四键:transcriptId 现在**行上也可能有**(此前只在通知帧),有就登锚 ——
|
|
186
|
+
// 委派 prompt 取件腿据此在「只看行卡」的场景也能补齐详情页 Prompt 段。
|
|
187
|
+
if (typeof row.transcriptId === 'string' && row.transcriptId.length > 0) {
|
|
188
|
+
recordEngineTranscriptId(rowIdTail(row.id), row.transcriptId);
|
|
189
|
+
}
|
|
190
|
+
// 终态帧 → 留存池(remove 同毫秒尾随,probe 实证;留存让 completed band 数得到它)。
|
|
191
|
+
if (TERMINAL_FLEET_TASK_STATUSES.has(row.status ?? '')) {
|
|
192
|
+
retained.set(row.id, { row, expireAt: frame.ts + TERMINAL_RETAIN_MS });
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
case 'task_remove':
|
|
197
|
+
taskMap.delete(frame.id);
|
|
198
|
+
rowMeta.delete(frame.id);
|
|
199
|
+
break;
|
|
200
|
+
case 'workflow':
|
|
201
|
+
if (frame.row?.id)
|
|
202
|
+
wfMap.set(frame.row.id, frame.row);
|
|
203
|
+
break;
|
|
204
|
+
case 'workflow_remove':
|
|
205
|
+
wfMap.delete(frame.id);
|
|
206
|
+
break;
|
|
207
|
+
case 'hook_notice':
|
|
208
|
+
// hook_notice(server 1.280.0,[1790]):「本轮 Stop 守卫未能评估」纯 observe 帧。
|
|
209
|
+
// 判定本体在 notifications.classifyHookNoticeFrame(#105 F3:判别不许留在分发处,否则
|
|
210
|
+
// 离线门只能 grep);写 store 归宿主。SDK 0.0.117 起本帧**已在 FleetFrame 联合里**,
|
|
211
|
+
// 不再走 default 臂的 `type` 字面判别。
|
|
212
|
+
hooks.onHookNotice?.(frame, sessionScopedMeta === true);
|
|
213
|
+
break;
|
|
214
|
+
case 'bg_notification':
|
|
215
|
+
applyBgNotification(frame.notification);
|
|
216
|
+
break;
|
|
217
|
+
default:
|
|
218
|
+
// 未知未来帧 —— 开集,忽略。
|
|
219
|
+
// ⚠️ 老 SDK 时代这里还兜着「平铺形 bg_notification / 字面 hook_notice」两条 belt;
|
|
220
|
+
// 0.0.117 的 FleetFrame 两者都是具名臂,belt 随之退役(旧回放素材由端自己升级)。
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/** bg 子代终态通知帧([446] 帧契约,service 1.119):常开 lane 即时到达 → 转注入链。 */
|
|
225
|
+
function applyBgNotification(n) {
|
|
226
|
+
if (typeof n?.taskId !== 'string' || typeof n?.status !== 'string')
|
|
227
|
+
return;
|
|
228
|
+
// 🔴 own/foreign 判别(双开泄漏案帧级防线;[1498]④ 换锚重写):payload `sessionId` 是**子代
|
|
229
|
+
// 自身** session uuid,宿主会话在 ownerSessionId 上且出 wire 前被剥 —— 所以判别锚
|
|
230
|
+
// `parentTaskId`(spawn 该子代的 leader run id)对 own-run 台账:
|
|
231
|
+
// · ∈ 台账 → own,放行;
|
|
232
|
+
// · 在但不在台账 → 隔离优先丢弃;
|
|
233
|
+
// · 缺席 → 放行(absent-放行姿势)。
|
|
234
|
+
// [1510] 代际让位:meta 报 bgNotifyFailClosed 且本连接 session-bound ⇒ server 侧注入路已
|
|
235
|
+
// fail-closed,到达即本会话 → 端侧台账判别整体让位。
|
|
236
|
+
const serverFailClosed = bgNotifyFailClosedMeta && sessionScopedMeta === true;
|
|
237
|
+
// [1518] δ 投影:通知带 `rootSessionId`(委托树 root 宿主会话,固定点语义)=== 本会话即 own
|
|
238
|
+
// 硬证据。🔴 与行帧的 `rowSessionTag` 不同 —— server `streamFleet` 只从**行**上剥
|
|
239
|
+
// scope/sessionId,通知帧只剥 ownerScope/ownerSessionId,`rootSessionId` **照发**
|
|
240
|
+
// (1.291.0 dist 实读)⇒ 这条腿是活的,不在 G12 删除范围内。
|
|
241
|
+
const ownByRoot = typeof n.rootSessionId === 'string' &&
|
|
242
|
+
n.rootSessionId.length > 0 &&
|
|
243
|
+
n.rootSessionId === engineSessionParam();
|
|
244
|
+
if (!serverFailClosed &&
|
|
245
|
+
!ownByRoot &&
|
|
246
|
+
typeof n.parentTaskId === 'string' &&
|
|
247
|
+
n.parentTaskId.length > 0 &&
|
|
248
|
+
!isOwnEngineRun(n.parentTaskId)) {
|
|
249
|
+
debug(`[fleet-frame] bg_notification DROPPED foreign/unowned parentRun(taskId=${n.taskId} parentRun=${n.parentTaskId})`);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
debug(`[fleet-frame] bg_notification taskId=${n.taskId} status=${n.status} seq=${n.seq ?? '-'}`);
|
|
253
|
+
if (TERMINAL_FLEET_TASK_STATUSES.has(n.status)) {
|
|
254
|
+
if (typeof n.parentTaskId === 'string' && n.parentTaskId) {
|
|
255
|
+
recordBgParentRun(n.taskId, n.parentTaskId);
|
|
256
|
+
}
|
|
257
|
+
// `transcriptId`(server 1.278.0)= 委派 prompt 的取件锚。现势:通知帧是它**今天真的在场**
|
|
258
|
+
// 的主要位置(行帧那条腿已同款接好,引擎一发即亮 —— 见 case 'task')。
|
|
259
|
+
if (typeof n.transcriptId === 'string' && n.transcriptId.length > 0) {
|
|
260
|
+
recordEngineTranscriptId(n.taskId, n.transcriptId);
|
|
261
|
+
}
|
|
262
|
+
// [1481]① 查看态事实喂给:终态通知带 summary+recentSteps,这是端当前能拿到的关于后台子代
|
|
263
|
+
// 结局的【全部】内容(终报正文不在 wire 上)。查看态对无终报的终态行渲染这些真实事实。
|
|
264
|
+
recordBgTerminalFacts(n.taskId, {
|
|
265
|
+
status: n.status,
|
|
266
|
+
...(typeof n.summary === 'string' ? { summary: n.summary } : {}),
|
|
267
|
+
// 🔴 SDK 0.0.117 把 `recentSteps` 的三键 declare 成**必填** string,但那是**声称**不是保证:
|
|
268
|
+
// 帧从 wire 上来,脏项(null / 缺键 / 非串)在类型面之外仍可能到达,而 BgTerminalFacts
|
|
269
|
+
// 那侧三键**全可选** ⇒ 这里的逐键 typeof 不是「防御读没退干净」,是 wire 脏值过滤(与
|
|
270
|
+
// fleetProjection 的 `wire*` 值域校验同一档次)。谓词类型必须是元素型本身,不能是
|
|
271
|
+
// `Record<string, unknown>`(那不是参数型的子型 ⇒ TS2677)。
|
|
272
|
+
...(Array.isArray(n.recentSteps)
|
|
273
|
+
? {
|
|
274
|
+
recentSteps: n.recentSteps
|
|
275
|
+
.filter((s) => Boolean(s) && typeof s === 'object')
|
|
276
|
+
.map(s => ({
|
|
277
|
+
...(typeof s.tool === 'string' ? { tool: s.tool } : {}),
|
|
278
|
+
...(typeof s.target === 'string' ? { target: s.target } : {}),
|
|
279
|
+
...(typeof s.outcome === 'string' ? { outcome: s.outcome } : {}),
|
|
280
|
+
})),
|
|
281
|
+
}
|
|
282
|
+
: {}),
|
|
283
|
+
});
|
|
284
|
+
// belt:终态**行帧**丢失(kill 竞态等)时,已知行也随通知转终态进留存池。
|
|
285
|
+
const known = taskMap.get(n.taskId);
|
|
286
|
+
if (known && !retained.has(n.taskId)) {
|
|
287
|
+
retained.set(n.taskId, {
|
|
288
|
+
row: { ...known, status: n.status },
|
|
289
|
+
expireAt: Date.now() + TERMINAL_RETAIN_MS,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
enqueueBgChildNotification({
|
|
294
|
+
taskId: n.taskId,
|
|
295
|
+
status: n.status,
|
|
296
|
+
// seq = 周期号(SendMessage 复活即 +1)—— 去重键必须带,否则 revive 周期的完成通知被首周期
|
|
297
|
+
// 同 (taskId,status) 键吞掉(clay 图2 案根因,1.355 复验 seq=2 帧实锤)。
|
|
298
|
+
...(typeof n.seq === 'number' ? { seq: n.seq } : {}),
|
|
299
|
+
...(typeof n.summary === 'string' ? { summary: n.summary } : {}),
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
function project(nowMs, opts) {
|
|
303
|
+
const viewingTaskId = opts.viewingTaskId;
|
|
304
|
+
// ① 帧间时钟推进(非终态行):elapsedMs 基值 + 帧后空窗。终态行冻结在 wire 终值(诚实)。
|
|
305
|
+
const liveRows = [...taskMap.values()].map(r => {
|
|
306
|
+
if (TERMINAL_FLEET_TASK_STATUSES.has(r.status ?? ''))
|
|
307
|
+
return r;
|
|
308
|
+
// [1518] δ:server 1.250 起行带 startedAt(launch 真锚)⇒ elapsed 自算,不再依赖补偿链。
|
|
309
|
+
const started = r.startedAt;
|
|
310
|
+
if (typeof started === 'number' && started > 0 && started <= nowMs) {
|
|
311
|
+
return { ...r, elapsedMs: nowMs - started };
|
|
312
|
+
}
|
|
313
|
+
const meta = rowMeta.get(r.id);
|
|
314
|
+
if (!meta)
|
|
315
|
+
return r;
|
|
316
|
+
const idle = Math.max(0, nowMs - meta.receivedAt);
|
|
317
|
+
return idle > 0 ? { ...r, elapsedMs: (r.elapsedMs ?? 0) + idle } : r;
|
|
318
|
+
});
|
|
319
|
+
// ② 终态留存池:live 集没有的行在宽限窗内继续投影;被查看中的行不过期。
|
|
320
|
+
const retainedRows = [];
|
|
321
|
+
for (const [id, r] of retained) {
|
|
322
|
+
if (taskMap.has(id))
|
|
323
|
+
continue;
|
|
324
|
+
const isViewed = viewingTaskId !== undefined && rowIdTail(id) === viewingTaskId;
|
|
325
|
+
if (nowMs > r.expireAt && !isViewed) {
|
|
326
|
+
retained.delete(id);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
retainedRows.push(r.row);
|
|
330
|
+
}
|
|
331
|
+
// ③ 行级 own 判别(clay 2026-07-23 双 session 串扰案,1.251 引擎对行帧广播,B 壳 FleetView
|
|
332
|
+
// 直接看到 A 壳的子代行)。与通知帧的台账判别对称 —— 行帧此前只有连接级 `?session=`
|
|
333
|
+
// 一道防线,server 旁路(≤1.252 无标行广播 / 无 auth 部署)即裸奔。
|
|
334
|
+
// 🔴 B6:第三腿 `rowSessionTag`(rootSessionId/ownerSessionId/sessionId)已删 —— 见文件头
|
|
335
|
+
// G12 直证(server 1.291.0 `stripT`/`stripW` 把 sessionId 与 scope 双双剥掉,另两键
|
|
336
|
+
// 在行类型上从来不存在)。剩下的两腿一字未动。
|
|
337
|
+
// 诚实边界(码注声明):老引擎 + 壳重启/resume 场景,自己的历史行会被藏 —— 宁藏勿串;
|
|
338
|
+
// 1.244+ server 过滤在场时不受此限。
|
|
339
|
+
const allRowsById = new Map();
|
|
340
|
+
for (const r of [...liveRows, ...retainedRows])
|
|
341
|
+
allRowsById.set(r.id, r);
|
|
342
|
+
const ownTaskRow = (r) => {
|
|
343
|
+
if (sessionScopedMeta === true)
|
|
344
|
+
return true;
|
|
345
|
+
// 祖链闭包:孙代行 parentId=直接父(a… handle)而非 spawn run([1502] wire 事实)——
|
|
346
|
+
// 沿 parentId 链上溯,任一环(或环的 parentId)∈ 本端 run 台账即 own。
|
|
347
|
+
const seen = new Set();
|
|
348
|
+
let cur = r;
|
|
349
|
+
while (cur && !seen.has(cur.id)) {
|
|
350
|
+
seen.add(cur.id);
|
|
351
|
+
if (isOwnEngineRun(cur.id))
|
|
352
|
+
return true;
|
|
353
|
+
if (cur.parentId !== undefined && isOwnEngineRun(cur.parentId))
|
|
354
|
+
return true;
|
|
355
|
+
cur = cur.parentId !== undefined ? allRowsById.get(cur.parentId) : undefined;
|
|
356
|
+
}
|
|
357
|
+
return false;
|
|
358
|
+
};
|
|
359
|
+
const ownWorkflowRow = (r) => sessionScopedMeta === true || isOwnWorkflowRun(r.id);
|
|
360
|
+
return {
|
|
361
|
+
tasks: projectTasks([...liveRows, ...retainedRows].filter(ownTaskRow), {
|
|
362
|
+
rowFilter: opts.rowFilter,
|
|
363
|
+
...(viewingTaskId !== undefined ? { viewingTaskId } : {}),
|
|
364
|
+
}),
|
|
365
|
+
workflows: projectWorkflows([...wfMap.values()].filter(ownWorkflowRow)),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
applyFrame(frame) {
|
|
370
|
+
if (disposed)
|
|
371
|
+
return;
|
|
372
|
+
applyFrame(frame);
|
|
373
|
+
},
|
|
374
|
+
setConnected(v) {
|
|
375
|
+
connected = v;
|
|
376
|
+
},
|
|
377
|
+
project,
|
|
378
|
+
status: () => ({
|
|
379
|
+
connected,
|
|
380
|
+
version,
|
|
381
|
+
scoped,
|
|
382
|
+
sessionScoped: sessionScopedMeta,
|
|
383
|
+
bgNotifyFailClosed: bgNotifyFailClosedMeta,
|
|
384
|
+
taskCount: taskMap.size,
|
|
385
|
+
workflowCount: wfMap.size,
|
|
386
|
+
}),
|
|
387
|
+
dispose() {
|
|
388
|
+
disposed = true;
|
|
389
|
+
liveBgViews.delete(bgView);
|
|
390
|
+
},
|
|
391
|
+
__rowsForTest: () => [...taskMap.values()],
|
|
392
|
+
};
|
|
393
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B6 批搬迁(2026-07-27,设计稿 §3 B6 + §2.5 拆缝表):cli `src/sema/fleetClient.ts` 的
|
|
3
|
+
* **投影半场** —— `coerceTaskStatus` / `coerceWorkflowStatus` / `deriveAgentLabel` /
|
|
4
|
+
* `projectDescription` / `CONTROL_TOOL_VERBS` / `projectTasks` / `projectWorkflows` /
|
|
5
|
+
* 四个 `wire*` 读取器 + `TERMINAL_FLEET_TASK_STATUSES`。
|
|
6
|
+
*
|
|
7
|
+
* 留在 TUI 的(设计稿 §2.4.2-B「看着像库、其实是端」):`main` 合成行 / `selection` 游标 /
|
|
8
|
+
* `viewed` 的**所有者**(AppState.viewingAgentTaskId)/ `footerRowBelt.isFooterTaskRow` 的
|
|
9
|
+
* 「丢弃所有无 parentId 顶层行」—— 后者是端专属决策(主 run 由壳自己合成头部行渲染),
|
|
10
|
+
* 所以本文件的 `projectTasks` 把它做成**必填**的 `rowFilter`,不是可选钩子:
|
|
11
|
+
* 🔴 做成可选 = 端漏传时主 run 行会与端自己的头部行**双渲**,而两张都长得像对的
|
|
12
|
+
* ([paired-mechanisms-must-share-premise] 的同族失败形)。必填 ⇒ 编译期就逼端表态。
|
|
13
|
+
*
|
|
14
|
+
* 🔴 SDK 0.0.117 提货(B6):`toolUses` / `transcriptId` / `currentTool` / `startedCount` /
|
|
15
|
+
* `parentToolCallId` / `stoppedBy` / `usage` / `resumable` / `editedFiles` **全部已进
|
|
16
|
+
* `FleetTaskRow`/`FleetWorkflowRow` 类型面** ⇒ 壳里那四个 "permissive read"(注释自述
|
|
17
|
+
* 「SDK 类型面滞后,bump 后可改直读」)本批**兑现成 typed 直读**;保留的只有**值域校验**
|
|
18
|
+
* (`Number.isFinite`/`>=0`/非空串),那不是防御读,是 wire 脏值的诚实过滤。
|
|
19
|
+
*/
|
|
20
|
+
import type { FleetTaskRow, FleetWorkflowRow } from '@sema-agent/sdk';
|
|
21
|
+
/** 渲染面的 fleet 状态词汇(cli `overrides/fleet-tree.tsx` 的 `FleetTaskStatus` 同集同序)。 */
|
|
22
|
+
export type FleetTaskStatus = 'queued' | 'running' | 'waiting' | 'stopping' | 'awaiting approval' | 'idle' | 'completed' | 'failed' | 'killed';
|
|
23
|
+
/**
|
|
24
|
+
* 投影后的一条 task 行。
|
|
25
|
+
* 🔴 与 cli `overrides/fleet-tree.tsx` 的 `FleetTask` **结构等同**(字段名/可选性/类型逐一对齐)——
|
|
26
|
+
* 渲染器类型留在端上(它是渲染契约),这里是产出侧类型。两侧漂移由 pure 门的键集断言钉死。
|
|
27
|
+
*/
|
|
28
|
+
export interface FleetTaskView {
|
|
29
|
+
id: string;
|
|
30
|
+
/** agent 身份名(187 `tjl`:agentName ?? agentType;都缺才退 objective 派生的单行 label)。 */
|
|
31
|
+
name: string;
|
|
32
|
+
/** 行正文 = 进度摘要,绝不是控制面工具动词。 */
|
|
33
|
+
description: string;
|
|
34
|
+
depth: number;
|
|
35
|
+
parentId?: string;
|
|
36
|
+
status: FleetTaskStatus;
|
|
37
|
+
elapsedMs: number;
|
|
38
|
+
tokens: number;
|
|
39
|
+
tokenDir: 'down' | 'up';
|
|
40
|
+
queuedCount: number;
|
|
41
|
+
awaitingPlanApproval?: boolean;
|
|
42
|
+
descendantCount?: number;
|
|
43
|
+
viewed?: boolean;
|
|
44
|
+
/** 🔴 三态:有键 ⇒ 真值(含真 0);缺键 ⇒ 「不知道」。绝不 `?? 0`。 */
|
|
45
|
+
toolUses?: number;
|
|
46
|
+
transcriptId?: string;
|
|
47
|
+
currentTool?: {
|
|
48
|
+
toolName: string;
|
|
49
|
+
target?: string;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* [1832] P1-2(server ≥1.290 / engine 1.432):发起本子代的父 tool_use id **原值**。
|
|
53
|
+
* B6 新落键 —— 归属不再靠猜,按 tool_use id 建卡树的消费端直接绑定。缺席 = 旧端。
|
|
54
|
+
*/
|
|
55
|
+
parentToolCallId?: string;
|
|
56
|
+
/**
|
|
57
|
+
* [1839]①(server ≥1.291)**终态行帧四键**,只在终态帧出现、全部 tolerate-absent:
|
|
58
|
+
* 看行卡(不订通知帧)的 idle 观察者此前拿不到终局,现在拿得到。
|
|
59
|
+
* `resumable`/`editedFiles` **仅 bg 子代有源**(顶层 run 的 TaskResult 无这两位,永缺席、不造)。
|
|
60
|
+
*/
|
|
61
|
+
stoppedBy?: string;
|
|
62
|
+
usage?: FleetRowUsage;
|
|
63
|
+
resumable?: boolean;
|
|
64
|
+
editedFiles?: Array<{
|
|
65
|
+
path: string;
|
|
66
|
+
edits: number;
|
|
67
|
+
}>;
|
|
68
|
+
}
|
|
69
|
+
/** 终态行帧 / bg 通知帧共用的 usage 形(SDK `FleetTaskRow.usage` ≡ `FleetBgNotification.usage`)。 */
|
|
70
|
+
export interface FleetRowUsage {
|
|
71
|
+
totalTokens?: number;
|
|
72
|
+
toolUses?: number;
|
|
73
|
+
durationMs?: number;
|
|
74
|
+
tokens?: number;
|
|
75
|
+
turns?: number;
|
|
76
|
+
costMicroUsd?: number;
|
|
77
|
+
}
|
|
78
|
+
/** 投影后的一条 workflow 行(与 cli `FleetWorkflow` 结构等同)。 */
|
|
79
|
+
export interface FleetWorkflowView {
|
|
80
|
+
id: string;
|
|
81
|
+
name: string;
|
|
82
|
+
description: string;
|
|
83
|
+
status: FleetTaskStatus;
|
|
84
|
+
doneCount: number;
|
|
85
|
+
totalCount: number;
|
|
86
|
+
elapsedMs: number;
|
|
87
|
+
tokens: number;
|
|
88
|
+
failedCount: number;
|
|
89
|
+
/** 🔴 缺席 ≠ 0:0 = 引擎说「一个都没起」,undefined = 老引擎「不知道」(退 done+failed 下界)。 */
|
|
90
|
+
startedCount?: number;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* SDK `FleetTaskStatus` 的终态词汇 —— 非终态即在飞(未知未来值按在飞算,保守)。
|
|
94
|
+
* cli 侧 `engineBgGate.TERMINAL_STATUSES` 是**刻意的第二份**(零模块图污染),drift 由壳的
|
|
95
|
+
* `terminalStatuses.test.ts` 锁定 —— 本批不动那份。
|
|
96
|
+
*/
|
|
97
|
+
export declare const TERMINAL_FLEET_TASK_STATUSES: ReadonlySet<string>;
|
|
98
|
+
/**
|
|
99
|
+
* 未来/未知 wire 状态 → 渲染词汇(闭集但读侧开集)。
|
|
100
|
+
*
|
|
101
|
+
* ⚠️ **B6 未删 `parked`,与任务书的 G11 指令相反 —— 理由与直证写在这里**:
|
|
102
|
+
* · 直证(server 1.291.0 dist,`fleet/fleet-bus.d.ts` + `fleet-bus.js` 12 处 publishTask):
|
|
103
|
+
* `FleetTaskStatus` 闭集**不含** `parked`,且每处 publishTask 的 status 都出自
|
|
104
|
+
* `completed|failed|killed|running` 四值三元链 ⇒ 今天 `'parked'` 在 fleet 行 wire 上**不可达**,
|
|
105
|
+
* 这一支确实是死分支。
|
|
106
|
+
* · 但**后果方向不对称**([anchor-on-the-deciding-quantity]「先定这个量门控动作的正负号」):
|
|
107
|
+
* 本支是**保护型**映射(把非活非终态的 parked 映到 'awaiting approval');删掉之后
|
|
108
|
+
* 它落 `default` ⇒ 渲成 `running` = **谎报活跃**。core 1.382 已有该状态([1561],
|
|
109
|
+
* design/153 §7.1),server 只是尚未投影 ⇒ 这不是「猜测层」,是**预挂**。
|
|
110
|
+
* 删的收益 = 一行;删的代价 = server 补上透传那天静默说谎。
|
|
111
|
+
* ⇒ 保留 + 把「候 server 预挂」的注释换成上面这条可复核的直证。定谳权交回 PM。
|
|
112
|
+
*/
|
|
113
|
+
export declare function coerceTaskStatus(s: string | undefined): FleetTaskStatus;
|
|
114
|
+
/** 中性 workflow run 状态(running|completed|failed)→ 渲染词汇。 */
|
|
115
|
+
export declare function coerceWorkflowStatus(s: string | undefined): FleetTaskStatus;
|
|
116
|
+
/**
|
|
117
|
+
* 187 给 fleet 行命名用的是 agent 的**身份**,从不是它的 objective:
|
|
118
|
+
* `tjl(e) = e.type==="in_process_teammate" ? e.identity.agentName : e.agentType`
|
|
119
|
+
* (ui-modules/0616_053_uiux_ejl.js:20-21)。SDK `FleetTaskRow` 带 `agentType`/`agentName`
|
|
120
|
+
* (§K-7),所以这里**重建** tjl:`agentName`(in-process teammate 实例名)优先,否则 `agentType`
|
|
121
|
+
* (subagent_type/teammate 类型名)。只有引擎还没发身份时才退到 objective 派生的 `name` ——
|
|
122
|
+
* 折成单行、绝不整段多行 prompt、绝不空(187 的显示名恒是非空短身份)。全空 → 短 id 尾。
|
|
123
|
+
*/
|
|
124
|
+
export declare function deriveAgentLabel(agentType: string | undefined, agentName: string | undefined, name: string | undefined, id: string): string;
|
|
125
|
+
/**
|
|
126
|
+
* 引擎把 fleet 行的 `description` 设成 agent 的**最后一个工具名**(service fleet-bus
|
|
127
|
+
* `description = ev.toolName`)。187 的行正文是进度摘要、从不是工具动词,所以控制面/合成动词
|
|
128
|
+
* 泄漏进行(clay 见过的 `report_blocked`)是纯分歧。这里压掉**已知**的控制面动词(保守集合),
|
|
129
|
+
* 真实工作工具(Read/Bash/Edit…)原样放行。
|
|
130
|
+
*
|
|
131
|
+
* 集合 = core `RESERVED_AGENT_NAMES`(subagent.ts)的 CC-parity PascalCase 形 + goal 模式的
|
|
132
|
+
* `DeclareDone`。引擎 ≥ core 1.176 经 `canonicalToolName` 发 PascalCase。
|
|
133
|
+
*/
|
|
134
|
+
export declare const CONTROL_TOOL_VERBS: ReadonlySet<string>;
|
|
135
|
+
/** 行的活动正文:控制面动词 ⇒ 空(187 从不显示它),否则原样。 */
|
|
136
|
+
export declare function projectDescription(description: string | undefined): string;
|
|
137
|
+
/**
|
|
138
|
+
* `toolUses` —— 子代**累计**工具调用数。语义两条(接错会显示一个「看起来很合理」的错数字):
|
|
139
|
+
* ① **累计不是增量** —— bg 子代 sink 的 usage 是累计口径,与 `turn_end` 的每轮增量相反。直取,绝不累加。
|
|
140
|
+
* ② **缺席 ≠ 0** —— 老 server 不发这个键。有键 ⇒ 真值(含真 0);无键 ⇒ undefined(不知道)。
|
|
141
|
+
*/
|
|
142
|
+
export declare function wireToolUses(r: Pick<FleetTaskRow, 'toolUses'>): number | undefined;
|
|
143
|
+
/** `transcriptId` —— 子代转录锚(core 铸的 childSessionId);委派 prompt 凭它去转录读面自取。 */
|
|
144
|
+
export declare function wireTranscriptId(r: Pick<FleetTaskRow, 'transcriptId'>): string | undefined;
|
|
145
|
+
/** `currentTool: {toolName, target?}` —— `currentAction` 的结构化同源体(core 1.429 → server 1.288)。 */
|
|
146
|
+
export declare function wireCurrentTool(r: Pick<FleetTaskRow, 'currentTool'>): {
|
|
147
|
+
toolName: string;
|
|
148
|
+
target?: string;
|
|
149
|
+
} | undefined;
|
|
150
|
+
/** `startedCount` —— 「已起过的 agent 数」。🔴 缺席 ≠ 0(见 FleetWorkflowView 键注)。 */
|
|
151
|
+
export declare function wireStartedCount(r: Pick<FleetWorkflowRow, 'startedCount'>): number | undefined;
|
|
152
|
+
/** `parentToolCallId`(P1-2)—— 归属直读键。空串按缺席处理(引擎不该发,发了也别当真)。 */
|
|
153
|
+
export declare function wireParentToolCallId(r: Pick<FleetTaskRow, 'parentToolCallId'>): string | undefined;
|
|
154
|
+
/** 终态四键之 `usage`(数值 verbatim,只挑数字位;全脏 ⇒ 整键缺席,不造空对象)。 */
|
|
155
|
+
export declare function wireRowUsage(r: Pick<FleetTaskRow, 'usage'>): FleetRowUsage | undefined;
|
|
156
|
+
/** 终态四键之 `editedFiles`(bg 子代专属;脏项逐条丢,不整键丢)。 */
|
|
157
|
+
export declare function wireEditedFiles(r: Pick<FleetTaskRow, 'editedFiles'>): Array<{
|
|
158
|
+
path: string;
|
|
159
|
+
edits: number;
|
|
160
|
+
}> | undefined;
|
|
161
|
+
/** 终态四键之 `stoppedBy`(开放枚举 verbatim:"user"/"parent"/"system"/…)。 */
|
|
162
|
+
export declare function wireStoppedBy(r: Pick<FleetTaskRow, 'stoppedBy'>): string | undefined;
|
|
163
|
+
/** 终态四键之 `resumable`(仅 bg 子代有源;非 boolean ⇒ 缺席,**不当 false**)。 */
|
|
164
|
+
export declare function wireResumable(r: Pick<FleetTaskRow, 'resumable'>): boolean | undefined;
|
|
165
|
+
export interface ProjectTasksOptions {
|
|
166
|
+
/**
|
|
167
|
+
* 🔴 **必填的端专属过滤器**(设计稿 §2.4.2-B):壳传 `filterFooterTaskRows`
|
|
168
|
+
* (丢无 parentId 顶层行 + workflow 双生行四判据);别的端按自己的树形状给。
|
|
169
|
+
* 想要「全都要」显式传 `rows => [...rows]` —— 必须写出来,不给缺省。
|
|
170
|
+
*/
|
|
171
|
+
rowFilter(rows: readonly FleetTaskRow[]): FleetTaskRow[];
|
|
172
|
+
/** 正在被查看的 agent taskId(端的 `viewingAgentTaskId`);行 id 尾段等于它 ⇒ `viewed:true`。 */
|
|
173
|
+
viewingTaskId?: string | undefined;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* wire task 行(Map values)→ 渲染行。树 depth/parentId 来自 wire:无 parentId = 顶层(depth 0),
|
|
177
|
+
* 子行挂在父下面,depth 沿 parent 链算(自引用/环由 `seen` 断)。
|
|
178
|
+
*/
|
|
179
|
+
export declare function projectTasks(allRows: readonly FleetTaskRow[], opts: ProjectTasksOptions): FleetTaskView[];
|
|
180
|
+
/** wire workflow 行 → 渲染行。 */
|
|
181
|
+
export declare function projectWorkflows(rows: readonly FleetWorkflowRow[]): FleetWorkflowView[];
|