@winmatrix/supervisor 1.0.5 → 1.0.6
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/package.json +1 -5
- package/src/claude-session-lib.mjs +84 -0
- package/src/engine-run-success.mjs +118 -0
- package/src/env-sanitizer.mjs +4 -0
- package/src/run-agent-engine.mjs +195 -49
- package/src/run-engine.mjs +133 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@winmatrix/supervisor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/run-agent-engine.mjs",
|
|
6
6
|
"bin": {
|
|
@@ -14,10 +14,6 @@
|
|
|
14
14
|
"build": "node -e \"console.log('no build step')\"",
|
|
15
15
|
"test": "node --test tests/**/*.test.mjs"
|
|
16
16
|
},
|
|
17
|
-
"dependencies": {
|
|
18
|
-
"@winmatrix/agent-sdk": "^1.0.5",
|
|
19
|
-
"@winmatrix/protocol": "^1.0.5"
|
|
20
|
-
},
|
|
21
17
|
"engines": {
|
|
22
18
|
"node": ">=20"
|
|
23
19
|
}
|
|
@@ -15,6 +15,7 @@ import { createRequire } from 'module';
|
|
|
15
15
|
const require = createRequire(import.meta.url);
|
|
16
16
|
|
|
17
17
|
export const TRUNCATE_LEN = 4096;
|
|
18
|
+
export const SESSION_DETAIL_TRANSPORT_MAX_BYTES = 48 * 1024;
|
|
18
19
|
export const REDACTED = '[REDACTED]';
|
|
19
20
|
export const SENSITIVE_KEY_PATTERN =
|
|
20
21
|
/(authorization|password|passwd|pwd|secret|token|api[_-]?key|apikey|access[_-]?key|private[_-]?key|credential|session[_-]?key|jwt)/i;
|
|
@@ -326,6 +327,89 @@ export function previewValue(value, max = 1000) {
|
|
|
326
327
|
return text.length > max ? text.slice(0, max) + `…[${text.length - max} chars]` : text;
|
|
327
328
|
}
|
|
328
329
|
|
|
330
|
+
/**
|
|
331
|
+
* 将 session-get 结果压缩到 Sandbox exec 的安全传输预算内,避免 stdout 截断后产生非法 JSON。
|
|
332
|
+
* @param {Record<string, unknown>} detail
|
|
333
|
+
* @param {number} maxBytes
|
|
334
|
+
* @returns {Record<string, unknown>}
|
|
335
|
+
*/
|
|
336
|
+
export function compactSessionDetailForTransport(detail, maxBytes = SESSION_DETAIL_TRANSPORT_MAX_BYTES) {
|
|
337
|
+
const serializedBytes = (value) => Buffer.byteLength(JSON.stringify(value));
|
|
338
|
+
if (serializedBytes(detail) <= maxBytes) return detail;
|
|
339
|
+
|
|
340
|
+
const compacted = {
|
|
341
|
+
...detail,
|
|
342
|
+
messages: Array.isArray(detail.messages)
|
|
343
|
+
? detail.messages.map((message) => ({
|
|
344
|
+
...message,
|
|
345
|
+
content: Array.isArray(message.content)
|
|
346
|
+
? message.content.map((block) => ({
|
|
347
|
+
...block,
|
|
348
|
+
...(typeof block.text === 'string' && block.text.length > 1200
|
|
349
|
+
? { text: `${block.text.slice(0, 1200)}…[transport-truncated]` }
|
|
350
|
+
: {}),
|
|
351
|
+
}))
|
|
352
|
+
: message.content,
|
|
353
|
+
}))
|
|
354
|
+
: [],
|
|
355
|
+
metadata: detail.metadata && typeof detail.metadata === 'object'
|
|
356
|
+
? {
|
|
357
|
+
...detail.metadata,
|
|
358
|
+
...(typeof detail.metadata.assistantText === 'string'
|
|
359
|
+
? { assistantText: detail.metadata.assistantText.slice(0, 8000) }
|
|
360
|
+
: {}),
|
|
361
|
+
...(Array.isArray(detail.metadata.userPrompts)
|
|
362
|
+
? { userPrompts: detail.metadata.userPrompts.slice(-20).map((prompt) => (
|
|
363
|
+
typeof prompt === 'string' && prompt.length > 800
|
|
364
|
+
? `${prompt.slice(0, 800)}…[transport-truncated]`
|
|
365
|
+
: prompt
|
|
366
|
+
)) }
|
|
367
|
+
: {}),
|
|
368
|
+
...(detail.metadata.diagnostics && typeof detail.metadata.diagnostics === 'object'
|
|
369
|
+
? {
|
|
370
|
+
diagnostics: {
|
|
371
|
+
...detail.metadata.diagnostics,
|
|
372
|
+
...(Array.isArray(detail.metadata.diagnostics.toolCalls)
|
|
373
|
+
? { toolCalls: detail.metadata.diagnostics.toolCalls.slice(-20) }
|
|
374
|
+
: {}),
|
|
375
|
+
...(Array.isArray(detail.metadata.diagnostics.toolResults)
|
|
376
|
+
? { toolResults: detail.metadata.diagnostics.toolResults.slice(-20) }
|
|
377
|
+
: {}),
|
|
378
|
+
...(Array.isArray(detail.metadata.diagnostics.timeline)
|
|
379
|
+
? { timeline: detail.metadata.diagnostics.timeline.slice(-40) }
|
|
380
|
+
: {}),
|
|
381
|
+
},
|
|
382
|
+
}
|
|
383
|
+
: {}),
|
|
384
|
+
...(Array.isArray(detail.metadata.rawMessages) ? { rawMessages: undefined, rawMessagesIncluded: false } : {}),
|
|
385
|
+
transportCompacted: true,
|
|
386
|
+
}
|
|
387
|
+
: detail.metadata,
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
if (serializedBytes(compacted) <= maxBytes) return compacted;
|
|
391
|
+
|
|
392
|
+
const minimal = {
|
|
393
|
+
...compacted,
|
|
394
|
+
messages: [],
|
|
395
|
+
metadata: compacted.metadata && typeof compacted.metadata === 'object'
|
|
396
|
+
? {
|
|
397
|
+
...compacted.metadata,
|
|
398
|
+
assistantText: typeof compacted.metadata.assistantText === 'string'
|
|
399
|
+
? compacted.metadata.assistantText.slice(0, 4000)
|
|
400
|
+
: compacted.metadata.assistantText,
|
|
401
|
+
userPrompts: Array.isArray(compacted.metadata.userPrompts)
|
|
402
|
+
? compacted.metadata.userPrompts.map((prompt) => (
|
|
403
|
+
typeof prompt === 'string' ? prompt.slice(0, 800) : prompt
|
|
404
|
+
))
|
|
405
|
+
: compacted.metadata.userPrompts,
|
|
406
|
+
diagnostics: undefined,
|
|
407
|
+
}
|
|
408
|
+
: compacted.metadata,
|
|
409
|
+
};
|
|
410
|
+
return minimal;
|
|
411
|
+
}
|
|
412
|
+
|
|
329
413
|
/** @param {string} text */
|
|
330
414
|
export function redactPreviewText(text) {
|
|
331
415
|
return text
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工作站引擎终态成败:不得只看进程 exitCode。
|
|
3
|
+
*
|
|
4
|
+
* recover-workstation-context-overflow D2:SDK result 帧 `is_error` /
|
|
5
|
+
* 失败 `subtype`(至少 error_during_execution / error_max_turns)须写成失败,
|
|
6
|
+
* 失败原因进 error,可见正文仍保留。缺字段时回退 exitCode(旧镜像靠 server P0 短语)。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** @type {ReadonlySet<string>} */
|
|
10
|
+
export const SDK_FAILURE_SUBTYPES = Object.freeze(new Set([
|
|
11
|
+
'error_during_execution',
|
|
12
|
+
'error_max_turns',
|
|
13
|
+
]));
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {unknown} subtype
|
|
17
|
+
* @returns {subtype is string}
|
|
18
|
+
*/
|
|
19
|
+
export function isSdkFailureSubtype(subtype) {
|
|
20
|
+
return typeof subtype === 'string' && SDK_FAILURE_SUBTYPES.has(subtype);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 从 events.jsonl 条目(envelope 或裸事件)提取最后一条 result 上的 SDK 成败信号。
|
|
25
|
+
*
|
|
26
|
+
* @param {unknown[]} events
|
|
27
|
+
* @returns {{
|
|
28
|
+
* isError: boolean,
|
|
29
|
+
* subtype: string | undefined,
|
|
30
|
+
* visibleText: string | undefined,
|
|
31
|
+
* error: string | undefined,
|
|
32
|
+
* sessionId: string | undefined,
|
|
33
|
+
* }}
|
|
34
|
+
*/
|
|
35
|
+
export function extractSdkResultSignals(events) {
|
|
36
|
+
const empty = {
|
|
37
|
+
isError: false,
|
|
38
|
+
subtype: undefined,
|
|
39
|
+
visibleText: undefined,
|
|
40
|
+
error: undefined,
|
|
41
|
+
sessionId: undefined,
|
|
42
|
+
};
|
|
43
|
+
if (!Array.isArray(events)) return empty;
|
|
44
|
+
|
|
45
|
+
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
46
|
+
const raw = events[i];
|
|
47
|
+
const evt = raw && typeof raw === 'object' && raw.event && typeof raw.event === 'object'
|
|
48
|
+
? raw.event
|
|
49
|
+
: raw;
|
|
50
|
+
if (!evt || typeof evt !== 'object' || evt.type !== 'result') continue;
|
|
51
|
+
|
|
52
|
+
const payload = evt.result && typeof evt.result === 'object' && !Array.isArray(evt.result)
|
|
53
|
+
? evt.result
|
|
54
|
+
: evt;
|
|
55
|
+
|
|
56
|
+
const isError = payload.is_error === true || payload.isError === true;
|
|
57
|
+
const subtype = typeof payload.subtype === 'string'
|
|
58
|
+
? payload.subtype
|
|
59
|
+
: typeof payload.sdkSubtype === 'string'
|
|
60
|
+
? payload.sdkSubtype
|
|
61
|
+
: undefined;
|
|
62
|
+
const visibleText = typeof payload.text === 'string'
|
|
63
|
+
? payload.text
|
|
64
|
+
: typeof payload.content === 'string'
|
|
65
|
+
? payload.content
|
|
66
|
+
: undefined;
|
|
67
|
+
const error = typeof payload.error === 'string' ? payload.error : undefined;
|
|
68
|
+
const sessionId = typeof payload.sessionId === 'string' ? payload.sessionId : undefined;
|
|
69
|
+
|
|
70
|
+
return { isError, subtype, visibleText, error, sessionId };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return empty;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {object} input
|
|
78
|
+
* @param {boolean} [input.cancelled]
|
|
79
|
+
* @param {number | null | undefined} [input.exitCode]
|
|
80
|
+
* @param {boolean} [input.isError]
|
|
81
|
+
* @param {string} [input.subtype]
|
|
82
|
+
* @param {string} [input.visibleText]
|
|
83
|
+
* @param {string} [input.existingError]
|
|
84
|
+
* @returns {{ success: boolean, status: 'cancelled' | 'completed' | 'failed', error?: string }}
|
|
85
|
+
*/
|
|
86
|
+
export function resolveEngineRunOutcome({
|
|
87
|
+
cancelled = false,
|
|
88
|
+
exitCode,
|
|
89
|
+
isError = false,
|
|
90
|
+
subtype,
|
|
91
|
+
visibleText,
|
|
92
|
+
existingError,
|
|
93
|
+
} = {}) {
|
|
94
|
+
if (cancelled) {
|
|
95
|
+
return { success: false, status: 'cancelled', error: 'cancelled' };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const sdkFailed = isError === true || isSdkFailureSubtype(subtype);
|
|
99
|
+
if (sdkFailed) {
|
|
100
|
+
const trimmedVisible = typeof visibleText === 'string' ? visibleText.trim() : '';
|
|
101
|
+
const trimmedExisting = typeof existingError === 'string' ? existingError.trim() : '';
|
|
102
|
+
const error = trimmedExisting
|
|
103
|
+
|| trimmedVisible
|
|
104
|
+
|| (isSdkFailureSubtype(subtype) ? `SDK result subtype=${subtype}` : 'SDK result is_error=true');
|
|
105
|
+
return { success: false, status: 'failed', error };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (exitCode !== 0) {
|
|
109
|
+
const trimmedExisting = typeof existingError === 'string' ? existingError.trim() : '';
|
|
110
|
+
return {
|
|
111
|
+
success: false,
|
|
112
|
+
status: 'failed',
|
|
113
|
+
error: trimmedExisting || `Engine exited with code ${exitCode}`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { success: true, status: 'completed' };
|
|
118
|
+
}
|
package/src/env-sanitizer.mjs
CHANGED
|
@@ -69,6 +69,10 @@ export function isFilteredEnvKey(key) {
|
|
|
69
69
|
// 精确匹配的过滤项(优先级最高)
|
|
70
70
|
if (FILTERED_EXACT_KEYS.has(key)) return true;
|
|
71
71
|
|
|
72
|
+
// Pod 创建时注入的 Claude 持久根目录必须和 HOME 一起传给 Engine。
|
|
73
|
+
// 其余 CLAUDE_CODE_* 仍按下方规则过滤,避免 Supervisor 内部标记泄漏。
|
|
74
|
+
if (/^CLAUDE_CODE_HOME$/i.test(key)) return false;
|
|
75
|
+
|
|
72
76
|
// 先检查是否匹配过滤模式(过滤优先于白名单)
|
|
73
77
|
// 这样 CLAUDE_CODE_ENTRYPOINT 被过滤,即使 CLAUDE_ 在白名单中
|
|
74
78
|
for (const pattern of FILTERED_ENV_PATTERNS) {
|
package/src/run-agent-engine.mjs
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
cloneMessagesForJson,
|
|
40
40
|
buildCliReplayGuide,
|
|
41
41
|
buildDiagnostics,
|
|
42
|
+
compactSessionDetailForTransport,
|
|
42
43
|
maybeTruncate,
|
|
43
44
|
resolveDirViaGlobalListSessions,
|
|
44
45
|
} from './claude-session-lib.mjs';
|
|
@@ -50,19 +51,29 @@ import { filterBlockedArgs, formatBlockedArgsAudit } from './blocked-args-filter
|
|
|
50
51
|
import { createDeliverableState, updateDeliverable, extractDeliverable, computeDeliverableFromEvents } from './deliverable-tracker.mjs';
|
|
51
52
|
import { extractTokenUsage } from './token-usage.mjs';
|
|
52
53
|
import { isResumeRejected, buildContinuityNotice, buildResumeDegradedEvent, isResumeFailureOnExit } from './resume-degradation.mjs';
|
|
54
|
+
import { extractSdkResultSignals, resolveEngineRunOutcome } from './engine-run-success.mjs';
|
|
53
55
|
|
|
54
56
|
const require = createRequire(import.meta.url);
|
|
55
57
|
const fs = require('node:fs');
|
|
56
58
|
const path = require('node:path');
|
|
57
59
|
const { spawn, spawnSync } = require('node:child_process');
|
|
58
60
|
const http = require('node:http');
|
|
61
|
+
const https = require('node:https');
|
|
59
62
|
const readline = require('node:readline');
|
|
60
63
|
const SCRIPT_PATH = fileURLToPath(import.meta.url);
|
|
61
64
|
|
|
62
|
-
// Supervisor 根目录(每个 runKey
|
|
63
|
-
|
|
65
|
+
// Supervisor 根目录(每个 runKey 一个子目录)。必须位于 Pod 的持久 agent-home,
|
|
66
|
+
// 容器重建后可继续读取状态与事件;禁止回退到 /tmp,避免表面成功但状态丢失。
|
|
67
|
+
const SUPERVISOR_HOME = process.env.HOME?.trim();
|
|
68
|
+
const SUPERVISOR_ROOT = process.env.WINMATRIX_SUPERVISOR_ROOT?.trim()
|
|
69
|
+
|| (SUPERVISOR_HOME ? path.join(SUPERVISOR_HOME, '.winmatrix', 'agent-engine-supervisor') : '');
|
|
70
|
+
if (!SUPERVISOR_ROOT) {
|
|
71
|
+
throw new Error('WINMATRIX_SUPERVISOR_ROOT 或 HOME 必须指向持久 agent-home');
|
|
72
|
+
}
|
|
64
73
|
const CALLBACK_TIMEOUT_MS = Number(process.env.WINMATRIX_CALLBACK_TIMEOUT_MS) || 5000;
|
|
65
74
|
const MAX_ACTIVE_RUNS = Number(process.env.WINMATRIX_MAX_ACTIVE_RUNS) || 100;
|
|
75
|
+
/** 单次 events 请求返回的最大事件数,避免大量事件堆积导致响应超限。 */
|
|
76
|
+
const MAX_EVENTS_PAGE_SIZE = 500;
|
|
66
77
|
|
|
67
78
|
// 确保根目录存在
|
|
68
79
|
if (!fs.existsSync(SUPERVISOR_ROOT)) {
|
|
@@ -316,6 +327,9 @@ function buildTerminalCallbackPayload(runKey, endMetadata, result, startMetadata
|
|
|
316
327
|
};
|
|
317
328
|
}
|
|
318
329
|
|
|
330
|
+
/** 重定向最大跳数,防重定向环。 */
|
|
331
|
+
const MAX_CALLBACK_REDIRECTS = 5;
|
|
332
|
+
|
|
319
333
|
/**
|
|
320
334
|
* 发送 callback 到 Server
|
|
321
335
|
* @param {string} callbackUrl - Callback URL
|
|
@@ -324,44 +338,76 @@ function buildTerminalCallbackPayload(runKey, endMetadata, result, startMetadata
|
|
|
324
338
|
* @returns {Promise<void>}
|
|
325
339
|
*/
|
|
326
340
|
async function sendCallback(callbackUrl, token, payload) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
341
|
+
const data = JSON.stringify(payload);
|
|
342
|
+
let currentUrl = new URL(callbackUrl);
|
|
343
|
+
const originalHostname = currentUrl.hostname;
|
|
344
|
+
|
|
345
|
+
for (let redirects = 0; redirects <= MAX_CALLBACK_REDIRECTS; redirects += 1) {
|
|
346
|
+
const result = await new Promise((resolve, reject) => {
|
|
347
|
+
const isHttps = currentUrl.protocol === 'https:';
|
|
348
|
+
const client = isHttps ? https : http;
|
|
349
|
+
const options = {
|
|
350
|
+
hostname: currentUrl.hostname,
|
|
351
|
+
port: currentUrl.port || (isHttps ? 443 : 80),
|
|
352
|
+
path: currentUrl.pathname + currentUrl.search,
|
|
353
|
+
method: 'POST',
|
|
354
|
+
headers: {
|
|
355
|
+
'Content-Type': 'application/json',
|
|
356
|
+
'Content-Length': Buffer.byteLength(data),
|
|
357
|
+
'Authorization': `Bearer ${token}`,
|
|
358
|
+
},
|
|
359
|
+
timeout: CALLBACK_TIMEOUT_MS,
|
|
360
|
+
};
|
|
342
361
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
362
|
+
const req = client.request(options, (res) => {
|
|
363
|
+
const redirectStatus = [301, 302, 307, 308];
|
|
364
|
+
const location = res.headers.location;
|
|
365
|
+
if (redirectStatus.includes(res.statusCode) && location) {
|
|
366
|
+
res.resume();
|
|
367
|
+
resolve({ redirect: location });
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
371
|
+
res.resume();
|
|
372
|
+
resolve({ ok: true });
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
res.resume();
|
|
376
|
+
resolve({ ok: false, status: res.statusCode });
|
|
377
|
+
});
|
|
351
378
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
379
|
+
req.on('error', (error) => {
|
|
380
|
+
reject(error);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
req.on('timeout', () => {
|
|
384
|
+
req.destroy();
|
|
385
|
+
reject(new Error('Callback timeout'));
|
|
386
|
+
});
|
|
356
387
|
|
|
357
|
-
|
|
358
|
-
req.
|
|
359
|
-
reject(new Error('Callback timeout'));
|
|
388
|
+
req.write(data);
|
|
389
|
+
req.end();
|
|
360
390
|
});
|
|
361
391
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
392
|
+
if (result.ok) {
|
|
393
|
+
console.log(`[run-agent-engine] Callback sent successfully: ${callbackUrl}`);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (result.ok === false) {
|
|
397
|
+
throw new Error(`Callback failed with status ${result.status}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// 跟随重定向:仅允许同 hostname 的 http↔https 跳转(防 SSRF)。
|
|
401
|
+
const nextUrl = new URL(result.redirect, currentUrl);
|
|
402
|
+
if (nextUrl.hostname !== originalHostname) {
|
|
403
|
+
throw new Error(`Callback redirect to different host rejected: ${nextUrl.hostname}`);
|
|
404
|
+
}
|
|
405
|
+
if (redirects === MAX_CALLBACK_REDIRECTS) {
|
|
406
|
+
throw new Error(`Callback redirect limit exceeded (${MAX_CALLBACK_REDIRECTS})`);
|
|
407
|
+
}
|
|
408
|
+
currentUrl = nextUrl;
|
|
409
|
+
}
|
|
410
|
+
throw new Error(`Callback redirect limit exceeded (${MAX_CALLBACK_REDIRECTS})`);
|
|
365
411
|
}
|
|
366
412
|
|
|
367
413
|
/**
|
|
@@ -449,12 +495,12 @@ async function runOwned(payload) {
|
|
|
449
495
|
// D11.2: detached=true 用于创建独立进程组,两阶段关闭时可 SIGTERM 整组
|
|
450
496
|
detached: process.platform !== 'win32',
|
|
451
497
|
});
|
|
498
|
+
// 链路日志走 stderr:stdout 是协议通道(ack JSON),console.log 会污染
|
|
499
|
+
console.error(`[run-agent-engine] runKey=${runKey} engine spawn: engineId=${launchSpec.engineId} pid=${child.pid} argv=${argv.join(' ')} workDir=${launchSpec.workDir || '(cwd)'}`);
|
|
452
500
|
|
|
453
501
|
// 写入 stdin
|
|
454
|
-
if (stdinData)
|
|
455
|
-
|
|
456
|
-
child.stdin.end();
|
|
457
|
-
}
|
|
502
|
+
if (stdinData) child.stdin.write(stdinData);
|
|
503
|
+
child.stdin.end();
|
|
458
504
|
|
|
459
505
|
// 读取 stdout JSONL 事件,同时写入 stdout.log
|
|
460
506
|
const stdoutWriteStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
@@ -585,6 +631,7 @@ async function runOwned(payload) {
|
|
|
585
631
|
let resultText = '';
|
|
586
632
|
let usage = null;
|
|
587
633
|
let resultSessionId = null;
|
|
634
|
+
let sdkSignals = extractSdkResultSignals([]);
|
|
588
635
|
try {
|
|
589
636
|
const eventsFile = path.join(getRunDir(runKey), 'events.jsonl');
|
|
590
637
|
if (fs.existsSync(eventsFile)) {
|
|
@@ -611,14 +658,30 @@ async function runOwned(payload) {
|
|
|
611
658
|
break;
|
|
612
659
|
}
|
|
613
660
|
}
|
|
661
|
+
|
|
662
|
+
sdkSignals = extractSdkResultSignals(events);
|
|
663
|
+
if (!resultSessionId && sdkSignals.sessionId) {
|
|
664
|
+
resultSessionId = sdkSignals.sessionId;
|
|
665
|
+
}
|
|
666
|
+
if (!resultText?.trim() && sdkSignals.visibleText) {
|
|
667
|
+
resultText = sdkSignals.visibleText;
|
|
668
|
+
}
|
|
614
669
|
}
|
|
615
670
|
} catch (err) {
|
|
616
671
|
console.error(`[run-agent-engine] Failed to compute deliverable/usage for ${runKey}:`, err.message);
|
|
617
672
|
}
|
|
673
|
+
const outcome = resolveEngineRunOutcome({
|
|
674
|
+
cancelled,
|
|
675
|
+
exitCode: code,
|
|
676
|
+
isError: sdkSignals.isError,
|
|
677
|
+
subtype: sdkSignals.subtype,
|
|
678
|
+
visibleText: resultText,
|
|
679
|
+
existingError: sdkSignals.error,
|
|
680
|
+
});
|
|
618
681
|
|
|
619
682
|
const endMetadata = {
|
|
620
683
|
...metadata,
|
|
621
|
-
status:
|
|
684
|
+
status: outcome.status,
|
|
622
685
|
endTime: new Date().toISOString(),
|
|
623
686
|
exitCode: code,
|
|
624
687
|
...(signal ? { signal } : {}),
|
|
@@ -628,35 +691,56 @@ async function runOwned(payload) {
|
|
|
628
691
|
activeRuns.delete(runKey);
|
|
629
692
|
saveActiveRuns();
|
|
630
693
|
|
|
631
|
-
//
|
|
694
|
+
// D2:success 综合 SDK is_error / 失败 subtype,exitCode === 0 不得单独判成功
|
|
632
695
|
const result = {
|
|
633
|
-
success:
|
|
696
|
+
success: outcome.success,
|
|
634
697
|
text: resultText || undefined,
|
|
635
698
|
...(resultSessionId ? { sessionId: resultSessionId } : {}),
|
|
636
699
|
...(usage ? { usage } : {}),
|
|
637
|
-
...(
|
|
700
|
+
...(outcome.error ? { error: outcome.error } : {}),
|
|
638
701
|
metadata: {
|
|
639
702
|
exitCode: code,
|
|
640
703
|
...(signal ? { signal } : {}),
|
|
641
704
|
...(resumeFailed ? { resumeDegraded: true } : {}),
|
|
642
705
|
...(usage?.source ? { usageSource: usage.source } : {}),
|
|
706
|
+
...(sdkSignals.isError ? { is_error: true } : {}),
|
|
707
|
+
...(sdkSignals.subtype ? { subtype: sdkSignals.subtype } : {}),
|
|
643
708
|
},
|
|
644
709
|
};
|
|
645
710
|
saveResult(runKey, result);
|
|
646
711
|
appendEvent(runKey, endMetadata, { type: 'result', result });
|
|
712
|
+
console.error(`[run-agent-engine] runKey=${runKey} run terminal: status=${endMetadata.status} exitCode=${code}${signal ? ` signal=${signal}` : ''} success=${result.success} deliverableLen=${(resultText || '').length} sessionId=${resultSessionId ?? 'none'}${resumeFailed ? ' resumeDegraded=true' : ''}`);
|
|
647
713
|
|
|
648
714
|
// 发送 callback(如果有)
|
|
649
715
|
// merge-workstation-tools R11 / 9b.5:payload 对齐 V1 run-claude-agent.mjs
|
|
650
716
|
// 字段语义,满足 /api/v1/workstation-task-callbacks 的 zod schema(recordId 必填),
|
|
651
717
|
// 使 HTTP callback 快路径对 V2 run 也能闭合(此前仅 observer 投影 + reconcile 兜底)。
|
|
652
718
|
if (privateBindings?.callback) {
|
|
719
|
+
console.error(`[run-agent-engine] runKey=${runKey} callback 发送: url=${privateBindings.callback.url} status=${endMetadata.status}`);
|
|
653
720
|
sendCallback(
|
|
654
721
|
privateBindings.callback.url,
|
|
655
722
|
privateBindings.callback.token,
|
|
656
723
|
buildTerminalCallbackPayload(runKey, endMetadata, result, metadata),
|
|
657
724
|
).catch((error) => {
|
|
658
|
-
|
|
725
|
+
// callback 失败必须可观测:失败原因写入既有 result.json(reconcile 已读该文件),
|
|
726
|
+
// 而非仅 console.error(worker stdio 被上层 ignore,会静默吞掉)。
|
|
727
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
728
|
+
console.error(`[run-agent-engine] Callback failed for ${runKey}:`, errorMsg);
|
|
729
|
+
try {
|
|
730
|
+
const current = loadResult(runKey) ?? {};
|
|
731
|
+
saveResult(runKey, {
|
|
732
|
+
...current,
|
|
733
|
+
metadata: {
|
|
734
|
+
...(current.metadata && typeof current.metadata === 'object' ? current.metadata : {}),
|
|
735
|
+
callbackError: errorMsg,
|
|
736
|
+
},
|
|
737
|
+
});
|
|
738
|
+
} catch (persistError) {
|
|
739
|
+
console.error(`[run-agent-engine] Callback failed to persist for ${runKey}:`, persistError.message);
|
|
740
|
+
}
|
|
659
741
|
});
|
|
742
|
+
} else {
|
|
743
|
+
console.error(`[run-agent-engine] runKey=${runKey} 无 callback 配置,终态依赖 observer 投影/reconcile`);
|
|
660
744
|
}
|
|
661
745
|
resolve();
|
|
662
746
|
}));
|
|
@@ -797,7 +881,8 @@ function listEvents(runKey, afterSequence = 0) {
|
|
|
797
881
|
if (!fs.existsSync(file)) return [];
|
|
798
882
|
return fs.readFileSync(file, 'utf8').split('\n').filter(Boolean)
|
|
799
883
|
.map((line) => JSON.parse(line))
|
|
800
|
-
.filter((event) => event.sequence > afterSequence)
|
|
884
|
+
.filter((event) => event.sequence > afterSequence)
|
|
885
|
+
.slice(0, MAX_EVENTS_PAGE_SIZE);
|
|
801
886
|
}
|
|
802
887
|
|
|
803
888
|
/**
|
|
@@ -847,6 +932,11 @@ function buildEngineArgv(launchSpec) {
|
|
|
847
932
|
}
|
|
848
933
|
}
|
|
849
934
|
|
|
935
|
+
// G9: 透传上下文预算
|
|
936
|
+
if (launchSpec.contextBudget) {
|
|
937
|
+
argv.push('--context-budget', JSON.stringify(launchSpec.contextBudget));
|
|
938
|
+
}
|
|
939
|
+
|
|
850
940
|
// 添加 input(第一个 text block)
|
|
851
941
|
if (input?.blocks) {
|
|
852
942
|
for (const block of input.blocks) {
|
|
@@ -886,7 +976,26 @@ function buildEngineArgv(launchSpec) {
|
|
|
886
976
|
function status(runKey) {
|
|
887
977
|
const metadata = loadRunMetadata(runKey);
|
|
888
978
|
if (!metadata) {
|
|
889
|
-
|
|
979
|
+
// status.json 尚未写入,说明 run 还在启动阶段(launch 中 saveRunMetadata 之前的耗时操作)。
|
|
980
|
+
// 返回 starting 状态而非抛异常,避免 inspect 轮询返回 502。
|
|
981
|
+
return {
|
|
982
|
+
ref: {
|
|
983
|
+
instance: { instanceId: '', hostKind: 'workstation' },
|
|
984
|
+
executionSnapshot: {},
|
|
985
|
+
runKey,
|
|
986
|
+
engineId: '',
|
|
987
|
+
invocationFingerprint: '',
|
|
988
|
+
},
|
|
989
|
+
status: 'starting',
|
|
990
|
+
executionDurability: 'host_persistent',
|
|
991
|
+
eventStreamMode: 'replay',
|
|
992
|
+
eventRetention: { maxEvents: 10000 },
|
|
993
|
+
lastSequence: 0,
|
|
994
|
+
pendingInteractions: [],
|
|
995
|
+
recoverable: true,
|
|
996
|
+
runtimeVersion: 'run-agent-engine/1',
|
|
997
|
+
protocolVersion: '1',
|
|
998
|
+
};
|
|
890
999
|
}
|
|
891
1000
|
|
|
892
1001
|
const result = loadResult(runKey);
|
|
@@ -1194,6 +1303,8 @@ async function sessionGet() {
|
|
|
1194
1303
|
const includeRawMessages = hasFlag(argv, '--include-raw-messages');
|
|
1195
1304
|
const includeSystem = hasFlag(argv, '--include-system');
|
|
1196
1305
|
const noTruncate = hasFlag(argv, '--no-truncate');
|
|
1306
|
+
const noMessages = hasFlag(argv, '--no-messages');
|
|
1307
|
+
const joinedMessages = hasFlag(argv, '--joined-messages');
|
|
1197
1308
|
const normalizedWorkDir = getOptValue(argv, '--normalized-work-dir');
|
|
1198
1309
|
|
|
1199
1310
|
const { sdk, error } = loadSdk();
|
|
@@ -1239,6 +1350,20 @@ async function sessionGet() {
|
|
|
1239
1350
|
}
|
|
1240
1351
|
}
|
|
1241
1352
|
|
|
1353
|
+
// --no-messages:仅返回 session 元数据,跳过消息体(避免大 payload 触发 sandbox stdout 截断)。
|
|
1354
|
+
if (noMessages) {
|
|
1355
|
+
return {
|
|
1356
|
+
session: sessionSummary(sessionMeta, engineId),
|
|
1357
|
+
messages: [],
|
|
1358
|
+
rawMessagesIncluded: false,
|
|
1359
|
+
metadata: {
|
|
1360
|
+
messageCount: sessionMeta.messageCount,
|
|
1361
|
+
sessionMessagesProjectDir: msgOpts.dir != null ? String(msgOpts.dir) : undefined,
|
|
1362
|
+
sessionMessagesDirSource: dirSource,
|
|
1363
|
+
},
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1242
1367
|
/** @type {unknown[]} */
|
|
1243
1368
|
let messages = [];
|
|
1244
1369
|
try {
|
|
@@ -1307,12 +1432,34 @@ async function sessionGet() {
|
|
|
1307
1432
|
const cliReplayGuide = buildCliReplayGuide(sessionMeta, sessionId);
|
|
1308
1433
|
const diagnostics = deepRedactSessionValue(buildDiagnostics(messages, noTruncate));
|
|
1309
1434
|
|
|
1435
|
+
// --joined-messages:拼接消息文本,替代结构化 messages[],避免大会话 stdout 截断。
|
|
1436
|
+
let joinedMessagesText = '';
|
|
1437
|
+
if (joinedMessages) {
|
|
1438
|
+
const parts = [];
|
|
1439
|
+
for (const m of messages) {
|
|
1440
|
+
if (m == null || typeof m !== 'object') continue;
|
|
1441
|
+
const rec = /** @type {Record<string, unknown>} */ (m);
|
|
1442
|
+
const type = String(rec.type ?? '');
|
|
1443
|
+
const text = extractText(rec.message);
|
|
1444
|
+
if (text) {
|
|
1445
|
+
parts.push(`${type}: ${text}`);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
joinedMessagesText = parts.join('\n\n');
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1310
1451
|
/** @type {Record<string, unknown>} */
|
|
1311
1452
|
const detail = {
|
|
1312
1453
|
session: sessionSummary(sessionMeta, engineId),
|
|
1313
|
-
messages: structuredMessages,
|
|
1314
1454
|
rawMessagesIncluded: includeRawMessages,
|
|
1315
1455
|
};
|
|
1456
|
+
// joinedMessages 模式:messages 置空数组(schema 必填),正文走 joinedMessages 文本。
|
|
1457
|
+
if (joinedMessages) {
|
|
1458
|
+
detail.messages = [];
|
|
1459
|
+
detail.joinedMessages = joinedMessagesText;
|
|
1460
|
+
} else {
|
|
1461
|
+
detail.messages = structuredMessages;
|
|
1462
|
+
}
|
|
1316
1463
|
|
|
1317
1464
|
/** @type {Record<string, unknown>} */
|
|
1318
1465
|
const metadata = {
|
|
@@ -1333,7 +1480,7 @@ async function sessionGet() {
|
|
|
1333
1480
|
if (!ok && messages.length > 0) metadata.rawMessagesSerializationFailed = true;
|
|
1334
1481
|
}
|
|
1335
1482
|
|
|
1336
|
-
return { ...detail, metadata };
|
|
1483
|
+
return compactSessionDetailForTransport({ ...detail, metadata });
|
|
1337
1484
|
}
|
|
1338
1485
|
|
|
1339
1486
|
/**
|
|
@@ -1349,7 +1496,6 @@ async function sessionSearch() {
|
|
|
1349
1496
|
const cursor = getOptValue(argv, '--cursor');
|
|
1350
1497
|
const offset = decodeOffsetCursor(cursor);
|
|
1351
1498
|
const normalizedWorkDir = getOptValue(argv, '--normalized-work-dir');
|
|
1352
|
-
|
|
1353
1499
|
const { sdk, error } = loadSdk();
|
|
1354
1500
|
if (!sdk || typeof sdk.listSessions !== 'function') {
|
|
1355
1501
|
return { hits: [], error: error ?? 'SDK 不支持 listSessions' };
|
package/src/run-engine.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* --engine <claude-code|codex|hermes|openclaw> \
|
|
7
7
|
* [--model <model>] [--permission-mode <mode>] \
|
|
8
8
|
* [--allowed-tools <tool1,tool2>] [--resume <sessionId>|--fork <sessionId>] \
|
|
9
|
+
* [--context-budget <json>] \
|
|
9
10
|
* [--timeout <seconds>] [--work-dir <dir>] \
|
|
10
11
|
* <prompt>
|
|
11
12
|
*
|
|
@@ -13,11 +14,75 @@
|
|
|
13
14
|
* 输出到 stdout,供 run-agent-engine.mjs 解析并写入 events.jsonl。
|
|
14
15
|
*/
|
|
15
16
|
import { createRequire } from 'node:module';
|
|
17
|
+
import { isSdkFailureSubtype } from './engine-run-success.mjs';
|
|
16
18
|
|
|
17
19
|
const require = createRequire(import.meta.url);
|
|
18
20
|
|
|
19
21
|
const SDK_PATH = process.env.WINMATRIX_AGENT_SDK_PATH
|
|
20
22
|
|| '/home/node/.local/lib/node_modules/@winmatrix/agent-sdk';
|
|
23
|
+
const MAX_PRIVATE_BINDINGS_BYTES = 64 * 1024;
|
|
24
|
+
const MCP_DYNAMIC_SECRET_KEYS = new Set([
|
|
25
|
+
'WINMATRIX_TOOL_PROXY_URL',
|
|
26
|
+
'WINMATRIX_TOOL_PROXY_TOKEN',
|
|
27
|
+
'WINMATRIX_TOOL_PROXY_PROJECT_ID',
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
function normalizeEventText(value) {
|
|
31
|
+
if (typeof value === 'string') return value;
|
|
32
|
+
if (value === null || value === undefined) return '';
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
return value.map(normalizeEventText).filter(Boolean).join('\n');
|
|
35
|
+
}
|
|
36
|
+
if (typeof value === 'object') {
|
|
37
|
+
if (typeof value.text === 'string') return value.text;
|
|
38
|
+
try {
|
|
39
|
+
return JSON.stringify(value);
|
|
40
|
+
} catch {
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return String(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function readMcpBinding() {
|
|
48
|
+
const chunks = [];
|
|
49
|
+
let size = 0;
|
|
50
|
+
for await (const chunk of process.stdin) {
|
|
51
|
+
size += chunk.length;
|
|
52
|
+
if (size > MAX_PRIVATE_BINDINGS_BYTES) {
|
|
53
|
+
return { error: 'MCP_BINDING_REQUIRED: 私有控制绑定超过允许大小。' };
|
|
54
|
+
}
|
|
55
|
+
chunks.push(chunk);
|
|
56
|
+
}
|
|
57
|
+
if (chunks.length === 0) return {};
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const raw = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
61
|
+
if (!raw || raw.hostKind !== 'workstation') {
|
|
62
|
+
return { error: 'MCP_BINDING_REQUIRED: 私有控制绑定 hostKind 无效。' };
|
|
63
|
+
}
|
|
64
|
+
const dynamicSecrets = raw.dynamicSecrets;
|
|
65
|
+
if (dynamicSecrets === undefined) {
|
|
66
|
+
return { error: 'MCP_BINDING_REQUIRED: 工作站缺少 MCP 绑定。' };
|
|
67
|
+
}
|
|
68
|
+
if (!dynamicSecrets || typeof dynamicSecrets !== 'object' || Array.isArray(dynamicSecrets)) {
|
|
69
|
+
return { error: 'MCP_BINDING_REQUIRED: MCP 绑定格式无效。' };
|
|
70
|
+
}
|
|
71
|
+
for (const [key, value] of Object.entries(dynamicSecrets)) {
|
|
72
|
+
if (!MCP_DYNAMIC_SECRET_KEYS.has(key) || typeof value !== 'string') {
|
|
73
|
+
return { error: 'MCP_BINDING_REQUIRED: MCP 绑定包含不允许字段。' };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const url = dynamicSecrets.WINMATRIX_TOOL_PROXY_URL?.trim();
|
|
77
|
+
const token = dynamicSecrets.WINMATRIX_TOOL_PROXY_TOKEN?.trim();
|
|
78
|
+
if (!url || !token) {
|
|
79
|
+
return { error: 'MCP_BINDING_REQUIRED: MCP 绑定不完整。' };
|
|
80
|
+
}
|
|
81
|
+
return { binding: { url, token } };
|
|
82
|
+
} catch {
|
|
83
|
+
return { error: 'MCP_BINDING_REQUIRED: 私有控制绑定不是有效 JSON。' };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
21
86
|
|
|
22
87
|
function parseArgs(argv) {
|
|
23
88
|
const options = {
|
|
@@ -29,6 +94,7 @@ function parseArgs(argv) {
|
|
|
29
94
|
fork: undefined,
|
|
30
95
|
timeoutSec: undefined,
|
|
31
96
|
workDir: undefined,
|
|
97
|
+
contextBudget: undefined,
|
|
32
98
|
};
|
|
33
99
|
const positionals = [];
|
|
34
100
|
|
|
@@ -50,6 +116,18 @@ function parseArgs(argv) {
|
|
|
50
116
|
options.timeoutSec = argv[++i];
|
|
51
117
|
} else if (arg === '--work-dir') {
|
|
52
118
|
options.workDir = argv[++i];
|
|
119
|
+
} else if (arg === '--context-budget') {
|
|
120
|
+
const raw = argv[++i];
|
|
121
|
+
try {
|
|
122
|
+
const parsed = JSON.parse(raw);
|
|
123
|
+
if (parsed && typeof parsed === 'object') {
|
|
124
|
+
options.contextBudget = parsed;
|
|
125
|
+
} else {
|
|
126
|
+
console.error('[run-engine] --context-budget 不是有效 JSON 对象, 已忽略');
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
console.error('[run-engine] --context-budget JSON 解析失败, 已忽略');
|
|
130
|
+
}
|
|
53
131
|
} else if (arg.startsWith('--')) {
|
|
54
132
|
// 忽略未来可能透传的 engine-specific 选项;后续可按 engineId 路由
|
|
55
133
|
console.error(`[run-engine] 忽略未知选项: ${arg}`);
|
|
@@ -78,6 +156,13 @@ async function main() {
|
|
|
78
156
|
process.exit(1);
|
|
79
157
|
}
|
|
80
158
|
|
|
159
|
+
const mcpBinding = await readMcpBinding();
|
|
160
|
+
if (mcpBinding.error) {
|
|
161
|
+
writeEvent({ type: 'error', error: mcpBinding.error, code: 'MCP_CONNECTION_FAILED' });
|
|
162
|
+
process.exitCode = 1;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
81
166
|
let sdk;
|
|
82
167
|
try {
|
|
83
168
|
sdk = require(SDK_PATH);
|
|
@@ -92,8 +177,19 @@ async function main() {
|
|
|
92
177
|
CodexAdapter,
|
|
93
178
|
HermesAdapter,
|
|
94
179
|
OpenClawAdapter,
|
|
180
|
+
validateMcpBridgeUrl,
|
|
95
181
|
} = sdk;
|
|
96
182
|
|
|
183
|
+
if (mcpBinding.binding) {
|
|
184
|
+
try {
|
|
185
|
+
validateMcpBridgeUrl(mcpBinding.binding.url);
|
|
186
|
+
} catch (err) {
|
|
187
|
+
writeEvent({ type: 'error', error: `MCP_BINDING_REQUIRED: MCP URL 无效: ${err.message}`, code: 'MCP_CONNECTION_FAILED' });
|
|
188
|
+
process.exitCode = 1;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
97
193
|
const registry = new AdapterRegistry();
|
|
98
194
|
registry.register('claude-code', ClaudeAdapter);
|
|
99
195
|
registry.register('codex', CodexAdapter);
|
|
@@ -105,7 +201,7 @@ async function main() {
|
|
|
105
201
|
process.exit(1);
|
|
106
202
|
}
|
|
107
203
|
|
|
108
|
-
const adapterConfig = {};
|
|
204
|
+
const adapterConfig = mcpBinding.binding ? { mcpBridgeUrl: mcpBinding.binding.url } : {};
|
|
109
205
|
const adapter = registry.create(options.engineId, adapterConfig);
|
|
110
206
|
|
|
111
207
|
/** @type {import('@winmatrix/agent-sdk').AgentTaskContext} */
|
|
@@ -116,9 +212,13 @@ async function main() {
|
|
|
116
212
|
engineId: options.engineId,
|
|
117
213
|
workDir: options.workDir,
|
|
118
214
|
};
|
|
215
|
+
if (mcpBinding.binding) task.mcpToken = mcpBinding.binding.token;
|
|
216
|
+
if (options.contextBudget) task.contextBudget = options.contextBudget;
|
|
119
217
|
|
|
120
218
|
if (options.engineId === 'claude-code') {
|
|
121
|
-
|
|
219
|
+
// Coding workstation 使用 Agent SDK 路径:任务 MCP 仅随 query() 显式传入,
|
|
220
|
+
// 不落盘到 artifact,也不改写 Pod 注入的持久 agent-home。
|
|
221
|
+
task.claude = { useAgentSdk: true };
|
|
122
222
|
if (options.permissionMode) task.claude.permissionMode = options.permissionMode;
|
|
123
223
|
if (options.allowedTools) task.claude.tools = options.allowedTools.split(',').map((s) => s.trim());
|
|
124
224
|
} else if (options.engineId === 'hermes') {
|
|
@@ -150,28 +250,53 @@ async function main() {
|
|
|
150
250
|
}
|
|
151
251
|
|
|
152
252
|
try {
|
|
253
|
+
const startedAt = Date.now();
|
|
254
|
+
// 链路日志走 stderr:stdout 是 JSONL 协议通道
|
|
255
|
+
console.error(`[run-engine] pid=${process.pid} adapter.execute 开始: engineId=${options.engineId} workDir=${options.workDir ?? '(cwd)'} resume=${options.resume ?? 'none'} fork=${options.fork ?? 'none'} timeoutSec=${options.timeoutSec ?? 'none'}`);
|
|
256
|
+
let eventCount = 0;
|
|
153
257
|
for await (const event of adapter.execute(task, abortController?.signal)) {
|
|
258
|
+
eventCount += 1;
|
|
154
259
|
switch (event.type) {
|
|
155
260
|
case 'delta':
|
|
156
|
-
|
|
261
|
+
if (event.engineEvent) {
|
|
262
|
+
writeEvent(event.engineEvent);
|
|
263
|
+
} else {
|
|
264
|
+
writeEvent({ type: 'content_delta', text: normalizeEventText(event.content) });
|
|
265
|
+
}
|
|
157
266
|
break;
|
|
158
267
|
case 'thinking':
|
|
159
|
-
|
|
268
|
+
if (event.engineEvent) {
|
|
269
|
+
writeEvent(event.engineEvent);
|
|
270
|
+
} else {
|
|
271
|
+
writeEvent({ type: 'thinking_delta', text: normalizeEventText(event.content) });
|
|
272
|
+
}
|
|
160
273
|
break;
|
|
161
274
|
case 'tool_call':
|
|
162
275
|
writeEvent({ type: 'tool_call', name: event.name, input: event.args });
|
|
163
276
|
break;
|
|
164
|
-
case 'result':
|
|
277
|
+
case 'result': {
|
|
278
|
+
const isError = event.isError === true;
|
|
279
|
+
const subtype = typeof event.sdkSubtype === 'string' ? event.sdkSubtype : undefined;
|
|
280
|
+
const sdkFailed = isError || isSdkFailureSubtype(subtype);
|
|
281
|
+
const text = normalizeEventText(event.content);
|
|
282
|
+
const error = sdkFailed
|
|
283
|
+
? (text.trim() || (subtype ? `SDK result subtype=${subtype}` : 'SDK result is_error=true'))
|
|
284
|
+
: undefined;
|
|
285
|
+
console.error(`[run-engine] pid=${process.pid} adapter.execute 结束: engineId=${options.engineId} events=${eventCount} durationMs=${Date.now() - startedAt} sessionId=${event.sessionId ?? 'none'} success=${!sdkFailed} subtype=${subtype ?? 'none'}`);
|
|
165
286
|
writeEvent({
|
|
166
287
|
type: 'result',
|
|
167
288
|
result: {
|
|
168
|
-
success:
|
|
169
|
-
text
|
|
289
|
+
success: !sdkFailed,
|
|
290
|
+
text,
|
|
170
291
|
...(event.sessionId ? { sessionId: event.sessionId } : {}),
|
|
171
292
|
...(event.usage ? { usage: event.usage } : {}),
|
|
293
|
+
...(isError ? { is_error: true } : {}),
|
|
294
|
+
...(subtype ? { subtype } : {}),
|
|
295
|
+
...(error ? { error } : {}),
|
|
172
296
|
},
|
|
173
297
|
});
|
|
174
298
|
break;
|
|
299
|
+
}
|
|
175
300
|
case 'error':
|
|
176
301
|
writeEvent({ type: 'error', error: event.message, code: event.code });
|
|
177
302
|
break;
|
|
@@ -182,6 +307,7 @@ async function main() {
|
|
|
182
307
|
}
|
|
183
308
|
process.exit(0);
|
|
184
309
|
} catch (e) {
|
|
310
|
+
console.error(`[run-engine] pid=${process.pid} adapter.execute 异常: engineId=${options.engineId} error=${e instanceof Error ? e.message : String(e)}`);
|
|
185
311
|
writeEvent({ type: 'error', error: e instanceof Error ? e.message : String(e) });
|
|
186
312
|
process.exit(1);
|
|
187
313
|
} finally {
|