@muyichengshayu/promptx 0.2.9 → 0.2.10
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/CHANGELOG.md +8 -0
- package/apps/runner/src/engines/claudeCodeRunner.js +71 -9
- package/apps/runner/src/engines/claudeCodeRunner.test.js +189 -1
- package/apps/runner/src/engines/kimiCodeRunner.js +34 -11
- package/apps/runner/src/engines/openCodeRunner.test.js +164 -1
- package/apps/{server/src/agents → runner/src/engines}/runnerContract.test.js +67 -0
- package/apps/runner/src/runManager.test.js +1 -1
- package/apps/server/src/agents/claudeCodeRunner.js +1 -839
- package/apps/server/src/agents/kimiCodeRunner.js +1 -552
- package/apps/server/src/agents/openCodeRunner.js +1 -636
- package/apps/web/dist/assets/{CodexSessionManagerDialog-_qLljY7F.js → CodexSessionManagerDialog-Bq8GNsSY.js} +1 -1
- package/apps/web/dist/assets/{TaskDiffReviewDialog-DpW8S8yT.js → TaskDiffReviewDialog-pec7Va_O.js} +2 -2
- package/apps/web/dist/assets/{WorkbenchSettingsDialog-CYfh5G7c.js → WorkbenchSettingsDialog-Bzk1BUM2.js} +1 -1
- package/apps/web/dist/assets/{WorkbenchView-A8nm0NH9.js → WorkbenchView-BYtJENv9.js} +22 -22
- package/apps/web/dist/assets/{index-DHF_zkYI.js → index-DW5iQdjP.js} +2 -2
- package/apps/web/dist/index.html +1 -1
- package/package.json +1 -1
- package/apps/server/src/agents/claudeCodeRunner.test.js +0 -433
- package/apps/server/src/agents/openCodeRunner.test.js +0 -236
- /package/apps/{server/src/agents → runner/src/engines}/kimiCodeRunner.test.js +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.10
|
|
4
|
+
|
|
5
|
+
- 修复 Claude Code 结果完成时偶发卡死的问题:当 Claude CLI 在 result 事件后延迟退出时,runner 会进入优雅退出等待,避免强制 kill 导致消息丢失。
|
|
6
|
+
- 完善 Kimi Code 执行过程展示:思考过程(thinking)不再被隐藏,可直接在执行面板中查看;工具调用(ReadFile / WriteFile / Shell 等)正确映射到前端分类(读取/写入/命令),便于分组展示。
|
|
7
|
+
- 补齐 Kimi Code 事件流完整性:新增 thread.started 事件发送,修复会话创建元信息泄漏到过程日志的问题。
|
|
8
|
+
- 健壮化 Kimi Code 参数解析:支持 arguments 既为字符串也可为对象,避免空参数展示为 `{}`。
|
|
9
|
+
- 代码变更弹窗轮次选择器日期精确到秒:select option 中显示完整时分秒,方便在密集 run 中快速定位目标轮次。
|
|
10
|
+
|
|
3
11
|
## 0.2.9
|
|
4
12
|
|
|
5
13
|
- 接入 `Kimi Code CLI` 执行引擎:工作台 Agent 选择器新增 Kimi Code 选项,支持本地会话发现、线程复用、TodoList 过程展示与停止控制,与现有 Codex / Claude Code / OpenCode 并列为第四类 Agent。
|
|
@@ -21,6 +21,14 @@ import { createManagedSpawnOptions, forceStopChildProcess } from '../processCont
|
|
|
21
21
|
const CLAUDE_CODE_BIN = process.env.CLAUDE_CODE_BIN || 'claude'
|
|
22
22
|
const CLAUDE_DEFAULT_ARGS = ['--dangerously-skip-permissions']
|
|
23
23
|
const RESOLVED_CLAUDE_CODE_BIN = resolveClaudeCodeBinary()
|
|
24
|
+
const CLAUDE_RESULT_EXIT_GRACE_MS = Math.max(
|
|
25
|
+
0,
|
|
26
|
+
Number(process.env.PROMPTX_CLAUDE_RESULT_EXIT_GRACE_MS) || 3000
|
|
27
|
+
)
|
|
28
|
+
const CLAUDE_RESULT_FORCE_STOP_GRACE_MS = Math.max(
|
|
29
|
+
200,
|
|
30
|
+
Number(process.env.PROMPTX_CLAUDE_RESULT_FORCE_STOP_GRACE_MS) || 1000
|
|
31
|
+
)
|
|
24
32
|
|
|
25
33
|
function resolveClaudeCodeBinary() {
|
|
26
34
|
if (process.platform !== 'win32') {
|
|
@@ -798,8 +806,60 @@ export function streamPromptToClaudeCodeSession(sessionInput, prompt, callbacks
|
|
|
798
806
|
let finalSessionId = String(session.engineSessionId || session.engineThreadId || session.codexThreadId || '').trim()
|
|
799
807
|
let fatalClaudeErrorMessage = ''
|
|
800
808
|
let fatalClaudeErrorTriggered = false
|
|
809
|
+
let resultExitGraceTimer = null
|
|
810
|
+
let settled = false
|
|
811
|
+
let resolveResult = null
|
|
812
|
+
let rejectResult = null
|
|
801
813
|
const normalizationState = createClaudeNormalizationState()
|
|
802
814
|
|
|
815
|
+
const clearResultExitGraceTimer = () => {
|
|
816
|
+
if (resultExitGraceTimer) {
|
|
817
|
+
clearTimeout(resultExitGraceTimer)
|
|
818
|
+
resultExitGraceTimer = null
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const settleCompleted = (options = {}) => {
|
|
823
|
+
if (settled) {
|
|
824
|
+
return
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
settled = true
|
|
828
|
+
clearResultExitGraceTimer()
|
|
829
|
+
onEvent(createCompletedEnvelopeEvent(finalMessage))
|
|
830
|
+
resolveResult?.({
|
|
831
|
+
sessionId: session.id,
|
|
832
|
+
threadId: finalSessionId,
|
|
833
|
+
message: finalMessage,
|
|
834
|
+
})
|
|
835
|
+
|
|
836
|
+
if (options.stopChild && child.exitCode === null && child.signalCode === null) {
|
|
837
|
+
forceStopChildProcess(child, { graceMs: CLAUDE_RESULT_FORCE_STOP_GRACE_MS })
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const settleError = (error) => {
|
|
842
|
+
if (settled) {
|
|
843
|
+
return
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
settled = true
|
|
847
|
+
clearResultExitGraceTimer()
|
|
848
|
+
rejectResult?.(error)
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const scheduleResultExitGrace = () => {
|
|
852
|
+
if (settled || resultExitGraceTimer || CLAUDE_RESULT_EXIT_GRACE_MS <= 0) {
|
|
853
|
+
return
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
resultExitGraceTimer = setTimeout(() => {
|
|
857
|
+
resultExitGraceTimer = null
|
|
858
|
+
settleCompleted({ stopChild: true })
|
|
859
|
+
}, CLAUDE_RESULT_EXIT_GRACE_MS)
|
|
860
|
+
resultExitGraceTimer.unref?.()
|
|
861
|
+
}
|
|
862
|
+
|
|
803
863
|
const rememberSessionId = (sessionId) => {
|
|
804
864
|
const value = String(sessionId || '').trim()
|
|
805
865
|
if (!value || value === finalSessionId) {
|
|
@@ -836,6 +896,7 @@ export function streamPromptToClaudeCodeSession(sessionInput, prompt, callbacks
|
|
|
836
896
|
|
|
837
897
|
if (String(event?.type || '').trim().toLowerCase() === 'result') {
|
|
838
898
|
finalMessage = extractClaudeResultText(event) || finalMessage
|
|
899
|
+
scheduleResultExitGrace()
|
|
839
900
|
}
|
|
840
901
|
}
|
|
841
902
|
|
|
@@ -857,11 +918,18 @@ export function streamPromptToClaudeCodeSession(sessionInput, prompt, callbacks
|
|
|
857
918
|
})
|
|
858
919
|
|
|
859
920
|
const result = new Promise((resolve, reject) => {
|
|
921
|
+
resolveResult = resolve
|
|
922
|
+
rejectResult = reject
|
|
923
|
+
|
|
860
924
|
child.on('error', (error) => {
|
|
861
|
-
|
|
925
|
+
settleError(normalizeSpawnError(error))
|
|
862
926
|
})
|
|
863
927
|
|
|
864
928
|
child.on('close', (code) => {
|
|
929
|
+
if (settled) {
|
|
930
|
+
return
|
|
931
|
+
}
|
|
932
|
+
|
|
865
933
|
flushBufferedText(stdoutBuffer).forEach(emitClaudeJsonLine)
|
|
866
934
|
flushBufferedText(stderrBuffer).forEach((line) => {
|
|
867
935
|
lastStderrLine = line
|
|
@@ -870,17 +938,11 @@ export function streamPromptToClaudeCodeSession(sessionInput, prompt, callbacks
|
|
|
870
938
|
|
|
871
939
|
if (code !== 0) {
|
|
872
940
|
const detail = fatalClaudeErrorMessage || lastStderrLine || 'Claude Code 执行失败。'
|
|
873
|
-
|
|
941
|
+
settleError(new Error(detail))
|
|
874
942
|
return
|
|
875
943
|
}
|
|
876
944
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
resolve({
|
|
880
|
-
sessionId: session.id,
|
|
881
|
-
threadId: finalSessionId,
|
|
882
|
-
message: finalMessage,
|
|
883
|
-
})
|
|
945
|
+
settleCompleted()
|
|
884
946
|
})
|
|
885
947
|
})
|
|
886
948
|
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import test from 'node:test'
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createClaudeNormalizationState,
|
|
6
|
+
extractClaudeAssistantText,
|
|
7
|
+
extractClaudeResultText,
|
|
8
|
+
extractClaudeSessionId,
|
|
9
|
+
normalizeClaudeEvent,
|
|
10
|
+
normalizeClaudeEvents,
|
|
11
|
+
} from './claudeCodeRunner.js'
|
|
5
12
|
|
|
6
13
|
test('runner claudeCodeRunner maps fatal auth api_retry to error event', () => {
|
|
7
14
|
assert.deepEqual(
|
|
@@ -277,3 +284,184 @@ test('runner claudeCodeRunner maps TodoWrite into todo_list events', () => {
|
|
|
277
284
|
}]
|
|
278
285
|
)
|
|
279
286
|
})
|
|
287
|
+
|
|
288
|
+
test('extractClaudeAssistantText joins nested text parts', () => {
|
|
289
|
+
const text = extractClaudeAssistantText({
|
|
290
|
+
type: 'assistant',
|
|
291
|
+
message: {
|
|
292
|
+
content: [
|
|
293
|
+
{ type: 'text', text: '第一段' },
|
|
294
|
+
{ type: 'text', text: '第二段' },
|
|
295
|
+
],
|
|
296
|
+
},
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
assert.equal(text, '第一段\n第二段')
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
test('extractClaudeSessionId reads common session id fields', () => {
|
|
303
|
+
assert.equal(extractClaudeSessionId({ session_id: 'claude-session-1' }), 'claude-session-1')
|
|
304
|
+
assert.equal(extractClaudeSessionId({ result: { session_id: 'claude-session-2' } }), 'claude-session-2')
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
test('normalizeClaudeEvent maps assistant output to agent message', () => {
|
|
308
|
+
assert.deepEqual(
|
|
309
|
+
normalizeClaudeEvent({
|
|
310
|
+
type: 'assistant',
|
|
311
|
+
message: {
|
|
312
|
+
content: [{ type: 'text', text: '已完成修改' }],
|
|
313
|
+
},
|
|
314
|
+
}),
|
|
315
|
+
{
|
|
316
|
+
type: 'item.completed',
|
|
317
|
+
item: {
|
|
318
|
+
type: 'agent_message',
|
|
319
|
+
text: '已完成修改',
|
|
320
|
+
},
|
|
321
|
+
}
|
|
322
|
+
)
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
test('normalizeClaudeEvent maps result output to turn completion', () => {
|
|
326
|
+
assert.deepEqual(
|
|
327
|
+
normalizeClaudeEvent({
|
|
328
|
+
type: 'result',
|
|
329
|
+
result: '最终回复',
|
|
330
|
+
}),
|
|
331
|
+
{
|
|
332
|
+
type: 'turn.completed',
|
|
333
|
+
result: '最终回复',
|
|
334
|
+
}
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
assert.equal(extractClaudeResultText({ result: '最终回复' }), '最终回复')
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
test('normalizeClaudeEvents maps system init to thread start', () => {
|
|
341
|
+
assert.deepEqual(
|
|
342
|
+
normalizeClaudeEvents({
|
|
343
|
+
type: 'system',
|
|
344
|
+
subtype: 'init',
|
|
345
|
+
session_id: 'claude-session-init',
|
|
346
|
+
}),
|
|
347
|
+
[{
|
|
348
|
+
type: 'thread.started',
|
|
349
|
+
thread_id: 'claude-session-init',
|
|
350
|
+
}]
|
|
351
|
+
)
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
test('normalizeClaudeEvents maps thinking, tool use and text blocks', () => {
|
|
355
|
+
const state = createClaudeNormalizationState()
|
|
356
|
+
|
|
357
|
+
assert.deepEqual(
|
|
358
|
+
normalizeClaudeEvents({
|
|
359
|
+
type: 'assistant',
|
|
360
|
+
message: {
|
|
361
|
+
content: [
|
|
362
|
+
{ type: 'thinking', thinking: '先看看目录结构' },
|
|
363
|
+
{ type: 'tool_use', id: 'tool-1', name: 'Bash', input: { command: 'ls -1' } },
|
|
364
|
+
{ type: 'text', text: '已查看完成' },
|
|
365
|
+
],
|
|
366
|
+
},
|
|
367
|
+
}, state),
|
|
368
|
+
[
|
|
369
|
+
{
|
|
370
|
+
type: 'item.started',
|
|
371
|
+
item: {
|
|
372
|
+
type: 'reasoning',
|
|
373
|
+
text: '先看看目录结构',
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
type: 'item.started',
|
|
378
|
+
item: {
|
|
379
|
+
type: 'command_execution',
|
|
380
|
+
command: 'Bash: ls -1',
|
|
381
|
+
status: 'in_progress',
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
type: 'item.completed',
|
|
386
|
+
item: {
|
|
387
|
+
type: 'agent_message',
|
|
388
|
+
text: '已查看完成',
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
]
|
|
392
|
+
)
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
test('normalizeClaudeEvents maps tool results back to remembered tool call', () => {
|
|
396
|
+
const state = createClaudeNormalizationState()
|
|
397
|
+
normalizeClaudeEvents({
|
|
398
|
+
type: 'assistant',
|
|
399
|
+
message: {
|
|
400
|
+
content: [
|
|
401
|
+
{ type: 'tool_use', id: 'tool-2', name: 'Bash', input: { command: 'pwd' } },
|
|
402
|
+
],
|
|
403
|
+
},
|
|
404
|
+
}, state)
|
|
405
|
+
|
|
406
|
+
assert.deepEqual(
|
|
407
|
+
normalizeClaudeEvents({
|
|
408
|
+
type: 'user',
|
|
409
|
+
message: {
|
|
410
|
+
content: [
|
|
411
|
+
{ type: 'tool_result', tool_use_id: 'tool-2', content: '/tmp/demo', is_error: false },
|
|
412
|
+
],
|
|
413
|
+
},
|
|
414
|
+
}, state),
|
|
415
|
+
[{
|
|
416
|
+
type: 'item.completed',
|
|
417
|
+
item: {
|
|
418
|
+
type: 'command_execution',
|
|
419
|
+
command: 'Bash: pwd',
|
|
420
|
+
status: 'completed',
|
|
421
|
+
exit_code: 0,
|
|
422
|
+
aggregated_output: '/tmp/demo',
|
|
423
|
+
},
|
|
424
|
+
}]
|
|
425
|
+
)
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
test('normalizeClaudeEvents stringifies structured tool results', () => {
|
|
429
|
+
const state = createClaudeNormalizationState()
|
|
430
|
+
normalizeClaudeEvents({
|
|
431
|
+
type: 'assistant',
|
|
432
|
+
message: {
|
|
433
|
+
content: [
|
|
434
|
+
{ type: 'tool_use', id: 'tool-3', name: 'Read', input: { file_path: '/tmp/demo.txt' } },
|
|
435
|
+
],
|
|
436
|
+
},
|
|
437
|
+
}, state)
|
|
438
|
+
|
|
439
|
+
assert.deepEqual(
|
|
440
|
+
normalizeClaudeEvents({
|
|
441
|
+
type: 'user',
|
|
442
|
+
message: {
|
|
443
|
+
content: [
|
|
444
|
+
{
|
|
445
|
+
type: 'tool_result',
|
|
446
|
+
tool_use_id: 'tool-3',
|
|
447
|
+
content: [
|
|
448
|
+
{ type: 'text', text: '<path>/tmp/demo.txt</path>' },
|
|
449
|
+
{ type: 'text', text: '<type>file</type>' },
|
|
450
|
+
],
|
|
451
|
+
},
|
|
452
|
+
],
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
state),
|
|
456
|
+
[{
|
|
457
|
+
type: 'item.completed',
|
|
458
|
+
item: {
|
|
459
|
+
type: 'command_execution',
|
|
460
|
+
command: 'Read: /tmp/demo.txt',
|
|
461
|
+
status: 'completed',
|
|
462
|
+
exit_code: 0,
|
|
463
|
+
aggregated_output: '<path>/tmp/demo.txt</path>\n<type>file</type>',
|
|
464
|
+
},
|
|
465
|
+
}]
|
|
466
|
+
)
|
|
467
|
+
})
|
|
@@ -162,30 +162,46 @@ function stringifyKimiToolResultContent(value) {
|
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
function mapKimiToolNameForDisplay(name = '') {
|
|
166
|
+
const normalized = String(name || '').trim().toLowerCase()
|
|
167
|
+
const aliasMap = {
|
|
168
|
+
readfile: 'read',
|
|
169
|
+
writefile: 'write',
|
|
170
|
+
editfile: 'edit',
|
|
171
|
+
search: 'search',
|
|
172
|
+
shell: 'shell',
|
|
173
|
+
settodolist: 'todo',
|
|
174
|
+
}
|
|
175
|
+
return aliasMap[normalized] || normalized || 'kimi tool'
|
|
176
|
+
}
|
|
177
|
+
|
|
165
178
|
function buildKimiToolCommand(name = '', input = {}) {
|
|
166
|
-
const
|
|
179
|
+
const displayName = mapKimiToolNameForDisplay(name)
|
|
167
180
|
if (!input || typeof input !== 'object') {
|
|
168
|
-
return
|
|
181
|
+
return displayName
|
|
169
182
|
}
|
|
170
183
|
|
|
171
184
|
const command = String(input.command || '').trim()
|
|
172
185
|
if (command) {
|
|
173
|
-
return `${
|
|
186
|
+
return `${displayName}: ${command}`
|
|
174
187
|
}
|
|
175
188
|
|
|
176
189
|
const singleValueKeys = ['file_path', 'path', 'pattern', 'query', 'url', 'description']
|
|
177
190
|
for (const key of singleValueKeys) {
|
|
178
191
|
const value = String(input[key] || '').trim()
|
|
179
192
|
if (value) {
|
|
180
|
-
return `${
|
|
193
|
+
return `${displayName}: ${value}`
|
|
181
194
|
}
|
|
182
195
|
}
|
|
183
196
|
|
|
184
197
|
try {
|
|
185
198
|
const compact = JSON.stringify(input)
|
|
186
|
-
|
|
199
|
+
if (compact === '{}') {
|
|
200
|
+
return displayName
|
|
201
|
+
}
|
|
202
|
+
return compact.length <= 240 ? `${displayName}: ${compact}` : `${displayName}: ${compact.slice(0, 237)}...`
|
|
187
203
|
} catch {
|
|
188
|
-
return
|
|
204
|
+
return displayName
|
|
189
205
|
}
|
|
190
206
|
}
|
|
191
207
|
|
|
@@ -296,12 +312,18 @@ export function normalizeKimiEvents(event = {}, state = createKimiNormalizationS
|
|
|
296
312
|
toolCalls.forEach((toolCall) => {
|
|
297
313
|
const toolUseId = String(toolCall?.id || '').trim()
|
|
298
314
|
const name = String(toolCall?.function?.name || toolCall?.name || 'Kimi tool').trim() || 'Kimi tool'
|
|
299
|
-
const argsText = toolCall?.function?.arguments || toolCall?.arguments || '{}'
|
|
300
315
|
let parsedArgs = {}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
316
|
+
const rawArgs = toolCall?.function?.arguments ?? toolCall?.arguments
|
|
317
|
+
if (rawArgs != null) {
|
|
318
|
+
if (typeof rawArgs === 'string') {
|
|
319
|
+
try {
|
|
320
|
+
parsedArgs = JSON.parse(rawArgs)
|
|
321
|
+
} catch {
|
|
322
|
+
parsedArgs = {}
|
|
323
|
+
}
|
|
324
|
+
} else if (typeof rawArgs === 'object') {
|
|
325
|
+
parsedArgs = rawArgs
|
|
326
|
+
}
|
|
305
327
|
}
|
|
306
328
|
const command = buildKimiToolCommand(name, parsedArgs)
|
|
307
329
|
const isTodoTool = isKimiTodoToolName(name)
|
|
@@ -446,6 +468,7 @@ export function streamPromptToKimiCodeSession(sessionInput, prompt, callbacks =
|
|
|
446
468
|
|
|
447
469
|
finalSessionId = value
|
|
448
470
|
onThreadStarted(value)
|
|
471
|
+
onEvent(createAgentEventEnvelopeEvent(createThreadStartedEvent(value)))
|
|
449
472
|
}
|
|
450
473
|
|
|
451
474
|
const emitKimiJsonLine = (line) => {
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import test from 'node:test'
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createOpenCodeNormalizationState,
|
|
6
|
+
extractOpenCodeErrorMessage,
|
|
7
|
+
extractOpenCodeSessionId,
|
|
8
|
+
extractOpenCodeText,
|
|
9
|
+
extractOpenCodeUsage,
|
|
10
|
+
normalizeOpenCodeEvent,
|
|
11
|
+
normalizeOpenCodeEvents,
|
|
12
|
+
} from './openCodeRunner.js'
|
|
5
13
|
|
|
6
14
|
test('runner openCodeRunner maps sub-agent task tool_use to collaboration events', () => {
|
|
7
15
|
assert.deepEqual(
|
|
@@ -71,3 +79,158 @@ test('runner openCodeRunner maps sub-agent task tool_use to collaboration events
|
|
|
71
79
|
]
|
|
72
80
|
)
|
|
73
81
|
})
|
|
82
|
+
|
|
83
|
+
test('extractOpenCodeSessionId reads common session id fields', () => {
|
|
84
|
+
assert.equal(extractOpenCodeSessionId({ sessionID: 'opencode-session-1' }), 'opencode-session-1')
|
|
85
|
+
assert.equal(extractOpenCodeSessionId({ sessionId: 'opencode-session-2' }), 'opencode-session-2')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('extractOpenCodeText trims text payload', () => {
|
|
89
|
+
assert.equal(
|
|
90
|
+
extractOpenCodeText({
|
|
91
|
+
type: 'text',
|
|
92
|
+
part: {
|
|
93
|
+
type: 'text',
|
|
94
|
+
text: '\n\n已完成修改\n',
|
|
95
|
+
},
|
|
96
|
+
}),
|
|
97
|
+
'已完成修改'
|
|
98
|
+
)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('normalizeOpenCodeEvent maps text output to agent message', () => {
|
|
102
|
+
assert.deepEqual(
|
|
103
|
+
normalizeOpenCodeEvent({
|
|
104
|
+
type: 'text',
|
|
105
|
+
part: {
|
|
106
|
+
type: 'text',
|
|
107
|
+
text: '已完成修改',
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
{
|
|
111
|
+
type: 'item.completed',
|
|
112
|
+
item: {
|
|
113
|
+
type: 'agent_message',
|
|
114
|
+
text: '已完成修改',
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('normalizeOpenCodeEvents maps first step_start to turn.started only once', () => {
|
|
121
|
+
const state = createOpenCodeNormalizationState()
|
|
122
|
+
|
|
123
|
+
assert.deepEqual(
|
|
124
|
+
normalizeOpenCodeEvents({
|
|
125
|
+
type: 'step_start',
|
|
126
|
+
sessionID: 'ses_1',
|
|
127
|
+
}, state),
|
|
128
|
+
[{ type: 'turn.started' }]
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
assert.deepEqual(
|
|
132
|
+
normalizeOpenCodeEvents({
|
|
133
|
+
type: 'step_start',
|
|
134
|
+
sessionID: 'ses_1',
|
|
135
|
+
}, state),
|
|
136
|
+
[]
|
|
137
|
+
)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('normalizeOpenCodeEvents maps completed tool_use to command events', () => {
|
|
141
|
+
assert.deepEqual(
|
|
142
|
+
normalizeOpenCodeEvents({
|
|
143
|
+
type: 'tool_use',
|
|
144
|
+
sessionID: 'ses_2',
|
|
145
|
+
part: {
|
|
146
|
+
type: 'tool',
|
|
147
|
+
tool: 'read',
|
|
148
|
+
state: {
|
|
149
|
+
status: 'completed',
|
|
150
|
+
input: {
|
|
151
|
+
filePath: '/tmp/demo.txt',
|
|
152
|
+
},
|
|
153
|
+
output: '<path>/tmp/demo.txt</path>',
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
}),
|
|
157
|
+
[
|
|
158
|
+
{
|
|
159
|
+
type: 'item.started',
|
|
160
|
+
item: {
|
|
161
|
+
type: 'command_execution',
|
|
162
|
+
command: 'read: /tmp/demo.txt',
|
|
163
|
+
status: 'in_progress',
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
type: 'item.completed',
|
|
168
|
+
item: {
|
|
169
|
+
type: 'command_execution',
|
|
170
|
+
command: 'read: /tmp/demo.txt',
|
|
171
|
+
status: 'completed',
|
|
172
|
+
exit_code: 0,
|
|
173
|
+
aggregated_output: '<path>/tmp/demo.txt</path>',
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
]
|
|
177
|
+
)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('normalizeOpenCodeEvent maps step_finish stop to turn completion with usage', () => {
|
|
181
|
+
const event = normalizeOpenCodeEvent({
|
|
182
|
+
type: 'step_finish',
|
|
183
|
+
part: {
|
|
184
|
+
type: 'step-finish',
|
|
185
|
+
reason: 'stop',
|
|
186
|
+
tokens: {
|
|
187
|
+
input: 321,
|
|
188
|
+
output: 12,
|
|
189
|
+
cache: {
|
|
190
|
+
read: 256,
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
assert.deepEqual(event, {
|
|
197
|
+
type: 'turn.completed',
|
|
198
|
+
usage: {
|
|
199
|
+
input_tokens: 321,
|
|
200
|
+
output_tokens: 12,
|
|
201
|
+
cached_input_tokens: 256,
|
|
202
|
+
},
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
assert.deepEqual(extractOpenCodeUsage({
|
|
206
|
+
part: {
|
|
207
|
+
tokens: {
|
|
208
|
+
input: 321,
|
|
209
|
+
output: 12,
|
|
210
|
+
cache: {
|
|
211
|
+
read: 256,
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
}), {
|
|
216
|
+
input_tokens: 321,
|
|
217
|
+
output_tokens: 12,
|
|
218
|
+
cached_input_tokens: 256,
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
test('extractOpenCodeErrorMessage reads nested API errors', () => {
|
|
223
|
+
assert.equal(
|
|
224
|
+
extractOpenCodeErrorMessage({
|
|
225
|
+
type: 'error',
|
|
226
|
+
error: {
|
|
227
|
+
name: 'APIError',
|
|
228
|
+
data: {
|
|
229
|
+
message: 'openai_error',
|
|
230
|
+
responseBody: '{"error":{"message":"bad gateway"}}',
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
}),
|
|
234
|
+
'openai_error'
|
|
235
|
+
)
|
|
236
|
+
})
|
|
@@ -154,6 +154,43 @@ process.stdout.write(JSON.stringify({
|
|
|
154
154
|
return cmdPath
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
function createHangingFakeClaudeBinary(tempDir) {
|
|
158
|
+
const scriptPath = path.join(tempDir, process.platform === 'win32' ? 'fake-claude-hang.js' : 'fake-claude-hang')
|
|
159
|
+
const script = `#!/usr/bin/env node
|
|
160
|
+
process.stdout.write(JSON.stringify({
|
|
161
|
+
type: 'system',
|
|
162
|
+
subtype: 'init',
|
|
163
|
+
session_id: 'thread-contract-1',
|
|
164
|
+
}) + '\\n')
|
|
165
|
+
|
|
166
|
+
process.stdout.write(JSON.stringify({
|
|
167
|
+
type: 'assistant',
|
|
168
|
+
message: {
|
|
169
|
+
content: [
|
|
170
|
+
{ type: 'text', text: '已完成修改' },
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
}) + '\\n')
|
|
174
|
+
|
|
175
|
+
process.stdout.write(JSON.stringify({
|
|
176
|
+
type: 'result',
|
|
177
|
+
result: '最终回复',
|
|
178
|
+
}) + '\\n')
|
|
179
|
+
|
|
180
|
+
setInterval(() => {}, 1000)
|
|
181
|
+
`
|
|
182
|
+
|
|
183
|
+
fs.writeFileSync(scriptPath, script, { mode: 0o755 })
|
|
184
|
+
|
|
185
|
+
if (process.platform !== 'win32') {
|
|
186
|
+
return scriptPath
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const cmdPath = path.join(tempDir, 'fake-claude-hang.cmd')
|
|
190
|
+
fs.writeFileSync(cmdPath, '@echo off\r\nnode "%~dp0fake-claude-hang.js" %*\r\n')
|
|
191
|
+
return cmdPath
|
|
192
|
+
}
|
|
193
|
+
|
|
157
194
|
function createFakeOpenCodeBinary(tempDir) {
|
|
158
195
|
const scriptPath = path.join(tempDir, process.platform === 'win32' ? 'fake-opencode.js' : 'fake-opencode')
|
|
159
196
|
const script = `#!/usr/bin/env node
|
|
@@ -380,3 +417,33 @@ test('Codex / Claude Code / OpenCode runner 会产出兼容的核心事件结构
|
|
|
380
417
|
}
|
|
381
418
|
)
|
|
382
419
|
})
|
|
420
|
+
|
|
421
|
+
test('Claude Code runner 在 result 后进程不退出时会按 grace timeout 完成', async () => {
|
|
422
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-runner-claude-hang-'))
|
|
423
|
+
const fakeClaudeBin = createHangingFakeClaudeBinary(tempDir)
|
|
424
|
+
|
|
425
|
+
await withEnv(
|
|
426
|
+
{
|
|
427
|
+
CLAUDE_CODE_BIN: fakeClaudeBin,
|
|
428
|
+
PROMPTX_CLAUDE_RESULT_EXIT_GRACE_MS: '30',
|
|
429
|
+
PROMPTX_CLAUDE_RESULT_FORCE_STOP_GRACE_MS: '30',
|
|
430
|
+
},
|
|
431
|
+
async () => {
|
|
432
|
+
const { streamPromptToClaudeCodeSession } = await importFreshRunnerModules()
|
|
433
|
+
const result = await collectRunnerContractEvents(streamPromptToClaudeCodeSession)
|
|
434
|
+
|
|
435
|
+
assert.deepEqual(result.result, {
|
|
436
|
+
sessionId: 'session-1',
|
|
437
|
+
threadId: 'thread-contract-1',
|
|
438
|
+
message: '最终回复',
|
|
439
|
+
})
|
|
440
|
+
assertOrderedSubsequence(projectRunnerContractPhases(result.events), [
|
|
441
|
+
'status',
|
|
442
|
+
'thread.started',
|
|
443
|
+
'agent_message',
|
|
444
|
+
'turn.completed',
|
|
445
|
+
'completed',
|
|
446
|
+
])
|
|
447
|
+
}
|
|
448
|
+
)
|
|
449
|
+
})
|
|
@@ -461,7 +461,7 @@ test('runManager 会为长时间静默的 running run 补发可见进度事件',
|
|
|
461
461
|
.filter((payload) => payload.type === 'status')
|
|
462
462
|
|
|
463
463
|
assert.equal(
|
|
464
|
-
statusEvents.some((payload) =>
|
|
464
|
+
statusEvents.some((payload) => payload.messageKey === 'runner.status.thinking'),
|
|
465
465
|
true
|
|
466
466
|
)
|
|
467
467
|
|