@muyichengshayu/promptx 0.2.7 → 0.2.9
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 +12 -0
- package/apps/runner/src/engines/claudeCodeRunner.js +69 -1
- package/apps/runner/src/engines/claudeCodeRunner.test.js +279 -0
- package/apps/runner/src/engines/index.js +2 -0
- package/apps/runner/src/engines/kimiCodeRunner.js +561 -0
- package/apps/runner/src/engines/openCodeRunner.test.js +73 -0
- package/apps/runner/src/engines/shellRunner.test.js +46 -0
- package/apps/runner/src/runManager.js +115 -11
- package/apps/runner/src/runManager.test.js +913 -0
- package/apps/runner/src/serverClient.test.js +93 -0
- package/apps/server/src/agentSessionDiscovery.js +136 -0
- package/apps/server/src/agentSessionDiscovery.test.js +186 -0
- package/apps/server/src/agents/claudeCodeRunner.test.js +433 -0
- package/apps/server/src/agents/index.js +2 -0
- package/apps/server/src/agents/kimiCodeRunner.js +565 -0
- package/apps/server/src/agents/kimiCodeRunner.test.js +127 -0
- package/apps/server/src/agents/openCodeRunner.test.js +236 -0
- package/apps/server/src/agents/runnerContract.test.js +382 -0
- package/apps/server/src/appPaths.test.js +52 -0
- package/apps/server/src/assetRoutes.test.js +168 -0
- package/apps/server/src/codex.test.js +518 -0
- package/apps/server/src/codexRoutes.test.js +376 -0
- package/apps/server/src/codexRuns.test.js +160 -0
- package/apps/server/src/codexSessions.js +1 -1
- package/apps/server/src/codexSessions.test.js +369 -0
- package/apps/server/src/db.test.js +182 -0
- package/apps/server/src/gitDiff.test.js +542 -0
- package/apps/server/src/gitDiffClient.test.js +140 -0
- package/apps/server/src/internalRoutes.test.js +134 -0
- package/apps/server/src/maintenance.test.js +154 -0
- package/apps/server/src/processControl.test.js +147 -0
- package/apps/server/src/relayClient.test.js +478 -0
- package/apps/server/src/relayConfig.test.js +73 -0
- package/apps/server/src/relayProtocol.test.js +49 -0
- package/apps/server/src/relayServer.test.js +798 -0
- package/apps/server/src/relayTenants.test.js +137 -0
- package/apps/server/src/relayUsageStore.test.js +65 -0
- package/apps/server/src/repository.test.js +150 -0
- package/apps/server/src/runDispatchService.js +14 -2
- package/apps/server/src/runDispatchService.test.js +563 -0
- package/apps/server/src/runEventIngest.test.js +225 -0
- package/apps/server/src/runRecovery.test.js +73 -0
- package/apps/server/src/runnerClient.test.js +80 -0
- package/apps/server/src/runnerDispatch.test.js +136 -0
- package/apps/server/src/systemConfig.test.js +112 -0
- package/apps/server/src/systemRoutes.test.js +319 -0
- package/apps/server/src/taskRoutes.test.js +726 -0
- package/apps/server/src/upload.test.js +30 -0
- package/apps/server/src/webAppRoutes.test.js +67 -0
- package/apps/server/src/workspaceFiles.test.js +262 -0
- package/apps/web/dist/assets/{CodexSessionManagerDialog-B_F9ZWKy.js → CodexSessionManagerDialog-_qLljY7F.js} +1 -1
- package/apps/web/dist/assets/{TaskDiffReviewDialog-CPqGk_q2.js → TaskDiffReviewDialog-DpW8S8yT.js} +1 -1
- package/apps/web/dist/assets/{WorkbenchSettingsDialog-CWl81vlG.js → WorkbenchSettingsDialog-CYfh5G7c.js} +1 -1
- package/apps/web/dist/assets/WorkbenchView-A8nm0NH9.js +60 -0
- package/apps/web/dist/assets/index-DHF_zkYI.js +2 -0
- package/apps/web/dist/index.html +1 -1
- package/package.json +14 -21
- package/packages/shared/src/index.js +6 -0
- package/scripts/doctor.mjs +8 -0
- package/apps/web/dist/assets/WorkbenchView-gbRu02Lv.js +0 -60
- package/apps/web/dist/assets/index-5LxHpYf5.js +0 -2
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { execFileSync, spawn } from 'node:child_process'
|
|
4
|
+
import {
|
|
5
|
+
AGENT_ENGINES,
|
|
6
|
+
AGENT_RUN_ITEM_TYPES,
|
|
7
|
+
createAgentEventEnvelopeEvent,
|
|
8
|
+
createCompletedEnvelopeEvent,
|
|
9
|
+
createItemCompletedEvent,
|
|
10
|
+
createItemStartedEvent,
|
|
11
|
+
createStatusEnvelopeEvent,
|
|
12
|
+
createStderrEnvelopeEvent,
|
|
13
|
+
createStdoutEnvelopeEvent,
|
|
14
|
+
createThreadStartedEvent,
|
|
15
|
+
createTurnCompletedEvent,
|
|
16
|
+
getAgentEngineLabel,
|
|
17
|
+
} from '../../../../packages/shared/src/index.js'
|
|
18
|
+
import { createManagedSpawnOptions, forceStopChildProcess } from '../processControl.js'
|
|
19
|
+
|
|
20
|
+
const KIMI_CODE_BIN = process.env.KIMI_CODE_BIN || 'kimi'
|
|
21
|
+
const RESOLVED_KIMI_CODE_BIN = resolveKimiCodeBinary()
|
|
22
|
+
|
|
23
|
+
function resolveKimiCodeBinary() {
|
|
24
|
+
if (process.platform !== 'win32') {
|
|
25
|
+
return KIMI_CODE_BIN
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (path.extname(KIMI_CODE_BIN)) {
|
|
29
|
+
return KIMI_CODE_BIN
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (fs.existsSync(`${KIMI_CODE_BIN}.cmd`)) {
|
|
33
|
+
return `${KIMI_CODE_BIN}.cmd`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (fs.existsSync(`${KIMI_CODE_BIN}.bat`)) {
|
|
37
|
+
return `${KIMI_CODE_BIN}.bat`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (fs.existsSync(KIMI_CODE_BIN)) {
|
|
41
|
+
return KIMI_CODE_BIN
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const output = execFileSync('where.exe', [KIMI_CODE_BIN], {
|
|
46
|
+
encoding: 'utf8',
|
|
47
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
48
|
+
windowsHide: true,
|
|
49
|
+
}).trim()
|
|
50
|
+
|
|
51
|
+
if (!output) {
|
|
52
|
+
return KIMI_CODE_BIN
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const candidates = output
|
|
56
|
+
.split(/\r?\n/g)
|
|
57
|
+
.map((line) => line.trim())
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
|
|
60
|
+
return candidates.find((item) => /\.(cmd|bat)$/i.test(item))
|
|
61
|
+
|| candidates.find((item) => /\.(exe|com)$/i.test(item))
|
|
62
|
+
|| candidates[0]
|
|
63
|
+
|| KIMI_CODE_BIN
|
|
64
|
+
} catch {
|
|
65
|
+
return KIMI_CODE_BIN
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function createKimiSpawn(commandArgs = [], cwd = '') {
|
|
70
|
+
const options = createManagedSpawnOptions({
|
|
71
|
+
cwd,
|
|
72
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(RESOLVED_KIMI_CODE_BIN)) {
|
|
76
|
+
return spawn(
|
|
77
|
+
process.env.ComSpec || 'cmd.exe',
|
|
78
|
+
['/d', '/s', '/c', RESOLVED_KIMI_CODE_BIN, ...commandArgs],
|
|
79
|
+
options
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return spawn(RESOLVED_KIMI_CODE_BIN, commandArgs, options)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizeSpawnError(error) {
|
|
87
|
+
if (error?.code === 'ENOENT') {
|
|
88
|
+
const attempted = RESOLVED_KIMI_CODE_BIN === KIMI_CODE_BIN
|
|
89
|
+
? KIMI_CODE_BIN
|
|
90
|
+
: `${KIMI_CODE_BIN} -> ${RESOLVED_KIMI_CODE_BIN}`
|
|
91
|
+
return new Error(
|
|
92
|
+
`找不到 Kimi Code CLI(尝试执行:${attempted})。请先确认终端里可以运行 \`kimi --version\`,或设置环境变量 \`KIMI_CODE_BIN\`。`
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return error
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parseJsonLine(line = '') {
|
|
100
|
+
const text = String(line || '').trim()
|
|
101
|
+
if (!text) {
|
|
102
|
+
return null
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
return JSON.parse(text)
|
|
107
|
+
} catch {
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function splitBufferedLines(buffer = '') {
|
|
113
|
+
const text = String(buffer || '')
|
|
114
|
+
if (!text) {
|
|
115
|
+
return { lines: [], rest: '' }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const normalized = text.replace(/\r\n/g, '\n')
|
|
119
|
+
const parts = normalized.split('\n')
|
|
120
|
+
const rest = parts.pop() || ''
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
lines: parts.map((line) => line.trim()).filter(Boolean),
|
|
124
|
+
rest,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function flushBufferedText(buffer = '') {
|
|
129
|
+
const { lines, rest } = splitBufferedLines(buffer)
|
|
130
|
+
const tail = String(rest || '').trim()
|
|
131
|
+
return tail ? [...lines, tail] : lines
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function stringifyKimiToolResultContent(value) {
|
|
135
|
+
if (typeof value === 'string') {
|
|
136
|
+
return value.trim()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (value == null) {
|
|
140
|
+
return ''
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (Array.isArray(value)) {
|
|
144
|
+
const parts = []
|
|
145
|
+
for (const item of value) {
|
|
146
|
+
if (item && typeof item === 'object') {
|
|
147
|
+
const text = String(item.text || '').trim()
|
|
148
|
+
if (text) parts.push(text)
|
|
149
|
+
} else if (typeof item === 'string') {
|
|
150
|
+
const text = item.trim()
|
|
151
|
+
if (text) parts.push(text)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return parts.join('\n').trim()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const compact = JSON.stringify(value)
|
|
159
|
+
return compact.length <= 12000 ? compact : `${compact.slice(0, 11997)}...`
|
|
160
|
+
} catch {
|
|
161
|
+
return String(value || '').trim()
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildKimiToolCommand(name = '', input = {}) {
|
|
166
|
+
const toolName = String(name || 'Kimi tool').trim() || 'Kimi tool'
|
|
167
|
+
if (!input || typeof input !== 'object') {
|
|
168
|
+
return toolName
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const command = String(input.command || '').trim()
|
|
172
|
+
if (command) {
|
|
173
|
+
return `${toolName}: ${command}`
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const singleValueKeys = ['file_path', 'path', 'pattern', 'query', 'url', 'description']
|
|
177
|
+
for (const key of singleValueKeys) {
|
|
178
|
+
const value = String(input[key] || '').trim()
|
|
179
|
+
if (value) {
|
|
180
|
+
return `${toolName}: ${value}`
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const compact = JSON.stringify(input)
|
|
186
|
+
return compact.length <= 240 ? `${toolName}: ${compact}` : `${toolName}: ${compact.slice(0, 237)}...`
|
|
187
|
+
} catch {
|
|
188
|
+
return toolName
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function isKimiTodoToolName(name = '') {
|
|
193
|
+
return String(name || '').trim().toLowerCase() === 'settodolist'
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function normalizeKimiTodoStatus(status = '') {
|
|
197
|
+
const normalized = String(status || '').trim().toLowerCase()
|
|
198
|
+
if (normalized === 'done') {
|
|
199
|
+
return 'completed'
|
|
200
|
+
}
|
|
201
|
+
if (normalized === 'in_progress') {
|
|
202
|
+
return 'in_progress'
|
|
203
|
+
}
|
|
204
|
+
return 'pending'
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function normalizeKimiTodoItems(items = []) {
|
|
208
|
+
return (Array.isArray(items) ? items : [])
|
|
209
|
+
.map((entry) => {
|
|
210
|
+
if (!entry || typeof entry !== 'object') {
|
|
211
|
+
return null
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const status = normalizeKimiTodoStatus(entry.status)
|
|
215
|
+
const text = String(entry.title || entry.text || '').trim()
|
|
216
|
+
if (!text) {
|
|
217
|
+
return null
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
text,
|
|
222
|
+
status,
|
|
223
|
+
completed: status === 'completed',
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
.filter(Boolean)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function extractKimiSessionIdFromStderrLine(line = '') {
|
|
230
|
+
const match = String(line || '').match(/To resume this session:\s*kimi\s+(?:-r|--session|--resume)\s+([a-f0-9-]+)/i)
|
|
231
|
+
return match?.[1] ? String(match[1]).trim() : ''
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function isKimiInfoStderrLine(line = '') {
|
|
235
|
+
const text = String(line || '').trim()
|
|
236
|
+
if (!text) {
|
|
237
|
+
return true
|
|
238
|
+
}
|
|
239
|
+
if (/^To resume this session:/i.test(text)) {
|
|
240
|
+
return true
|
|
241
|
+
}
|
|
242
|
+
if (/^Shell cwd was reset to /i.test(text)) {
|
|
243
|
+
return true
|
|
244
|
+
}
|
|
245
|
+
return false
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function createKimiNormalizationState() {
|
|
249
|
+
return {
|
|
250
|
+
turnStarted: false,
|
|
251
|
+
toolUses: new Map(),
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function normalizeKimiEvents(event = {}, state = createKimiNormalizationState()) {
|
|
256
|
+
const role = String(event?.role || '').trim().toLowerCase()
|
|
257
|
+
const normalizedEvents = []
|
|
258
|
+
|
|
259
|
+
if (role === 'assistant') {
|
|
260
|
+
if (!state.turnStarted) {
|
|
261
|
+
state.turnStarted = true
|
|
262
|
+
normalizedEvents.push({ type: 'turn.started' })
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const content = Array.isArray(event.content) ? event.content : []
|
|
266
|
+
content.forEach((block) => {
|
|
267
|
+
const blockType = String(block?.type || '').trim().toLowerCase()
|
|
268
|
+
if (blockType === 'think') {
|
|
269
|
+
const text = String(block?.think || '').trim()
|
|
270
|
+
if (text) {
|
|
271
|
+
normalizedEvents.push({
|
|
272
|
+
...createItemStartedEvent({
|
|
273
|
+
type: AGENT_RUN_ITEM_TYPES.REASONING,
|
|
274
|
+
text,
|
|
275
|
+
}),
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (blockType === 'text') {
|
|
282
|
+
const text = String(block?.text || '').trim()
|
|
283
|
+
if (text) {
|
|
284
|
+
normalizedEvents.push({
|
|
285
|
+
...createItemCompletedEvent({
|
|
286
|
+
type: AGENT_RUN_ITEM_TYPES.AGENT_MESSAGE,
|
|
287
|
+
text,
|
|
288
|
+
}),
|
|
289
|
+
})
|
|
290
|
+
}
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
const toolCalls = Array.isArray(event.tool_calls) ? event.tool_calls : []
|
|
296
|
+
toolCalls.forEach((toolCall) => {
|
|
297
|
+
const toolUseId = String(toolCall?.id || '').trim()
|
|
298
|
+
const name = String(toolCall?.function?.name || toolCall?.name || 'Kimi tool').trim() || 'Kimi tool'
|
|
299
|
+
const argsText = toolCall?.function?.arguments || toolCall?.arguments || '{}'
|
|
300
|
+
let parsedArgs = {}
|
|
301
|
+
try {
|
|
302
|
+
parsedArgs = JSON.parse(argsText)
|
|
303
|
+
} catch {
|
|
304
|
+
parsedArgs = {}
|
|
305
|
+
}
|
|
306
|
+
const command = buildKimiToolCommand(name, parsedArgs)
|
|
307
|
+
const isTodoTool = isKimiTodoToolName(name)
|
|
308
|
+
const todoItems = normalizeKimiTodoItems(parsedArgs.todos)
|
|
309
|
+
|
|
310
|
+
if (toolUseId) {
|
|
311
|
+
state.toolUses.set(toolUseId, {
|
|
312
|
+
name,
|
|
313
|
+
command,
|
|
314
|
+
kind: isTodoTool ? 'todo' : 'command',
|
|
315
|
+
todoItems,
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (isTodoTool) {
|
|
320
|
+
normalizedEvents.push({
|
|
321
|
+
...createItemStartedEvent({
|
|
322
|
+
type: AGENT_RUN_ITEM_TYPES.TODO_LIST,
|
|
323
|
+
items: todoItems,
|
|
324
|
+
}),
|
|
325
|
+
})
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
normalizedEvents.push({
|
|
330
|
+
...createItemStartedEvent({
|
|
331
|
+
type: AGENT_RUN_ITEM_TYPES.COMMAND_EXECUTION,
|
|
332
|
+
command,
|
|
333
|
+
status: 'in_progress',
|
|
334
|
+
}),
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
return normalizedEvents
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (role === 'tool') {
|
|
342
|
+
const toolCallId = String(event?.tool_call_id || '').trim()
|
|
343
|
+
const remembered = toolCallId ? state.toolUses.get(toolCallId) : null
|
|
344
|
+
const output = stringifyKimiToolResultContent(event?.content)
|
|
345
|
+
const todoTool = remembered?.kind === 'todo'
|
|
346
|
+
|
|
347
|
+
if (toolCallId) {
|
|
348
|
+
state.toolUses.delete(toolCallId)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (todoTool) {
|
|
352
|
+
normalizedEvents.push({
|
|
353
|
+
...createItemCompletedEvent({
|
|
354
|
+
type: AGENT_RUN_ITEM_TYPES.TODO_LIST,
|
|
355
|
+
items: Array.isArray(remembered?.todoItems) ? remembered.todoItems : [],
|
|
356
|
+
}),
|
|
357
|
+
})
|
|
358
|
+
return normalizedEvents
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
normalizedEvents.push({
|
|
362
|
+
...createItemCompletedEvent({
|
|
363
|
+
type: AGENT_RUN_ITEM_TYPES.COMMAND_EXECUTION,
|
|
364
|
+
command: remembered?.command || remembered?.name || 'Kimi tool',
|
|
365
|
+
status: 'completed',
|
|
366
|
+
exit_code: 0,
|
|
367
|
+
aggregated_output: output,
|
|
368
|
+
}),
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
return normalizedEvents
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return [{
|
|
375
|
+
type: `kimi.${role || 'event'}`,
|
|
376
|
+
detail: stringifyKimiToolResultContent(event?.content),
|
|
377
|
+
}]
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function normalizeKimiEvent(event = {}, state = createKimiNormalizationState()) {
|
|
381
|
+
return normalizeKimiEvents(event, state)[0] || null
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function createExecArgs(session, prompt) {
|
|
385
|
+
const args = [
|
|
386
|
+
'--print',
|
|
387
|
+
'--output-format', 'stream-json',
|
|
388
|
+
]
|
|
389
|
+
|
|
390
|
+
const sessionId = String(session?.engineSessionId || session?.engineThreadId || session?.codexThreadId || '').trim()
|
|
391
|
+
if (sessionId) {
|
|
392
|
+
args.push('--session', sessionId)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (session?.cwd) {
|
|
396
|
+
args.push('--work-dir', session.cwd)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
args.push('-p', String(prompt || ''))
|
|
400
|
+
|
|
401
|
+
return args
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function createKimiRunStatusEvent(session = {}) {
|
|
405
|
+
const hasExistingThread = Boolean(
|
|
406
|
+
String(session?.engineSessionId || session?.engineThreadId || session?.codexThreadId || '').trim()
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
return createStatusEnvelopeEvent({
|
|
410
|
+
stage: hasExistingThread ? 'resuming' : 'starting',
|
|
411
|
+
message: hasExistingThread
|
|
412
|
+
? '已连接 PromptX 项目,正在继续这轮执行。'
|
|
413
|
+
: '已创建 PromptX 项目,正在启动第一轮执行。',
|
|
414
|
+
})
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function streamPromptToKimiCodeSession(sessionInput, prompt, callbacks = {}) {
|
|
418
|
+
const session = sessionInput && typeof sessionInput === 'object' ? sessionInput : null
|
|
419
|
+
const normalizedPrompt = String(prompt || '').trim()
|
|
420
|
+
|
|
421
|
+
if (!session?.id || !session?.cwd) {
|
|
422
|
+
throw new Error('缺少 PromptX 项目。')
|
|
423
|
+
}
|
|
424
|
+
if (!normalizedPrompt) {
|
|
425
|
+
throw new Error('没有可发送的提示词。')
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const onEvent = typeof callbacks.onEvent === 'function' ? callbacks.onEvent : () => {}
|
|
429
|
+
const onThreadStarted = typeof callbacks.onThreadStarted === 'function' ? callbacks.onThreadStarted : () => {}
|
|
430
|
+
|
|
431
|
+
const child = createKimiSpawn(createExecArgs(session, normalizedPrompt), session.cwd)
|
|
432
|
+
onEvent(createKimiRunStatusEvent(session))
|
|
433
|
+
|
|
434
|
+
let stdoutBuffer = ''
|
|
435
|
+
let stderrBuffer = ''
|
|
436
|
+
let lastStderrLine = ''
|
|
437
|
+
let finalMessage = ''
|
|
438
|
+
let finalSessionId = String(session.engineSessionId || session.engineThreadId || session.codexThreadId || '').trim()
|
|
439
|
+
const normalizationState = createKimiNormalizationState()
|
|
440
|
+
|
|
441
|
+
const rememberSessionId = (sessionId) => {
|
|
442
|
+
const value = String(sessionId || '').trim()
|
|
443
|
+
if (!value || value === finalSessionId) {
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
finalSessionId = value
|
|
448
|
+
onThreadStarted(value)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const emitKimiJsonLine = (line) => {
|
|
452
|
+
const event = parseJsonLine(line)
|
|
453
|
+
if (!event) {
|
|
454
|
+
onEvent(createStdoutEnvelopeEvent(line))
|
|
455
|
+
return
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const normalizedEvents = normalizeKimiEvents(event, normalizationState)
|
|
459
|
+
normalizedEvents.forEach((normalizedEvent) => {
|
|
460
|
+
onEvent(createAgentEventEnvelopeEvent(normalizedEvent))
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
const role = String(event?.role || '').trim().toLowerCase()
|
|
464
|
+
if (role === 'assistant') {
|
|
465
|
+
const content = Array.isArray(event.content) ? event.content : []
|
|
466
|
+
const textBlocks = content
|
|
467
|
+
.filter((block) => String(block?.type || '').trim().toLowerCase() === 'text')
|
|
468
|
+
.map((block) => String(block?.text || '').trim())
|
|
469
|
+
.filter(Boolean)
|
|
470
|
+
|
|
471
|
+
if (textBlocks.length) {
|
|
472
|
+
const text = textBlocks.join('\n').trim()
|
|
473
|
+
finalMessage = `${finalMessage}${finalMessage ? '\n' : ''}${text}`
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
child.stdout.on('data', (chunk) => {
|
|
479
|
+
stdoutBuffer += chunk.toString()
|
|
480
|
+
const { lines, rest } = splitBufferedLines(stdoutBuffer)
|
|
481
|
+
stdoutBuffer = rest
|
|
482
|
+
lines.forEach(emitKimiJsonLine)
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
child.stderr.on('data', (chunk) => {
|
|
486
|
+
stderrBuffer += chunk.toString()
|
|
487
|
+
const { lines, rest } = splitBufferedLines(stderrBuffer)
|
|
488
|
+
stderrBuffer = rest
|
|
489
|
+
lines.forEach((line) => {
|
|
490
|
+
const sessionId = extractKimiSessionIdFromStderrLine(line)
|
|
491
|
+
if (sessionId) {
|
|
492
|
+
rememberSessionId(sessionId)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (isKimiInfoStderrLine(line)) {
|
|
496
|
+
return
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
lastStderrLine = line
|
|
500
|
+
onEvent(createStderrEnvelopeEvent(line))
|
|
501
|
+
})
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
const result = new Promise((resolve, reject) => {
|
|
505
|
+
child.on('error', (error) => {
|
|
506
|
+
reject(normalizeSpawnError(error))
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
child.on('close', (code) => {
|
|
510
|
+
flushBufferedText(stdoutBuffer).forEach(emitKimiJsonLine)
|
|
511
|
+
flushBufferedText(stderrBuffer).forEach((line) => {
|
|
512
|
+
const sessionId = extractKimiSessionIdFromStderrLine(line)
|
|
513
|
+
if (sessionId) {
|
|
514
|
+
rememberSessionId(sessionId)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (isKimiInfoStderrLine(line)) {
|
|
518
|
+
return
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
lastStderrLine = line
|
|
522
|
+
onEvent(createStderrEnvelopeEvent(line))
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
if (code !== 0) {
|
|
526
|
+
reject(new Error(lastStderrLine || 'Kimi Code 执行失败。'))
|
|
527
|
+
return
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const message = finalMessage.trim()
|
|
531
|
+
onEvent(createAgentEventEnvelopeEvent(createTurnCompletedEvent()))
|
|
532
|
+
onEvent(createCompletedEnvelopeEvent(message))
|
|
533
|
+
|
|
534
|
+
resolve({
|
|
535
|
+
sessionId: session.id,
|
|
536
|
+
threadId: finalSessionId,
|
|
537
|
+
message,
|
|
538
|
+
})
|
|
539
|
+
})
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
return {
|
|
543
|
+
child,
|
|
544
|
+
result,
|
|
545
|
+
cancel(options = {}) {
|
|
546
|
+
forceStopChildProcess(child, options)
|
|
547
|
+
},
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export const kimiCodeRunner = {
|
|
552
|
+
engine: AGENT_ENGINES.KIMI_CODE,
|
|
553
|
+
label: getAgentEngineLabel(AGENT_ENGINES.KIMI_CODE),
|
|
554
|
+
supportsWorkspaceHistory: false,
|
|
555
|
+
listKnownWorkspaces() {
|
|
556
|
+
return []
|
|
557
|
+
},
|
|
558
|
+
streamSessionPrompt(session, prompt, callbacks = {}) {
|
|
559
|
+
return streamPromptToKimiCodeSession(session, prompt, callbacks)
|
|
560
|
+
},
|
|
561
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { normalizeOpenCodeEvents } from './openCodeRunner.js'
|
|
5
|
+
|
|
6
|
+
test('runner openCodeRunner maps sub-agent task tool_use to collaboration events', () => {
|
|
7
|
+
assert.deepEqual(
|
|
8
|
+
normalizeOpenCodeEvents({
|
|
9
|
+
type: 'tool_use',
|
|
10
|
+
sessionID: 'ses_main',
|
|
11
|
+
part: {
|
|
12
|
+
type: 'tool',
|
|
13
|
+
tool: 'task',
|
|
14
|
+
state: {
|
|
15
|
+
status: 'completed',
|
|
16
|
+
input: {
|
|
17
|
+
description: '分析 a.js 文件',
|
|
18
|
+
prompt: '请分析 /tmp/demo/a.js 文件',
|
|
19
|
+
subagent_type: 'explore',
|
|
20
|
+
},
|
|
21
|
+
output: 'task_id: ses_child_1\n\n<task_result>ok</task_result>',
|
|
22
|
+
metadata: {
|
|
23
|
+
sessionId: 'ses_child_1',
|
|
24
|
+
model: {
|
|
25
|
+
providerID: 'opencode',
|
|
26
|
+
modelID: 'minimax-m2.5-free',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
[
|
|
33
|
+
{
|
|
34
|
+
type: 'item.completed',
|
|
35
|
+
item: {
|
|
36
|
+
type: 'collab_tool_call',
|
|
37
|
+
tool: 'spawn_agent',
|
|
38
|
+
receiver_thread_ids: ['ses_child_1'],
|
|
39
|
+
prompt: '请分析 /tmp/demo/a.js 文件',
|
|
40
|
+
agents_states: {
|
|
41
|
+
ses_child_1: {
|
|
42
|
+
status: 'completed',
|
|
43
|
+
message: 'task_id: ses_child_1\n\n<task_result>ok</task_result>',
|
|
44
|
+
title: '分析 a.js 文件',
|
|
45
|
+
role: 'explore',
|
|
46
|
+
target: 'a.js',
|
|
47
|
+
model: 'opencode/minimax-m2.5-free',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
type: 'item.completed',
|
|
54
|
+
item: {
|
|
55
|
+
type: 'collab_tool_call',
|
|
56
|
+
tool: 'wait',
|
|
57
|
+
receiver_thread_ids: ['ses_child_1'],
|
|
58
|
+
prompt: '请分析 /tmp/demo/a.js 文件',
|
|
59
|
+
agents_states: {
|
|
60
|
+
ses_child_1: {
|
|
61
|
+
status: 'completed',
|
|
62
|
+
message: 'task_id: ses_child_1\n\n<task_result>ok</task_result>',
|
|
63
|
+
title: '分析 a.js 文件',
|
|
64
|
+
role: 'explore',
|
|
65
|
+
target: 'a.js',
|
|
66
|
+
model: 'opencode/minimax-m2.5-free',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
]
|
|
72
|
+
)
|
|
73
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { shellRunner } from './shellRunner.js'
|
|
5
|
+
|
|
6
|
+
function collectShellEvents(command) {
|
|
7
|
+
const events = []
|
|
8
|
+
const stream = shellRunner.streamSessionPrompt({
|
|
9
|
+
id: 'session-shell-test',
|
|
10
|
+
cwd: process.cwd(),
|
|
11
|
+
}, command, {
|
|
12
|
+
onEvent(event) {
|
|
13
|
+
events.push(event)
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
events,
|
|
19
|
+
stream,
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test('shellRunner runs a one-shot shell command and emits command events', async () => {
|
|
24
|
+
const command = `"${process.execPath}" -e "console.log('hello shell')"`
|
|
25
|
+
const { events, stream } = collectShellEvents(command)
|
|
26
|
+
const result = await stream.result
|
|
27
|
+
|
|
28
|
+
assert.match(result.message, /hello shell/)
|
|
29
|
+
assert.ok(events.some((event) => event?.event?.type === 'item.started'))
|
|
30
|
+
assert.ok(events.some((event) => event?.event?.type === 'item.completed'))
|
|
31
|
+
assert.ok(events.some((event) => event?.type === 'stdout' && /hello shell/.test(event.text || '')))
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('shellRunner surfaces non-zero exit output on errors', async () => {
|
|
35
|
+
const command = `"${process.execPath}" -e "console.error('shell failed'); process.exit(3)"`
|
|
36
|
+
const { events, stream } = collectShellEvents(command)
|
|
37
|
+
|
|
38
|
+
await assert.rejects(
|
|
39
|
+
stream.result,
|
|
40
|
+
(error) => /exit 3/.test(String(error?.message || '')) && /shell failed/.test(String(error?.output || ''))
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
const completed = events.find((event) => event?.event?.type === 'item.completed')
|
|
44
|
+
assert.equal(completed?.event?.item?.exit_code, 3)
|
|
45
|
+
assert.equal(completed?.event?.item?.status, 'failed')
|
|
46
|
+
})
|