@sema-agent/client-core 0.6.0 → 0.8.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 +31 -11
- package/dist/adapt.d.ts +4 -2
- package/dist/adapt.js +225 -25
- 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/diff/patch.d.ts +29 -0
- package/dist/diff/patch.js +45 -0
- package/dist/engineSessionParam.d.ts +11 -0
- package/dist/engineSessionParam.js +41 -0
- package/dist/engineWireTarget.d.ts +14 -0
- package/dist/engineWireTarget.js +53 -0
- package/dist/finalVerifyWire.d.ts +74 -0
- package/dist/finalVerifyWire.js +63 -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/headlessPermissionModeWire.d.ts +55 -0
- package/dist/headlessPermissionModeWire.js +111 -0
- package/dist/headlessReconnectWire.d.ts +96 -0
- package/dist/headlessReconnectWire.js +141 -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 +74 -1
- package/dist/index.js +104 -1
- package/dist/interactiveToolsWire.d.ts +48 -0
- package/dist/interactiveToolsWire.js +86 -0
- package/dist/limitsWire.d.ts +89 -0
- package/dist/limitsWire.js +225 -0
- 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/notifications.d.ts +7 -0
- package/dist/notifications.js +32 -0
- package/dist/sandboxWire.d.ts +75 -0
- package/dist/sandboxWire.js +138 -0
- package/dist/scenarioWire.d.ts +62 -0
- package/dist/scenarioWire.js +115 -0
- package/dist/seam.d.ts +52 -5
- package/dist/seam.js +15 -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 +90 -0
- package/dist/subagent/engineTaskHandleWire.js +212 -0
- package/dist/toolResult.d.ts +118 -0
- package/dist/toolResult.js +774 -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 +5 -3
|
@@ -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,90 @@
|
|
|
1
|
+
export { engineTaskHandlesCapable } from './engineRowStopGate.js';
|
|
2
|
+
export interface EngineTaskOutputResult {
|
|
3
|
+
/** 累积后的输出正文(游标两形兼容,见头注)。 */
|
|
4
|
+
content: string;
|
|
5
|
+
/** wire output 投影原样的常用位(UI 一律以这些 flags 为准,别解析 content 猜状态)。 */
|
|
6
|
+
status?: string;
|
|
7
|
+
stoppedBy?: string;
|
|
8
|
+
partialResult?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** 测试钩:清累积缓存。 */
|
|
11
|
+
export declare function __resetTaskHandleWireForTests(): void;
|
|
12
|
+
/**
|
|
13
|
+
* 读一个引擎侧任务句柄的输出(新面 A)。null = 读不到(无 wire / caps 缺 / 无宿主 run / 404
|
|
14
|
+
* 域外或已 reap / 网错)——调用方回落既有渲染(receipt/outputFile tail)。绝不 throw。
|
|
15
|
+
*/
|
|
16
|
+
export declare function fetchEngineTaskOutput(handle: string, opts?: {
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
}): Promise<EngineTaskOutputResult | null>;
|
|
19
|
+
/**
|
|
20
|
+
* [1505] 契约标记判别(头注):spool 全量 → 替换;cursor 增量 → append;无标记 → 保守
|
|
21
|
+
* append-if-changed(与上次逐字节相同视为幂等重读,不重复)。空 content 不动累积。
|
|
22
|
+
* 🔴 B6 提出成具名纯函数 —— 它是这条读面唯一会「把输出显示成两遍」的地方,值得被直接断言。
|
|
23
|
+
*/
|
|
24
|
+
export declare function mergeTaskOutput(prev: string, fresh: string): string;
|
|
25
|
+
export type EngineTaskStopOutcome =
|
|
26
|
+
/** 200:停止动作完成(status = wire 终态照实;重复 stop 幂等)。 */
|
|
27
|
+
{
|
|
28
|
+
ok: true;
|
|
29
|
+
status?: string;
|
|
30
|
+
stoppedBy?: string;
|
|
31
|
+
}
|
|
32
|
+
/** 409 stop.not_landed:kill 没落地,进程可能还在跑——UI 绝不能渲染成「已停止」。 */
|
|
33
|
+
| {
|
|
34
|
+
ok: false;
|
|
35
|
+
reason: 'not_landed';
|
|
36
|
+
detail?: string;
|
|
37
|
+
}
|
|
38
|
+
/** 409 stop.not_local(server 1.250+ durable 臂):任务在**别的实例**上还在跑,此处没有 kill
|
|
39
|
+
* 被尝试——「别处在跑,此处停不了」,非失败重试件([1518]§一)。UI 同样不翻行。 */
|
|
40
|
+
| {
|
|
41
|
+
ok: false;
|
|
42
|
+
reason: 'not_local';
|
|
43
|
+
detail?: string;
|
|
44
|
+
}
|
|
45
|
+
/** 409 stop.parked(server 1.263 parked 状态机):子代停在待审批检查点——不是在跑,也不该被
|
|
46
|
+
* stop 杀(人先 /decide);409 stop.park_resume_won:复活赛跑输给了别处的 resume。两者 UI
|
|
47
|
+
* 都不翻行(fleet 行渲染词汇已由 coerceTaskStatus 映射 'awaiting approval')。 */
|
|
48
|
+
| {
|
|
49
|
+
ok: false;
|
|
50
|
+
reason: 'parked' | 'park_resume_won';
|
|
51
|
+
detail?: string;
|
|
52
|
+
}
|
|
53
|
+
/** 409 stop.park_arbiter_unreachable(core 1.397 三分,G13 补齐的第五形):仲裁店不可达 ⇒
|
|
54
|
+
* **真相未知**(既不是「停了」也不是「没落地」)。UI 同样不翻行,但文案必须说「状态未知」——
|
|
55
|
+
* 塌进 not_landed 就是把「不知道」渲染成一个具体的失败原因。 */
|
|
56
|
+
| {
|
|
57
|
+
ok: false;
|
|
58
|
+
reason: 'park_arbiter_unreachable';
|
|
59
|
+
detail?: string;
|
|
60
|
+
}
|
|
61
|
+
/** 未知/未来 `stop.*` 码(开集 default)。不翻行;`detail` 带原文供诊断。 */
|
|
62
|
+
| {
|
|
63
|
+
ok: false;
|
|
64
|
+
reason: 'unknown_conflict';
|
|
65
|
+
detail?: string;
|
|
66
|
+
} | {
|
|
67
|
+
ok: false;
|
|
68
|
+
reason: 'unavailable' | 'error';
|
|
69
|
+
detail?: string;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* 🔴 **G13 删债(先红后绿)**:taskStop 的 409 冲突分类 —— 手抄 code 判 → typed
|
|
73
|
+
* `TaskStopConflictError`(SDK 把**任何** `stop.*` 前缀码映到该类,errors.js:277-278;
|
|
74
|
+
* SDK 自己的注释还点名了壳的手抄位置 `engineTaskHandleWire.ts:186-197`)。
|
|
75
|
+
*
|
|
76
|
+
* 手抄版的实际缺陷(pure 门先红实证):SDK 明列**五**形,手抄只认前四形 ⇒ 第五形
|
|
77
|
+
* `stop.park_arbiter_unreachable`(core 1.397 三分:仲裁店不可达 ⇒ **真相未知**)静默塌进
|
|
78
|
+
* `not_landed`。
|
|
79
|
+
* 🔴 后果方向先定正负号([anchor-on-the-deciding-quantity]):门控的量 = 「UI 翻不翻这一行」,
|
|
80
|
+
* 五形与 default **全都是不翻** ⇒ 这不是安全缺陷,是**诚实缺陷** —— 把「真相未知」渲染成
|
|
81
|
+
* 「kill 没落地」,用户与诊断双双被误导。所以修法是给第五形一个自己的名字,而不是调整门。
|
|
82
|
+
*
|
|
83
|
+
* 开集纪律:`switch` 留 `default`(未来新 `stop.*` 码落 `unknown_conflict`,照样不翻行、
|
|
84
|
+
* 照样带原始 `code`),绝不塌进任何一个已知形 —— 塌进去就等于替 server 编了一个它没说的原因。
|
|
85
|
+
*/
|
|
86
|
+
export declare function classifyTaskStopConflict(e: unknown): EngineTaskStopOutcome;
|
|
87
|
+
/** 停一个引擎侧任务句柄(新面 B)。只有 ok:true(=HTTP 200)算停了。绝不 throw。 */
|
|
88
|
+
export declare function stopEngineTask(handle: string, opts?: {
|
|
89
|
+
signal?: AbortSignal;
|
|
90
|
+
}): Promise<EngineTaskStopOutcome>;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B6 批搬迁(2026-07-27,设计稿 §3 B6):cli `src/sema/engineTaskHandleWire.ts` 整搬。
|
|
3
|
+
*
|
|
4
|
+
* 🔴 搬迁差分:`wireConfig()` → `engineWireTarget()`;`getActiveEngineTaskId()` →
|
|
5
|
+
* `activeEngineRunId()`;`logForDebugging` → `hostLog`;`process.env.SEMA_DEBUG` 前置判
|
|
6
|
+
* → `engineWireDebugEnabled()`。
|
|
7
|
+
*
|
|
8
|
+
* ── 以下为原文件的领域说明(逐字保留)────────────────────────────────────────────────────────
|
|
9
|
+
*
|
|
10
|
+
* engineTaskHandleWire — CC TaskOutput/TaskStop 人侧对位的 wire 消费([1501] server 1.246.0 /
|
|
11
|
+
* SDK 0.0.67 提货)。
|
|
12
|
+
*
|
|
13
|
+
* Wire(两面都是出生即强制带 `?session=`,与 subagentOutput 同姿势;非阻塞、副本本地):
|
|
14
|
+
* · 读:`GET /v1/runs/:id/tasks/:handle/output?session=` → SDK `runs.taskOutput`
|
|
15
|
+
* · 停:`POST /v1/runs/:id/tasks/:handle/stop?session=` → SDK `runs.taskStop`
|
|
16
|
+
* · :id = 宿主 run;:handle = canonical 句柄 ONLY(b… bash / m… monitor / a… agent)——
|
|
17
|
+
* 不收名字、不收 legacy shellId,传了就是 404;workflow(w…/wa…)恒 404(journal 是那边的
|
|
18
|
+
* 读面,永不打这里)。
|
|
19
|
+
* · 能力门 = capabilities.taskHandles(engineCapsCache 通用缓存,[1501] 明令:用能力位判断,
|
|
20
|
+
* 别 trial-by-501/404)。老引擎(≤1.245)缺该键 → 两动词都回落 null/不可用,version-safe。
|
|
21
|
+
*
|
|
22
|
+
* 读面游标语义([1501]§一 + [1505] must-fix 修正,UI 必须按此做):是否「读即消费」取决于句柄
|
|
23
|
+
* 形态——spooled 腿全量可重读、GET 幂等;cursor-only 腿每次 GET 只出新字节、读走就没了。
|
|
24
|
+
* 两形判别用 **wire 契约自带标记**(server [1505] 钉版承诺稳定,变更走 CHANGELOG+黑板;core
|
|
25
|
+
* CC-verbatim 固定形):content 含 `--- stdout (full, re-readable) ---` = spool 全量形 → 整体
|
|
26
|
+
* 替换;含 `--- new stdout ---` = cursor 增量形 → append。初版的「前缀延展」启发式被 [1505]
|
|
27
|
+
* 否决——spool content 首行 `status: <displayStatus>` 在任务完成时刻从 running 翻 exited,
|
|
28
|
+
* 前缀检测恰在最常见时刻失败 → 全量被 append = 输出重复。无标记(极端/未知形)按保守形
|
|
29
|
+
* append-if-changed 处理;空 content 不动累积。绝不假设重拉能拿到同样字节。
|
|
30
|
+
*
|
|
31
|
+
* ⚠️ cursor-only 腿双消费者 caveat([1505]):同一 b* 句柄若同时被模型侧 TaskOutput 工具读,
|
|
32
|
+
* 两个消费者互抢增量(工具读走的字节本读面拿不到,反之亦然),端侧累积在该形下不保证完整;
|
|
33
|
+
* spool 腿(server 正常接线下的常态)无此问题。UI 在 cursor 形下不承诺完整性。
|
|
34
|
+
*
|
|
35
|
+
* 停面语义([1501]§二):**只有 200 算停了**;409 `stop.not_landed` = kill 没落地、进程可能
|
|
36
|
+
* 还在跑(UI 绝不能渲染成「已停止」);终态句柄重复 stop 幂等诚实(200,状态照实)。
|
|
37
|
+
*
|
|
38
|
+
* `?filter=` 禁用(带了=400):CC 的 filter 语义由调用方拿回 content 后本地做,本模块不透传。
|
|
39
|
+
*/
|
|
40
|
+
import { TaskStopConflictError } from '@sema-agent/sdk';
|
|
41
|
+
import { hostLog } from '../host.js';
|
|
42
|
+
import { makeEngineWireClient } from '../engineWireSdk.js';
|
|
43
|
+
import { engineWireDebugEnabled, engineWireTarget } from '../engineWireTarget.js';
|
|
44
|
+
import { activeEngineRunId, engineSessionParamSpread } from '../engineSessionParam.js';
|
|
45
|
+
import { getBgParentRun } from '../subagentContentStore.js';
|
|
46
|
+
import { engineTaskHandlesCapable } from './engineRowStopGate.js';
|
|
47
|
+
// 能力门同步读口:#98 起真源在零依赖叶 `engineRowStopGate`(批量停要同步分类,不能拉本模块的
|
|
48
|
+
// SDK 图),这里原样 re-export,消费方不动。
|
|
49
|
+
export { engineTaskHandlesCapable } from './engineRowStopGate.js';
|
|
50
|
+
/** 宿主 run 解析:fleet 行/通知帧喂的 parent 映射优先(行 id=`${runId} ${handle}` 的 parentId
|
|
51
|
+
* 投影),缺席退当前交互 run(register 时刻的活跃 run 即宿主——engineSide bash 行的出生形)。 */
|
|
52
|
+
function resolveHostRun(handle) {
|
|
53
|
+
return getBgParentRun(handle) ?? activeEngineRunId() ?? null;
|
|
54
|
+
}
|
|
55
|
+
// 游标累积(按 handle;进程内存态,与查看态生命周期同级——端重启即空,重读从引擎再取)
|
|
56
|
+
const accumulated = new Map();
|
|
57
|
+
/** 测试钩:清累积缓存。 */
|
|
58
|
+
export function __resetTaskHandleWireForTests() {
|
|
59
|
+
accumulated.clear();
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 读一个引擎侧任务句柄的输出(新面 A)。null = 读不到(无 wire / caps 缺 / 无宿主 run / 404
|
|
63
|
+
* 域外或已 reap / 网错)——调用方回落既有渲染(receipt/outputFile tail)。绝不 throw。
|
|
64
|
+
*/
|
|
65
|
+
export async function fetchEngineTaskOutput(handle, opts) {
|
|
66
|
+
const cfg = engineWireTarget();
|
|
67
|
+
if (!cfg || !engineTaskHandlesCapable())
|
|
68
|
+
return null;
|
|
69
|
+
const runId = resolveHostRun(handle);
|
|
70
|
+
if (!runId)
|
|
71
|
+
return null;
|
|
72
|
+
const client = makeEngineWireClient({
|
|
73
|
+
baseUrl: cfg.baseUrl,
|
|
74
|
+
...(cfg.token ? { token: cfg.token } : {}),
|
|
75
|
+
principal: cfg.principal,
|
|
76
|
+
});
|
|
77
|
+
if (!client)
|
|
78
|
+
return null;
|
|
79
|
+
try {
|
|
80
|
+
const r = await client.runs.taskOutput(runId, handle, {
|
|
81
|
+
...(opts?.signal ? { signal: opts.signal } : {}),
|
|
82
|
+
...engineSessionParamSpread(),
|
|
83
|
+
});
|
|
84
|
+
const fresh = typeof r?.content === 'string' ? r.content : '';
|
|
85
|
+
const prev = accumulated.get(handle) ?? '';
|
|
86
|
+
const merged = mergeTaskOutput(prev, fresh);
|
|
87
|
+
accumulated.set(handle, merged);
|
|
88
|
+
const out = r?.output ?? {};
|
|
89
|
+
if (engineWireDebugEnabled()) {
|
|
90
|
+
hostLog('debug', `[task-handle] output ${handle} via run ${runId}: ok fresh=${fresh.length}b merged=${merged.length}b status=${typeof out.status === 'string' ? out.status : '-'}`);
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
content: merged,
|
|
94
|
+
...(typeof out.status === 'string' ? { status: out.status } : {}),
|
|
95
|
+
...(typeof out.stoppedBy === 'string' ? { stoppedBy: out.stoppedBy } : {}),
|
|
96
|
+
...(out.partial_result === true ? { partialResult: true } : {}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
const status = e?.status;
|
|
101
|
+
hostLog('debug', `[task-handle] output ${handle} via run ${runId}: ${typeof status === 'number' ? status : String(e).slice(0, 160)}`);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* [1505] 契约标记判别(头注):spool 全量 → 替换;cursor 增量 → append;无标记 → 保守
|
|
107
|
+
* append-if-changed(与上次逐字节相同视为幂等重读,不重复)。空 content 不动累积。
|
|
108
|
+
* 🔴 B6 提出成具名纯函数 —— 它是这条读面唯一会「把输出显示成两遍」的地方,值得被直接断言。
|
|
109
|
+
*/
|
|
110
|
+
export function mergeTaskOutput(prev, fresh) {
|
|
111
|
+
if (fresh.length === 0)
|
|
112
|
+
return prev;
|
|
113
|
+
if (fresh.includes('--- stdout (full, re-readable) ---'))
|
|
114
|
+
return fresh;
|
|
115
|
+
if (fresh.includes('--- new stdout ---'))
|
|
116
|
+
return prev + fresh;
|
|
117
|
+
return fresh === prev ? prev : prev + fresh;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 🔴 **G13 删债(先红后绿)**:taskStop 的 409 冲突分类 —— 手抄 code 判 → typed
|
|
121
|
+
* `TaskStopConflictError`(SDK 把**任何** `stop.*` 前缀码映到该类,errors.js:277-278;
|
|
122
|
+
* SDK 自己的注释还点名了壳的手抄位置 `engineTaskHandleWire.ts:186-197`)。
|
|
123
|
+
*
|
|
124
|
+
* 手抄版的实际缺陷(pure 门先红实证):SDK 明列**五**形,手抄只认前四形 ⇒ 第五形
|
|
125
|
+
* `stop.park_arbiter_unreachable`(core 1.397 三分:仲裁店不可达 ⇒ **真相未知**)静默塌进
|
|
126
|
+
* `not_landed`。
|
|
127
|
+
* 🔴 后果方向先定正负号([anchor-on-the-deciding-quantity]):门控的量 = 「UI 翻不翻这一行」,
|
|
128
|
+
* 五形与 default **全都是不翻** ⇒ 这不是安全缺陷,是**诚实缺陷** —— 把「真相未知」渲染成
|
|
129
|
+
* 「kill 没落地」,用户与诊断双双被误导。所以修法是给第五形一个自己的名字,而不是调整门。
|
|
130
|
+
*
|
|
131
|
+
* 开集纪律:`switch` 留 `default`(未来新 `stop.*` 码落 `unknown_conflict`,照样不翻行、
|
|
132
|
+
* 照样带原始 `code`),绝不塌进任何一个已知形 —— 塌进去就等于替 server 编了一个它没说的原因。
|
|
133
|
+
*/
|
|
134
|
+
export function classifyTaskStopConflict(e) {
|
|
135
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
136
|
+
const raw = e?.code;
|
|
137
|
+
// typed 优先:SDK ≥0.0.11x 恒给 TaskStopConflictError 且 `code` 是 errorCode 原文。
|
|
138
|
+
// 兜底(老 SDK / 非 typed 抛点):code 非串时从 message 里认字面 —— 与手抄版同姿势,只是
|
|
139
|
+
// 现在是**回落**而不是主判据。
|
|
140
|
+
const code = typeof raw === 'string'
|
|
141
|
+
? raw
|
|
142
|
+
: (STOP_CONFLICT_CODES.find(c => detail.includes(c)) ?? '');
|
|
143
|
+
switch (code) {
|
|
144
|
+
case 'stop.not_local':
|
|
145
|
+
return { ok: false, reason: 'not_local', detail };
|
|
146
|
+
case 'stop.parked':
|
|
147
|
+
return { ok: false, reason: 'parked', detail };
|
|
148
|
+
case 'stop.park_resume_won':
|
|
149
|
+
return { ok: false, reason: 'park_resume_won', detail };
|
|
150
|
+
case 'stop.park_arbiter_unreachable':
|
|
151
|
+
return { ok: false, reason: 'park_arbiter_unreachable', detail };
|
|
152
|
+
case 'stop.not_landed':
|
|
153
|
+
return { ok: false, reason: 'not_landed', detail };
|
|
154
|
+
default:
|
|
155
|
+
// 开集:未知/未来 `stop.*`(以及 code 完全缺席的残破形)。诚实说「有冲突但我不认识它」,
|
|
156
|
+
// 不冒充 not_landed。UI 处置与其余五形相同(不翻行)。
|
|
157
|
+
return { ok: false, reason: 'unknown_conflict', detail };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** 兜底字面认码用的已知集(顺序 = 长码优先,避免 `stop.parked` 抢走 `stop.park_resume_won`)。 */
|
|
161
|
+
const STOP_CONFLICT_CODES = [
|
|
162
|
+
'stop.park_arbiter_unreachable',
|
|
163
|
+
'stop.park_resume_won',
|
|
164
|
+
'stop.not_landed',
|
|
165
|
+
'stop.not_local',
|
|
166
|
+
'stop.parked',
|
|
167
|
+
];
|
|
168
|
+
/** 停一个引擎侧任务句柄(新面 B)。只有 ok:true(=HTTP 200)算停了。绝不 throw。 */
|
|
169
|
+
export async function stopEngineTask(handle, opts) {
|
|
170
|
+
const cfg = engineWireTarget();
|
|
171
|
+
if (!cfg || !engineTaskHandlesCapable())
|
|
172
|
+
return { ok: false, reason: 'unavailable' };
|
|
173
|
+
const runId = resolveHostRun(handle);
|
|
174
|
+
if (!runId)
|
|
175
|
+
return { ok: false, reason: 'unavailable', detail: 'no host run mapping' };
|
|
176
|
+
const client = makeEngineWireClient({
|
|
177
|
+
baseUrl: cfg.baseUrl,
|
|
178
|
+
...(cfg.token ? { token: cfg.token } : {}),
|
|
179
|
+
principal: cfg.principal,
|
|
180
|
+
});
|
|
181
|
+
if (!client)
|
|
182
|
+
return { ok: false, reason: 'unavailable' };
|
|
183
|
+
try {
|
|
184
|
+
const r = await client.runs.taskStop(runId, handle, {
|
|
185
|
+
...(opts?.signal ? { signal: opts.signal } : {}),
|
|
186
|
+
...engineSessionParamSpread(),
|
|
187
|
+
});
|
|
188
|
+
const out = r?.output ?? {};
|
|
189
|
+
if (engineWireDebugEnabled()) {
|
|
190
|
+
hostLog('debug', `[task-handle] stop ${handle} via run ${runId}: ok status=${typeof out.status === 'string' ? out.status : '-'} stoppedBy=${typeof out.stoppedBy === 'string' ? out.stoppedBy : '-'}`);
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
ok: true,
|
|
194
|
+
...(typeof out.status === 'string' ? { status: out.status } : {}),
|
|
195
|
+
...(typeof out.stoppedBy === 'string' ? { stoppedBy: out.stoppedBy } : {}),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
catch (e) {
|
|
199
|
+
const status = e?.status;
|
|
200
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
201
|
+
hostLog('debug', `[task-handle] stop ${handle} via run ${runId}: ${typeof status === 'number' ? status : detail.slice(0, 160)}`);
|
|
202
|
+
// [1521]F2 订正:server 文案句里不含 errorCode 字面(message 判恒 miss=死分形),而 SDK
|
|
203
|
+
// APIError.code 一直透 errorCode——锚 code(对文案措辞变化免疫),message 兜底保留。
|
|
204
|
+
// 🔴 G13:判别锚从 `status===409` 换成 **typed 类**(`TaskStopConflictError`)—— SDK 把任何
|
|
205
|
+
// `stop.*` 码都映到它,以后 server 换 HTTP 码(或同码复用别的语义)时这条腿不会失灵;
|
|
206
|
+
// status 判保留为兜底(老 SDK 不抛 typed 类时仍要认)。
|
|
207
|
+
if (e instanceof TaskStopConflictError || status === 409) {
|
|
208
|
+
return classifyTaskStopConflict(e);
|
|
209
|
+
}
|
|
210
|
+
return { ok: false, reason: 'error', detail };
|
|
211
|
+
}
|
|
212
|
+
}
|