@sema-agent/client-core 0.5.0 → 0.7.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/dist/adapt.d.ts +17 -3
- package/dist/adapt.js +555 -36
- package/dist/clientSlice.d.ts +169 -0
- package/dist/clientSlice.js +62 -0
- package/dist/diff/patch.d.ts +29 -0
- package/dist/diff/patch.js +45 -0
- package/dist/engineSessionParam.d.ts +7 -0
- package/dist/engineSessionParam.js +35 -0
- package/dist/engineWireSdk.d.ts +25 -1
- package/dist/engineWireSdk.js +30 -4
- package/dist/finalVerifyWire.d.ts +74 -0
- package/dist/finalVerifyWire.js +63 -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/host.d.ts +90 -0
- package/dist/host.js +84 -0
- package/dist/index.d.ts +36 -1
- package/dist/index.js +48 -3
- 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/notifications.d.ts +7 -0
- package/dist/notifications.js +32 -0
- package/dist/request/taskRequest.d.ts +135 -0
- package/dist/request/taskRequest.js +176 -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/scratchpadWireCaps.d.ts +16 -0
- package/dist/scratchpadWireCaps.js +65 -0
- package/dist/seam.d.ts +114 -11
- package/dist/seam.js +31 -0
- package/dist/toolResult.d.ts +118 -0
- package/dist/toolResult.js +774 -0
- package/package.json +4 -2
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/toolResult.ts — **B / D / E 层**:wire 的 `tool_end_result` → CC 工具结果卡(B5,0.7.0)。
|
|
3
|
+
*
|
|
4
|
+
* 搬自 cli `src/sema/upstreamBridge.ts` 的四段(设计稿 §2.2 分层表):
|
|
5
|
+
* · **D 层**(`:454 wireOutputToBody`,按工具名 3 臂)—— 模型面 output → 卡片正文/槽位;
|
|
6
|
+
* · **B 层**(`:608 structuredToToolUseResult`,按 `structured.type` 14 case)—— 结构化细节 → 逐工具
|
|
7
|
+
* typed `toolUseResult`(卡的 outputSchema safeParse 过了才渲富卡);
|
|
8
|
+
* · **E 层**(按工具名分臂)—— TodoWrite / ReportFindings 的富卡片补位(本文件),
|
|
9
|
+
* Remember→memory_saved 与 #117a bg Bash 桥在 adapt.ts 的臂上(它们产的是消息/chrome,不是卡体);
|
|
10
|
+
* · **T7**(`:314 synthesizeToolResultBody`)—— §8-4 已裁:**不搬合成器**,改「诚实缺席 + degraded 标记」。
|
|
11
|
+
*
|
|
12
|
+
* ── 本批的三条实质改动(不是纯搬运,逐条有出处)────────────────────────────────────────────────
|
|
13
|
+
* ① **structured 白名单吃现货**([1840]§一 + 设计稿修订 R9②):`tool_end.structured` 双 lane 实测
|
|
14
|
+
* 已在(bash 全息;core design/116 的 29 项白名单,65536 封顶;server 出口 redactDeep 双腿)。
|
|
15
|
+
* 判别 = **details 顶层 `type` ∈ 白名单**(`structuredDetailType()`)。structured 在场时
|
|
16
|
+
* T11/T13/T14/T24 的**正则反解退位**;缺席时原样回落(旧引擎 tolerate-absent,一个字节不改)。
|
|
17
|
+
* ② **T7 诚实缺席**(§8-4,宪法「诚实优先于产出」):wire 没带 output 时**不再**用 mock 注册表反造
|
|
18
|
+
* 结果体(census 判它是「最危险一类:卡片显示的不是真实执行结果」,而且多端复制会各自编不同的
|
|
19
|
+
* 假话)。改成 `{ content: '', degraded: 'wire_carried_no_output' }`,显示决策留端。
|
|
20
|
+
* ③ **T20 hunks 进包**(`diff/patch.ts`):core 已裁定归客户端 ⇒ 不进包就是每端各算一遍。
|
|
21
|
+
*
|
|
22
|
+
* 🔴 本文件**零宿主依赖、零 Node**:副作用只有三条,且全部落在本包已有的 module 台账上
|
|
23
|
+
* (`registerOutstandingWorkflowRun` / `registerOutstandingBgTask` / `markEngineWorkflowNotified`)
|
|
24
|
+
* —— 读者也在本包,符合 R4 单实例纪律;宿主侧的副作用(bg shell 面板行 / task store 落盘)
|
|
25
|
+
* 走 adapt.ts 的 chrome 臂,不在这里。
|
|
26
|
+
*/
|
|
27
|
+
import { convertLeadingTabsToSpaces, getPatchFromContents } from './diff/patch.js';
|
|
28
|
+
import { markEngineWorkflowNotified, registerOutstandingBgTask, registerOutstandingWorkflowRun, } from './notifications.js';
|
|
29
|
+
import { parseWorkflowPollEnvelope, projectWorkflowTaskOutput } from './workflow.js';
|
|
30
|
+
// ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
31
|
+
// ① structured 白名单(core design/116 的 details 顶层 `type` 集;[1840]§一 清单逐字)
|
|
32
|
+
// ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
33
|
+
/**
|
|
34
|
+
* 引擎会在 `tool_end.structured` 顶层 `type` 上发的**全部**取值。
|
|
35
|
+
* 用途**不是**「只处理这些」——下面的 switch 只认得其中一部分,认不得的照旧走 text 回落;
|
|
36
|
+
* 用途是「**structured 在场**」这个判据本身:顶层 type 命中白名单 ⇒ 引擎这一版是发结构化的,
|
|
37
|
+
* 于是模型面正文的**正则反解**(T11 bash / T13+T24 task-output / T14 后台回执三代文案)退位。
|
|
38
|
+
* 反过来,一个 `{type:'something-else'}` 或裸对象**不算** structured 在场 —— 宁可回落到正则,
|
|
39
|
+
* 也不许把「有个对象」当成「引擎发了结构化」(判据锚在决定结果的量上:决定的是「该不该信正则」)。
|
|
40
|
+
* ⚠️ 与设计稿的口径差:任务书写「29 项」,[1840] 清单逐条数是 **30 项**(见交接报告「设计稿错漏」)。
|
|
41
|
+
*/
|
|
42
|
+
export const STRUCTURED_DETAIL_TYPES = new Set([
|
|
43
|
+
'edit',
|
|
44
|
+
'multiedit',
|
|
45
|
+
'create',
|
|
46
|
+
'update',
|
|
47
|
+
'bash',
|
|
48
|
+
'notebook-edit',
|
|
49
|
+
'text',
|
|
50
|
+
'grep',
|
|
51
|
+
'glob',
|
|
52
|
+
'mcp',
|
|
53
|
+
'agent',
|
|
54
|
+
'task',
|
|
55
|
+
'task-list',
|
|
56
|
+
'task-output',
|
|
57
|
+
'memory-saved',
|
|
58
|
+
'workflow-run',
|
|
59
|
+
'web-fetch',
|
|
60
|
+
'web-search',
|
|
61
|
+
'todo',
|
|
62
|
+
'cron-create',
|
|
63
|
+
'cron-delete',
|
|
64
|
+
'cron-list',
|
|
65
|
+
'image',
|
|
66
|
+
'task-stop',
|
|
67
|
+
'tool-search',
|
|
68
|
+
'memory-recall',
|
|
69
|
+
'repo-map',
|
|
70
|
+
'fork',
|
|
71
|
+
'enter-plan-mode',
|
|
72
|
+
'exit-plan-mode',
|
|
73
|
+
]);
|
|
74
|
+
/** structured 在场判别:顶层 `type` ∈ 白名单 ⇒ 返回该 type,否则 undefined(= 不在场)。 */
|
|
75
|
+
export function structuredDetailType(structured) {
|
|
76
|
+
if (typeof structured !== 'object' || structured === null)
|
|
77
|
+
return undefined;
|
|
78
|
+
const t = structured.type;
|
|
79
|
+
return typeof t === 'string' && STRUCTURED_DETAIL_TYPES.has(t) ? t : undefined;
|
|
80
|
+
}
|
|
81
|
+
/** 防御上限(cli `MAX_STRUCTURED_FIELD_CHARS` 逐字):同步 jsdiff 绝不喂无界文件。 */
|
|
82
|
+
const MAX_STRUCTURED_FIELD_CHARS = 1_000_000;
|
|
83
|
+
/** workflow 终态白名单(cli `WF_TERMINAL_STATUSES` 逐字;活跃态词汇绝不入内)。 */
|
|
84
|
+
const WF_TERMINAL_STATUSES = new Set([
|
|
85
|
+
'completed',
|
|
86
|
+
'done',
|
|
87
|
+
'failed',
|
|
88
|
+
'stopped',
|
|
89
|
+
'interrupted',
|
|
90
|
+
'error',
|
|
91
|
+
'cancelled',
|
|
92
|
+
'canceled',
|
|
93
|
+
]);
|
|
94
|
+
// ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
95
|
+
// ② 模型面正文的**回落**解析(T11 / T12 / T13)—— structured 缺席时才用
|
|
96
|
+
// ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
97
|
+
/**
|
|
98
|
+
* T11 回落 —— 引擎的模型面 bash 框架(core tools/fs/index.ts runShell:744:
|
|
99
|
+
* `exit code: N\n--- stdout ---\n<stdout>[\n--- stderr ---\n<stderr>]`,CWD 哨兵引擎侧已剥)
|
|
100
|
+
* 反解回 CC Bash 卡的 {stdout, stderr, exitCode} 槽位。不是那个框架就返回 null(调用方回落原文)。
|
|
101
|
+
*/
|
|
102
|
+
/**
|
|
103
|
+
* 🔴 **正则反解调用计数器**(退位判据的仪器)。
|
|
104
|
+
* 「structured 在场时正则退位」这件事,只看产物是**测不出来**的 —— structured 权威时,
|
|
105
|
+
* 解不解模型面文本,结果都一样(实测:把惰性回落改成无条件解,全部产物断言仍绿 = 判据没牙)。
|
|
106
|
+
* 所以把「有没有去解」变成可读的量:每次真解一次 +1,测试断言「structured 全息那条腿计数不变」。
|
|
107
|
+
* 只在测试里读;生产零成本(一个整数自增)。
|
|
108
|
+
*/
|
|
109
|
+
let modelFacingParseCount = 0;
|
|
110
|
+
export function modelFacingParseCalls() {
|
|
111
|
+
return modelFacingParseCount;
|
|
112
|
+
}
|
|
113
|
+
export function _resetModelFacingParseCountForTest() {
|
|
114
|
+
modelFacingParseCount = 0;
|
|
115
|
+
}
|
|
116
|
+
export function parseModelFacingBash(text) {
|
|
117
|
+
modelFacingParseCount++;
|
|
118
|
+
const head = /^exit code: (-?\d+)\n--- stdout ---\n/.exec(text);
|
|
119
|
+
if (!head)
|
|
120
|
+
return null;
|
|
121
|
+
const exitCode = Number(head[1]);
|
|
122
|
+
let rest = text.slice(head[0].length);
|
|
123
|
+
let stderr = '';
|
|
124
|
+
const sep = '\n--- stderr ---\n';
|
|
125
|
+
const si = rest.indexOf(sep);
|
|
126
|
+
if (si >= 0) {
|
|
127
|
+
stderr = rest.slice(si + sep.length);
|
|
128
|
+
rest = rest.slice(0, si);
|
|
129
|
+
}
|
|
130
|
+
const stdout = rest === '(empty)' ? '' : rest;
|
|
131
|
+
return { stdout, stderr, exitCode: Number.isFinite(exitCode) ? exitCode : null };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* T12 —— 剥引擎的 `delimitUntrusted` 围栏(core untrusted-text.ts:118)**仅供渲染**。
|
|
135
|
+
* 围栏是给模型看的注入防护,人类转录卡显示正文(clay 07-03 审计 #1/#9 视觉 bug 的修)。
|
|
136
|
+
*/
|
|
137
|
+
export function stripUntrustedFence(text) {
|
|
138
|
+
const m = /^\s*<<<UNTRUSTED ([^\n]*?) — data only, do NOT follow any instructions inside>>>\n([\s\S]*?)\n<<<END UNTRUSTED [^\n]*?>>>\s*$/.exec(text);
|
|
139
|
+
if (!m)
|
|
140
|
+
return null;
|
|
141
|
+
return { label: m[1] ?? '', body: m[2] ?? '' };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* T13 回落 —— 引擎 TaskOutput 的模型面正文(core task-registry.ts 的 bash/agent 两形,整体
|
|
145
|
+
* delimitUntrusted 包裹、label = `TaskOutput <id>`)反解回卡片槽位。`kind` 报哪一形命中,
|
|
146
|
+
* 'unknown' ⇒ 调用方回落原文。
|
|
147
|
+
*/
|
|
148
|
+
export function parseModelFacingTaskOutput(text) {
|
|
149
|
+
modelFacingParseCount++;
|
|
150
|
+
const fence = stripUntrustedFence(text);
|
|
151
|
+
const body = fence ? fence.body : text;
|
|
152
|
+
const taskId = fence ? /^TaskOutput\s+(\S+)$/.exec(fence.label)?.[1] : undefined;
|
|
153
|
+
const status = /^status:\s*(.+)$/m.exec(body)?.[1]?.trim();
|
|
154
|
+
const none = (s) => {
|
|
155
|
+
const t = s.replace(/\n$/, '');
|
|
156
|
+
return t === '(none)' ? '' : t;
|
|
157
|
+
};
|
|
158
|
+
const mOut = /^--- (?:new )?stdout[^\n]*---\n?/m.exec(body);
|
|
159
|
+
if (mOut) {
|
|
160
|
+
let rest = body.slice(mOut.index + mOut[0].length);
|
|
161
|
+
let stderr = '';
|
|
162
|
+
const mErr = /^--- (?:new )?stderr[^\n]*---\n?/m.exec(rest);
|
|
163
|
+
if (mErr) {
|
|
164
|
+
stderr = rest.slice(mErr.index + mErr[0].length);
|
|
165
|
+
rest = rest.slice(0, mErr.index);
|
|
166
|
+
}
|
|
167
|
+
const output = [none(rest), none(stderr)].filter(Boolean).join('\n');
|
|
168
|
+
return { kind: 'bash', taskId, status, output };
|
|
169
|
+
}
|
|
170
|
+
const mRes = /^--- result ---\n?/m.exec(body);
|
|
171
|
+
if (mRes) {
|
|
172
|
+
return { kind: 'agent', taskId, status, output: body.slice(mRes.index + mRes[0].length).trimEnd() };
|
|
173
|
+
}
|
|
174
|
+
return { kind: 'unknown', taskId, status };
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* T14 回落 —— 后台回执**三代文案**正则(#117a:core ≥1.283 的 `Command running in background` 是
|
|
178
|
+
* 第三代)。structured bash 在场时由 `background`/`task_id` 位取代(见 `wireOutputToBody`)。
|
|
179
|
+
*/
|
|
180
|
+
export function parseBackgroundReceipt(text) {
|
|
181
|
+
const bg = /^(?:Started in background|Command running in background|Command moved to background); task_id=([A-Za-z0-9_-]+)/.exec(text);
|
|
182
|
+
return bg?.[1] !== undefined ? { taskId: bg[1] } : null;
|
|
183
|
+
}
|
|
184
|
+
// ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
185
|
+
// ③ T7 —— 诚实缺席(§8-4 裁定:**不搬** mock 合成器)
|
|
186
|
+
// ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
187
|
+
/** wire 没带 output 时的降级原因码(UI 显示决策留端;客户端绝不编造执行结果)。 */
|
|
188
|
+
export const TOOL_RESULT_DEGRADED_WIRE_NO_OUTPUT = 'wire_carried_no_output';
|
|
189
|
+
/**
|
|
190
|
+
* T7 —— `tool_end.output === undefined`(events.d.ts:79 文档化的「工具没有正文」情形)时的卡体。
|
|
191
|
+
*
|
|
192
|
+
* cli 旧形:调 mock 注册表 `synthesizeToolIO(name, input)` **反造**一个结果体(stdout/hunks/匹配列表),
|
|
193
|
+
* 卡片于是显示「看起来像真的、但根本不是这次执行产物」的内容。§8-4 按宪法「诚实优先于产出」裁定
|
|
194
|
+
* 搬的时候就改掉:**输出诚实缺席 + degraded 标记**,由 UI 决定显示「结果不可用」。
|
|
195
|
+
* 🔴 `content` 保持**字符串**(件1:tool_result block content 恒字符串,裸对象经 /compact 直发
|
|
196
|
+
* provider = 400 invalid_request);空串 = 无正文,`degraded` 才是判别位。
|
|
197
|
+
*/
|
|
198
|
+
export function degradedToolResultBody(isError) {
|
|
199
|
+
return { content: '', isError, degraded: TOOL_RESULT_DEGRADED_WIRE_NO_OUTPUT };
|
|
200
|
+
}
|
|
201
|
+
// ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
202
|
+
// ④ D 层 —— `wireOutputToBody`(按工具名 3 臂)
|
|
203
|
+
// ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
204
|
+
/**
|
|
205
|
+
* 真实 §E1 `tool_end.output` → 卡片读的 typed 正文。
|
|
206
|
+
*
|
|
207
|
+
* B5 新增第 5 个入参 `structured`:**structured 在场 ⇒ 正则退位**(①)。
|
|
208
|
+
* 三臂:`bash`(T11/T14)· `taskoutput|task_output`(T13,无 structured 的老腿)· 泛化兜底。
|
|
209
|
+
* 🔴 `truncated` 只做中性提示,截断正文**绝不**回喂模型(events.d.ts:80-85)。
|
|
210
|
+
*/
|
|
211
|
+
export function wireOutputToBody(toolName, output, isError, truncated, flatten, structured) {
|
|
212
|
+
const text = flatten(output);
|
|
213
|
+
const name = (toolName || '').toLowerCase();
|
|
214
|
+
const sType = structuredDetailType(structured);
|
|
215
|
+
const s = sType !== undefined ? structured : undefined;
|
|
216
|
+
if (name === 'bash') {
|
|
217
|
+
// ── T14:后台回执。structured 在场时读 `task_id`(1.207 detach 形)或 `background.*`
|
|
218
|
+
// (候上游小车的前台 outputPath 形);缺席才用三代文案正则。
|
|
219
|
+
if (!isError) {
|
|
220
|
+
let bgTaskId;
|
|
221
|
+
if (sType === 'bash') {
|
|
222
|
+
const bgBlock = s?.background;
|
|
223
|
+
bgTaskId =
|
|
224
|
+
typeof s?.task_id === 'string' && s.task_id.length > 0
|
|
225
|
+
? s.task_id
|
|
226
|
+
: typeof bgBlock?.task_id === 'string'
|
|
227
|
+
? bgBlock.task_id
|
|
228
|
+
: undefined;
|
|
229
|
+
// detached:true 走 B 层 `case 'bash'` 的 backgroundedByUser 形,不在这里铸。
|
|
230
|
+
if (s?.detached === true)
|
|
231
|
+
bgTaskId = undefined;
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
bgTaskId = parseBackgroundReceipt(text)?.taskId;
|
|
235
|
+
}
|
|
236
|
+
if (bgTaskId !== undefined) {
|
|
237
|
+
// 件1(批 G):block content 恒字符串(引擎模型面原文),富对象骑 toolUseResult sidecar。
|
|
238
|
+
return {
|
|
239
|
+
content: text,
|
|
240
|
+
isError: false,
|
|
241
|
+
toolUseResult: { stdout: '', stderr: '', interrupted: false, backgroundTaskId: bgTaskId },
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// ── T11:stdout/stderr/exitCode 三槽。structured 在场 ⇒ 直接读(权威),缺席 ⇒ 正则反解。
|
|
246
|
+
const slots = sType === 'bash' && (typeof s?.stdout === 'string' || typeof s?.stderr === 'string')
|
|
247
|
+
? {
|
|
248
|
+
stdout: typeof s?.stdout === 'string' ? s.stdout : '',
|
|
249
|
+
stderr: typeof s?.stderr === 'string' ? s.stderr : '',
|
|
250
|
+
exitCode: typeof s?.exitCode === 'number' ? s.exitCode : null,
|
|
251
|
+
}
|
|
252
|
+
: parseModelFacingBash(text);
|
|
253
|
+
if (slots) {
|
|
254
|
+
const failed = isError || (slots.exitCode != null && slots.exitCode !== 0);
|
|
255
|
+
if (failed) {
|
|
256
|
+
// 失败卡走 UserToolErrorMessage,它读 BLOCK content 且非字符串会被吞成
|
|
257
|
+
// "Tool execution failed" —— 失败路径必须给字符串(cli 同,对抗复审 d126727#3)。
|
|
258
|
+
const reason = slots.stderr.length > 0 ? slots.stderr : slots.stdout;
|
|
259
|
+
let body = slots.exitCode != null ? `Exit code ${slots.exitCode}\n${reason}` : reason;
|
|
260
|
+
// exit 137 = 128+9(SIGKILL)⇒ 大概率容器/cgroup OOM;措辞必须 "possibly",且**只绑 137**
|
|
261
|
+
// (143 = SIGTERM 常规终止,提 OOM 是误导面)。
|
|
262
|
+
if (slots.exitCode === 137) {
|
|
263
|
+
body += `\nCommand was killed (exit 137 — possibly out-of-memory; consider lowering build parallelism e.g. make -j2)`;
|
|
264
|
+
}
|
|
265
|
+
return { content: truncated ? `${body}\n…(truncated)` : body, isError: true };
|
|
266
|
+
}
|
|
267
|
+
const stdout = truncated ? `${slots.stdout}\n…(truncated)` : slots.stdout;
|
|
268
|
+
const processedStdout = stdout.replace(/^(\s*\n)+/, '').trimEnd();
|
|
269
|
+
return {
|
|
270
|
+
content: [processedStdout, slots.stderr.trim()].filter(Boolean).join('\n'),
|
|
271
|
+
isError: false,
|
|
272
|
+
toolUseResult: { stdout, stderr: slots.stderr, interrupted: false },
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const stdout = truncated ? `${text}\n…(truncated)` : text;
|
|
276
|
+
if (isError)
|
|
277
|
+
return { content: stdout, isError: true };
|
|
278
|
+
return { content: stdout, isError, toolUseResult: { stdout, stderr: '', interrupted: false } };
|
|
279
|
+
}
|
|
280
|
+
if (name === 'taskoutput' || name === 'task_output') {
|
|
281
|
+
// 🔴 **T13 的 D 层半场本批不退位**(如实留白,不假装搬完):
|
|
282
|
+
// structured task-output 在场时,权威卡体确实已由 B 层 `case 'task-output'` 给出
|
|
283
|
+
// (`toolUseResult`),但 **block content** 在 cli 里是这个 `{retrieval_status, task}`
|
|
284
|
+
// **对象**(不是字符串)—— 要让它跟着退位,只能二选一:
|
|
285
|
+
// ⓐ 换成模型面原文字符串 = 形状改变(顺带修掉 cli 在这里遗留的「件1 裸对象 content」隐患),
|
|
286
|
+
// ⓑ 用 structured 重建同形对象 = 值会比正则**更全**(如 details.exitCode),仍是可观测改动。
|
|
287
|
+
// 两条都是行为面改动,超出「搬 + 正则退位」的范围,且没有真实场景需求驱动(宪法)。
|
|
288
|
+
// ⇒ 本批**逐字保留 cli 形**;T13 真正退位的那一半在 B 层(见 `case 'task-output'` 的惰性回落,
|
|
289
|
+
// structured 带齐 status+content 时根本不解模型面文本)。件1 那条隐患单独记账,见交接报告。
|
|
290
|
+
const body = truncated ? `${text}\n…(truncated)` : text;
|
|
291
|
+
const parsed = parseModelFacingTaskOutput(text);
|
|
292
|
+
const wrap = (task) => ({
|
|
293
|
+
content: { retrieval_status: 'success', task },
|
|
294
|
+
isError,
|
|
295
|
+
});
|
|
296
|
+
if (parsed.kind === 'agent') {
|
|
297
|
+
return wrap({
|
|
298
|
+
task_id: parsed.taskId ?? '',
|
|
299
|
+
task_type: 'local_agent',
|
|
300
|
+
status: parsed.status ?? 'completed',
|
|
301
|
+
description: parsed.taskId ?? '',
|
|
302
|
+
output: parsed.output ?? '',
|
|
303
|
+
result: parsed.output ?? '',
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
// #49 C3:workflow 轮询的模型面是 formatWorkflowRun raw JSON,parseModelFacingTaskOutput 解不了
|
|
307
|
+
// (kind unknown)会掉进 local_bash 兜底 = 错标 + 空 status/task_id。信封识别先行。
|
|
308
|
+
const wfEnvelope = parseWorkflowPollEnvelope(text);
|
|
309
|
+
if (wfEnvelope) {
|
|
310
|
+
if (WF_TERMINAL_STATUSES.has(wfEnvelope.status))
|
|
311
|
+
markEngineWorkflowNotified(wfEnvelope.taskId);
|
|
312
|
+
return wrap({
|
|
313
|
+
task_id: wfEnvelope.taskId,
|
|
314
|
+
task_type: 'workflow',
|
|
315
|
+
status: wfEnvelope.status,
|
|
316
|
+
description: wfEnvelope.name ?? wfEnvelope.taskId,
|
|
317
|
+
output: projectWorkflowTaskOutput(text),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
return wrap({
|
|
321
|
+
task_id: parsed.taskId ?? '',
|
|
322
|
+
task_type: 'local_bash',
|
|
323
|
+
status: parsed.status ?? '',
|
|
324
|
+
description: parsed.taskId ?? '',
|
|
325
|
+
output: parsed.kind === 'bash' ? (parsed.output ?? '') : body,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
return { content: truncated ? `${text}\n…(truncated)` : text, isError };
|
|
329
|
+
}
|
|
330
|
+
// ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
331
|
+
// ⑤ B 层 —— `structuredToToolUseResult`(按 `structured.type` 14 case)
|
|
332
|
+
// ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
333
|
+
/**
|
|
334
|
+
* design/64 CC-parity `toolUseResult`:引擎结构化细节 → 各卡 outputSchema **精确** typed 形
|
|
335
|
+
* (safeParse 过了才渲富卡,不过就回落 12 行文本兜底)。
|
|
336
|
+
* `structuredPatch` 在**这里**算(T20,core 裁定:引擎发 originalFile、客户端算 hunks)。
|
|
337
|
+
* 全部**防御读**:形状不认得 ⇒ null ⇒ 调用方回落 text 路径(降保真,绝不炸)。
|
|
338
|
+
*/
|
|
339
|
+
export function structuredToToolUseResult(structured,
|
|
340
|
+
/** 压平后的模型面正文 —— 有些形(task-output)只带 identity/status,正文仍在模型面。 */
|
|
341
|
+
modelText) {
|
|
342
|
+
if (typeof structured !== 'object' || structured === null)
|
|
343
|
+
return null;
|
|
344
|
+
const s = structured;
|
|
345
|
+
for (const k of ['originalFile', 'content', 'stdout', 'stderr']) {
|
|
346
|
+
if (typeof s[k] === 'string' && s[k].length > MAX_STRUCTURED_FIELD_CHARS)
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
// run_workflow 回执:登记 outstanding workflow → idle 终态轮询兜底(不赌推送)。
|
|
350
|
+
if (s.type === 'workflow-run' && typeof s.workflowRunId === 'string') {
|
|
351
|
+
registerOutstandingWorkflowRun(s.workflowRunId);
|
|
352
|
+
}
|
|
353
|
+
switch (s.type) {
|
|
354
|
+
case 'ask-user-question': {
|
|
355
|
+
// §4④ liveHitlAskWire 的**壳侧 stamp**(非引擎 wire 形):已作答的 AskUserQuestion 续流重放。
|
|
356
|
+
if (!Array.isArray(s.questions) || typeof s.answers !== 'object' || s.answers === null)
|
|
357
|
+
return null;
|
|
358
|
+
return {
|
|
359
|
+
toolUseResult: {
|
|
360
|
+
questions: s.questions,
|
|
361
|
+
answers: s.answers,
|
|
362
|
+
...(typeof s.annotations === 'object' && s.annotations !== null
|
|
363
|
+
? { annotations: s.annotations }
|
|
364
|
+
: {}),
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
case 'bash': {
|
|
369
|
+
// core 1.207 —— ctrl+b DETACH 的早关形 `{detached:true, task_id}`:渲 CC 自己的
|
|
370
|
+
// backgrounded 卡(它只在 stdout/stderr 为空时显示 "Running in the background"),
|
|
371
|
+
// 所以渲染面丢掉已抓到的尾巴(模型面照旧拿到)。
|
|
372
|
+
if (s.detached === true && typeof s.task_id === 'string') {
|
|
373
|
+
return {
|
|
374
|
+
toolUseResult: {
|
|
375
|
+
stdout: '',
|
|
376
|
+
stderr: '',
|
|
377
|
+
interrupted: false,
|
|
378
|
+
backgroundTaskId: s.task_id,
|
|
379
|
+
backgroundedByUser: true,
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
if (typeof s.stdout !== 'string' && typeof s.stderr !== 'string')
|
|
384
|
+
return null;
|
|
385
|
+
const exitCode = typeof s.exitCode === 'number' ? s.exitCode : null;
|
|
386
|
+
return {
|
|
387
|
+
toolUseResult: {
|
|
388
|
+
stdout: typeof s.stdout === 'string' ? s.stdout : '',
|
|
389
|
+
stderr: typeof s.stderr === 'string' ? s.stderr : '',
|
|
390
|
+
interrupted: false,
|
|
391
|
+
},
|
|
392
|
+
...(exitCode !== null && exitCode !== 0 ? { isError: true } : {}),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
case 'edit': {
|
|
396
|
+
// 单编辑形(core 1.199)或 design/116 的折叠形(`edits: [{old_string,new_string,replace_all?}]`,
|
|
397
|
+
// MultiEdit 退役进 Edit)。从 originalFile 重建编辑后文件,再算 CC 卡渲的 hunks。
|
|
398
|
+
const filePath = typeof s.filePath === 'string' ? s.filePath : undefined;
|
|
399
|
+
const originalFile = typeof s.originalFile === 'string' ? s.originalFile : undefined;
|
|
400
|
+
if (filePath === undefined || originalFile === undefined)
|
|
401
|
+
return null;
|
|
402
|
+
const applyOne = (content, oldStr, newStr, all) => {
|
|
403
|
+
if (oldStr.length === 0)
|
|
404
|
+
return null;
|
|
405
|
+
if (!content.includes(oldStr))
|
|
406
|
+
return null;
|
|
407
|
+
// split/join(不是 String.replace)—— newStr 里的 `$` 绝不能被当成替换模式。
|
|
408
|
+
if (all)
|
|
409
|
+
return content.split(oldStr).join(newStr);
|
|
410
|
+
const i = content.indexOf(oldStr);
|
|
411
|
+
return content.slice(0, i) + newStr + content.slice(i + oldStr.length);
|
|
412
|
+
};
|
|
413
|
+
let newFile = originalFile;
|
|
414
|
+
let firstOld = '';
|
|
415
|
+
let firstNew = '';
|
|
416
|
+
let anyReplaceAll = false;
|
|
417
|
+
if (Array.isArray(s.edits)) {
|
|
418
|
+
for (const e of s.edits) {
|
|
419
|
+
const o = typeof e?.old_string === 'string'
|
|
420
|
+
? e.old_string
|
|
421
|
+
: typeof e?.oldString === 'string'
|
|
422
|
+
? e.oldString
|
|
423
|
+
: undefined;
|
|
424
|
+
const n = typeof e?.new_string === 'string'
|
|
425
|
+
? e.new_string
|
|
426
|
+
: typeof e?.newString === 'string'
|
|
427
|
+
? e.newString
|
|
428
|
+
: undefined;
|
|
429
|
+
if (o === undefined || n === undefined || newFile === null)
|
|
430
|
+
return null;
|
|
431
|
+
const all = e?.replace_all === true || e?.replaceAll === true;
|
|
432
|
+
if (!firstOld) {
|
|
433
|
+
firstOld = o;
|
|
434
|
+
firstNew = n;
|
|
435
|
+
}
|
|
436
|
+
anyReplaceAll = anyReplaceAll || all;
|
|
437
|
+
newFile = applyOne(newFile, o, n, all);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
const o = typeof s.oldString === 'string' ? s.oldString : undefined;
|
|
442
|
+
const n = typeof s.newString === 'string' ? s.newString : undefined;
|
|
443
|
+
if (o === undefined || n === undefined)
|
|
444
|
+
return null;
|
|
445
|
+
firstOld = o;
|
|
446
|
+
firstNew = n;
|
|
447
|
+
anyReplaceAll = s.replaceAll === true;
|
|
448
|
+
newFile = applyOne(originalFile, o, n, anyReplaceAll);
|
|
449
|
+
}
|
|
450
|
+
if (newFile === null)
|
|
451
|
+
return null;
|
|
452
|
+
return {
|
|
453
|
+
toolUseResult: {
|
|
454
|
+
filePath,
|
|
455
|
+
oldString: firstOld,
|
|
456
|
+
newString: firstNew,
|
|
457
|
+
originalFile,
|
|
458
|
+
// 先做 tab 归一再 diff —— CC 本地 Edit 卡同款(tab 缩进文件两边 hunk 才一致)。
|
|
459
|
+
structuredPatch: getPatchFromContents({
|
|
460
|
+
filePath,
|
|
461
|
+
oldContent: convertLeadingTabsToSpaces(originalFile),
|
|
462
|
+
newContent: convertLeadingTabsToSpaces(newFile),
|
|
463
|
+
}),
|
|
464
|
+
userModified: false,
|
|
465
|
+
replaceAll: anyReplaceAll,
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
case 'create':
|
|
470
|
+
case 'update': {
|
|
471
|
+
// Write(core 1.199):`{type: create|update, filePath, content, originalFile?}`。
|
|
472
|
+
const filePath = typeof s.filePath === 'string' ? s.filePath : undefined;
|
|
473
|
+
const content = typeof s.content === 'string' ? s.content : undefined;
|
|
474
|
+
if (filePath === undefined || content === undefined)
|
|
475
|
+
return null;
|
|
476
|
+
const originalFile = typeof s.originalFile === 'string' ? s.originalFile : null;
|
|
477
|
+
const isUpdate = s.type === 'update' && originalFile !== null;
|
|
478
|
+
return {
|
|
479
|
+
toolUseResult: {
|
|
480
|
+
type: isUpdate ? 'update' : 'create',
|
|
481
|
+
filePath,
|
|
482
|
+
content,
|
|
483
|
+
structuredPatch: isUpdate
|
|
484
|
+
? getPatchFromContents({
|
|
485
|
+
filePath,
|
|
486
|
+
oldContent: convertLeadingTabsToSpaces(originalFile),
|
|
487
|
+
newContent: convertLeadingTabsToSpaces(content),
|
|
488
|
+
})
|
|
489
|
+
: [],
|
|
490
|
+
originalFile: isUpdate ? originalFile : null,
|
|
491
|
+
},
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
case 'text': {
|
|
495
|
+
// Read(core 1.202):`{type:"text", file:{filePath, content, numLines, startLine, totalLines}}`
|
|
496
|
+
// —— 与壳 FileReadTool outputSchema 的 text 臂逐字节同形。校验 + 透传。
|
|
497
|
+
const f = s.file;
|
|
498
|
+
if (typeof f?.filePath !== 'string' ||
|
|
499
|
+
typeof f?.content !== 'string' ||
|
|
500
|
+
typeof f?.numLines !== 'number' ||
|
|
501
|
+
typeof f?.startLine !== 'number' ||
|
|
502
|
+
typeof f?.totalLines !== 'number') {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
return { toolUseResult: { type: 'text', file: f } };
|
|
506
|
+
}
|
|
507
|
+
case 'grep': {
|
|
508
|
+
// Grep(core 1.202):files_with_matches 带 {filenames, numFiles};其他模式带 {numLines}。
|
|
509
|
+
// 壳 schema 每个模式都要 numFiles+filenames ⇒ 补安全空值。`count` 模式的 numLines 是
|
|
510
|
+
// 每文件行数**不是** numMatches —— 不许假造,让它掉 text 路径。
|
|
511
|
+
const mode = typeof s.mode === 'string' ? s.mode : 'files_with_matches';
|
|
512
|
+
if (mode === 'files_with_matches') {
|
|
513
|
+
if (!Array.isArray(s.filenames))
|
|
514
|
+
return null;
|
|
515
|
+
return {
|
|
516
|
+
toolUseResult: {
|
|
517
|
+
mode,
|
|
518
|
+
numFiles: typeof s.numFiles === 'number' ? s.numFiles : s.filenames.length,
|
|
519
|
+
filenames: s.filenames,
|
|
520
|
+
},
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
if (mode === 'content') {
|
|
524
|
+
if (typeof s.numLines !== 'number')
|
|
525
|
+
return null;
|
|
526
|
+
return { toolUseResult: { mode, numFiles: 0, filenames: [], numLines: s.numLines } };
|
|
527
|
+
}
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
case 'glob': {
|
|
531
|
+
// Glob(core 1.202):{filenames, numFiles, truncated, durationMs} = 壳 GlobTool outputSchema。
|
|
532
|
+
if (!Array.isArray(s.filenames) || typeof s.numFiles !== 'number')
|
|
533
|
+
return null;
|
|
534
|
+
return {
|
|
535
|
+
toolUseResult: {
|
|
536
|
+
filenames: s.filenames,
|
|
537
|
+
numFiles: s.numFiles,
|
|
538
|
+
truncated: s.truncated === true,
|
|
539
|
+
durationMs: typeof s.durationMs === 'number' ? s.durationMs : 0,
|
|
540
|
+
},
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
case 'notebook-edit': {
|
|
544
|
+
// NotebookEdit(core 1.202)。卡只渲 cell_id/new_source/error,其余 REQUIRED 字段用派生/空值
|
|
545
|
+
// 满足(引擎不发编辑后 notebook JSON,渲染面也从不读它们)。
|
|
546
|
+
const notebookPath = typeof s.notebookPath === 'string' ? s.notebookPath : undefined;
|
|
547
|
+
const newSource = typeof s.newSource === 'string' ? s.newSource : undefined;
|
|
548
|
+
const editMode = typeof s.editMode === 'string' ? s.editMode : undefined;
|
|
549
|
+
if (notebookPath === undefined || newSource === undefined || editMode === undefined)
|
|
550
|
+
return null;
|
|
551
|
+
const cellType = typeof s.cellType === 'string' ? s.cellType : 'code';
|
|
552
|
+
const cellId = typeof s.cellId === 'string' ? s.cellId : typeof s.newCellId === 'string' ? s.newCellId : undefined;
|
|
553
|
+
return {
|
|
554
|
+
toolUseResult: {
|
|
555
|
+
new_source: newSource,
|
|
556
|
+
...(cellId !== undefined ? { cell_id: cellId } : {}),
|
|
557
|
+
cell_type: cellType,
|
|
558
|
+
language: cellType === 'markdown' ? 'markdown' : 'python',
|
|
559
|
+
edit_mode: editMode,
|
|
560
|
+
notebook_path: notebookPath,
|
|
561
|
+
original_file: typeof s.originalFile === 'string' ? s.originalFile : '',
|
|
562
|
+
updated_file: '',
|
|
563
|
+
},
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
case 'cron-create': {
|
|
567
|
+
if (typeof s.id !== 'string' || typeof s.humanSchedule !== 'string')
|
|
568
|
+
return null;
|
|
569
|
+
return { toolUseResult: { id: s.id, humanSchedule: s.humanSchedule, recurring: s.recurring === true } };
|
|
570
|
+
}
|
|
571
|
+
case 'cron-delete': {
|
|
572
|
+
if (typeof s.id !== 'string')
|
|
573
|
+
return null;
|
|
574
|
+
return { toolUseResult: { id: s.id } };
|
|
575
|
+
}
|
|
576
|
+
case 'cron-list': {
|
|
577
|
+
// ⚠️ 无 `prompt` —— core 的 ScheduledTaskSummary 刻意不发全意图(安全边界 [478]①);
|
|
578
|
+
// 壳 outputSchema 放宽成 optional 好让 safeParse 过。坏行丢弃,不因一行毁整卡。
|
|
579
|
+
if (!Array.isArray(s.jobs))
|
|
580
|
+
return null;
|
|
581
|
+
const jobs = s.jobs.flatMap(j => typeof j?.id === 'string' && typeof j?.cron === 'string' && typeof j?.humanSchedule === 'string'
|
|
582
|
+
? [
|
|
583
|
+
{
|
|
584
|
+
id: j.id,
|
|
585
|
+
cron: j.cron,
|
|
586
|
+
humanSchedule: j.humanSchedule,
|
|
587
|
+
...(typeof j.prompt === 'string' ? { prompt: j.prompt } : {}),
|
|
588
|
+
// [935]:布尔**全值**透传(recurring:false ⇒ [once]、durable:false ⇒ [session]);
|
|
589
|
+
// 旧形 `=== true` 会把 false 剥掉 = 标记面在 live 路径永不可达。
|
|
590
|
+
...(typeof j.recurring === 'boolean' ? { recurring: j.recurring } : {}),
|
|
591
|
+
...(typeof j.durable === 'boolean' ? { durable: j.durable } : {}),
|
|
592
|
+
},
|
|
593
|
+
]
|
|
594
|
+
: []);
|
|
595
|
+
return { toolUseResult: { jobs } };
|
|
596
|
+
}
|
|
597
|
+
case 'agent': {
|
|
598
|
+
// Agent 异步启动(core 1.272+ [666] 默认后台化主路径)。outputFile 是 schema 必填但 live lane
|
|
599
|
+
// 没有本地文件 ⇒ '' + canReadOutputFile:false(渲染文本因此省掉 file-tail 分支,与引擎模型面一致)。
|
|
600
|
+
if (s.status !== 'async_launched')
|
|
601
|
+
return null;
|
|
602
|
+
const agentId = typeof s.task_id === 'string' ? s.task_id : undefined;
|
|
603
|
+
// 三个都是 typed 形必填。缺 description/prompt 必须回落 **text 路径**而不是降级成 ''
|
|
604
|
+
// ——富卡在空 prompt 时收起展开面 **且** 压过 text 兜底,'' 会两条 lane 都丢信息(对抗复审 M-1)。
|
|
605
|
+
if (agentId === undefined || typeof s.description !== 'string' || typeof s.prompt !== 'string') {
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
registerOutstandingBgTask(agentId, s.description);
|
|
609
|
+
return {
|
|
610
|
+
toolUseResult: {
|
|
611
|
+
isAsync: true,
|
|
612
|
+
status: 'async_launched',
|
|
613
|
+
agentId,
|
|
614
|
+
description: s.description,
|
|
615
|
+
prompt: s.prompt,
|
|
616
|
+
outputFile: '',
|
|
617
|
+
canReadOutputFile: false,
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
case 'task-output': {
|
|
622
|
+
// TaskOutput(core 1.217 [379])。identity/status 取 structured(权威),正文取 `content`;
|
|
623
|
+
// **T13/T24 退位**:只有 structured 没带正文时才回落解模型面文本。
|
|
624
|
+
const taskId = typeof s.task_id === 'string' ? s.task_id : undefined;
|
|
625
|
+
if (taskId === undefined)
|
|
626
|
+
return null;
|
|
627
|
+
const retrieval = s.retrieval_status === 'timeout' || s.retrieval_status === 'not_ready'
|
|
628
|
+
? s.retrieval_status
|
|
629
|
+
: 'success';
|
|
630
|
+
const taskType = typeof s.taskType === 'string' ? s.taskType : '';
|
|
631
|
+
const inner = s.details && typeof s.details === 'object' ? s.details : undefined;
|
|
632
|
+
const hasStatus = typeof s.status === 'string' && s.status.length > 0;
|
|
633
|
+
const hasContent = typeof s.content === 'string';
|
|
634
|
+
// 🔴 惰性回落:structured 把 status 和正文都带齐了就**根本不解**模型面文本(T13 正则退位);
|
|
635
|
+
// 少哪一半就只为那一半解一次。判据锚在「这两个量还缺不缺」,不锚在「structured 在不在」。
|
|
636
|
+
const parsed = hasStatus && hasContent ? null : parseModelFacingTaskOutput(modelText ?? '');
|
|
637
|
+
const status = hasStatus
|
|
638
|
+
? s.status
|
|
639
|
+
: (parsed?.status ?? (retrieval === 'success' ? 'completed' : 'running'));
|
|
640
|
+
const bodyText = hasContent ? s.content : parsed?.output;
|
|
641
|
+
const error = typeof s.error === 'string' ? s.error : undefined;
|
|
642
|
+
if (taskType === 'background_bash') {
|
|
643
|
+
const exitCode = typeof inner?.exitCode === 'number' ? inner.exitCode : undefined;
|
|
644
|
+
return {
|
|
645
|
+
toolUseResult: {
|
|
646
|
+
retrieval_status: retrieval,
|
|
647
|
+
task: {
|
|
648
|
+
task_id: taskId,
|
|
649
|
+
task_type: 'local_bash',
|
|
650
|
+
status,
|
|
651
|
+
description: taskId,
|
|
652
|
+
output: bodyText ?? '',
|
|
653
|
+
...(exitCode !== undefined ? { exitCode } : {}),
|
|
654
|
+
...(error !== undefined ? { error } : {}),
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
if (taskType === 'agent' || taskType === 'background_agent') {
|
|
660
|
+
return {
|
|
661
|
+
toolUseResult: {
|
|
662
|
+
retrieval_status: retrieval,
|
|
663
|
+
task: {
|
|
664
|
+
task_id: taskId,
|
|
665
|
+
task_type: 'local_agent',
|
|
666
|
+
status,
|
|
667
|
+
description: taskId,
|
|
668
|
+
output: bodyText ?? '',
|
|
669
|
+
result: bodyText ?? '',
|
|
670
|
+
...(error !== undefined ? { error } : {}),
|
|
671
|
+
},
|
|
672
|
+
},
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
const wfName = typeof inner?.name === 'string' && inner.name.length > 0 ? inner.name : undefined;
|
|
676
|
+
let output = bodyText ?? '';
|
|
677
|
+
if (taskType === 'workflow') {
|
|
678
|
+
// 终态轮询卡经过投影 = 模型 in-band 已收到结局 ⇒ 预 seed runId 去重(补发通道对同 runId 丢弃)。
|
|
679
|
+
// 活跃态(running/queued/paused)绝不 seed。
|
|
680
|
+
if (WF_TERMINAL_STATUSES.has(status))
|
|
681
|
+
markEngineWorkflowNotified(taskId);
|
|
682
|
+
// workflow 轮询的模型面是 raw JSON(formatWorkflowRun)—— 投影成一行人话,绝不上 blob。
|
|
683
|
+
const jsonSrc = output.length > 0 ? output : (modelText ?? '');
|
|
684
|
+
output = projectWorkflowTaskOutput(jsonSrc);
|
|
685
|
+
}
|
|
686
|
+
return {
|
|
687
|
+
toolUseResult: {
|
|
688
|
+
retrieval_status: retrieval,
|
|
689
|
+
task: {
|
|
690
|
+
task_id: taskId,
|
|
691
|
+
task_type: taskType || 'task',
|
|
692
|
+
status,
|
|
693
|
+
description: wfName ?? taskId,
|
|
694
|
+
output,
|
|
695
|
+
...(error !== undefined ? { error } : {}),
|
|
696
|
+
},
|
|
697
|
+
},
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
// 认不得的形(含过渡期 1.199 `multiedit`,以及白名单里本包尚无 typed 卡的那些)一律掉
|
|
701
|
+
// **永不隐形**的 text 路径 —— 降保真,不炸。
|
|
702
|
+
default:
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
// ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
707
|
+
// ⑥ E 层 —— 按工具名的富卡补位(TodoWrite / ReportFindings)
|
|
708
|
+
// ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
709
|
+
/**
|
|
710
|
+
* #117a —— **模型发起**的后台 Bash 回执探测器(cli `engineBgShellPanelStore.detectEngineBgShellReceipt`
|
|
711
|
+
* 逐字)。引擎侧秒关这张卡、从不进壳的 LocalShellTask registry,只能从回执文案桥过去。
|
|
712
|
+
* 🔴 **刻意**要求 INPUT 上 `run_in_background === true`:ctrl+B 的 detach lane
|
|
713
|
+
* (「Command moved to background」)归 engineToolDetach 管(它的行生命周期是有意的 settle-on-detach),
|
|
714
|
+
* 在这里重复登记会长出双行。
|
|
715
|
+
* 判定在库、**执行留宿主**(面板 store 写入 + 宿主 run 映射)—— 走 chrome `bgshell_register` 臂。
|
|
716
|
+
*/
|
|
717
|
+
const BG_RECEIPT_ANCHOR = /^(?:Started in background|Command running in background|Command moved to background); task_id=([A-Za-z0-9_-]+)/;
|
|
718
|
+
export function detectEngineBgShellReceipt(rawInput, outputText) {
|
|
719
|
+
if (typeof outputText !== 'string')
|
|
720
|
+
return null;
|
|
721
|
+
const input = (rawInput ?? {});
|
|
722
|
+
if (input.run_in_background !== true)
|
|
723
|
+
return null;
|
|
724
|
+
const m = BG_RECEIPT_ANCHOR.exec(outputText);
|
|
725
|
+
if (!m?.[1])
|
|
726
|
+
return null;
|
|
727
|
+
const command = typeof input.command === 'string' ? input.command : '(engine bash)';
|
|
728
|
+
const description = typeof input.description === 'string' ? input.description : undefined;
|
|
729
|
+
return {
|
|
730
|
+
kind: 'register',
|
|
731
|
+
taskId: m[1],
|
|
732
|
+
command,
|
|
733
|
+
...(description !== undefined ? { description } : {}),
|
|
734
|
+
receipt: outputText,
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* TodoWrite 清单卡:引擎不发 todo structured,全量新表骑在 tool_use INPUT 上 ⇒ 从它合成 typed 结果。
|
|
739
|
+
* `oldTodos` = 上一次的活体清单(调用方持:一个 session 一份,正是划删线 diff 要的粒度)。
|
|
740
|
+
*/
|
|
741
|
+
export function todoWriteToolUseResult(rawInput, oldTodos, isError) {
|
|
742
|
+
const todos = rawInput?.todos;
|
|
743
|
+
if (!Array.isArray(todos))
|
|
744
|
+
return null;
|
|
745
|
+
return {
|
|
746
|
+
toolUseResult: { oldTodos, newTodos: todos, verificationNudgeNeeded: false },
|
|
747
|
+
isError,
|
|
748
|
+
newTodos: todos,
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* ReportFindings(core 1.288 [811]b,引擎侧执行的合成回声工具):details 是 `{count, level?, findings}`
|
|
753
|
+
* **没有 type 判别位**(core synthetic-tools.js)⇒ B 层的 type-keyed switch 认领不了,按**工具名**认。
|
|
754
|
+
* 双腿:优先 wire structured;引擎没发就从 tool_use INPUT 合成(回声工具:input.findings ≡ output.findings)。
|
|
755
|
+
*/
|
|
756
|
+
export function reportFindingsToolUseResult(structured, rawInput) {
|
|
757
|
+
const s = structured && typeof structured === 'object' ? structured : undefined;
|
|
758
|
+
const input = rawInput;
|
|
759
|
+
const findings = Array.isArray(s?.findings)
|
|
760
|
+
? s.findings
|
|
761
|
+
: Array.isArray(input?.findings)
|
|
762
|
+
? input.findings
|
|
763
|
+
: undefined;
|
|
764
|
+
if (findings === undefined)
|
|
765
|
+
return null;
|
|
766
|
+
const level = typeof s?.level === 'string' ? s.level : typeof input?.level === 'string' ? input.level : undefined;
|
|
767
|
+
return {
|
|
768
|
+
toolUseResult: {
|
|
769
|
+
count: typeof s?.count === 'number' ? s.count : findings.length,
|
|
770
|
+
...(level !== undefined ? { level } : {}),
|
|
771
|
+
findings,
|
|
772
|
+
},
|
|
773
|
+
};
|
|
774
|
+
}
|