aicodeswitch 5.2.12 → 6.0.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.
@@ -0,0 +1,383 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectTurnEnd = detectTurnEnd;
4
+ exports.extractActivityEvents = extractActivityEvents;
5
+ exports.deriveLastActivity = deriveLastActivity;
6
+ const MAX_SUMMARY_LEN = 120;
7
+ function summarizeText(text) {
8
+ const clean = (text || '').replace(/\s+/g, ' ').trim();
9
+ if (!clean)
10
+ return '';
11
+ if (clean.length <= MAX_SUMMARY_LEN)
12
+ return clean;
13
+ return clean.slice(0, MAX_SUMMARY_LEN) + '…';
14
+ }
15
+ /** 提取一条 user 消息里的真实文本(忽略 tool_result / image 等非文本块);无文本返回空串 */
16
+ function summarizeUserText(content) {
17
+ if (typeof content === 'string')
18
+ return content.trim() ? summarizeText(content) : '';
19
+ if (Array.isArray(content)) {
20
+ const texts = [];
21
+ for (const block of content) {
22
+ if ((block === null || block === void 0 ? void 0 : block.type) === 'text' && block.text)
23
+ texts.push(block.text);
24
+ else if (typeof block === 'string')
25
+ texts.push(block);
26
+ // tool_result / image 等不计入
27
+ }
28
+ return texts.length ? summarizeText(texts.join('\n')) : '';
29
+ }
30
+ return '';
31
+ }
32
+ /**
33
+ * 仅当「本轮请求的末条消息是一条新的用户文本提问」时返回其摘要;否则返回空串。
34
+ *
35
+ * 关键:Claude Code / Codex 每轮会把完整历史重发,工具调用后续轮的末条 user 消息是
36
+ * tool_result(无文本)。若像以前那样向前回溯找「最近一条含文本的 user」,会反复命中
37
+ * 同一条原始提问 → 活动流/路径出现大量重复用户消息。改为只认末条,从根上消除重复。
38
+ */
39
+ function extractPromptSummary(body) {
40
+ const messages = body === null || body === void 0 ? void 0 : body.messages;
41
+ if (Array.isArray(messages) && messages.length > 0) {
42
+ const last = messages[messages.length - 1];
43
+ if (last && last.role === 'user')
44
+ return summarizeUserText(last.content);
45
+ return ''; // 末条非 user(assistant / tool_result 续轮)→ 不是新一轮提问
46
+ }
47
+ // OpenAI Responses / 简单 input
48
+ const input = body === null || body === void 0 ? void 0 : body.input;
49
+ if (typeof input === 'string')
50
+ return summarizeText(input);
51
+ if (Array.isArray(input) && input.length > 0) {
52
+ const last = input[input.length - 1];
53
+ if (last && last.role === 'user')
54
+ return summarizeUserText(last.content);
55
+ }
56
+ return '';
57
+ }
58
+ /** 从 assistant content 数组(Claude 风格)抽取 tool_use 块 */
59
+ function pushClaudeToolUses(content, out) {
60
+ for (const block of content) {
61
+ if (!block || typeof block !== 'object')
62
+ continue;
63
+ if (block.type === 'tool_use' && block.name) {
64
+ out.push({
65
+ kind: 'tool_use',
66
+ toolName: block.name,
67
+ summary: summarizeToolInput(block.name, block.input),
68
+ });
69
+ }
70
+ }
71
+ }
72
+ /** 美化工具调用的关键入参为一行摘要 */
73
+ function summarizeToolInput(name, input) {
74
+ if (!input || typeof input !== 'object')
75
+ return name;
76
+ try {
77
+ if (name === 'Edit' || name === 'Write' || name === 'MultiEdit') {
78
+ return `${name}(${input.file_path || input.filePath || '?'})`;
79
+ }
80
+ if (name === 'Read')
81
+ return `Read(${input.file_path || input.filePath || '?'})`;
82
+ if (name === 'Bash' || name === 'BashOutput' || name === 'KillShell') {
83
+ const cmd = input.command || input.cmd || '';
84
+ return `Bash(${summarizeText(String(cmd))})`;
85
+ }
86
+ if (name === 'Grep' || name === 'Glob') {
87
+ return `${name}(${input.pattern || input.query || '?'})`;
88
+ }
89
+ if (name === 'WebFetch' || name === 'WebSearch') {
90
+ return `${name}(${input.url || input.query || '?'})`;
91
+ }
92
+ if (name === 'Agent' || name === 'Task') {
93
+ return `${name}(${input.description || input.subagent_type || '?'})`;
94
+ }
95
+ // 兜底:取第一个字符串值
96
+ const firstStr = Object.values(input).find(v => typeof v === 'string');
97
+ return firstStr ? `${name}(${summarizeText(firstStr)})` : name;
98
+ }
99
+ catch (_a) {
100
+ return name;
101
+ }
102
+ }
103
+ /**
104
+ * 从响应侧解析出本轮产生的活动(tool_use / response / error)。
105
+ * 流式优先(downstreamResponseBody 含 SSE),其次 responseBody 对象。
106
+ */
107
+ function extractResponseActivities(input) {
108
+ var _a, _b, _c, _d, _e;
109
+ const out = [];
110
+ const { downstreamResponseBody, responseBody } = input;
111
+ // 1) 流式:尝试解析 SSE,收集 tool_use 与末尾文本
112
+ if (typeof downstreamResponseBody === 'string' &&
113
+ (downstreamResponseBody.includes('event:') || downstreamResponseBody.includes('data:'))) {
114
+ const collected = collectFromSSE(downstreamResponseBody);
115
+ for (const tu of collected.toolUses)
116
+ out.push({ kind: 'tool_use', toolName: tu.name, summary: tu.summary });
117
+ if (collected.text)
118
+ out.push({ kind: 'response', summary: summarizeText(collected.text) });
119
+ if (collected.thinking)
120
+ out.push({ kind: 'thinking', summary: '[思考]' });
121
+ if (out.length > 0)
122
+ return out;
123
+ }
124
+ // 2) 非流式对象
125
+ const parsed = (_a = parseJSONObject(downstreamResponseBody)) !== null && _a !== void 0 ? _a : parseJSONObject(responseBody);
126
+ if (parsed) {
127
+ // Claude 风格 content 数组
128
+ if (Array.isArray(parsed.content)) {
129
+ pushClaudeToolUses(parsed.content, out);
130
+ let text = '';
131
+ for (const block of parsed.content) {
132
+ if ((block === null || block === void 0 ? void 0 : block.type) === 'text' && block.text)
133
+ text += block.text;
134
+ }
135
+ if (text)
136
+ out.push({ kind: 'response', summary: summarizeText(text) });
137
+ if (out.length > 0)
138
+ return out;
139
+ }
140
+ // OpenAI Chat choices
141
+ if ((_c = (_b = parsed.choices) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.message) {
142
+ const msg = parsed.choices[0].message;
143
+ if (Array.isArray(msg.tool_calls)) {
144
+ for (const tc of msg.tool_calls) {
145
+ const name = ((_d = tc.function) === null || _d === void 0 ? void 0 : _d.name) || '';
146
+ out.push({ kind: 'tool_use', toolName: name, summary: summarizeToolInput(name, safeParseJSON((_e = tc.function) === null || _e === void 0 ? void 0 : _e.arguments)) });
147
+ }
148
+ }
149
+ if (msg.content)
150
+ out.push({ kind: 'response', summary: summarizeText(String(msg.content)) });
151
+ if (out.length > 0)
152
+ return out;
153
+ }
154
+ // OpenAI Responses output
155
+ if (parsed.output) {
156
+ const outputs = Array.isArray(parsed.output) ? parsed.output : [parsed.output];
157
+ let text = '';
158
+ for (const item of outputs) {
159
+ if ((item === null || item === void 0 ? void 0 : item.type) === 'function_call') {
160
+ const name = item.name || '';
161
+ out.push({ kind: 'tool_use', toolName: name, summary: summarizeToolInput(name, safeParseJSON(item.arguments)) });
162
+ }
163
+ else if ((item === null || item === void 0 ? void 0 : item.type) === 'message' && Array.isArray(item.content)) {
164
+ for (const c of item.content) {
165
+ if ((c === null || c === void 0 ? void 0 : c.type) === 'output_text' && c.text)
166
+ text += c.text;
167
+ }
168
+ }
169
+ }
170
+ if (text)
171
+ out.push({ kind: 'response', summary: summarizeText(text) });
172
+ if (out.length > 0)
173
+ return out;
174
+ }
175
+ }
176
+ return out;
177
+ }
178
+ function parseJSONObject(v) {
179
+ if (!v)
180
+ return null;
181
+ if (typeof v === 'object')
182
+ return v;
183
+ if (typeof v === 'string') {
184
+ try {
185
+ return JSON.parse(v);
186
+ }
187
+ catch (_a) {
188
+ return null;
189
+ }
190
+ }
191
+ return null;
192
+ }
193
+ function safeParseJSON(v) {
194
+ if (typeof v !== 'string')
195
+ return v;
196
+ try {
197
+ return JSON.parse(v);
198
+ }
199
+ catch (_a) {
200
+ return v;
201
+ }
202
+ }
203
+ /** 从 SSE 文本中粗略收集 tool_use 名称与拼接文本(兼容 Anthropic / OpenAI 事件) */
204
+ function collectFromSSE(raw) {
205
+ var _a, _b, _c, _d;
206
+ const toolUses = [];
207
+ let text = '';
208
+ let thinking = '';
209
+ const seenToolIds = new Set();
210
+ // 按 "data: " 分块
211
+ const blocks = raw.split(/\r?\n/);
212
+ let buffer = null;
213
+ for (const line of blocks) {
214
+ if (!line.startsWith('data:'))
215
+ continue;
216
+ const payload = line.slice(5).trim();
217
+ if (!payload || payload === '[DONE]')
218
+ continue;
219
+ try {
220
+ buffer = JSON.parse(payload);
221
+ }
222
+ catch (_e) {
223
+ continue;
224
+ }
225
+ if (!buffer)
226
+ continue;
227
+ // Anthropic content_block_delta / content_block_start
228
+ if (buffer.type === 'content_block_start' && buffer.content_block) {
229
+ const cb = buffer.content_block;
230
+ if (cb.type === 'tool_use' && cb.name) {
231
+ const id = buffer.index != null ? String(buffer.index) : cb.id || Math.random().toString();
232
+ if (!seenToolIds.has(id)) {
233
+ seenToolIds.add(id);
234
+ toolUses.push({ name: cb.name, summary: cb.name });
235
+ }
236
+ }
237
+ }
238
+ else if (buffer.type === 'content_block_delta' && buffer.delta) {
239
+ const d = buffer.delta;
240
+ if (d.type === 'text_delta' && d.text)
241
+ text += d.text;
242
+ else if (d.type === 'thinking_delta' && d.thinking)
243
+ thinking += d.thinking;
244
+ else if (d.type === 'input_json_delta' && d.partial_json) {
245
+ // 工具入参增量,尝试补全 summary(best-effort,最后一条 tool_use)
246
+ const last = toolUses[toolUses.length - 1];
247
+ if (last && last.summary === last.name) {
248
+ // 不做完整拼接,保持工具名即可
249
+ }
250
+ }
251
+ }
252
+ // OpenAI Chat 流式 choices
253
+ else if ((_b = (_a = buffer.choices) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.delta) {
254
+ const d = buffer.choices[0].delta;
255
+ if (d.content)
256
+ text += typeof d.content === 'string' ? d.content : '';
257
+ if (Array.isArray(d.tool_calls)) {
258
+ for (const tc of d.tool_calls) {
259
+ const name = (_c = tc.function) === null || _c === void 0 ? void 0 : _c.name;
260
+ if (name) {
261
+ const id = tc.id || name + ((_d = tc.index) !== null && _d !== void 0 ? _d : '');
262
+ if (!seenToolIds.has(id)) {
263
+ seenToolIds.add(id);
264
+ toolUses.push({ name, summary: name });
265
+ }
266
+ }
267
+ }
268
+ }
269
+ }
270
+ // OpenAI Responses 流式
271
+ else if (buffer.type === 'response.output_item.added' && buffer.item) {
272
+ if (buffer.item.type === 'function_call' && buffer.item.name) {
273
+ toolUses.push({ name: buffer.item.name, summary: buffer.item.name });
274
+ }
275
+ }
276
+ else if (buffer.type === 'response.output_text.delta' && buffer.delta) {
277
+ text += buffer.delta;
278
+ }
279
+ else if (buffer.type === 'response.function_call_arguments.delta') {
280
+ // ignore
281
+ }
282
+ }
283
+ return { toolUses, text, thinking };
284
+ }
285
+ let eventCounter = 0;
286
+ function nextId(ts) {
287
+ eventCounter = (eventCounter + 1) % 1000000;
288
+ return `evt_${ts}_${eventCounter}`;
289
+ }
290
+ /**
291
+ * 检测本轮响应是否表示「一轮工作结束」(turn end),比 60s 空闲延时更精确、即时。
292
+ *
293
+ * 这正是官方 SDK 用来判定一轮完成的语义(Claude Agent SDK 的 query() 迭代器结束、
294
+ * Codex SDK 的 thread.run() resolve)在响应里的具体字段:
295
+ * - Claude(Messages):响应 stop_reason。`tool_use` = 还要继续调工具(未结束);
296
+ * `end_turn` / `stop_sequence` / `max_tokens` / `refusal` 等 = 本轮结束、交回用户。
297
+ * - Codex(Responses):本轮若含 function_call = 还要继续;否则仅 message/output_text 且
298
+ * 有 response.completed = 本轮结束。
299
+ *
300
+ * 返回:true=本轮结束;false=仍将继续;null=无法判定(回退到时间窗启发式)。
301
+ * 解析下游响应(downstreamResponseBody,已是客户端协议格式);兼容流式 SSE 文本与非流式 JSON。
302
+ */
303
+ function detectTurnEnd(agent, downstream, responseBody) {
304
+ const downRaw = typeof downstream === 'string' ? downstream : (downstream ? JSON.stringify(downstream) : '');
305
+ const bodyRaw = typeof responseBody === 'string' ? responseBody : (responseBody ? JSON.stringify(responseBody) : '');
306
+ const raw = downRaw || bodyRaw;
307
+ if (!raw)
308
+ return null;
309
+ if (agent === 'codex') {
310
+ // Responses:含 function_call → 还要继续;否则有完成态 → 本轮结束
311
+ if (/"type"\s*:\s*"function_call"/.test(raw))
312
+ return false;
313
+ if (/"type"\s*:\s*"response\.completed"/.test(raw) || /"status"\s*:\s*"completed"/.test(raw))
314
+ return true;
315
+ return null;
316
+ }
317
+ // Claude:看 stop_reason(流式 message_delta 或非流式 JSON 都带该字段)
318
+ const m = raw.match(/"stop_reason"\s*:\s*"([a-z_]+)"/);
319
+ const stopReason = m ? m[1] : null;
320
+ if (stopReason === 'tool_use')
321
+ return false;
322
+ if (stopReason)
323
+ return true; // end_turn / stop_sequence / max_tokens / refusal / ...
324
+ return null;
325
+ }
326
+ /**
327
+ * 从一次代理请求抽取本轮 ActivityEvent 列表。
328
+ * 顺序:[prompt?]? → [thinking]? → tool_use* → response? → error?
329
+ * 末尾按需追加 error 事件(statusCode >= 400)。
330
+ */
331
+ function extractActivityEvents(input) {
332
+ const events = [];
333
+ const base = {
334
+ id: '',
335
+ ts: input.timestamp,
336
+ sessionId: input.sessionId,
337
+ agent: input.agent,
338
+ source: input.source,
339
+ keyId: input.keyId,
340
+ keyName: input.keyName,
341
+ tokensDelta: input.tokensDelta,
342
+ statusCode: input.statusCode,
343
+ };
344
+ // 用户提问(仅当能提取到文本)
345
+ const promptSummary = extractPromptSummary(input.body);
346
+ if (promptSummary) {
347
+ events.push(Object.assign(Object.assign({}, base), { id: nextId(input.timestamp), kind: 'prompt', summary: promptSummary }));
348
+ }
349
+ // 响应侧活动
350
+ const respActs = extractResponseActivities(input);
351
+ for (const a of respActs) {
352
+ events.push(Object.assign(Object.assign(Object.assign({}, base), { id: nextId(input.timestamp) }), a));
353
+ }
354
+ // 499 = 客户端主动断开(用户放弃停止任务),视为「已取消」而非错误
355
+ if (input.statusCode === 499) {
356
+ events.push(Object.assign(Object.assign({}, base), { id: nextId(input.timestamp), kind: 'cancelled', summary: '已取消 (499)' }));
357
+ }
358
+ else if (input.statusCode && input.statusCode >= 400) {
359
+ events.push(Object.assign(Object.assign({}, base), { id: nextId(input.timestamp), kind: 'error', summary: `请求失败 (${input.statusCode})` }));
360
+ }
361
+ return events;
362
+ }
363
+ /**
364
+ * 从本轮活动里取一个"最近活动摘要"用于节点副标 + 最近工具名。
365
+ */
366
+ function deriveLastActivity(events) {
367
+ for (let i = events.length - 1; i >= 0; i--) {
368
+ const e = events[i];
369
+ if (e.kind === 'tool_use' && e.toolName) {
370
+ return { summary: e.summary, toolName: e.toolName };
371
+ }
372
+ }
373
+ // 没有工具调用,取最后一条非 prompt 事件
374
+ for (let i = events.length - 1; i >= 0; i--) {
375
+ const e = events[i];
376
+ if (e.kind === 'response' || e.kind === 'error') {
377
+ return { summary: e.summary };
378
+ }
379
+ }
380
+ // 兜底取 prompt
381
+ const prompt = events.find(e => e.kind === 'prompt');
382
+ return prompt ? { summary: prompt.summary } : {};
383
+ }