@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,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B6 批搬迁(2026-07-27,设计稿 §3 B6):cli `src/sema/engineSubagentOutput.ts` 整搬。
|
|
3
|
+
*
|
|
4
|
+
* 🔴 搬迁三处差分(全部是「端资产上移到 port」,行为一字节不变):
|
|
5
|
+
* · `wireConfig()`(六份手抄之一)→ `engineWireTarget()`;
|
|
6
|
+
* · `getActiveEngineTaskId()`(engineToolDetach,pty 绑定的活跃 run)→ `activeEngineRunId()`
|
|
7
|
+
* (SessionPort.activeRunId);
|
|
8
|
+
* · `logForDebugging` → `hostLog('debug', …)`。
|
|
9
|
+
*
|
|
10
|
+
* ── 以下为原文件的领域说明(逐字保留)────────────────────────────────────────────────────────
|
|
11
|
+
*
|
|
12
|
+
* engineSubagentOutput — 后台子代终报读面消费(server 1.244 / SDK 0.0.65 提货,提货单 [1488]③(b))。
|
|
13
|
+
*
|
|
14
|
+
* Wire: `GET /v1/runs/:runId/subagents/:handle/output` → {taskId, target, content, output:{status,…}}
|
|
15
|
+
* · content = 子代最终 assistant 正文(终态)或诚实 running 状态文本;
|
|
16
|
+
* · runId = spawn 该子代的 run(registry access 从 run 行推导——寻址映射由 fleet 行 parentId /
|
|
17
|
+
* bg_notification.parentTaskId 喂 subagentContentStore.recordBgParentRun);
|
|
18
|
+
* · a… 域(background_agent)专用——wa… 观测行恒 404(workflow journal 是那边的读面),调用方
|
|
19
|
+
* 按 null 回落 bg 事实渲染,fail-soft;
|
|
20
|
+
* · caps 探测 capabilities.subagentOutput(selfKnowledge caps 同款纪律:true 固化/false TTL/
|
|
21
|
+
* 探测失败不缓存)。老引擎(<1.244)= caps 无该键 → 恒走回落,version-safe。
|
|
22
|
+
*
|
|
23
|
+
* 副本域注意([1491]④):TaskRegistry 是进程内存态——引擎重启后 handle 消失(404),按 null 回落,
|
|
24
|
+
* 绝不把 404 当异常上抛。
|
|
25
|
+
*
|
|
26
|
+
* session 作用域([1498]③,server 1.245.0 / SDK 0.0.66):session-bound run 的 subagentOutput
|
|
27
|
+
* 必须带匹配的 `?session=`,否则 404 fail-closed(这个读面正是 [1493]② 双开串扰的读侧入口,
|
|
28
|
+
* server 侧已把它闩死)。端无条件带 session 即双版本兼容——1.244 忽略该参数,1.245 起必需。
|
|
29
|
+
* 🔴 端漏装 SessionPort ⇒ `engineSessionParam()` 恒 undefined ⇒ 本读面对 session-bound run
|
|
30
|
+
* 一律 404 ⇒ 整条子代终报静默哑掉(所以 `hostSession()` 计 miss,见 host.ts)。
|
|
31
|
+
*/
|
|
32
|
+
import { hostLog } from '../host.js';
|
|
33
|
+
import { makeEngineWireClient } from '../engineWireSdk.js';
|
|
34
|
+
import { engineWireTarget } from '../engineWireTarget.js';
|
|
35
|
+
import { getBgParentRun } from '../subagentContentStore.js';
|
|
36
|
+
import { activeEngineRunId, engineSessionParam } from '../engineSessionParam.js';
|
|
37
|
+
// ── caps 探测(true 固化 / false TTL / 失败不缓存)────────────────────────────────────────────
|
|
38
|
+
const capByBase = new Map();
|
|
39
|
+
const CAP_FALSE_RETRY_TTL_MS = 5 * 60_000;
|
|
40
|
+
async function subagentOutputCapable(client, baseUrl, signal) {
|
|
41
|
+
const hit = capByBase.get(baseUrl);
|
|
42
|
+
if (hit && (hit.ok || Date.now() - hit.at < CAP_FALSE_RETRY_TTL_MS))
|
|
43
|
+
return hit.ok;
|
|
44
|
+
try {
|
|
45
|
+
// SDK 0.0.77 起 capabilities() 有真型(Capabilities 开集)——宽形 cast 已撤;键仍防御读。
|
|
46
|
+
const caps = await client.capabilities({ ...(signal ? { signal } : {}) });
|
|
47
|
+
const ok = caps.subagentOutput === true;
|
|
48
|
+
capByBase.set(baseUrl, { ok, at: Date.now() });
|
|
49
|
+
return ok;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false; // 探测失败不缓存(引擎瞬断):本次回落,下次再判
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** 测试钩:清 caps 缓存。 */
|
|
56
|
+
export function __resetSubagentOutputCapCacheForTests() {
|
|
57
|
+
capByBase.clear();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 拉取一个后台子代的终报正文。null = 读不到(无 wire / caps 缺 / 404 域外或副本重启 / 网错)——
|
|
61
|
+
* 调用方回落 bg 事实渲染。绝不 throw。
|
|
62
|
+
*/
|
|
63
|
+
export async function fetchEngineSubagentReport(taskId, opts) {
|
|
64
|
+
const cfg = engineWireTarget();
|
|
65
|
+
if (!cfg)
|
|
66
|
+
return null;
|
|
67
|
+
const runId = getBgParentRun(taskId) ?? activeEngineRunId();
|
|
68
|
+
if (!runId)
|
|
69
|
+
return null;
|
|
70
|
+
const client = makeEngineWireClient({
|
|
71
|
+
baseUrl: cfg.baseUrl,
|
|
72
|
+
...(cfg.token ? { token: cfg.token } : {}),
|
|
73
|
+
principal: cfg.principal,
|
|
74
|
+
});
|
|
75
|
+
if (!client)
|
|
76
|
+
return null;
|
|
77
|
+
if (!(await subagentOutputCapable(client, cfg.baseUrl, opts?.signal)))
|
|
78
|
+
return null;
|
|
79
|
+
try {
|
|
80
|
+
// [1498]③:无条件带 session(1.244 忽略/1.245 必需,双版本兼容);缺席时(端未装 SessionPort /
|
|
81
|
+
// 端自己还没有值)不带——1.245 对 session-bound run 会 404,按既有 null 回落,下次重试自然带上。
|
|
82
|
+
const session = engineSessionParam();
|
|
83
|
+
const r = await client.runs.subagentOutput(runId, taskId, {
|
|
84
|
+
...(opts?.signal ? { signal: opts.signal } : {}),
|
|
85
|
+
...(session ? { session } : {}),
|
|
86
|
+
});
|
|
87
|
+
const content = typeof r?.content === 'string' ? r.content : '';
|
|
88
|
+
if (!content.trim())
|
|
89
|
+
return null;
|
|
90
|
+
const status = typeof r?.output?.status === 'string' ? r.output.status : undefined;
|
|
91
|
+
return { content, ...(status !== undefined ? { status } : {}) };
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
const status = e?.status;
|
|
95
|
+
hostLog('debug', `[subagent-output] ${taskId} via run ${runId}: ${typeof status === 'number' ? status : String(e).slice(0, 160)}`);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type SubagentSteerOutcome = {
|
|
2
|
+
ok: true;
|
|
3
|
+
receipt: string;
|
|
4
|
+
} | {
|
|
5
|
+
ok: false;
|
|
6
|
+
reason: 'no-wire' | 'no-run' | 'not-running' | 'error';
|
|
7
|
+
detail?: string;
|
|
8
|
+
};
|
|
9
|
+
/** Steer a running engine subagent (target = its parentToolCallId, or agentName). */
|
|
10
|
+
export declare function steerEngineSubagent(target: string, text: string): Promise<SubagentSteerOutcome>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B6 批搬迁(2026-07-27,设计稿 §3 B6):cli `src/sema/engineSubagentSteer.ts` 整搬。
|
|
3
|
+
*
|
|
4
|
+
* 🔴 搬迁三处差分(端资产上移到 port,行为一字节不变):`wireConfig()`(六份手抄之一,而且
|
|
5
|
+
* **这一份的 token 读法与另外五份不同步** —— 收编前它是 `token: process.env.SEMA_LIVE_TOKEN`
|
|
6
|
+
* 直挂而非 `...(cfg.token ? … )` spread,漂移已实际发生)→ `engineWireTarget()`;
|
|
7
|
+
* `getActiveEngineTaskId()` → `activeEngineRunId()`;`logForDebugging` → `hostLog`。
|
|
8
|
+
*
|
|
9
|
+
* ── 以下为原文件的领域说明(逐字保留)────────────────────────────────────────────────────────
|
|
10
|
+
*
|
|
11
|
+
* engineSubagentSteer — C2 (service 1.89): direct steer to a RUNNING engine subagent.
|
|
12
|
+
*
|
|
13
|
+
* Wire: `POST /v1/runs/:runId/subagents/:target/steer` (SubagentSteerRegistry, three-legged onSubagentSpawn;
|
|
14
|
+
* target = parentToolCallId or agentName — the client always has the parentToolCallId from the diverted C1
|
|
15
|
+
* content events / progress binding). SDK 宪法迁移批:SDK 0.0.52 有 runs.steerSubagent verb(同 wire 同
|
|
16
|
+
* encodeURIComponent 同 body {content}),raw fetch 退役 —— 改走 engineWireSdk 工厂 client;auth
|
|
17
|
+
* (Bearer SEMA_LIVE_TOKEN)+ x-agent-principal(owner-gated route — must match the run's stamped
|
|
18
|
+
* principal)姿势 = Transport buildHeaders,与原手抄同三头。
|
|
19
|
+
*
|
|
20
|
+
* Semantics (CC AGe parity): the message drains at the subagent's next tool round; the service echoes CC's
|
|
21
|
+
* verbatim receipt "Message queued for delivery…". 409 = subagent not running (settled/unknown) — surfaced
|
|
22
|
+
* honestly so the caller can fall back (steer the lead) or tell the user the agent already ended.
|
|
23
|
+
* 迁移差分记账:原 raw POST 无超时 → SDK 缺省 60s 上界(steer 是入队即回的 verb,纯上界改善);
|
|
24
|
+
* 'not-running' 的 detail 从「响应体原文」→「APIError.message(server body.error 原文)」——该 detail
|
|
25
|
+
* 只进 debug 日志(REPL 对 not-running 显示固定文案),error 形语义不劣化。
|
|
26
|
+
*/
|
|
27
|
+
import { hostLog } from '../host.js';
|
|
28
|
+
import { makeEngineWireClient } from '../engineWireSdk.js';
|
|
29
|
+
import { engineWireTarget } from '../engineWireTarget.js';
|
|
30
|
+
import { activeEngineRunId, engineSessionParamSpread } from '../engineSessionParam.js';
|
|
31
|
+
/** Steer a running engine subagent (target = its parentToolCallId, or agentName). */
|
|
32
|
+
export async function steerEngineSubagent(target, text) {
|
|
33
|
+
const cfg = engineWireTarget();
|
|
34
|
+
if (!cfg)
|
|
35
|
+
return { ok: false, reason: 'no-wire' };
|
|
36
|
+
const runId = activeEngineRunId();
|
|
37
|
+
if (!runId)
|
|
38
|
+
return { ok: false, reason: 'no-run' };
|
|
39
|
+
const client = makeEngineWireClient({
|
|
40
|
+
baseUrl: cfg.baseUrl,
|
|
41
|
+
...(cfg.token ? { token: cfg.token } : {}),
|
|
42
|
+
principal: cfg.principal,
|
|
43
|
+
});
|
|
44
|
+
if (!client)
|
|
45
|
+
return { ok: false, reason: 'no-wire' };
|
|
46
|
+
try {
|
|
47
|
+
// service 1.89 receipt shape: { note: "Message queued for delivery to <name> …", delivery, … }
|
|
48
|
+
const body = await client.runs.steerSubagent(runId, target, { content: text }, engineSessionParamSpread());
|
|
49
|
+
let receipt = 'Message queued for delivery at the next tool round.';
|
|
50
|
+
const copy = body?.note ?? body?.message;
|
|
51
|
+
if (typeof copy === 'string' && copy.length > 0)
|
|
52
|
+
receipt = copy;
|
|
53
|
+
return { ok: true, receipt };
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
const status = e?.status;
|
|
57
|
+
if (typeof status === 'number') {
|
|
58
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
59
|
+
hostLog('debug', `engineSubagentSteer: ${status} ${detail.slice(0, 200)}`);
|
|
60
|
+
if (status === 409 || status === 404) {
|
|
61
|
+
return { ok: false, reason: 'not-running', detail };
|
|
62
|
+
}
|
|
63
|
+
return { ok: false, reason: 'error', detail: `${status}` };
|
|
64
|
+
}
|
|
65
|
+
hostLog('debug', `engineSubagentSteer: fetch failed ${String(e)}`);
|
|
66
|
+
return { ok: false, reason: 'error', detail: String(e) };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** 测试钩:清 caps 缓存。 */
|
|
2
|
+
export declare function __resetSubagentStreamCapCacheForTests(): void;
|
|
3
|
+
/**
|
|
4
|
+
* 一次子代工具活动(详情页 `Progress` 段的数据面,CC 2.1.220 `Zja` 的 recentActivities)。
|
|
5
|
+
*
|
|
6
|
+
* 🔴 `toolName` 必须是**真工具名**、`args` 必须是**真参数** —— 端的 `renderToolActivity` 拿
|
|
7
|
+
* `toolName` 去工具注册表查、再用工具自己的 `userFacingName(input)` 渲染。把 `currentAction`
|
|
8
|
+
* 那类自由文本塞进 `toolName` 只会走 `!tool` 回落分支把原文吐出来,是「看起来能显示」的假绑定。
|
|
9
|
+
* 自由文本只能进 `activityDescription` 展示位。
|
|
10
|
+
*/
|
|
11
|
+
export interface SubagentActivity {
|
|
12
|
+
taskId: string;
|
|
13
|
+
toolName: string;
|
|
14
|
+
/** `tool_start.args` 原样(非对象由端归一;端会拿它过工具 schema 的 safeParse)。 */
|
|
15
|
+
args: unknown;
|
|
16
|
+
/** UNTRUSTED 展示文本(server 已 redact+bound);绝不冒充结构化 args。 */
|
|
17
|
+
activityDescription?: string;
|
|
18
|
+
}
|
|
19
|
+
/** 端装的活动落点(执行半场)。不装 ⇒ Progress 段无数据(不是错误态,只是该面不启用)。 */
|
|
20
|
+
export type SubagentActivitySink = (a: SubagentActivity) => void;
|
|
21
|
+
/** 装/卸 Progress 段落点。传 null 卸。返回还原函数。 */
|
|
22
|
+
export declare function installSubagentActivitySink(sink: SubagentActivitySink | null): () => void;
|
|
23
|
+
/** tool_end.output 经 server redactDeep,原型不保 string——渲染面统一字符串化(对象走 JSON)。 */
|
|
24
|
+
export declare function coerceOutput(v: unknown): string | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* tail 是否正对该行开着 —— **两源互斥判别口**(行帧 `currentTool` 臂在 tail 活跃时让位:
|
|
27
|
+
* tail 的 tool_start 带真 args,渲染更细;两源同录 = 每步双行)。
|
|
28
|
+
*/
|
|
29
|
+
export declare function isEngineSubagentTailActive(taskId: string): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* 打开一个后台子代的 live tail(幂等:已开着的同 id 直接返回)。内容帧进 subagentContentStore,
|
|
32
|
+
* 渲染由既有 C1 notify 链接管。绝不 throw;开不了(无 wire/caps 缺/寻址失败)= 静默维持现状。
|
|
33
|
+
*/
|
|
34
|
+
export declare function tailEngineSubagent(taskId: string): void;
|
|
35
|
+
/** 关闭一个子代的 tail(幂等;查看态退出/终态/行消失时调)。 */
|
|
36
|
+
export declare function stopEngineSubagentTail(taskId: string): void;
|
|
37
|
+
/** 在飞判别([1543]① 重开路用):true = 该子代已有 tail 挂着。 */
|
|
38
|
+
export declare function hasActiveSubagentTail(taskId: string): boolean;
|
|
39
|
+
/** 测试钩:在飞 tail 数。 */
|
|
40
|
+
export declare function __activeTailCountForTests(): number;
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B6 批搬迁(2026-07-27,设计稿 §3 B6):cli `src/sema/engineSubagentTail.ts` 的**流消费半场**。
|
|
3
|
+
*
|
|
4
|
+
* ── 拆缝(T35/T36/T44 同一条原则:判定半场进包、执行半场留宿主)────────────────────────────────
|
|
5
|
+
* **进包**:caps 探测 · tail 台账(每 taskId 至多一条)· SSE 消费循环 · 帧 → subagentContentStore
|
|
6
|
+
* 的四条投影 · `coerceOutput` · settle 收口 · 诊断锚 · 两源互斥判别口 `isEngineSubagentTailActive`。
|
|
7
|
+
* **留宿主**:`recordSubagentActivity` / `recordFleetCurrentTool` 里对
|
|
8
|
+
* `AppState.tasks[taskId].progress.recentActivities` 的读改写 —— 那是**端的任务状态形状**
|
|
9
|
+
* (`LocalAgentTaskState` / `ToolActivity`),web/桌面各有各的形。
|
|
10
|
+
* 本包改为在 `tool_start` 上调 `SubagentActivitySink`(下面的可装口),壳侧 shim 装一个
|
|
11
|
+
* 把原 `recordSubagentActivity` 逐字搬过去的 sink ⇒ 行为一字节不变。
|
|
12
|
+
*
|
|
13
|
+
* 🔴 单实例纪律:`activeTails` 是 module 级 Map。写者 = 本文件;读者 = **两源互斥判别**
|
|
14
|
+
* (行帧 `currentTool` 臂在 tail 活跃时必须让位)与查看态 watcher 的「重开」判别。
|
|
15
|
+
* 两份实例 ⇒ 让位判别恒读 false ⇒ tail 与行帧两源同时录 Progress 段 = 每步双行
|
|
16
|
+
* ([new-data-source-systemic-checks]「两臂共写台账要整臂让位」)。
|
|
17
|
+
*
|
|
18
|
+
* 🔴 搬迁差分:`wireConfig()`(六份手抄的第六份,至此**全部收编**)→ `engineWireTarget()`;
|
|
19
|
+
* `getActiveEngineTaskId()` → `activeEngineRunId()`;`logForDebugging` → `hostLog`。
|
|
20
|
+
*
|
|
21
|
+
* ── 以下为原文件的领域说明(逐字保留)────────────────────────────────────────────────────────
|
|
22
|
+
*
|
|
23
|
+
* engineSubagentTail — 后台子代 live tail(A2,server 1.251 subagentStream 提货;1.253 起
|
|
24
|
+
* probe→subscribe 竞态已修([1524] HIGH-2 → [1526] 即修),接线以 1.253+ 为期望域)。
|
|
25
|
+
*
|
|
26
|
+
* Wire: `GET /v1/runs/:runId/subagents/:handle/stream`(SSE)——SDK runs.subagentStream 消费:
|
|
27
|
+
* · `event: meta` {version:1, runId, target, status, seq?, …}:status 非 running/pending 时
|
|
28
|
+
* server 立即收流(终态无 tail,回放面=subagentOutput);
|
|
29
|
+
* · `event: forward` = projectTailFrame 投影帧:text_delta/reasoning_delta {delta,…}、
|
|
30
|
+
* tool_start {toolCallId,toolName,args,label?}、tool_end {toolCallId,toolName,isError,output?}、
|
|
31
|
+
* task_progress {…}(rollup,面板已有 task_progress 腿,这里不重复消费)、
|
|
32
|
+
* task_settled {taskId,status,seq?,summary?} = 收口帧(server 随即 end);
|
|
33
|
+
* · `event: heartbeat` {} 每 15s(连接活性由 server 保障,端侧无需 idle timer)。
|
|
34
|
+
*
|
|
35
|
+
* 消费面:帧 → subagentContentStore.publishSubagentContentEvent(C1 同一 store)——既有
|
|
36
|
+
* notify→重组链零改动接管渲染;running 查看态的「live transcript pending engine wire」占位行在
|
|
37
|
+
* 首个内容项落地时自然消失。
|
|
38
|
+
*
|
|
39
|
+
* 防双注入:task_settled → settleSubagentContent(flush 流式尾巴)后关流;终报正文不从
|
|
40
|
+
* tail 合成——终态渲染仍走既有 subagentOutput 读面 / bg 事实臂,臂序(content prefix → report)
|
|
41
|
+
* 保证两面不重复。
|
|
42
|
+
*
|
|
43
|
+
* 生命周期:查看态进入开,退出/终态/行消失关;每 taskId 至多一条(幂等);caps 探测
|
|
44
|
+
* capabilities.subagentStream(true 固化/false TTL/失败不缓存,engineSubagentOutput 同纪律);
|
|
45
|
+
* 寻址 runId=getBgParentRun ?? 活跃 run、无条件带 `?session=`(同 [1498]③ 纪律)。
|
|
46
|
+
* 一切失败 fail-soft:tail 断=回到占位行现状,绝不 throw。
|
|
47
|
+
*/
|
|
48
|
+
import { hostLog } from '../host.js';
|
|
49
|
+
import { makeEngineWireClient } from '../engineWireSdk.js';
|
|
50
|
+
import { engineWireTarget } from '../engineWireTarget.js';
|
|
51
|
+
import { activeEngineRunId, engineSessionParam } from '../engineSessionParam.js';
|
|
52
|
+
import { getBgParentRun, parentToolCallIdOf, publishSubagentContentEvent, settleSubagentContent, } from '../subagentContentStore.js';
|
|
53
|
+
// ── caps 探测(true 固化 / false TTL / 失败不缓存)────────────────────────────────────────────
|
|
54
|
+
const capByBase = new Map();
|
|
55
|
+
const CAP_FALSE_RETRY_TTL_MS = 5 * 60_000;
|
|
56
|
+
async function subagentStreamCapable(client, baseUrl, signal) {
|
|
57
|
+
const hit = capByBase.get(baseUrl);
|
|
58
|
+
if (hit && (hit.ok || Date.now() - hit.at < CAP_FALSE_RETRY_TTL_MS))
|
|
59
|
+
return hit.ok;
|
|
60
|
+
try {
|
|
61
|
+
// SDK 0.0.77 起 capabilities() 有真型(Capabilities 开集)——宽形 cast 已撤;键仍防御读。
|
|
62
|
+
const caps = await client.capabilities({ ...(signal ? { signal } : {}) });
|
|
63
|
+
const ok = caps.subagentStream === true;
|
|
64
|
+
capByBase.set(baseUrl, { ok, at: Date.now() });
|
|
65
|
+
return ok;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false; // 探测失败不缓存(引擎瞬断):本次不开,下次再判
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** 测试钩:清 caps 缓存。 */
|
|
72
|
+
export function __resetSubagentStreamCapCacheForTests() {
|
|
73
|
+
capByBase.clear();
|
|
74
|
+
}
|
|
75
|
+
// ── tail 台账(每 taskId 至多一条)──────────────────────────────────────────────────────────────
|
|
76
|
+
const activeTails = new Map();
|
|
77
|
+
let activitySink = null;
|
|
78
|
+
/** 装/卸 Progress 段落点。传 null 卸。返回还原函数。 */
|
|
79
|
+
export function installSubagentActivitySink(sink) {
|
|
80
|
+
const prev = activitySink;
|
|
81
|
+
activitySink = sink;
|
|
82
|
+
return () => {
|
|
83
|
+
activitySink = prev;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** tool_end.output 经 server redactDeep,原型不保 string——渲染面统一字符串化(对象走 JSON)。 */
|
|
87
|
+
export function coerceOutput(v) {
|
|
88
|
+
if (v === undefined || v === null)
|
|
89
|
+
return undefined;
|
|
90
|
+
if (typeof v === 'string')
|
|
91
|
+
return v;
|
|
92
|
+
try {
|
|
93
|
+
return JSON.stringify(v);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return String(v);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* tail 是否正对该行开着 —— **两源互斥判别口**(行帧 `currentTool` 臂在 tail 活跃时让位:
|
|
101
|
+
* tail 的 tool_start 带真 args,渲染更细;两源同录 = 每步双行)。
|
|
102
|
+
*/
|
|
103
|
+
export function isEngineSubagentTailActive(taskId) {
|
|
104
|
+
return activeTails.has(taskId);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 打开一个后台子代的 live tail(幂等:已开着的同 id 直接返回)。内容帧进 subagentContentStore,
|
|
108
|
+
* 渲染由既有 C1 notify 链接管。绝不 throw;开不了(无 wire/caps 缺/寻址失败)= 静默维持现状。
|
|
109
|
+
*/
|
|
110
|
+
export function tailEngineSubagent(taskId) {
|
|
111
|
+
if (activeTails.has(taskId))
|
|
112
|
+
return;
|
|
113
|
+
const cfg = engineWireTarget();
|
|
114
|
+
if (!cfg)
|
|
115
|
+
return;
|
|
116
|
+
const runId = getBgParentRun(taskId) ?? activeEngineRunId();
|
|
117
|
+
if (!runId)
|
|
118
|
+
return;
|
|
119
|
+
const client = makeEngineWireClient({
|
|
120
|
+
baseUrl: cfg.baseUrl,
|
|
121
|
+
...(cfg.token ? { token: cfg.token } : {}),
|
|
122
|
+
principal: cfg.principal,
|
|
123
|
+
});
|
|
124
|
+
if (!client)
|
|
125
|
+
return;
|
|
126
|
+
const ac = new AbortController();
|
|
127
|
+
activeTails.set(taskId, ac);
|
|
128
|
+
// [1568] 诊断锚(SEMA_DEBUG 才落盘,量有界:每条 tail 一行首帧 + 一行收尾统计)——查看态
|
|
129
|
+
// 「慢/稀疏」类报障必须能把【数据面到端】与【端渲染】分层归因,首帧时延是那条分界线。
|
|
130
|
+
const openedAt = Date.now();
|
|
131
|
+
let frames = 0;
|
|
132
|
+
let connectLogged = false;
|
|
133
|
+
let firstContentLogged = false;
|
|
134
|
+
void (async () => {
|
|
135
|
+
try {
|
|
136
|
+
if (!(await subagentStreamCapable(client, cfg.baseUrl, ac.signal)))
|
|
137
|
+
return;
|
|
138
|
+
const session = engineSessionParam();
|
|
139
|
+
// SDK 0.0.77 起 runs.subagentStream 有真型([1547]① 候发版即消费兑现)——宽形 cast 已撤。
|
|
140
|
+
const stream = client.runs.subagentStream(runId, taskId, {
|
|
141
|
+
signal: ac.signal,
|
|
142
|
+
...(session ? { session } : {}),
|
|
143
|
+
});
|
|
144
|
+
hostLog('debug', `[subagent-tail] open ${taskId} via run ${runId}`);
|
|
145
|
+
const parentKey = () => parentToolCallIdOf(taskId) ?? taskId;
|
|
146
|
+
for await (const ev of stream) {
|
|
147
|
+
if (ac.signal.aborted)
|
|
148
|
+
break;
|
|
149
|
+
if (!connectLogged) {
|
|
150
|
+
connectLogged = true;
|
|
151
|
+
// 连接分界线:meta 帧即到 = SSE 建连成功(与「子代此刻恰好在跑长工具、没内容可发」区分开)
|
|
152
|
+
hostLog('debug', `[subagent-tail] ${taskId} stream connected +${Date.now() - openedAt}ms`);
|
|
153
|
+
}
|
|
154
|
+
if (ev.event !== 'forward')
|
|
155
|
+
continue; // meta/heartbeat:连接语义帧,不进内容面
|
|
156
|
+
const d = ev.data;
|
|
157
|
+
const t = d.type;
|
|
158
|
+
frames += 1;
|
|
159
|
+
if (!firstContentLogged &&
|
|
160
|
+
(t === 'text_delta' || t === 'reasoning_delta' || t === 'tool_start')) {
|
|
161
|
+
firstContentLogged = true;
|
|
162
|
+
hostLog('debug', `[subagent-tail] ${taskId} first content frame +${Date.now() - openedAt}ms (${String(t)})`);
|
|
163
|
+
}
|
|
164
|
+
if (t === 'text_delta' || t === 'reasoning_delta') {
|
|
165
|
+
if (typeof d.delta === 'string' && d.delta.length > 0) {
|
|
166
|
+
publishSubagentContentEvent({
|
|
167
|
+
type: t,
|
|
168
|
+
taskId,
|
|
169
|
+
parentToolCallId: parentKey(),
|
|
170
|
+
delta: d.delta,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else if (t === 'tool_start') {
|
|
175
|
+
publishSubagentContentEvent({
|
|
176
|
+
type: 'tool_start',
|
|
177
|
+
taskId,
|
|
178
|
+
parentToolCallId: parentKey(),
|
|
179
|
+
...(typeof d.toolCallId === 'string' ? { toolCallId: d.toolCallId } : {}),
|
|
180
|
+
...(typeof d.toolName === 'string' ? { toolName: d.toolName } : {}),
|
|
181
|
+
args: d.args,
|
|
182
|
+
});
|
|
183
|
+
// 详情页 `Progress` 段的数据面(执行半场在端,见 SubagentActivitySink)。
|
|
184
|
+
// 只有真拿到工具名才记 —— 无名帧不编。
|
|
185
|
+
if (typeof d.toolName === 'string' && d.toolName.length > 0) {
|
|
186
|
+
activitySink?.({ taskId, toolName: d.toolName, args: d.args });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else if (t === 'tool_end') {
|
|
190
|
+
const out = coerceOutput(d.output);
|
|
191
|
+
publishSubagentContentEvent({
|
|
192
|
+
type: 'tool_end',
|
|
193
|
+
taskId,
|
|
194
|
+
parentToolCallId: parentKey(),
|
|
195
|
+
...(typeof d.toolCallId === 'string' ? { toolCallId: d.toolCallId } : {}),
|
|
196
|
+
...(typeof d.toolName === 'string' ? { toolName: d.toolName } : {}),
|
|
197
|
+
...(out !== undefined ? { output: out } : {}),
|
|
198
|
+
...(d.isError === true ? { isError: true } : {}),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
else if (t === 'task_settled') {
|
|
202
|
+
// 收口:flush 流式尾巴;终报/结局事实由既有 hydrate/bg 事实臂渲染,不从 tail 合成。
|
|
203
|
+
// [1543]①:seq 骑帧(core 1.373 UnifiedTaskOutput.seq;1.377 起 live 臂同键全域投影,
|
|
204
|
+
// [1548]——settle 世代口径 spawn=1/复活承袭 claim 周期号)——≥2 = 复活周期(S3c revive)
|
|
205
|
+
// 的 settle。tail 台账按周期关流;重开由查看态 watcher 兜(viewing 行回 running 且无
|
|
206
|
+
// 在飞 tail 即重开,revive/断线同一恢复路)。
|
|
207
|
+
settleSubagentContent(taskId);
|
|
208
|
+
hostLog('debug', `[subagent-tail] ${taskId} settled (${String(d.status)}${typeof d.seq === 'number' ? ` seq=${d.seq}` : ''}${typeof d.summary === 'string' ? ` summary=${d.summary.slice(0, 80)}` : ''}) — close`);
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
// task_progress:面板 rollup 已有独立腿,这里不重复消费
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch (e) {
|
|
215
|
+
if (!ac.signal.aborted) {
|
|
216
|
+
hostLog('debug', `[subagent-tail] ${taskId} torn: ${String(e).slice(0, 160)}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
hostLog('debug', `[subagent-tail] ${taskId} closed after ${frames} content frame(s)`);
|
|
221
|
+
if (activeTails.get(taskId) === ac)
|
|
222
|
+
activeTails.delete(taskId);
|
|
223
|
+
}
|
|
224
|
+
})();
|
|
225
|
+
}
|
|
226
|
+
/** 关闭一个子代的 tail(幂等;查看态退出/终态/行消失时调)。 */
|
|
227
|
+
export function stopEngineSubagentTail(taskId) {
|
|
228
|
+
const ac = activeTails.get(taskId);
|
|
229
|
+
if (!ac)
|
|
230
|
+
return;
|
|
231
|
+
activeTails.delete(taskId);
|
|
232
|
+
ac.abort();
|
|
233
|
+
}
|
|
234
|
+
/** 在飞判别([1543]① 重开路用):true = 该子代已有 tail 挂着。 */
|
|
235
|
+
export function hasActiveSubagentTail(taskId) {
|
|
236
|
+
return activeTails.has(taskId);
|
|
237
|
+
}
|
|
238
|
+
/** 测试钩:在飞 tail 数。 */
|
|
239
|
+
export function __activeTailCountForTests() {
|
|
240
|
+
return activeTails.size;
|
|
241
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export interface EngineTaskOutputResult {
|
|
2
|
+
/** 累积后的输出正文(游标两形兼容,见头注)。 */
|
|
3
|
+
content: string;
|
|
4
|
+
/** wire output 投影原样的常用位(UI 一律以这些 flags 为准,别解析 content 猜状态)。 */
|
|
5
|
+
status?: string;
|
|
6
|
+
stoppedBy?: string;
|
|
7
|
+
partialResult?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** 测试钩:清累积缓存。 */
|
|
10
|
+
export declare function __resetTaskHandleWireForTests(): void;
|
|
11
|
+
/**
|
|
12
|
+
* 读一个引擎侧任务句柄的输出(新面 A)。null = 读不到(无 wire / caps 缺 / 无宿主 run / 404
|
|
13
|
+
* 域外或已 reap / 网错)——调用方回落既有渲染(receipt/outputFile tail)。绝不 throw。
|
|
14
|
+
*/
|
|
15
|
+
export declare function fetchEngineTaskOutput(handle: string, opts?: {
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
}): Promise<EngineTaskOutputResult | null>;
|
|
18
|
+
/**
|
|
19
|
+
* [1505] 契约标记判别(头注):spool 全量 → 替换;cursor 增量 → append;无标记 → 保守
|
|
20
|
+
* append-if-changed(与上次逐字节相同视为幂等重读,不重复)。空 content 不动累积。
|
|
21
|
+
* 🔴 B6 提出成具名纯函数 —— 它是这条读面唯一会「把输出显示成两遍」的地方,值得被直接断言。
|
|
22
|
+
*/
|
|
23
|
+
export declare function mergeTaskOutput(prev: string, fresh: string): string;
|
|
24
|
+
export type EngineTaskStopOutcome =
|
|
25
|
+
/** 200:停止动作完成(status = wire 终态照实;重复 stop 幂等)。 */
|
|
26
|
+
{
|
|
27
|
+
ok: true;
|
|
28
|
+
status?: string;
|
|
29
|
+
stoppedBy?: string;
|
|
30
|
+
}
|
|
31
|
+
/** 409 stop.not_landed:kill 没落地,进程可能还在跑——UI 绝不能渲染成「已停止」。 */
|
|
32
|
+
| {
|
|
33
|
+
ok: false;
|
|
34
|
+
reason: 'not_landed';
|
|
35
|
+
detail?: string;
|
|
36
|
+
}
|
|
37
|
+
/** 409 stop.not_local(server 1.250+ durable 臂):任务在**别的实例**上还在跑,此处没有 kill
|
|
38
|
+
* 被尝试——「别处在跑,此处停不了」,非失败重试件([1518]§一)。UI 同样不翻行。 */
|
|
39
|
+
| {
|
|
40
|
+
ok: false;
|
|
41
|
+
reason: 'not_local';
|
|
42
|
+
detail?: string;
|
|
43
|
+
}
|
|
44
|
+
/** 409 stop.parked(server 1.263 parked 状态机):子代停在待审批检查点——不是在跑,也不该被
|
|
45
|
+
* stop 杀(人先 /decide);409 stop.park_resume_won:复活赛跑输给了别处的 resume。两者 UI
|
|
46
|
+
* 都不翻行(fleet 行渲染词汇已由 coerceTaskStatus 映射 'awaiting approval')。 */
|
|
47
|
+
| {
|
|
48
|
+
ok: false;
|
|
49
|
+
reason: 'parked' | 'park_resume_won';
|
|
50
|
+
detail?: string;
|
|
51
|
+
}
|
|
52
|
+
/** 409 stop.park_arbiter_unreachable(core 1.397 三分,G13 补齐的第五形):仲裁店不可达 ⇒
|
|
53
|
+
* **真相未知**(既不是「停了」也不是「没落地」)。UI 同样不翻行,但文案必须说「状态未知」——
|
|
54
|
+
* 塌进 not_landed 就是把「不知道」渲染成一个具体的失败原因。 */
|
|
55
|
+
| {
|
|
56
|
+
ok: false;
|
|
57
|
+
reason: 'park_arbiter_unreachable';
|
|
58
|
+
detail?: string;
|
|
59
|
+
}
|
|
60
|
+
/** 未知/未来 `stop.*` 码(开集 default)。不翻行;`detail` 带原文供诊断。 */
|
|
61
|
+
| {
|
|
62
|
+
ok: false;
|
|
63
|
+
reason: 'unknown_conflict';
|
|
64
|
+
detail?: string;
|
|
65
|
+
} | {
|
|
66
|
+
ok: false;
|
|
67
|
+
reason: 'unavailable' | 'error';
|
|
68
|
+
detail?: string;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* 🔴 **G13 删债(先红后绿)**:taskStop 的 409 冲突分类 —— 手抄 code 判 → typed
|
|
72
|
+
* `TaskStopConflictError`(SDK 把**任何** `stop.*` 前缀码映到该类,errors.js:277-278;
|
|
73
|
+
* SDK 自己的注释还点名了壳的手抄位置 `engineTaskHandleWire.ts:186-197`)。
|
|
74
|
+
*
|
|
75
|
+
* 手抄版的实际缺陷(pure 门先红实证):SDK 明列**五**形,手抄只认前四形 ⇒ 第五形
|
|
76
|
+
* `stop.park_arbiter_unreachable`(core 1.397 三分:仲裁店不可达 ⇒ **真相未知**)静默塌进
|
|
77
|
+
* `not_landed`。
|
|
78
|
+
* 🔴 后果方向先定正负号([anchor-on-the-deciding-quantity]):门控的量 = 「UI 翻不翻这一行」,
|
|
79
|
+
* 五形与 default **全都是不翻** ⇒ 这不是安全缺陷,是**诚实缺陷** —— 把「真相未知」渲染成
|
|
80
|
+
* 「kill 没落地」,用户与诊断双双被误导。所以修法是给第五形一个自己的名字,而不是调整门。
|
|
81
|
+
*
|
|
82
|
+
* 开集纪律:`switch` 留 `default`(未来新 `stop.*` 码落 `unknown_conflict`,照样不翻行、
|
|
83
|
+
* 照样带原始 `code`),绝不塌进任何一个已知形 —— 塌进去就等于替 server 编了一个它没说的原因。
|
|
84
|
+
*/
|
|
85
|
+
export declare function classifyTaskStopConflict(e: unknown): EngineTaskStopOutcome;
|
|
86
|
+
/** 停一个引擎侧任务句柄(新面 B)。只有 ok:true(=HTTP 200)算停了。绝不 throw。 */
|
|
87
|
+
export declare function stopEngineTask(handle: string, opts?: {
|
|
88
|
+
signal?: AbortSignal;
|
|
89
|
+
}): Promise<EngineTaskStopOutcome>;
|