@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
package/dist/seam.d.ts
CHANGED
|
@@ -252,9 +252,25 @@ export type ChromeEvent = {
|
|
|
252
252
|
}
|
|
253
253
|
/**
|
|
254
254
|
* 带外后台 workflow 完成推送(service 1.75 Path B:引擎**没有** server-side 注入,壳负责喂模型)。
|
|
255
|
-
*
|
|
256
|
-
* 入「完成通知队列」,idle 时 auto-submit 给模型(CC RemoteAgentTask 先例,默认 'later'
|
|
257
|
-
*
|
|
255
|
+
* 宿主消费义务 = **只有投递一件**:把 `message`(模型面 `<task-notification>` 正文,适配器已按 core
|
|
256
|
+
* 同形铸好)入「完成通知队列」,idle 时 auto-submit 给模型(CC RemoteAgentTask 先例,默认 'later'
|
|
257
|
+
* 优先级)。
|
|
258
|
+
*
|
|
259
|
+
* 🔴 **切缝 = 「门归包、投递归宿主」**([1857] Path B 回归的定谳形,B5 壳侧收批实撞;
|
|
260
|
+
* [paired-mechanisms-must-share-premise]):`workflow_complete` 臂在**发本臂之前**已经在库内
|
|
261
|
+
* 一次做完三件 —— ① 跨通道去重门 `isEngineWorkflowNotified(runId)` 早退;
|
|
262
|
+
* ② `markEngineWorkflowNotified(runId)`;③ `noteWorkflowCompletionCardEnqueued(runId)`
|
|
263
|
+
* (adapt.ts `case 'workflow_complete'` 三行连坐)。⇒ 宿主这三件**一件都不要再做**:
|
|
264
|
+
* · 绝不要借任何「自带同一道门」的宿主老函数投递(cli 的 `enqueueEngineWorkflowNotification()`
|
|
265
|
+
* 首行就是 `if (notifiedRunIds.has(runId)) return`,读的正是库刚写的记账 ⇒ **恒早退,
|
|
266
|
+
* 一条都进不了队列**,两半各自都对、组合零结果);投递要用**裸入队**口。
|
|
267
|
+
* · 也不要再补 `noteWorkflowCompletionCardEnqueued` —— 库已落(幂等 Set.add,宿主照落无害,
|
|
268
|
+
* 但记号该删:整臂让位,不是两边都做)。
|
|
269
|
+
* 🔴 **这条建议在 B6 之前是不安全的,别照抄旧结论**:库里那一行原本写在 `yield` **之后**,
|
|
270
|
+
* 消费者早退时永不执行 ⇒ 那时壳 `upstreamBridge.ts:488` 的显式落记号是**真的在补一个洞**。
|
|
271
|
+
* B6 已把它移到 `yield` 之前(adapt.ts `case 'workflow_complete'`,pure 门「早退 ⇒
|
|
272
|
+
* cardEnqueued 仍落」先红后绿钉住)⇒ 库侧现在**无条件**落,壳那一行才成为可删的冗余。
|
|
273
|
+
* ⇒ 判据只能锚在「队列里到底有没有多出这一条」,不能锚在「谁调了什么」。
|
|
258
274
|
*/
|
|
259
275
|
| {
|
|
260
276
|
kind: 'workflow_notification_enqueue';
|
package/dist/seam.js
CHANGED
|
@@ -22,8 +22,20 @@ export const CHROME_ARMS = [
|
|
|
22
22
|
{ kind: 'attachment', required: false, duty: '渲 attachment 行(必须带 default 臂)' },
|
|
23
23
|
{ kind: 'notification_terminal', required: false, duty: '可选 UI 提示(记账已由库自持)' },
|
|
24
24
|
{ kind: 'subagent_lifecycle', required: true, duty: 'fire SubagentStart/Stop hooks(去重已由库解)' },
|
|
25
|
-
{
|
|
26
|
-
|
|
25
|
+
{
|
|
26
|
+
kind: 'workflow_notification_enqueue',
|
|
27
|
+
required: true,
|
|
28
|
+
// 门/notified 记账/cardEnqueued 记号三件已由库在发臂前做完 ⇒ 宿主只投递,且必须用**裸入队**口。
|
|
29
|
+
duty: 'message 裸入完成通知队列(不要用自带去重门的老函数),idle 时喂模型;门与两条记号归库',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
kind: 'task_ledger_sync',
|
|
33
|
+
required: true,
|
|
34
|
+
// 🔴 **两个 source 都要接**(B6 核实:同一张任务卡实测出两条)——`tool_use` 是卡刚开时的
|
|
35
|
+
// 乐观建行(旧引擎只有它),`structured` 是引擎回来的权威全量态(带真实 id/状态)。
|
|
36
|
+
// 只接前者 ⇒ 引擎真实 id 永不回填;只接后者 ⇒ 面板要等工具跑完才出现。幂等键 = toolUseId。
|
|
37
|
+
duty: '按 toolUseId 幂等落 TaskCreate/TaskUpdate —— source 的 tool_use(乐观建行)与 structured(权威全量态)两条都要接',
|
|
38
|
+
},
|
|
27
39
|
{ kind: 'inline_task_stats', required: true, duty: 'tick/settle/settle_all 转发给 engineInlineTaskStats' },
|
|
28
40
|
{ kind: 'prompt_suggestions', required: false, duty: '推 composer 上方 chips(绝不回喂模型)' },
|
|
29
41
|
{ kind: 'last_turn_usage', required: true, duty: '落最近一次 turn 真 usage(statusline 回落源)' },
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Test/introspection hook. */
|
|
2
|
+
export declare function isEngineCompactPending(): boolean;
|
|
3
|
+
/** Test hook: reset the cached capability (fresh engine / respawn). */
|
|
4
|
+
export declare function resetManualCompactCapability(): void;
|
|
5
|
+
/**
|
|
6
|
+
* The /compact command's live half. Mid-turn: fire now. Idle: arm for the next run bind.
|
|
7
|
+
* Returns what it did (for the command's display line / tests). No-op ('offline') without a live
|
|
8
|
+
* wire; 'unsupported' when the engine's capabilities said manualCompact:false ([488] TOC-local) —
|
|
9
|
+
* the client-side projection compaction still runs, but the ENGINE session context does not shrink.
|
|
10
|
+
*/
|
|
11
|
+
export declare function requestEngineCompact(): 'fired' | 'armed' | 'offline' | 'unsupported';
|
|
12
|
+
export declare function onEngineTaskBound(taskId: string): void;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B6 批搬迁(2026-07-27,设计稿 §3 B6):cli `src/sema/engineCompactWire.ts` 整搬。
|
|
3
|
+
*
|
|
4
|
+
* 🔴 搬迁差分:
|
|
5
|
+
* · `wireConfig()`(六份手抄的**最后一份**,而且这一份已经多长了一行 principal 注释 ——
|
|
6
|
+
* 漂移的活样本)→ `engineWireTarget()`;
|
|
7
|
+
* · `getActiveEngineTaskId()` → `activeEngineRunId()`(SessionPort);
|
|
8
|
+
* · `logForDebugging` → `hostLog('debug', …)`;
|
|
9
|
+
* · `setTimeout(...)+t.unref?.()` → `hostTimers()`(**本文件是 TimersPort 的第一个消费者**)。
|
|
10
|
+
* 🔴 端没装 TimersPort 的后果**说清楚**:两处延迟发射(404 单次重试 / bind 后 1.5s 发射)
|
|
11
|
+
* 会**整个不发生** —— `/compact` 在 idle armed 之后就再也不会真正打到引擎,而命令行显示的
|
|
12
|
+
* 还是 'armed'。这不是「少个定时器」,是一条静默失效的功能腿。所以缺 port 时留 debug 日志,
|
|
13
|
+
* 并且 `hostPortMisses()` 里能看见(端自检恒应为空)。
|
|
14
|
+
*
|
|
15
|
+
* ── 以下为原文件的领域说明(逐字保留)────────────────────────────────────────────────────────
|
|
16
|
+
*
|
|
17
|
+
* engineCompactWire — manual /compact reaches the ENGINE (K-1c), not just the client projection.
|
|
18
|
+
*
|
|
19
|
+
* Problem (clay dogfood 07-03): in live mode `/compact` ran only the CLIENT-side compaction (the local
|
|
20
|
+
* message projection) — the engine session's context was never compacted, so the next turn still paid
|
|
21
|
+
* the full window.
|
|
22
|
+
*
|
|
23
|
+
* Engine contract (service server.ts:1874, core 1.156 `TaskStream.compact()`):
|
|
24
|
+
* POST /v1/runs/:id/compact → 202; compaction runs at the next SAFE turn boundary and a
|
|
25
|
+
* `compacted{trigger:"manual"}` event rides the run's OWN stream. The verb only applies to a run LIVE
|
|
26
|
+
* on the replica — and `/compact` is typed while IDLE, when there is no live run. So the wire is
|
|
27
|
+
* arm-then-fire:
|
|
28
|
+
* - `/compact` mid-turn (active engine run bound) → fire immediately against that run.
|
|
29
|
+
* - `/compact` idle → arm a pending flag; the host's run_started bind (onEngineTaskBound) fires it
|
|
30
|
+
* against the NEXT turn's run — the engine compacts at that turn's first safe boundary, exactly
|
|
31
|
+
* the semantics CC users expect ("everything before this point is summarized").
|
|
32
|
+
*
|
|
33
|
+
* SDK 0.0.52 长出 runs.compact verb(SDK 宪法迁移批)—— raw fetch 退役,改走 AgentClient
|
|
34
|
+
* (engineWireSdk 工厂;baseUrl/token/principal 纪律与 live wire 同源)。Fire-and-forget + fail-soft
|
|
35
|
+
* 语义不变:409/501/网络错都只留 debug 日志,端侧投影压缩仍是可见结果。
|
|
36
|
+
* 迁移差分记账:①原 raw POST 无超时 → SDK 缺省 60s 预算(compact 是 202 即回的 arm verb,本地
|
|
37
|
+
* 口毫秒级,纯上界改善);②404 单次 2s 延迟重试(早 header bind 跑赢 run-row 持久化)逐字保留。
|
|
38
|
+
*/
|
|
39
|
+
import { hostLog, hostTimers } from '../host.js';
|
|
40
|
+
import { makeEngineWireClient } from '../engineWireSdk.js';
|
|
41
|
+
import { engineWireTarget } from '../engineWireTarget.js';
|
|
42
|
+
import { activeEngineRunId, engineSessionParamSpread } from '../engineSessionParam.js';
|
|
43
|
+
let pendingEngineCompact = false;
|
|
44
|
+
/** Test/introspection hook. */
|
|
45
|
+
export function isEngineCompactPending() {
|
|
46
|
+
return pendingEngineCompact;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* [488] 定谳(2026-07-07 指纹探针,server 1.130.0):TOC 本地引擎(无 TiDB runStore)的
|
|
50
|
+
* manual-compact verb 是 BY-DESIGN 501(handler 第一道闸 `!deps.runStore → 501`,capabilities
|
|
51
|
+
* 也如实报 `manualCompact:false`)——之前端没消费这个 gate,verb 打过去 501 被 fire-and-forget
|
|
52
|
+
* 吞掉,引擎 session 从未压缩=「静默 no-op」的真机理。这里 lazy 探一次 /v1/capabilities 并缓存;
|
|
53
|
+
* false ⇒ requestEngineCompact 返回 'unsupported'(不再装作 armed/fired)。探测失败 fail-open
|
|
54
|
+
* (null=未知 ⇒ 保持旧行为发 verb,错误面不变)。
|
|
55
|
+
*/
|
|
56
|
+
let manualCompactCapable = null;
|
|
57
|
+
let capabilitiesProbe = null;
|
|
58
|
+
function probeManualCompactCapability() {
|
|
59
|
+
if (capabilitiesProbe)
|
|
60
|
+
return;
|
|
61
|
+
const cfg = engineWireTarget();
|
|
62
|
+
if (!cfg)
|
|
63
|
+
return;
|
|
64
|
+
capabilitiesProbe = (async () => {
|
|
65
|
+
try {
|
|
66
|
+
// SDK 0.0.52 client.capabilities()(typed Capabilities.manualCompact)。5s 预算经 client
|
|
67
|
+
// timeoutMs(对抗复审 MED 原语义:hung engine 不 pin probe promise);非 2xx/网络错/超时 SDK
|
|
68
|
+
// 统一抛错 → 与原「non-200 也重试」同归 catch 置 null 允许后续重试。
|
|
69
|
+
const client = makeEngineWireClient({
|
|
70
|
+
baseUrl: cfg.baseUrl,
|
|
71
|
+
...(cfg.token ? { token: cfg.token } : {}),
|
|
72
|
+
principal: cfg.principal,
|
|
73
|
+
timeoutMs: 5000,
|
|
74
|
+
});
|
|
75
|
+
if (!client) {
|
|
76
|
+
capabilitiesProbe = null;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const caps = await client.capabilities();
|
|
80
|
+
if (typeof caps.manualCompact === 'boolean')
|
|
81
|
+
manualCompactCapable = caps.manualCompact;
|
|
82
|
+
else
|
|
83
|
+
capabilitiesProbe = null; // shape we don't understand — retry later
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
capabilitiesProbe = null; // non-2xx/network error/timeout: unknown stays fail-open, next call re-probes
|
|
87
|
+
}
|
|
88
|
+
})();
|
|
89
|
+
}
|
|
90
|
+
/** Test hook: reset the cached capability (fresh engine / respawn). */
|
|
91
|
+
export function resetManualCompactCapability() {
|
|
92
|
+
manualCompactCapable = null;
|
|
93
|
+
capabilitiesProbe = null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 延迟发射(`setTimeout` + `unref` 的 port 形)。缺 TimersPort ⇒ **不发射**并留日志 ——
|
|
97
|
+
* 静默 drop 会让 `/compact` 看起来 armed 了却永不落地(见文件头)。
|
|
98
|
+
*/
|
|
99
|
+
function deferFire(ms, fn, why) {
|
|
100
|
+
const timers = hostTimers();
|
|
101
|
+
if (!timers) {
|
|
102
|
+
hostLog('debug', `engineCompactWire: no TimersPort — ${why} 不会发射(端漏装 port,/compact 这条腿静默失效)`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
timers.setTimer(ms, fn);
|
|
106
|
+
}
|
|
107
|
+
async function postCompact(taskId, retriesLeft = 1) {
|
|
108
|
+
const cfg = engineWireTarget();
|
|
109
|
+
if (!cfg)
|
|
110
|
+
return;
|
|
111
|
+
// SDK runs.compact(0.0.52)——POST /v1/runs/:id/compact body {}(无 instructions 时与原 '{}' 逐字
|
|
112
|
+
// 同 wire);header 姿势 = Transport buildHeaders(Bearer 仅有 token 时 + x-agent-principal +
|
|
113
|
+
// content-type),与原手抄同三头。
|
|
114
|
+
const client = makeEngineWireClient({
|
|
115
|
+
baseUrl: cfg.baseUrl,
|
|
116
|
+
...(cfg.token ? { token: cfg.token } : {}),
|
|
117
|
+
principal: cfg.principal,
|
|
118
|
+
});
|
|
119
|
+
if (!client)
|
|
120
|
+
return;
|
|
121
|
+
try {
|
|
122
|
+
const ack = await client.runs.compact(taskId, engineSessionParamSpread());
|
|
123
|
+
hostLog('debug', `engineCompactWire: POST compact ${taskId} → ok${ack?.status ? ` (${ack.status})` : ''}`);
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
const status = e?.status;
|
|
127
|
+
if (typeof status === 'number') {
|
|
128
|
+
hostLog('debug', `engineCompactWire: POST compact ${taskId} → ${status}`);
|
|
129
|
+
// 404 = the early header bind outran the run-row persist — one deferred retry covers it.
|
|
130
|
+
if (status === 404 && retriesLeft > 0) {
|
|
131
|
+
deferFire(2000, () => void postCompact(taskId, retriesLeft - 1), '404 单次重试');
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
hostLog('debug', `engineCompactWire: compact fire failed (best-effort): ${String(e)}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The /compact command's live half. Mid-turn: fire now. Idle: arm for the next run bind.
|
|
140
|
+
* Returns what it did (for the command's display line / tests). No-op ('offline') without a live
|
|
141
|
+
* wire; 'unsupported' when the engine's capabilities said manualCompact:false ([488] TOC-local) —
|
|
142
|
+
* the client-side projection compaction still runs, but the ENGINE session context does not shrink.
|
|
143
|
+
*/
|
|
144
|
+
export function requestEngineCompact() {
|
|
145
|
+
if (!engineWireTarget())
|
|
146
|
+
return 'offline';
|
|
147
|
+
probeManualCompactCapability(); // lazy, cached; first call may still ride the old path (fail-open)
|
|
148
|
+
if (manualCompactCapable === false) {
|
|
149
|
+
hostLog('debug', 'engineCompactWire: engine capabilities report manualCompact:false — verb suppressed (TOC-local runStore-less engine, [488])');
|
|
150
|
+
return 'unsupported';
|
|
151
|
+
}
|
|
152
|
+
const taskId = activeEngineRunId();
|
|
153
|
+
if (taskId) {
|
|
154
|
+
void postCompact(taskId);
|
|
155
|
+
return 'fired';
|
|
156
|
+
}
|
|
157
|
+
pendingEngineCompact = true;
|
|
158
|
+
return 'armed';
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* host run_started hook: a new engine run just bound. If a manual compact is pending, fire it —
|
|
162
|
+
* the engine compacts at this turn's first safe boundary.
|
|
163
|
+
*
|
|
164
|
+
* Timing: the bind fires off the X-Task-Id RESPONSE HEADER — the earliest possible signal, which can
|
|
165
|
+
* precede the server's run-row persist (a straight POST here raced it to a 404 in the live harness).
|
|
166
|
+
* Compaction runs at the next safe TURN BOUNDARY anyway, so a short defer loses nothing; retry once
|
|
167
|
+
* more for slow persists. Both shots are best-effort (postCompact never throws).
|
|
168
|
+
*/
|
|
169
|
+
const BOUND_FIRE_DELAY_MS = 1500;
|
|
170
|
+
export function onEngineTaskBound(taskId) {
|
|
171
|
+
probeManualCompactCapability(); // warm the capability cache off the first turn bind, before any /compact
|
|
172
|
+
if (!pendingEngineCompact)
|
|
173
|
+
return;
|
|
174
|
+
pendingEngineCompact = false;
|
|
175
|
+
deferFire(BOUND_FIRE_DELAY_MS, () => void postCompact(taskId), 'bind 后发射');
|
|
176
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** 记一个子代行的转录锚(幂等;server <1.278 永不调用 = 台账恒空,取件腿整条静默不动)。 */
|
|
2
|
+
export declare function recordEngineTranscriptId(taskId: string, transcriptId: string): void;
|
|
3
|
+
/** 读面(取件腿 + 测试)。 */
|
|
4
|
+
export declare function getEngineTranscriptId(taskId: string): string | undefined;
|
|
5
|
+
/** 测试钩:清台账 + 在飞记号。 */
|
|
6
|
+
export declare function __resetEngineDelegatedPromptForTests(): void;
|
|
7
|
+
/**
|
|
8
|
+
* content → 纯文本。tool_result 型正文返回 undefined —— 那是子代自己跑工具的回填,**不是**委派
|
|
9
|
+
* prompt(这条判别就是「别把子代自己的消息当委派 prompt」的落点)。
|
|
10
|
+
*/
|
|
11
|
+
export declare function delegatedPromptText(content: unknown): string | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* 从转录里挑出委派 prompt = **最后一条非 tool_result 的 user 消息**(倒扫)。
|
|
14
|
+
* 🔴 倒扫不是随手选的:fork 型子代的转录以**父会话历史**开头,正扫会拿到宿主自己的首轮提示
|
|
15
|
+
* (live pty 实测的错答案);全新子会话型只有一条 user 文本消息,倒扫结果与正扫相同。
|
|
16
|
+
*/
|
|
17
|
+
export declare function pickDelegatedPrompt(messages: readonly unknown[]): string | undefined;
|
|
18
|
+
/** 读面注入点(离线测试用):替身返回转录窗口条目数组。 */
|
|
19
|
+
type TranscriptWindowReader = (transcriptId: string) => Promise<readonly unknown[] | null>;
|
|
20
|
+
export declare function __setTranscriptWindowReaderForTests(r: TranscriptWindowReader | null): void;
|
|
21
|
+
/** `fetchDelegatedPrompt` 的结果 —— 三态,**端必须分得开**(见下面 outcome 的键注)。 */
|
|
22
|
+
export interface DelegatedPromptFetch {
|
|
23
|
+
/** 挑到的委派 prompt;`undefined` = 没拿到(看 `outcome` 分清是哪种没拿到)。 */
|
|
24
|
+
prompt?: string;
|
|
25
|
+
/**
|
|
26
|
+
* · `'hit'` —— 拿到了,端可以落状态。
|
|
27
|
+
* · `'skipped'` —— 无转录锚 / 已有在飞 ⇒ 端什么都别做(不是失败)。
|
|
28
|
+
* · `'unreadable'` —— 压根没读到(无 wire / 构造失败 / throw)⇒ **记号已撤**,下次可重试。
|
|
29
|
+
* · `'absent'` —— 读成功但转录里挑不出委派 user 消息 ⇒ **定论**,记号保留,不再重复打读面。
|
|
30
|
+
*
|
|
31
|
+
* 🔴 `unreadable` 与 `absent` 分开是复审穷举第 5 条:混成一个会把「一次性取件」变成
|
|
32
|
+
* 「每次进查看态都打一发 `GET /v1/sessions/:id`」。
|
|
33
|
+
*/
|
|
34
|
+
outcome: 'hit' | 'skipped' | 'unreadable' | 'absent';
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 取一个后台子代行的**委派 prompt**(判定半场:防抖 + 取锚 + 读转录 + 挑正文)。
|
|
38
|
+
* 幂等、fail-soft、绝不 throw。**不碰任何端状态** —— 落状态是端的事(见文件头拆缝说明)。
|
|
39
|
+
*
|
|
40
|
+
* 端侧用法(壳 `hydrateEngineAgentPrompt` shim):先自己判「这行是不是 local_agent、prompt 是不是
|
|
41
|
+
* 已经有了」,是才调本函数;拿到 `outcome==='hit'` 再 `setState`。
|
|
42
|
+
*/
|
|
43
|
+
export declare function fetchDelegatedPrompt(taskId: string): Promise<DelegatedPromptFetch>;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B6 批搬迁(2026-07-27,设计稿 §3 B6 + §2.5 拆缝表):cli `src/sema/engineDelegatedPrompt.ts` 的
|
|
3
|
+
* **判定半场** —— 转录锚台账(module 级)+ 委派 prompt 挑选规则 + 转录窗口读面 + 在飞防抖。
|
|
4
|
+
*
|
|
5
|
+
* 留在 TUI(**执行半场**):`hydrateEngineAgentPrompt` 里对 `AppState.tasks[taskId]` 的
|
|
6
|
+
* `isLocalAgentTask` 判别 / `store.setState` 落 `prompt` / 查看态转录补插首条 user 消息 ——
|
|
7
|
+
* 那三件读写的是**端的任务状态形状**(`LocalAgentTaskState`),web/桌面各有各的形。
|
|
8
|
+
* 壳侧 shim 保留原函数名与签名,内部改成「调本包 `fetchDelegatedPrompt()` → 拿到串再 setState」。
|
|
9
|
+
*
|
|
10
|
+
* 🔴 **单实例纪律(这条是本文件搬迁的真正理由,不是顺手)**:`transcriptIdByTask` 是 module 级
|
|
11
|
+
* Map,**写者 = `fleetLedger` 的行帧/通知帧消费臂**(`recordEngineTranscriptId`),
|
|
12
|
+
* **读者 = 取件腿**。fleetLedger 已在本包内 ⇒ 台账若留在壳里就是「包写壳读」两份实例,
|
|
13
|
+
* 取件腿恒读空、Prompt 段恒留白,而两边代码看着都对
|
|
14
|
+
* ([paired-mechanisms-must-share-premise] 同族失败形)。所以台账必须与写者同包。
|
|
15
|
+
*
|
|
16
|
+
* ── 以下为原文件的领域说明(逐字保留,它记着两条踩过的坑)────────────────────────────────────
|
|
17
|
+
*
|
|
18
|
+
* engineDelegatedPrompt — 后台子代**委派 prompt** 的取件腿(server 1.278.0 `transcriptId` 提货)。
|
|
19
|
+
*
|
|
20
|
+
* ── 缺陷形
|
|
21
|
+
* 后台子代行是行帧 lane(fleet stream)投影出来的,行帧上**没有** prompt —— 于是
|
|
22
|
+
* `AsyncAgentDetailDialog` 的 `Prompt` 段恒空(row.prompt = '')。前台子代的 prompt 由
|
|
23
|
+
* `task_progress` tick 的绑定臂晚绑,后台子代整跑零 tick(tick lane 根本没通道),所以那条腿
|
|
24
|
+
* 对它无效。
|
|
25
|
+
*
|
|
26
|
+
* ── 修法(不发明:server 1.278.0 在 fleet 行上加了 `transcriptId`)
|
|
27
|
+
* `transcriptId` = core 铸的**子代 childSessionId**(core subagent.js:`transcriptId: child.sessionId`)。
|
|
28
|
+
* 它是**取件锚不是内容** —— 端凭它去 session 读面(`GET /v1/sessions/:id`,SDK
|
|
29
|
+
* `sessions.get`)取转录,从里面挑出委派 prompt。
|
|
30
|
+
*
|
|
31
|
+
* 🔴 取哪一条 —— **最后一条非 tool_result 的 user 消息**,不是第一条(live 一手取证订正):
|
|
32
|
+
* `fork` 型子代的 `child.sessionId` 是**父会话的 fork**(core subagent.js:1749
|
|
33
|
+
* `forkTranscriptId = child.sessionId ?? forkedId`),转录里先是**父会话的全部历史**,委派
|
|
34
|
+
* prompt 追加在 fork 点。所以「第一条 user 消息」拿到的是**宿主自己的首轮提示**——第一版就是
|
|
35
|
+
* 这么写的,live pty 实测拉回 6869 字符(委派 prompt 只有 ~330),典型的「看起来很合理的错答案」。
|
|
36
|
+
* 反过来,全新子会话型子代的转录里委派 prompt 是唯一一条 user 文本消息 —— 两种形状都满足
|
|
37
|
+
* 「最后一条非 tool_result 的 user 消息」,所以倒扫是同时正确的那一条规则。
|
|
38
|
+
* (`tool_result` 型 user 条目 = 子代自己跑工具的回填,必须跳过。)
|
|
39
|
+
* 窗口取 `tail=N`(转录尾部)+ `blobCap`:委派 prompt 在尾部方向,而 `messages` 整段可以非常大。
|
|
40
|
+
* 诚实边界:子代被 steer 过(steerSubagent 注入 user 消息)时倒扫会拿到最新那条指令 —— 已记账,
|
|
41
|
+
* wire 上没有「哪条是委派」的标,端不猜。
|
|
42
|
+
*
|
|
43
|
+
* 诚实边界:拉不到 / 没有 transcriptId / 读面 501/404 ⇒ **什么都不写**,Prompt 段维持留白。
|
|
44
|
+
* 绝不用「子代的首条输出」或行描述冒充委派 prompt(那是把一个看起来合理的错答案摆上屏)。
|
|
45
|
+
* 一切失败 fail-soft,绝不 throw。
|
|
46
|
+
*/
|
|
47
|
+
import { hostLog } from '../host.js';
|
|
48
|
+
import { makeEngineWireClient } from '../engineWireSdk.js';
|
|
49
|
+
import { engineWireTarget } from '../engineWireTarget.js';
|
|
50
|
+
/** 只拉转录**尾部**这么多条(委派 prompt 在尾部方向;见文件头的 fork 形状订正)。 */
|
|
51
|
+
const TAIL_WINDOW = 200;
|
|
52
|
+
/** 单条正文的上限:详情页自己还会截到 300 字符,这里只防拉回一个巨型 blob。 */
|
|
53
|
+
const BLOB_CAP = 8 * 1024;
|
|
54
|
+
// ── 转录锚台账(module-level:footer/REPL 谁重挂都不丢;写者=行帧消费臂)───────────────────────
|
|
55
|
+
const transcriptIdByTask = new Map();
|
|
56
|
+
/** 记一个子代行的转录锚(幂等;server <1.278 永不调用 = 台账恒空,取件腿整条静默不动)。 */
|
|
57
|
+
export function recordEngineTranscriptId(taskId, transcriptId) {
|
|
58
|
+
if (!taskId || !transcriptId)
|
|
59
|
+
return;
|
|
60
|
+
transcriptIdByTask.set(taskId, transcriptId);
|
|
61
|
+
}
|
|
62
|
+
/** 读面(取件腿 + 测试)。 */
|
|
63
|
+
export function getEngineTranscriptId(taskId) {
|
|
64
|
+
return transcriptIdByTask.get(taskId);
|
|
65
|
+
}
|
|
66
|
+
/** 测试钩:清台账 + 在飞记号。 */
|
|
67
|
+
export function __resetEngineDelegatedPromptForTests() {
|
|
68
|
+
transcriptIdByTask.clear();
|
|
69
|
+
promptHydrateInFlight.clear();
|
|
70
|
+
}
|
|
71
|
+
/** 每 task 一次在飞防抖;拉不到即撤记号,下次进入查看态可重试(副本重启/瞬态 404)。 */
|
|
72
|
+
const promptHydrateInFlight = new Set();
|
|
73
|
+
/** 一条转录条目的 role(permissive:`role` 或 CC jsonl 形的 `type`)。 */
|
|
74
|
+
function roleOf(entry) {
|
|
75
|
+
if (typeof entry !== 'object' || entry === null)
|
|
76
|
+
return undefined;
|
|
77
|
+
const e = entry;
|
|
78
|
+
if (typeof e.role === 'string')
|
|
79
|
+
return e.role;
|
|
80
|
+
if (typeof e.type === 'string')
|
|
81
|
+
return e.type;
|
|
82
|
+
// CC jsonl 形:{type:'user', message:{role:'user', content}}
|
|
83
|
+
if (typeof e.message === 'object' && e.message !== null) {
|
|
84
|
+
const m = e.message;
|
|
85
|
+
if (typeof m.role === 'string')
|
|
86
|
+
return m.role;
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
/** 一条转录条目的正文 content(permissive:直挂 或 嵌在 `message` 里)。 */
|
|
91
|
+
function contentOf(entry) {
|
|
92
|
+
if (typeof entry !== 'object' || entry === null)
|
|
93
|
+
return undefined;
|
|
94
|
+
const e = entry;
|
|
95
|
+
if (e.content !== undefined)
|
|
96
|
+
return e.content;
|
|
97
|
+
if (typeof e.message === 'object' && e.message !== null) {
|
|
98
|
+
return e.message.content;
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* content → 纯文本。tool_result 型正文返回 undefined —— 那是子代自己跑工具的回填,**不是**委派
|
|
104
|
+
* prompt(这条判别就是「别把子代自己的消息当委派 prompt」的落点)。
|
|
105
|
+
*/
|
|
106
|
+
export function delegatedPromptText(content) {
|
|
107
|
+
if (typeof content === 'string')
|
|
108
|
+
return content.trim() || undefined;
|
|
109
|
+
if (!Array.isArray(content))
|
|
110
|
+
return undefined;
|
|
111
|
+
const parts = [];
|
|
112
|
+
for (const block of content) {
|
|
113
|
+
if (typeof block === 'string') {
|
|
114
|
+
parts.push(block);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (typeof block !== 'object' || block === null)
|
|
118
|
+
continue;
|
|
119
|
+
const b = block;
|
|
120
|
+
if (b.type === 'tool_result')
|
|
121
|
+
return undefined; // 子代自己的工具回填,不是委派 prompt
|
|
122
|
+
if (b.type === 'text' && typeof b.text === 'string')
|
|
123
|
+
parts.push(b.text);
|
|
124
|
+
}
|
|
125
|
+
const text = parts.join('\n').trim();
|
|
126
|
+
return text || undefined;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* 从转录里挑出委派 prompt = **最后一条非 tool_result 的 user 消息**(倒扫)。
|
|
130
|
+
* 🔴 倒扫不是随手选的:fork 型子代的转录以**父会话历史**开头,正扫会拿到宿主自己的首轮提示
|
|
131
|
+
* (live pty 实测的错答案);全新子会话型只有一条 user 文本消息,倒扫结果与正扫相同。
|
|
132
|
+
*/
|
|
133
|
+
export function pickDelegatedPrompt(messages) {
|
|
134
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
135
|
+
const entry = messages[i];
|
|
136
|
+
if (roleOf(entry) !== 'user')
|
|
137
|
+
continue;
|
|
138
|
+
const text = delegatedPromptText(contentOf(entry));
|
|
139
|
+
if (text)
|
|
140
|
+
return text;
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
let transcriptWindowReader = null;
|
|
145
|
+
export function __setTranscriptWindowReaderForTests(r) {
|
|
146
|
+
transcriptWindowReader = r;
|
|
147
|
+
}
|
|
148
|
+
async function readTranscriptWindow(transcriptId) {
|
|
149
|
+
if (transcriptWindowReader)
|
|
150
|
+
return transcriptWindowReader(transcriptId);
|
|
151
|
+
// 🔴 B6:六份手抄 `wireConfig()` 收成 `engineWireTarget()`(env 读经 hostEnv,本包零 process)。
|
|
152
|
+
const cfg = engineWireTarget();
|
|
153
|
+
if (!cfg)
|
|
154
|
+
return null;
|
|
155
|
+
const client = makeEngineWireClient({
|
|
156
|
+
baseUrl: cfg.baseUrl,
|
|
157
|
+
...(cfg.token ? { token: cfg.token } : {}),
|
|
158
|
+
principal: cfg.principal,
|
|
159
|
+
});
|
|
160
|
+
if (!client)
|
|
161
|
+
return null;
|
|
162
|
+
// 窗口读:`tail=N` = 转录**尾部** N 条(server ≥1.162)。老引擎忽略 query 返回全量 ——
|
|
163
|
+
// 倒扫规则对全量同样正确,只是多传了点字节(行为等价)。
|
|
164
|
+
const rec = await client.sessions.get(transcriptId, {
|
|
165
|
+
tail: TAIL_WINDOW,
|
|
166
|
+
blobCap: BLOB_CAP,
|
|
167
|
+
});
|
|
168
|
+
const msgs = rec.messages;
|
|
169
|
+
return Array.isArray(msgs) ? msgs : null;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* 取一个后台子代行的**委派 prompt**(判定半场:防抖 + 取锚 + 读转录 + 挑正文)。
|
|
173
|
+
* 幂等、fail-soft、绝不 throw。**不碰任何端状态** —— 落状态是端的事(见文件头拆缝说明)。
|
|
174
|
+
*
|
|
175
|
+
* 端侧用法(壳 `hydrateEngineAgentPrompt` shim):先自己判「这行是不是 local_agent、prompt 是不是
|
|
176
|
+
* 已经有了」,是才调本函数;拿到 `outcome==='hit'` 再 `setState`。
|
|
177
|
+
*/
|
|
178
|
+
export async function fetchDelegatedPrompt(taskId) {
|
|
179
|
+
if (promptHydrateInFlight.has(taskId))
|
|
180
|
+
return { outcome: 'skipped' };
|
|
181
|
+
const transcriptId = transcriptIdByTask.get(taskId);
|
|
182
|
+
if (!transcriptId)
|
|
183
|
+
return { outcome: 'skipped' };
|
|
184
|
+
promptHydrateInFlight.add(taskId);
|
|
185
|
+
let msgs = null;
|
|
186
|
+
try {
|
|
187
|
+
msgs = await readTranscriptWindow(transcriptId);
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
promptHydrateInFlight.delete(taskId);
|
|
191
|
+
hostLog('debug', `[delegated-prompt] ${taskId} transcript ${transcriptId} read failed: ${String(e).slice(0, 160)}`);
|
|
192
|
+
return { outcome: 'unreadable' };
|
|
193
|
+
}
|
|
194
|
+
const prompt = msgs ? pickDelegatedPrompt(msgs) : undefined;
|
|
195
|
+
if (!prompt) {
|
|
196
|
+
if (msgs === null)
|
|
197
|
+
promptHydrateInFlight.delete(taskId);
|
|
198
|
+
hostLog('debug', `[delegated-prompt] ${taskId} transcript ${transcriptId}: ${msgs === null ? 'transcript unreadable (retryable)' : 'no delegated user message (final)'}`);
|
|
199
|
+
return { outcome: msgs === null ? 'unreadable' : 'absent' };
|
|
200
|
+
}
|
|
201
|
+
// 诊断锚(SEMA_DEBUG 才落盘):字符数 + 头 60 字符 —— 「拿到的到底是委派 prompt 还是
|
|
202
|
+
// fork 转录里宿主自己的首轮提示」只有看得见正文头才分得开(第一版倒在这上面)。
|
|
203
|
+
hostLog('debug', `[delegated-prompt] ${taskId} <- transcript ${transcriptId} (${prompt.length} chars) head=${JSON.stringify(prompt.slice(0, 60))}`);
|
|
204
|
+
return { prompt, outcome: 'hit' };
|
|
205
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** 引擎 bridge 从 `task_progress` tick / fleet 行帧建的行(module-level:footer 重挂不丢归属)。 */
|
|
2
|
+
export declare const engineOwnedRowIds: Set<string>;
|
|
3
|
+
/** fleet 树 enter 时为「面板无行的 fleet 行」stub 出来的纯查看载体行。 */
|
|
4
|
+
export declare const fleetViewStubRowIds: Set<string>;
|
|
5
|
+
/** 这一行的真身在引擎里(本地 abortController 管不到它)。 */
|
|
6
|
+
export declare function isEngineViewRow(taskId: string): boolean;
|
|
7
|
+
/** 能力门同步读口(liveClient 构造期已 kick /v1/capabilities 进 engineCapsCache)。 */
|
|
8
|
+
export declare function engineTaskHandlesCapable(): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* 停这一行**必须先拿到引擎回执**吗?
|
|
11
|
+
*
|
|
12
|
+
* true ⇒ 行的执行者是引擎,端写 `killed` 是一句它没证实过的断言 —— 必须 `POST …/stop` 拿到 200
|
|
13
|
+
* 才翻;409 `stop.not_landed` / `stop.not_local` / `stop.parked` 一律保持原状(引擎里
|
|
14
|
+
* 可能还在跑),下一拍由行帧/`stopped-by` 归因 settle(黑板 [1754]③)。
|
|
15
|
+
* false ⇒ 行跑在本进程(CC 逐字形态),`abortController.abort()` 就是执行本身,端写 `killed`
|
|
16
|
+
* 按构造恒真 ⇒ 同步翻,与 CC 2.1.220 `UPd`(pretty220.js:507299)一字不差。
|
|
17
|
+
*/
|
|
18
|
+
export declare function engineRowNeedsStopConfirm(taskId: string): boolean;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B6 批搬迁(2026-07-27,设计稿 §3 B6):cli `src/sema/engineRowStopGate.ts` 整搬。
|
|
3
|
+
* 唯一差分 = `process.env.SEMA_LIVE_BASEURL` → `engineWireTarget()?.baseUrl`
|
|
4
|
+
* (本包零 `process`;Node 宿主下 `hostEnv()` 返回的就是 `process.env` 同一对象 ⇒ 取值相同。
|
|
5
|
+
* **超集**:非 Node 宿主可用 `installEngineWireTarget()` 显式装,此前那条腿恒 false)。
|
|
6
|
+
*
|
|
7
|
+
* 🔴 单实例纪律(本文件两个 module 级 Set 都是判据真源):
|
|
8
|
+
* `engineOwnedRowIds` 写者 = 引擎 bridge 的 `task_progress` tick / fleet 行帧臂;
|
|
9
|
+
* `fleetViewStubRowIds` 写者 = fleet 树 enter 的 stub 造行处;
|
|
10
|
+
* 读者 = 批量停/单行停的同步分诊。两份实例 ⇒ 分诊恒判「纯本地行」⇒ 引擎侧行被壳直接写
|
|
11
|
+
* `killed` 而引擎里还在跑 = [1754]③ 那类「壳说了一句它没证实过的话」。
|
|
12
|
+
*
|
|
13
|
+
* ── 以下为原文件的领域说明(逐字保留)────────────────────────────────────────────────────────
|
|
14
|
+
*
|
|
15
|
+
* engineRowStopGate — 「这一行是引擎侧的吗 / 停它要不要等引擎回执」的**同步**判别叶模块(#98)。
|
|
16
|
+
*
|
|
17
|
+
* 为什么单独一个叶模块:批量停(`ctrl+x ctrl+k` → `killAllRunningAgentTasks`)必须在**同步**
|
|
18
|
+
* 那一拍就把行分成两半 —— 纯本地行照 CC 逐字立刻翻 `killed`(CC 的 markNotified→kill 顺序不容
|
|
19
|
+
* 插入一个 await,否则 runner 自己的完成通知会抢在 markNotified 前面发出去),引擎侧行才走
|
|
20
|
+
* 「等 200 再翻」。而原先这两个判据分别住在 `engineAgentView`(拉 React/ink/transcript 全图)和
|
|
21
|
+
* `engineTaskHandleWire`(拉 SDK),`LocalAgentTask` 静态 import 任一都是循环依赖风险 —— 所以此前
|
|
22
|
+
* 只能 `await import(...)`,分类天然异步。把判据本身下沉成零依赖叶(只依赖同样零依赖的
|
|
23
|
+
* `engineCapsCache`),两边改成 re-export,判据单一真源不变,调用方拿到同步口。
|
|
24
|
+
*
|
|
25
|
+
* 🔴 语义与单行 `x`(BackgroundTasksDialog.killEngineAwareAgentTask)必须逐字一致:
|
|
26
|
+
* 引擎视图行 ∧ `capabilities.taskHandles` 在场 → 发真停止动词、**只有 200 才翻行**;
|
|
27
|
+
* 缺任一(老引擎 / 无 wire / 纯本地 AgentTool 行) → 回落旧行为(本地 abort 即真停,立即翻)。
|
|
28
|
+
*/
|
|
29
|
+
import { engineCapTrue } from '../engineCapsCache.js';
|
|
30
|
+
import { engineWireTarget } from '../engineWireTarget.js';
|
|
31
|
+
/** 引擎 bridge 从 `task_progress` tick / fleet 行帧建的行(module-level:footer 重挂不丢归属)。 */
|
|
32
|
+
export const engineOwnedRowIds = new Set();
|
|
33
|
+
/** fleet 树 enter 时为「面板无行的 fleet 行」stub 出来的纯查看载体行。 */
|
|
34
|
+
export const fleetViewStubRowIds = new Set();
|
|
35
|
+
/** 这一行的真身在引擎里(本地 abortController 管不到它)。 */
|
|
36
|
+
export function isEngineViewRow(taskId) {
|
|
37
|
+
return engineOwnedRowIds.has(taskId) || fleetViewStubRowIds.has(taskId);
|
|
38
|
+
}
|
|
39
|
+
/** 能力门同步读口(liveClient 构造期已 kick /v1/capabilities 进 engineCapsCache)。 */
|
|
40
|
+
export function engineTaskHandlesCapable() {
|
|
41
|
+
return engineCapTrue(engineWireTarget()?.baseUrl, 'taskHandles');
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 停这一行**必须先拿到引擎回执**吗?
|
|
45
|
+
*
|
|
46
|
+
* true ⇒ 行的执行者是引擎,端写 `killed` 是一句它没证实过的断言 —— 必须 `POST …/stop` 拿到 200
|
|
47
|
+
* 才翻;409 `stop.not_landed` / `stop.not_local` / `stop.parked` 一律保持原状(引擎里
|
|
48
|
+
* 可能还在跑),下一拍由行帧/`stopped-by` 归因 settle(黑板 [1754]③)。
|
|
49
|
+
* false ⇒ 行跑在本进程(CC 逐字形态),`abortController.abort()` 就是执行本身,端写 `killed`
|
|
50
|
+
* 按构造恒真 ⇒ 同步翻,与 CC 2.1.220 `UPd`(pretty220.js:507299)一字不差。
|
|
51
|
+
*/
|
|
52
|
+
export function engineRowNeedsStopConfirm(taskId) {
|
|
53
|
+
return isEngineViewRow(taskId) && engineTaskHandlesCapable();
|
|
54
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** 测试钩:清 caps 缓存。 */
|
|
2
|
+
export declare function __resetSubagentOutputCapCacheForTests(): void;
|
|
3
|
+
export interface EngineSubagentReport {
|
|
4
|
+
/** 子代最终 assistant 正文(server 对 running 态给状态文本——由 status 区分)。 */
|
|
5
|
+
content: string;
|
|
6
|
+
/** output.status 原样(completed/failed/killed/running…;undefined = wire 未带)。 */
|
|
7
|
+
status?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 拉取一个后台子代的终报正文。null = 读不到(无 wire / caps 缺 / 404 域外或副本重启 / 网错)——
|
|
11
|
+
* 调用方回落 bg 事实渲染。绝不 throw。
|
|
12
|
+
*/
|
|
13
|
+
export declare function fetchEngineSubagentReport(taskId: string, opts?: {
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
}): Promise<EngineSubagentReport | null>;
|