@sema-agent/client-core 0.3.0 → 0.5.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 +57 -4
- package/dist/adapt.d.ts +30 -3
- package/dist/adapt.js +490 -354
- package/dist/adapter/downstream/eventToSdkMessage.d.ts +81 -0
- package/dist/adapter/downstream/eventToSdkMessage.js +330 -0
- package/dist/adapter/downstream/terminalToSdkResult.d.ts +71 -0
- package/dist/adapter/downstream/terminalToSdkResult.js +370 -0
- package/dist/adapter/downstream/turnUsageToModelUsage.d.ts +26 -0
- package/dist/adapter/downstream/turnUsageToModelUsage.js +12 -0
- package/dist/adapter/runStream.d.ts +38 -0
- package/dist/adapter/runStream.js +177 -0
- package/dist/adapter/types.d.ts +73 -0
- package/dist/adapter/types.js +23 -0
- package/dist/agentsWireCaps.d.ts +112 -0
- package/dist/agentsWireCaps.js +319 -0
- package/dist/attachmentsWireCaps.d.ts +47 -0
- package/dist/attachmentsWireCaps.js +46 -0
- package/dist/classifierVerdictWire.d.ts +45 -0
- package/dist/classifierVerdictWire.js +76 -0
- package/dist/clientContextWireCaps.d.ts +46 -0
- package/dist/clientContextWireCaps.js +48 -0
- package/dist/cloudConfigWireCaps.d.ts +110 -0
- package/dist/cloudConfigWireCaps.js +228 -0
- package/dist/controlRouter.d.ts +191 -0
- package/dist/controlRouter.js +244 -0
- package/dist/effortWire.d.ts +34 -0
- package/dist/effortWire.js +38 -0
- package/dist/engineWireSdk.d.ts +49 -0
- package/dist/engineWireSdk.js +61 -0
- package/dist/forkWireCaps.d.ts +25 -0
- package/dist/forkWireCaps.js +42 -0
- package/dist/hostEnv.d.ts +16 -0
- package/dist/hostEnv.js +17 -0
- package/dist/imagesWireCaps.d.ts +28 -0
- package/dist/imagesWireCaps.js +49 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +62 -0
- package/dist/liveModelCatalog.d.ts +62 -0
- package/dist/liveModelCatalog.js +79 -0
- package/dist/mcpWireCaps.d.ts +41 -0
- package/dist/mcpWireCaps.js +51 -0
- package/dist/modelBudgetRule.d.ts +44 -0
- package/dist/modelBudgetRule.js +73 -0
- package/dist/modelWireCaps.d.ts +17 -0
- package/dist/modelWireCaps.js +16 -0
- package/dist/notifications.d.ts +150 -4
- package/dist/notifications.js +472 -0
- package/dist/permissionWireCaps.d.ts +37 -0
- package/dist/permissionWireCaps.js +46 -0
- package/dist/promptProfileWireCaps.d.ts +17 -0
- package/dist/promptProfileWireCaps.js +20 -0
- package/dist/retainBackgroundWireCaps.d.ts +50 -0
- package/dist/retainBackgroundWireCaps.js +58 -0
- package/dist/rewindWireCaps.d.ts +36 -0
- package/dist/rewindWireCaps.js +41 -0
- package/dist/seam.d.ts +97 -2
- package/dist/selfOrchestrationWireCaps.d.ts +40 -0
- package/dist/selfOrchestrationWireCaps.js +48 -0
- package/dist/sessionModelLatch.d.ts +35 -0
- package/dist/sessionModelLatch.js +63 -0
- package/dist/skillsWireCaps.d.ts +47 -0
- package/dist/skillsWireCaps.js +41 -0
- package/dist/sseIdleTriage.d.ts +94 -0
- package/dist/sseIdleTriage.js +142 -0
- package/dist/ultracodeWireCaps.d.ts +90 -0
- package/dist/ultracodeWireCaps.js +159 -0
- package/dist/webSearchWireCaps.d.ts +54 -0
- package/dist/webSearchWireCaps.js +76 -0
- package/package.json +2 -2
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { stamp } from '../types.js';
|
|
2
|
+
/** Flatten TaskStats → the CC NonNullableUsage placeholder (z.unknown()). */
|
|
3
|
+
function flattenUsage(stats) {
|
|
4
|
+
return {
|
|
5
|
+
inputTokens: stats?.promptTokens ?? 0,
|
|
6
|
+
outputTokens: stats?.outputTokens ?? 0,
|
|
7
|
+
cacheReadInputTokens: stats?.cachedTokens ?? 0,
|
|
8
|
+
cacheCreationInputTokens: 0,
|
|
9
|
+
webSearchRequests: 0,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* MF-25 — map the wire's per-model usage split (`stats.modelUsage`, an open `{ <modelId>: usage }` map;
|
|
14
|
+
* e.g. `{ "deepseek-v4-pro": {...} }`) into the CC `SDKResult.modelUsage` record (model-id → CC ModelUsage).
|
|
15
|
+
* The wire usage names (`cacheReadTokens`/`cacheWriteTokens`/`costMicroUsd`) are re-pinned to the CC names
|
|
16
|
+
* (`cacheReadInputTokens`/`cacheCreationInputTokens`/`costUSD`, costMicroUsd/1e6 → USD), matching
|
|
17
|
+
* turnUsageToModelUsage. `contextWindow`/`maxOutputTokens` are not on the wire → 0. Returns `{}` when absent.
|
|
18
|
+
*/
|
|
19
|
+
/** P1-5 — real elapsed ms since the stream opened (runStream stamps startedAtMs); 0 only pre-stamp. */
|
|
20
|
+
function elapsedMs(ctx) {
|
|
21
|
+
return typeof ctx.startedAtMs === 'number' ? Math.max(0, Date.now() - ctx.startedAtMs) : 0;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* P1-5 — `total_cost_usd` truthfulness: `costUsd === 0` is AMBIGUOUS (free vs. no MODEL_COST_* configured
|
|
25
|
+
* on the worker — the local TOC engine ships unconfigured, so `-p --output-format json` was reporting a
|
|
26
|
+
* fake `0`). Emit the number only when it is a REAL positive cost; otherwise `null` (JSON consumers see
|
|
27
|
+
* "unknown", never a fabricated zero). CC's schema field is numeric — number-guarded readers
|
|
28
|
+
* (replEntry cost fold) already treat non-number as absent.
|
|
29
|
+
*/
|
|
30
|
+
function costOrNull(costUsd) {
|
|
31
|
+
return typeof costUsd === 'number' && costUsd > 0 ? costUsd : null;
|
|
32
|
+
}
|
|
33
|
+
// 件2a(中断事故修复批 G,2026-07-15,症状2 第一环):参数放宽 TaskStats | undefined + 全链守卫。
|
|
34
|
+
// 409 active-run 拒绝时引擎的 done 帧本体是 {status:'failed', errorMessage, activeTaskId} —— NO stats
|
|
35
|
+
// (ai-agent-service server.ts:1526 同型),此前 :59 裸读 (stats).modelUsage 直接 TypeError,炸穿
|
|
36
|
+
// runStream → seamQuery catch → 误诊 "engine is unreachable"(件2c)。缺 stats 时产出合法空 usage
|
|
37
|
+
// 形状(flattenUsage 本就守卫;这里返回 {},调用方 :98 再按 r.model 合成当前行)。
|
|
38
|
+
function mapModelUsage(stats) {
|
|
39
|
+
const raw = stats?.modelUsage;
|
|
40
|
+
if (!raw || typeof raw !== 'object')
|
|
41
|
+
return {};
|
|
42
|
+
const out = {};
|
|
43
|
+
for (const [modelId, u] of Object.entries(raw)) {
|
|
44
|
+
if (!u || typeof u !== 'object')
|
|
45
|
+
continue;
|
|
46
|
+
const m = u;
|
|
47
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
|
|
48
|
+
out[modelId] = {
|
|
49
|
+
inputTokens: num(m.inputTokens),
|
|
50
|
+
outputTokens: num(m.outputTokens),
|
|
51
|
+
cacheReadInputTokens: num(m.cacheReadTokens),
|
|
52
|
+
cacheCreationInputTokens: num(m.cacheWriteTokens),
|
|
53
|
+
webSearchRequests: 0,
|
|
54
|
+
costUSD: num(m.costMicroUsd) / 1e6,
|
|
55
|
+
contextWindow: 0,
|
|
56
|
+
maxOutputTokens: 0,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
/** `done` → SDKResultSuccess (contract 02 §2.10 / 08 CS-10). */
|
|
62
|
+
export function doneToSdkResult(ev, ctx) {
|
|
63
|
+
const r = ev.result;
|
|
64
|
+
// 件2a:409/failed 形态的 done 帧不带 stats —— 本函数全链按 undefined 守卫(:97/:121/:145)。
|
|
65
|
+
const stats = r.stats;
|
|
66
|
+
// P1-5 — the terminal frame's session_id must be the ENGINE-AUTHORITATIVE id (done.result.sessionId):
|
|
67
|
+
// on the headless `-p` path ctx was seeded from the REQUEST (historically the mock constant
|
|
68
|
+
// "sema-mock-session-0001" leaked to stdout, so `sema -r <session_id>` on the JSON output could never
|
|
69
|
+
// resume). The engine echo is the id its session store actually keyed — adopt it for the result stamp.
|
|
70
|
+
const echoedSid = r.sessionId;
|
|
71
|
+
if (typeof echoedSid === 'string' && echoedSid.length > 0)
|
|
72
|
+
ctx.sessionId = echoedSid;
|
|
73
|
+
// P1-5 — modelUsage: prefer the wire's per-model split; when absent (local engine pre-MF-25 stats or a
|
|
74
|
+
// split-less done), synthesize the CURRENT model's row from the flat TaskStats so JSON consumers always
|
|
75
|
+
// see which model produced the run's usage.
|
|
76
|
+
const usage = flattenUsage(stats);
|
|
77
|
+
let modelUsage = mapModelUsage(stats);
|
|
78
|
+
if (Object.keys(modelUsage).length === 0 && typeof r.model === 'string' && r.model.length > 0) {
|
|
79
|
+
modelUsage = {
|
|
80
|
+
[r.model]: {
|
|
81
|
+
...usage,
|
|
82
|
+
costUSD: typeof stats?.costUsd === 'number' ? stats.costUsd : 0,
|
|
83
|
+
contextWindow: 0,
|
|
84
|
+
maxOutputTokens: 0,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// clay P0 壳半场(黑板 [450],2026-07-05):上游 4xx(如无 vision 模型收到 image)时
|
|
89
|
+
// 引擎的 done 帧本体带 result.status='failed' + errorMessage —— 传播链没吞,断点在这:
|
|
90
|
+
// 此前无条件投影 subtype:'success'/is_error:false,把失败洗成"成功空回复"= 用户看到的
|
|
91
|
+
// 静默卡死。status failed → 走 SDKResultError 形态(errors[] 渲染线现成)。
|
|
92
|
+
// TS2367 note: `TaskResult & { status?: string }` does NOT actually widen `status` — TS
|
|
93
|
+
// intersects the pre-existing required `RunStatus` with the added optional `string`, and
|
|
94
|
+
// `RunStatus & string` stays `RunStatus` (the narrower member wins). That's exactly why the
|
|
95
|
+
// `rr.status === 'timeout'` / `'blocked'` branches below (real @sema-agent/sdk RunStatus is
|
|
96
|
+
// still a 5-word closed union — core's actual TaskStatus vocab is 6 words, see file header)
|
|
97
|
+
// used to type-error as "no overlap": the cast looked like a widening but wasn't one. `Omit`
|
|
98
|
+
// the narrow key first so the added wider `status?: string` actually applies.
|
|
99
|
+
const rr = r;
|
|
100
|
+
const durationMs = elapsedMs(ctx);
|
|
101
|
+
// [909]B1 — errorCode 透传(additive seam 字段;CC schema 无此键,与 success 臂 `model` 同款超集
|
|
102
|
+
// 姿势):引擎值原样,绝不铸造。所有 error 臂共用。
|
|
103
|
+
const errorCodeField = typeof rr.errorCode === 'string' && rr.errorCode.length > 0 ? { errorCode: rr.errorCode } : {};
|
|
104
|
+
if (rr.status === 'failed') {
|
|
105
|
+
// [909]B1 — failed 臂 subtype 语义化(见文件头对表):budget.* → error_max_budget_usd(与
|
|
106
|
+
// failedToSdkResult 事件臂同款分流,此前 done{failed} 恒 error_during_execution);
|
|
107
|
+
// limit.max_turns → error_max_turns;output.invalid → error_max_structured_output_retries
|
|
108
|
+
// (assemble-result.js:58 该 code 的语义字面就是 CC 这词:「did not produce a valid structured
|
|
109
|
+
// output within the retry limit」);其余(cancelled/conflict/provider 错…)维持
|
|
110
|
+
// error_during_execution。
|
|
111
|
+
const failedSubtype = rr.errorCode === 'budget.exceeded' || rr.errorCode === 'budget.precall'
|
|
112
|
+
? 'error_max_budget_usd'
|
|
113
|
+
: rr.errorCode === 'limit.max_turns'
|
|
114
|
+
? 'error_max_turns'
|
|
115
|
+
: rr.errorCode === 'output.invalid'
|
|
116
|
+
? 'error_max_structured_output_retries'
|
|
117
|
+
: 'error_during_execution';
|
|
118
|
+
return stamp(ctx, {
|
|
119
|
+
// Explicit (always-undefined) uuid/session_id: stamp() always overwrites session_id from
|
|
120
|
+
// ctx and falls back to a fresh uuid when body.uuid is nullish, so this is a pure type-level
|
|
121
|
+
// nudge (zero runtime effect) — without at least one property name in common with stamp's
|
|
122
|
+
// all-optional constraint, TS's "weak type" detection rejects the call (TS2559/TS2345/TS2353).
|
|
123
|
+
uuid: undefined,
|
|
124
|
+
session_id: undefined,
|
|
125
|
+
type: 'result',
|
|
126
|
+
subtype: failedSubtype,
|
|
127
|
+
duration_ms: durationMs, // P1-5 real elapsed (stream-open → terminal)
|
|
128
|
+
duration_api_ms: durationMs, // engine run == the API call from the shell's view
|
|
129
|
+
is_error: true,
|
|
130
|
+
num_turns: stats?.turns ?? 0,
|
|
131
|
+
stop_reason: null,
|
|
132
|
+
total_cost_usd: costOrNull(stats?.costUsd), // P1-5: null when unknown, never a fake 0
|
|
133
|
+
usage,
|
|
134
|
+
modelUsage,
|
|
135
|
+
permission_denials: [],
|
|
136
|
+
// rewind 族错误码透传(server 1.192 [838]③ 实测:stream 腿把 resume_at.before_* /
|
|
137
|
+
// rewind_snapshot.unresolvable 骑在 done{status:"failed"} 帧的 errorCode 上,非 `failed` 帧)——
|
|
138
|
+
// 与 failedToSdkResult 的同款 append 对齐,message 后附 code 便于对账。
|
|
139
|
+
errors: [
|
|
140
|
+
typeof rr.errorCode === 'string' &&
|
|
141
|
+
(rr.errorCode.startsWith('resume_at.') || rr.errorCode.startsWith('rewind_snapshot.')) &&
|
|
142
|
+
rr.errorMessage
|
|
143
|
+
? `${rr.errorMessage} (${rr.errorCode})`
|
|
144
|
+
: (rr.errorMessage ?? 'run failed'),
|
|
145
|
+
],
|
|
146
|
+
...errorCodeField,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
// [909]B1 — timeout 终态:--deadline 配速的受控截止收尾(assemble-result.js:69/88
|
|
150
|
+
// errorCode=limit.timeout,"walltime cutoff … preserve a write-out window")。此前落 success 臂
|
|
151
|
+
// = TB 的「4 个引擎已记 timeout 的 trial 全 subtype:success」+ deadline 收尾路径缺 timeout 语义
|
|
152
|
+
// 收尾帧([909]B2 关联半场;帧完整落地由 stdout drain 半场保证)。subtype 选型见文件头对表:
|
|
153
|
+
// CC 词表无 wall-clock 词,唯一 run-limit 族词 = error_max_turns;精确语义靠 errors[] 文案 +
|
|
154
|
+
// errorCode:"limit.timeout" 透传。rc 语义顺出:print.ts gracefulShutdownSync(is_error ? 1 : 0)。
|
|
155
|
+
// TaskResult 五键最小消费批([1543] SDK-M,2026-07-23):additive seam 字段——
|
|
156
|
+
// · degraded = 模型降级链实录(from/to/reason/atTurn;多模型网关部署的关键可观测面)——
|
|
157
|
+
// success/timeout/blocked/unknown 各终态都可能带,恒 additive 附上;
|
|
158
|
+
// · structuredOutput = outputSchema 任务的结构化产出(success 臂);
|
|
159
|
+
// · salvagedOutput = degenerate/timeout 抢救出的最后模型文本(timeout 臂 salvage 回落源)。
|
|
160
|
+
const degradedField = (() => {
|
|
161
|
+
const d = r.degraded;
|
|
162
|
+
return d !== undefined && d !== null && typeof d === 'object' ? { degraded: d } : {};
|
|
163
|
+
})();
|
|
164
|
+
if (rr.status === 'timeout') {
|
|
165
|
+
const salvagedOutput = r.salvagedOutput;
|
|
166
|
+
const salvaged = typeof r.result === 'string' && r.result.trim().length > 0
|
|
167
|
+
? r.result
|
|
168
|
+
: typeof salvagedOutput === 'string' && salvagedOutput.trim().length > 0
|
|
169
|
+
? salvagedOutput
|
|
170
|
+
: undefined;
|
|
171
|
+
return stamp(ctx, {
|
|
172
|
+
uuid: undefined,
|
|
173
|
+
session_id: undefined,
|
|
174
|
+
type: 'result',
|
|
175
|
+
subtype: 'error_max_turns',
|
|
176
|
+
duration_ms: durationMs,
|
|
177
|
+
duration_api_ms: durationMs,
|
|
178
|
+
is_error: true,
|
|
179
|
+
num_turns: stats?.turns ?? 0,
|
|
180
|
+
stop_reason: null,
|
|
181
|
+
total_cost_usd: costOrNull(stats?.costUsd),
|
|
182
|
+
usage,
|
|
183
|
+
modelUsage,
|
|
184
|
+
permission_denials: [],
|
|
185
|
+
errors: [
|
|
186
|
+
`${rr.errorMessage ?? 'run hit its wall-clock deadline and was finalized by the engine'} (${rr.errorCode ?? 'limit.timeout'})`,
|
|
187
|
+
],
|
|
188
|
+
...errorCodeField,
|
|
189
|
+
// 写出窗 salvage(additive seam 字段):deadline 前引擎救回的最终文本非空时随帧带出——
|
|
190
|
+
// CC error 帧无 result 键,但静默丢弃已救回的答案对 -p 集成面是净损失(TB qe** 实证)。
|
|
191
|
+
...(salvaged !== undefined ? { result: salvaged } : {}),
|
|
192
|
+
...degradedField,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
// [909]B1 — blocked 终态:agent 自报无法推进(assemble-result.js:99,只带 blockedReason 无
|
|
196
|
+
// errorCode)。此前落 success 臂(TB db-wal-recovery__uuH5cAg 实证:引擎终态 blocked、帧却
|
|
197
|
+
// subtype:success)。≠空成功 → SDKResultError,blockedReason 如实进 errors[]。
|
|
198
|
+
if (rr.status === 'blocked') {
|
|
199
|
+
const blockedReason = r.blockedReason;
|
|
200
|
+
return stamp(ctx, {
|
|
201
|
+
uuid: undefined,
|
|
202
|
+
session_id: undefined,
|
|
203
|
+
type: 'result',
|
|
204
|
+
subtype: 'error_during_execution',
|
|
205
|
+
duration_ms: durationMs,
|
|
206
|
+
duration_api_ms: durationMs,
|
|
207
|
+
is_error: true,
|
|
208
|
+
num_turns: stats?.turns ?? 0,
|
|
209
|
+
stop_reason: null,
|
|
210
|
+
total_cost_usd: costOrNull(stats?.costUsd),
|
|
211
|
+
usage,
|
|
212
|
+
modelUsage,
|
|
213
|
+
permission_denials: [],
|
|
214
|
+
errors: [
|
|
215
|
+
typeof blockedReason === 'string' && blockedReason.length > 0
|
|
216
|
+
? `Run blocked: ${blockedReason}`
|
|
217
|
+
: 'Run blocked (the agent reported it cannot proceed)',
|
|
218
|
+
],
|
|
219
|
+
...errorCodeField,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
// [884]A1(TB 公平 P0,2026-07-16):suspended/needs_review park 终帧不许洗成空成功。
|
|
223
|
+
// `-p` 恒 DURABLE_APPROVAL=true 起引擎;模型调 AskUserQuestion(或 plan-mode ExitPlanMode)时
|
|
224
|
+
// 引擎 park 吐 done{status:'suspended'|'needs_review'}(+ taskId/checkpointGate)。交互 REPL 有
|
|
225
|
+
// liveHitlAskWire/planReviewWire 把 park 接成对话框;print/非 REPL 无 overlay → 桥 fail-soft
|
|
226
|
+
// 原样吐终帧落到这里,此前落 success 臂 = subtype:'success'/is_error:false/result:'' →
|
|
227
|
+
// **exit 0 空成功**(评测 harness 判 0 分且零错误信号,比死等更隐蔽)。分流 SDKResultError:
|
|
228
|
+
// 三 output 形态经既有消费面各自正确(json 单文档 is_error、stream-json result 帧 is_error、
|
|
229
|
+
// text errors[0]→stderr formatTextModeExecutionError),rc≠0 由 print.ts
|
|
230
|
+
// gracefulShutdownSync(is_error ? 1 : 0) 顺出。文案带 run/session id 指路(交互 REPL resume 或
|
|
231
|
+
// approvals decide),与批 G 件2 的 409 active-run 文案同源指引——那是 409 拒的
|
|
232
|
+
// done{status:'failed', activeTaskId} 帧(上方 failed 臂),这是 park 终帧,两帧互不冲突。
|
|
233
|
+
// CC 对照:CC 2.1.207 AskUserQuestion.isEnabled 在非交互且无 SDK permissionPromptTool 时
|
|
234
|
+
// return false = 模型源头看不到该工具;core [884] 自领 1.296 加同款非交互闸(roster 源头不进)。
|
|
235
|
+
// 本分流是闸到货后的防御纵深:即便引擎吐了 suspended,壳也不许洗成空成功。
|
|
236
|
+
if (rr.status === 'suspended' || rr.status === 'needs_review') {
|
|
237
|
+
const parked = r;
|
|
238
|
+
const runId = typeof parked.taskId === 'string' && parked.taskId.length > 0 ? parked.taskId : undefined;
|
|
239
|
+
const gateTool = typeof parked.checkpointGate?.toolName === 'string' && parked.checkpointGate.toolName.length > 0
|
|
240
|
+
? parked.checkpointGate.toolName
|
|
241
|
+
: undefined;
|
|
242
|
+
const sid = typeof ctx.sessionId === 'string' && ctx.sessionId.length > 0 ? ctx.sessionId : undefined;
|
|
243
|
+
const awaiting = rr.status === 'needs_review'
|
|
244
|
+
? 'a plan review'
|
|
245
|
+
: `an interactive answer${gateTool ? ` (${gateTool})` : ''}`;
|
|
246
|
+
const wayOut = rr.status === 'needs_review'
|
|
247
|
+
? `Review the plan in the interactive REPL${sid ? ` (sema --resume ${sid})` : ''}.`
|
|
248
|
+
: `Answer it in the interactive REPL${sid ? ` (sema --resume ${sid})` : ''}, or decide the pending approval via the engine approvals API${sid ? ` (POST /v1/approvals/${sid}/decide)` : ''}.`;
|
|
249
|
+
return stamp(ctx, {
|
|
250
|
+
uuid: undefined,
|
|
251
|
+
session_id: undefined,
|
|
252
|
+
type: 'result',
|
|
253
|
+
subtype: 'error_during_execution',
|
|
254
|
+
duration_ms: durationMs,
|
|
255
|
+
duration_api_ms: durationMs,
|
|
256
|
+
is_error: true,
|
|
257
|
+
num_turns: stats?.turns ?? 0, // 件2a 同款守卫 —— park 帧多数不带 stats,不炸
|
|
258
|
+
stop_reason: null,
|
|
259
|
+
total_cost_usd: costOrNull(stats?.costUsd),
|
|
260
|
+
usage,
|
|
261
|
+
modelUsage,
|
|
262
|
+
permission_denials: [],
|
|
263
|
+
errors: [
|
|
264
|
+
`Turn suspended awaiting ${awaiting} that no one can provide in -p (non-interactive) mode${runId ? ` (run ${runId})` : ''}. ${wayOut}`,
|
|
265
|
+
],
|
|
266
|
+
...errorCodeField, // [909]B1 — needs_review 帧带 review.pending,原样透传
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
// [909]B1 — 防御臂:词表外的未知非 completed 终态(core 未来加词/未知 wire 变形)绝不洗成
|
|
270
|
+
// 空成功——error_during_execution + errors[] 如实报出该 status 字面。absent status(mock 车道 /
|
|
271
|
+
// 旧引擎 done 帧不带 status)维持 success 臂,零行为变化。
|
|
272
|
+
if (typeof rr.status === 'string' && rr.status.length > 0 && rr.status !== 'completed') {
|
|
273
|
+
return stamp(ctx, {
|
|
274
|
+
uuid: undefined,
|
|
275
|
+
session_id: undefined,
|
|
276
|
+
type: 'result',
|
|
277
|
+
subtype: 'error_during_execution',
|
|
278
|
+
duration_ms: durationMs,
|
|
279
|
+
duration_api_ms: durationMs,
|
|
280
|
+
is_error: true,
|
|
281
|
+
num_turns: stats?.turns ?? 0,
|
|
282
|
+
stop_reason: null,
|
|
283
|
+
total_cost_usd: costOrNull(stats?.costUsd),
|
|
284
|
+
usage,
|
|
285
|
+
modelUsage,
|
|
286
|
+
permission_denials: [],
|
|
287
|
+
errors: [
|
|
288
|
+
rr.errorMessage ??
|
|
289
|
+
`run ended with terminal status "${rr.status}" (not a success; this CLI predates that status word)`,
|
|
290
|
+
],
|
|
291
|
+
...errorCodeField,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
return stamp(ctx, {
|
|
295
|
+
uuid: undefined,
|
|
296
|
+
session_id: undefined,
|
|
297
|
+
type: 'result',
|
|
298
|
+
subtype: 'success',
|
|
299
|
+
duration_ms: durationMs, // P1-5 real elapsed (stream-open → terminal); the wire carries none
|
|
300
|
+
duration_api_ms: durationMs, // engine run == the API call from the shell's view
|
|
301
|
+
is_error: false,
|
|
302
|
+
num_turns: stats?.turns ?? 0, // 件2a:同 :121 —— 缺 stats 不炸
|
|
303
|
+
result: r.result ?? '',
|
|
304
|
+
// TaskResult 五键(additive):outputSchema 任务的结构化产出 + 降级实录(见上方共享注)。
|
|
305
|
+
...(() => {
|
|
306
|
+
const so = r.structuredOutput;
|
|
307
|
+
return so !== undefined ? { structuredOutput: so } : {};
|
|
308
|
+
})(),
|
|
309
|
+
...degradedField,
|
|
310
|
+
stop_reason: null, // dropped (contract 02 §2.10)
|
|
311
|
+
total_cost_usd: costOrNull(stats?.costUsd), // P1-5: costUsd===0 is "unconfigured pricing" → null, never a fake 0
|
|
312
|
+
usage,
|
|
313
|
+
// MF-25/P1-5 — the per-model usage split (wire `stats.modelUsage`, or the synthesized current-model row).
|
|
314
|
+
modelUsage,
|
|
315
|
+
permission_denials: [],
|
|
316
|
+
// MF-25 — the effective served model id (`done.result.model`, e.g. "deepseek-v4-pro"). The CC
|
|
317
|
+
// SDKResultSuccess schema has no `model` field, so this rides as an additive seam field a cost/overview
|
|
318
|
+
// consumer reads (it is ALSO surfaced as the `modelUsage` key). Omitted when the wire didn't carry it.
|
|
319
|
+
...(typeof r.model === 'string' ? { model: r.model } : {}),
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
/** `failed` → SDKResultError (contract 02 §2.11 / 08 CS-11). */
|
|
323
|
+
export function failedToSdkResult(ev, ctx) {
|
|
324
|
+
// Flatten the 4 CC error subtypes onto the single neutral errorCode.
|
|
325
|
+
// error_max_budget_usd is a non-error "budget exceeded" notice, not a crash
|
|
326
|
+
// (contract 02 §2.11) — the renderer branches on subtype.
|
|
327
|
+
const subtype = ev.errorCode === 'budget.exceeded' || ev.errorCode === 'budget.precall'
|
|
328
|
+
? 'error_max_budget_usd'
|
|
329
|
+
: 'error_during_execution';
|
|
330
|
+
const durationMs = elapsedMs(ctx);
|
|
331
|
+
return stamp(ctx, {
|
|
332
|
+
uuid: undefined,
|
|
333
|
+
session_id: undefined,
|
|
334
|
+
type: 'result',
|
|
335
|
+
subtype,
|
|
336
|
+
duration_ms: durationMs, // P1-5 real elapsed (stream-open → terminal)
|
|
337
|
+
duration_api_ms: durationMs,
|
|
338
|
+
is_error: true,
|
|
339
|
+
num_turns: 0,
|
|
340
|
+
stop_reason: null,
|
|
341
|
+
total_cost_usd: null, // P1-5: a failed run has no known cost — never a fake 0
|
|
342
|
+
usage: {
|
|
343
|
+
inputTokens: 0,
|
|
344
|
+
outputTokens: 0,
|
|
345
|
+
cacheReadInputTokens: 0,
|
|
346
|
+
cacheCreationInputTokens: 0,
|
|
347
|
+
webSearchRequests: 0,
|
|
348
|
+
},
|
|
349
|
+
modelUsage: {},
|
|
350
|
+
permission_denials: [],
|
|
351
|
+
// errorCode + errorMessage flattened into the single CC errors[] string.
|
|
352
|
+
// Rewind-family codes(core 1.292 [833]:resume_at.before_target_not_user /
|
|
353
|
+
// resume_at.before_root_unsupported / rewind_snapshot.unresolvable)透传给用户 —— 这些是
|
|
354
|
+
// 用户可自解的操作性错误(选错目标/回退过根/快照缺失),code 附在 message 后便于对账。
|
|
355
|
+
errors: [
|
|
356
|
+
typeof ev.errorCode === 'string' &&
|
|
357
|
+
(ev.errorCode.startsWith('resume_at.') || ev.errorCode.startsWith('rewind_snapshot.')) &&
|
|
358
|
+
ev.errorMessage
|
|
359
|
+
? `${ev.errorMessage} (${ev.errorCode})`
|
|
360
|
+
: (ev.errorMessage ?? ev.errorCode ?? 'run failed'),
|
|
361
|
+
],
|
|
362
|
+
// [909]B1 — errorCode 透传(additive seam 字段;done 臂同款):`failed` 事件的 cancelled/
|
|
363
|
+
// budget.* 等引擎值原样给集成面。
|
|
364
|
+
...(typeof ev.errorCode === 'string' && ev.errorCode.length > 0 ? { errorCode: ev.errorCode } : {}),
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
/** Dispatch a terminal AgentEvent to its SDKResult arm. */
|
|
368
|
+
export function terminalToSdkResult(ev, ctx) {
|
|
369
|
+
return ev.type === 'done' ? doneToSdkResult(ev, ctx) : failedToSdkResult(ev, ctx);
|
|
370
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B3 批搬迁(2026-07-27,多端改造设计稿 §3 B3):cli src/seam/adapter/downstream/turnUsageToModelUsage.ts
|
|
3
|
+
* **逐字整搬**(闭包 2 文件)。名字映射是 load-bearing 的,一个字节都别动。
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* CS-7 §2.7 (08 CS-7 / UC-1): `turn_end.usage` → CC `ModelUsage`, with the
|
|
7
|
+
* PINNED, load-bearing name mapping. Micro-USD is the wire unit.
|
|
8
|
+
*
|
|
9
|
+
* inputTokens → inputTokens
|
|
10
|
+
* outputTokens → outputTokens
|
|
11
|
+
* cacheReadTokens → cacheReadInputTokens (rename is load-bearing)
|
|
12
|
+
* cacheWriteTokens → cacheCreationInputTokens (rename is load-bearing)
|
|
13
|
+
* costMicroUsd → costUSD (= costMicroUsd / 1_000_000)
|
|
14
|
+
*
|
|
15
|
+
* DROPPED (no wire datum): `webSearchRequests`, `contextWindow`,
|
|
16
|
+
* `maxOutputTokens` — the missing `contextWindow` denominator is mock-fill from
|
|
17
|
+
* a static per-model table (contract 02 §2.7 VERIFY). Defaulted to 0 to satisfy
|
|
18
|
+
* the CC `ModelUsage` shape without inventing a number.
|
|
19
|
+
*/
|
|
20
|
+
import type { AgentEvent } from '@sema-agent/sdk';
|
|
21
|
+
import type { ModelUsage } from '../types.js';
|
|
22
|
+
type TurnUsage = NonNullable<Extract<AgentEvent, {
|
|
23
|
+
type: 'turn_end';
|
|
24
|
+
}>['usage']>;
|
|
25
|
+
export declare function turnUsageToModelUsage(usage: TurnUsage): ModelUsage;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function turnUsageToModelUsage(usage) {
|
|
2
|
+
return {
|
|
3
|
+
inputTokens: usage.inputTokens ?? 0,
|
|
4
|
+
outputTokens: usage.outputTokens ?? 0,
|
|
5
|
+
cacheReadInputTokens: usage.cacheReadTokens ?? 0,
|
|
6
|
+
cacheCreationInputTokens: usage.cacheWriteTokens ?? 0,
|
|
7
|
+
webSearchRequests: 0, // dropped on the wire
|
|
8
|
+
costUSD: usage.costMicroUsd !== undefined ? usage.costMicroUsd / 1_000_000 : 0,
|
|
9
|
+
contextWindow: 0, // dropped on the wire → mock-fill (static per-model table)
|
|
10
|
+
maxOutputTokens: 0, // dropped on the wire → mock-fill
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⇄ B3 批搬迁(2026-07-27,多端改造设计稿 §3 B3 / §2.5 末行):cli `src/seam/adapter/runStream.ts`
|
|
3
|
+
* 整文件搬入。**这个文件是本批的主角** —— 搬迁前它的传递闭包是 **1,571 个文件 / 26 种 Node 内建 /
|
|
4
|
+
* 412 个 react 文件**(实测,设计稿 §5.2 表),整棵 CC 树都在里面;搬迁后是 5 个文件、零 Node
|
|
5
|
+
* 内建、零 react。根因链只有三跳,两条外向边:
|
|
6
|
+
* · `:215 await import('../../sema/planReviewWire.js')` → `planReviewWire.ts:26 utils/debug` → 整棵树;
|
|
7
|
+
* · `:140 void import('../../sema/lastTurnUsageStore.js')`(闭包本身干净,但同样是壳内向的边)。
|
|
8
|
+
* 本批把这两条边改成 **chrome 事件**(`plan_review_park` / `last_turn_usage`,见 seam.ts 臂注释),
|
|
9
|
+
* 由宿主注入 `ctx.emitChrome` 消费 —— 库侧再也不 import 壳。
|
|
10
|
+
*
|
|
11
|
+
* 🔴 **时序逐点对齐**(搬迁不许顺手改语义):cli 原本 `await` 的那一处(plan_review park,终帧
|
|
12
|
+
* 之前)这里照样 `await`;原本 fire-and-forget 的那一处(last turn usage)照样不 await。
|
|
13
|
+
*
|
|
14
|
+
* ── 以下为原文件头(逐字保留)────────────────────────────────────────────────────────────────
|
|
15
|
+
* Stream driver (T24, slice): consume an `AgentEvent` async-iterable (the SDK's
|
|
16
|
+
* `runs.events` durable generator OR `tasks.stream` live generator — both yield
|
|
17
|
+
* `AsyncIterable<AgentEvent>`) and re-emit the renderable CC `SDKMessage`
|
|
18
|
+
* sequence (contract 02 §1.1/§7; 08 CS-12/IH-3).
|
|
19
|
+
*
|
|
20
|
+
* Responsibilities for the slice:
|
|
21
|
+
* - event-id idempotency: a replayed durable `id:` (resume / 416 full-sync)
|
|
22
|
+
* is deduped so no SDKMessage is double-rendered (contract 02 §1.1, sse.ts:5-6);
|
|
23
|
+
* - pipe each non-terminal arm through `eventToSdkMessage`;
|
|
24
|
+
* - fold `turn_end.usage` into the latest-usage handle (no standalone arm);
|
|
25
|
+
* - project the terminal `done`/`failed` via `terminalToSdkResult`, then END
|
|
26
|
+
* the generator (mirrors sse.ts:62 / tasks.ts:52).
|
|
27
|
+
*
|
|
28
|
+
* The SDK owns resume/416 INTERNALLY (sse.ts) — this driver only consumes the
|
|
29
|
+
* already-normalized AgentEvent iterable and guards against re-seen ids.
|
|
30
|
+
*/
|
|
31
|
+
import type { AgentEvent } from '@sema-agent/sdk';
|
|
32
|
+
import { type SDKMessage, type EmitContext, type ModelUsage } from './types.js';
|
|
33
|
+
export interface RunStreamHandle {
|
|
34
|
+
/** The latest folded turn usage (footer counters); updated on each turn_end. */
|
|
35
|
+
latestUsage?: ModelUsage;
|
|
36
|
+
}
|
|
37
|
+
export declare function isRunStreamActive(): boolean;
|
|
38
|
+
export declare function runStream(events: AsyncIterable<AgentEvent>, ctx: EmitContext, handle?: RunStreamHandle): AsyncGenerator<SDKMessage>;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { eventSeq, } from './types.js';
|
|
2
|
+
import { eventToSdkMessage, turnEndUsage } from './downstream/eventToSdkMessage.js';
|
|
3
|
+
import { terminalToSdkResult } from './downstream/terminalToSdkResult.js';
|
|
4
|
+
import { publishSubagentContentEvent } from '../subagentContentStore.js';
|
|
5
|
+
/** 本文件发的 chrome 事件全在 leader lane(子代内容在上面就被 divert 走了)。 */
|
|
6
|
+
const MAIN = { lane: 'main' };
|
|
7
|
+
/**
|
|
8
|
+
* Drive one AgentEvent source into a CC SDKMessage stream.
|
|
9
|
+
*
|
|
10
|
+
* @param events the SDK AgentEvent iterable (durable or live).
|
|
11
|
+
* @param ctx identity context (sessionId threaded onto every emitted arm).
|
|
12
|
+
* @param handle optional out-param updated with the latest folded usage.
|
|
13
|
+
*/
|
|
14
|
+
// In-flight turn counter (module-level: every interactive turn flows through
|
|
15
|
+
// runStream). Consumed by the engine versionDrift rolling-upgrade gate — the
|
|
16
|
+
// engine must NEVER be hot-swapped mid-turn (it would sever this stream), so
|
|
17
|
+
// the lifecycle loop defers the swap until this reads false (clay 2026-07-05
|
|
18
|
+
// 滚动升级/一体化继承).
|
|
19
|
+
let inFlightTurns = 0;
|
|
20
|
+
export function isRunStreamActive() {
|
|
21
|
+
return inFlightTurns > 0;
|
|
22
|
+
}
|
|
23
|
+
export async function* runStream(events, ctx, handle = {}) {
|
|
24
|
+
inFlightTurns++;
|
|
25
|
+
try {
|
|
26
|
+
yield* runStreamInner(events, ctx, handle);
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
inFlightTurns--;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function* runStreamInner(events, ctx, handle = {}) {
|
|
33
|
+
const seen = new Set();
|
|
34
|
+
// P1-5 (print `--output-format json` 元数据失真): stamp the stream-open instant once so the terminal
|
|
35
|
+
// projector can emit a REAL `duration_ms` (the wire carries no duration; the old hardcoded 0 was fake).
|
|
36
|
+
if (ctx.startedAtMs === undefined)
|
|
37
|
+
ctx.startedAtMs = Date.now();
|
|
38
|
+
for await (const ev of events) {
|
|
39
|
+
// event-id idempotency — drop a re-seen durable seq (contract 02 §1.1).
|
|
40
|
+
const seq = eventSeq(ev);
|
|
41
|
+
if (seq !== undefined) {
|
|
42
|
+
if (seen.has(seq))
|
|
43
|
+
continue;
|
|
44
|
+
seen.add(seq);
|
|
45
|
+
}
|
|
46
|
+
// C1 — SUBAGENT content divert (service 1.89 forwardSubagentEvents): a CONTENT event stamped with
|
|
47
|
+
// parentToolCallId belongs to a SUBAGENT run (EventIdentity contract: present ONLY on sub-agent
|
|
48
|
+
// events) — side-channel it to the 查看态 transcript store and keep it OFF the main pipeline
|
|
49
|
+
// (a child's tokens rendering as the leader's text is the pollution failure mode). turn_end's
|
|
50
|
+
// sub-flow guard below stays as-is (usage rollup only).
|
|
51
|
+
if ((ev.type === 'text_delta' ||
|
|
52
|
+
ev.type === 'reasoning_delta' ||
|
|
53
|
+
ev.type === 'tool_start' ||
|
|
54
|
+
ev.type === 'tool_end') &&
|
|
55
|
+
typeof ev.parentToolCallId === 'string') {
|
|
56
|
+
const sub = ev;
|
|
57
|
+
publishSubagentContentEvent({
|
|
58
|
+
type: sub.type,
|
|
59
|
+
// taskId is on every subagent event (EventIdentity); parentToolCallId keys the fallback
|
|
60
|
+
taskId: sub.taskId ?? sub.parentToolCallId,
|
|
61
|
+
parentToolCallId: sub.parentToolCallId,
|
|
62
|
+
delta: sub.delta,
|
|
63
|
+
toolCallId: sub.toolCallId,
|
|
64
|
+
toolName: sub.toolName,
|
|
65
|
+
args: sub.args,
|
|
66
|
+
output: typeof sub.output === 'string' ? sub.output : undefined,
|
|
67
|
+
isError: sub.isError,
|
|
68
|
+
});
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
// Fold usage (footer counters), AND surface the per-turn `outputTokens` as a NEUTRAL `turn_usage` arm so
|
|
72
|
+
// the upstream bridge can drive 187's C1a `end` arm (responseLength reconciles to REAL output_tokens×4 —
|
|
73
|
+
// the spinner-token feeder, verify/TOKEN-187-PORT.md step 3). `turn_end` carries no renderable content, so
|
|
74
|
+
// this arm is metrics-only; the bridge converts it to a `message_delta{usage.output_tokens}` StreamEvent.
|
|
75
|
+
if (ev.type === 'turn_end') {
|
|
76
|
+
const usage = turnEndUsage(ev);
|
|
77
|
+
// §E2 identity (service 1.78) — a SUB-FLOW's turn_end (orchestration/subagent round, carries
|
|
78
|
+
// parentToolCallId) must NOT drive the leader's C1a `end` reconcile: its outputTokens are the
|
|
79
|
+
// child's, and reconciling the leader's responseLength against them is the token-jump bug
|
|
80
|
+
// (blackboard dogfood #2 / upstreamBridge TODO #9). Fold its usage into the rollup only.
|
|
81
|
+
const isSubFlow = ev.parentToolCallId !== undefined;
|
|
82
|
+
if (usage) {
|
|
83
|
+
handle.latestUsage = usage;
|
|
84
|
+
// plugins 专项 G1(2026-07-21):同一折叠点多发一份给 lastTurnUsageStore——StatusLine
|
|
85
|
+
// 的 statusline 命令 stdin(context_window.current_usage)在消息面无 usage(seam 合成
|
|
86
|
+
// 消息不带)时回落到这里,claude-hud 类插件的 Context 条才有真值。sub-flow 的 turn_end
|
|
87
|
+
// 不发(§E2 同判据):statusline 显示的是主会话窗口,子 agent 的 usage 会把 Context%
|
|
88
|
+
// 打成孩子的窗口占用(自查+对抗复审确认)。
|
|
89
|
+
// B3 切边:此前是 `void import('../../sema/lastTurnUsageStore.js').then(publishLastTurnUsage)`
|
|
90
|
+
// ——壳内向的动态边。改成 chrome 事件(宿主消费义务见 seam.ts `last_turn_usage` 臂);
|
|
91
|
+
// 与原式同样**不 await**(fire-and-forget),sink 抛错不影响流(fail-soft 原语义)。
|
|
92
|
+
if (!isSubFlow && ctx.emitChrome) {
|
|
93
|
+
try {
|
|
94
|
+
void ctx.emitChrome({ kind: 'last_turn_usage', laneProof: MAIN, usage });
|
|
95
|
+
}
|
|
96
|
+
catch { /* fail-soft — statusline 退回 null,原语义 */ }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const outputTokens = ev.usage?.outputTokens;
|
|
100
|
+
if (typeof outputTokens === 'number' && !isSubFlow) {
|
|
101
|
+
yield { type: 'turn_usage', outputTokens };
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
// Terminals: project then END (the SDK generator also ends here).
|
|
106
|
+
if (ev.type === 'done' || ev.type === 'failed') {
|
|
107
|
+
// clay P0 报错体系(2026-07-06,402 实测定谳):failed 终态此前只投影成 SDKResult
|
|
108
|
+
// (interactive REPL 不渲染 result 帧)= 上游 4xx/欠费/超限对用户完全静默。这里
|
|
109
|
+
// 额外合成一条 assistant 错误消息(CC 的 isApiErrorMessage 红字渲染线现成),
|
|
110
|
+
// 用户永远能看到 turn 为什么失败。
|
|
111
|
+
const failedResult = ev.type === 'done' ? ev.result : null;
|
|
112
|
+
// [909]B1 — timeout/blocked 终态同享可见性行(此前只 status:'failed' 有行,timeout/blocked
|
|
113
|
+
// 在 REPL 面完全静默)。park(suspended/needs_review)不进此行:有各自 overlay/文案线,
|
|
114
|
+
// 非 provider 故障([884]A1 断言零 assistant 行)。
|
|
115
|
+
const errText = ev.type === 'failed'
|
|
116
|
+
? (ev.errorMessage ?? ev.errorCode ?? 'run failed')
|
|
117
|
+
: failedResult?.status === 'failed'
|
|
118
|
+
? (failedResult.errorMessage ?? failedResult.errorCode ?? 'run failed')
|
|
119
|
+
: failedResult?.status === 'timeout'
|
|
120
|
+
? (failedResult.errorMessage ?? 'run hit its wall-clock deadline and was finalized by the engine')
|
|
121
|
+
: failedResult?.status === 'blocked'
|
|
122
|
+
? (failedResult.blockedReason
|
|
123
|
+
? `Run blocked: ${failedResult.blockedReason}`
|
|
124
|
+
: 'Run blocked (the agent reported it cannot proceed)')
|
|
125
|
+
: null;
|
|
126
|
+
if (errText) {
|
|
127
|
+
// 件2b(中断事故修复批 G,2026-07-15,症状2 第二环):409 active-run 拒绝的专属人话。引擎在
|
|
128
|
+
// session 已有挂起 run(上一 turn 的 AskUserQuestion park 未解锁等)时把
|
|
129
|
+
// errorMessage 'session already has an active run'(+ result.activeTaskId)骑在
|
|
130
|
+
// done{status:'failed'} 帧上(ai-agent-service server.ts:1526 同型)——裸 "API Error:" 行看着
|
|
131
|
+
// 像 provider 故障,是误导。识别该终帧 → 一行专属文案(告知在等什么 + 出路);其它错误保持
|
|
132
|
+
// 原 API Error 行(未知错误 fallback 不变)。引擎侧解锁腿([866] server cancel suspended 改
|
|
133
|
+
// 语义 + reapSuspended TTL)到货前,这是壳侧最诚实的呈现。
|
|
134
|
+
const activeTaskId = failedResult?.activeTaskId ??
|
|
135
|
+
(ev.type === 'failed' ? ev.activeTaskId : undefined);
|
|
136
|
+
const isActiveRunBusy = errText === 'session already has an active run' || typeof activeTaskId === 'string';
|
|
137
|
+
// 件14 rev2 (2026-07-14): provider max_tokens cap rejection (e.g. glm-5.2 preset 131072 >
|
|
138
|
+
// a third-party gateway's 128000) → append the way out. Keyword match on the upstream text,
|
|
139
|
+
// supplement never mask (raw provider words stay on the row).
|
|
140
|
+
const maxTokHint = /max_tokens/i.test(errText)
|
|
141
|
+
? '\nyour provider caps max_tokens lower — lower it in /model (press m, or M to type a value)'
|
|
142
|
+
: '';
|
|
143
|
+
const rowText = isActiveRunBusy
|
|
144
|
+
? `Previous turn is still pending an answer${typeof activeTaskId === 'string' && activeTaskId.length > 0
|
|
145
|
+
? ` (run ${activeTaskId})`
|
|
146
|
+
: ''}. Press Esc to cancel it, or retry shortly.`
|
|
147
|
+
: `API Error: ${errText}${maxTokHint}`;
|
|
148
|
+
yield {
|
|
149
|
+
session_id: ctx.sessionId ?? '',
|
|
150
|
+
uuid: `err-${Date.now().toString(36)}`,
|
|
151
|
+
type: 'assistant',
|
|
152
|
+
message: { role: 'assistant', content: [{ type: 'text', text: rowText }] },
|
|
153
|
+
parent_tool_use_id: null,
|
|
154
|
+
isApiErrorMessage: true,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
// plan_review park(core [508]③ 拍板形状):done `status:"needs_review"` + gate plan_review 不是
|
|
158
|
+
// 普通完成——壳弹审批卡(planReviewWire 合成 question 借 AskUserQuestion overlay;决断走
|
|
159
|
+
// /v1/assistant/tasks/:id/plan_review resume verb)。
|
|
160
|
+
// B3 切边:此前是 `await import('../../sema/planReviewWire.js')` —— **整棵 CC 树进闭包的
|
|
161
|
+
// 那一行**(设计稿 §5.2 根因链)。改成 chrome 事件;`await` 保留(cli 原本就在终帧**之前**
|
|
162
|
+
// 同步等 arm 完成,卡片先于完成行出现),sink 失败 fail-soft 照常投影终帧。
|
|
163
|
+
if (ev.type === 'done' && failedResult?.status === 'needs_review' && ctx.emitChrome) {
|
|
164
|
+
try {
|
|
165
|
+
await ctx.emitChrome({ kind: 'plan_review_park', laneProof: MAIN, result: ev.result });
|
|
166
|
+
}
|
|
167
|
+
catch { /* fail-soft — the terminal frame below still renders */ }
|
|
168
|
+
}
|
|
169
|
+
yield terminalToSdkResult(ev, ctx);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
// Every other arm → zero-or-one renderable SDKMessage.
|
|
173
|
+
const msg = eventToSdkMessage(ev, ctx);
|
|
174
|
+
if (msg)
|
|
175
|
+
yield msg;
|
|
176
|
+
}
|
|
177
|
+
}
|