@winmatrix/daemon 0.2.4 → 0.2.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/README.md +28 -29
- package/dist/core/AgentProcessManager.d.ts +10 -7
- package/dist/core/AgentProcessManager.d.ts.map +1 -1
- package/dist/core/AgentProcessManager.js +27 -2
- package/dist/core/AgentProcessManager.js.map +1 -1
- package/dist/core/ConfigLoader.d.ts +3 -1
- package/dist/core/ConfigLoader.d.ts.map +1 -1
- package/dist/core/ConfigLoader.js +15 -10
- package/dist/core/ConfigLoader.js.map +1 -1
- package/dist/core/DaemonFileLogger.d.ts.map +1 -1
- package/dist/core/DaemonFileLogger.js +38 -3
- package/dist/core/DaemonFileLogger.js.map +1 -1
- package/dist/core/DaemonLifecycle.d.ts.map +1 -1
- package/dist/core/DaemonLifecycle.js +6 -5
- package/dist/core/DaemonLifecycle.js.map +1 -1
- package/dist/wrapper/AgentProcessManager.d.ts +80 -0
- package/dist/wrapper/AgentProcessManager.d.ts.map +1 -0
- package/dist/wrapper/AgentProcessManager.js +474 -0
- package/dist/wrapper/AgentProcessManager.js.map +1 -0
- package/dist/wrapper/AgentWrapper.d.ts +2 -18
- package/dist/wrapper/AgentWrapper.d.ts.map +1 -1
- package/dist/wrapper/AgentWrapper.js +32 -682
- package/dist/wrapper/AgentWrapper.js.map +1 -1
- package/dist/wrapper/claudeCommand.d.ts +1 -1
- package/dist/wrapper/claudeCommand.d.ts.map +1 -1
- package/dist/wrapper/claudeCommand.js +2 -2
- package/dist/wrapper/claudeCommand.js.map +1 -1
- package/package.json +4 -2
- package/dist/wrapper/ClaudeStreamParser.d.ts +0 -67
- package/dist/wrapper/ClaudeStreamParser.d.ts.map +0 -1
- package/dist/wrapper/ClaudeStreamParser.js +0 -116
- package/dist/wrapper/ClaudeStreamParser.js.map +0 -1
|
@@ -1,56 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @deprecated
|
|
3
|
-
* 保留供未来 multiprocess 模式参考,所有维护性修改应在此注明原因。
|
|
2
|
+
* @deprecated Scheme B runs adapters in-process; this multiprocess wrapper remains for reference.
|
|
4
3
|
*
|
|
5
|
-
* Agent Wrapper
|
|
6
|
-
*
|
|
7
|
-
* 由 Daemon 通过 agent.create 信令 spawn 启动。
|
|
8
|
-
* 通过 stdin/stdout JSON 帧与 Daemon 通信,Daemon 负责通过 WS 多路复用转发给 Server。
|
|
9
|
-
*
|
|
10
|
-
* 协议(stdin/stdout 均为 JSON Lines):
|
|
11
|
-
* stdin: {"type":"fwd","data":<Server 发来的帧>}
|
|
12
|
-
* stdout: {"type":"fwd","agentId":"...","data":<发往 Server 的帧>}
|
|
13
|
-
* stdout: {"type":"event","event":"agent.ready","payload":{...}}
|
|
14
|
-
* stdout: {"type":"event","event":"agent.stopped","payload":{...}}
|
|
15
|
-
* stdout: {"type":"event","event":"agent.error","payload":{...}}
|
|
16
|
-
*
|
|
17
|
-
* 任务模型:按 agentType 分发到不同执行模式:
|
|
18
|
-
* claude-code → spawn claude --print --output-format stream-json ...
|
|
19
|
-
* hermes → fetch() POST 到 HTTP endpoint
|
|
20
|
-
* default → spawn(runtime, ['--print', '--output-format', 'json', input])
|
|
4
|
+
* Agent Wrapper — stdin/stdout JSON Lines IPC with Daemon; task execution delegated to WrapperProcessManager.
|
|
21
5
|
*/
|
|
22
|
-
import { spawn } from 'node:child_process';
|
|
23
|
-
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
24
|
-
import { join } from 'node:path';
|
|
25
|
-
import { homedir } from 'node:os';
|
|
26
|
-
import { randomUUID } from 'node:crypto';
|
|
27
|
-
import { unwrapOpenClawPayloads } from '@winmatrix/agent-sdk';
|
|
28
|
-
import { parseClaudeLine, splitLines } from './ClaudeStreamParser.js';
|
|
29
|
-
import { resolveClaudeRuntimeCommand } from './claudeCommand.js';
|
|
30
6
|
import { scanWorkDir, detectRuntimeInfo } from '../core/WorkspaceScanner.js';
|
|
31
|
-
|
|
7
|
+
import { WrapperProcessManager, } from './AgentProcessManager.js';
|
|
32
8
|
let config = null;
|
|
33
|
-
|
|
34
|
-
/* ── Helpers ── */
|
|
9
|
+
let processManager = null;
|
|
35
10
|
function send(frame) {
|
|
36
11
|
process.stdout.write(JSON.stringify(frame) + '\n');
|
|
37
12
|
}
|
|
38
13
|
function parseStdin(line) {
|
|
39
14
|
try {
|
|
40
15
|
const parsed = JSON.parse(line);
|
|
41
|
-
if (parsed && typeof parsed === 'object') {
|
|
42
|
-
|
|
43
|
-
if (parsed.type === 'tool_result') {
|
|
44
|
-
handleToolResult(parsed);
|
|
45
|
-
return null; // Not a server frame, don't forward
|
|
46
|
-
}
|
|
47
|
-
if (parsed.type === 'fwd') {
|
|
48
|
-
return parsed;
|
|
49
|
-
}
|
|
16
|
+
if (parsed && typeof parsed === 'object' && parsed.type === 'fwd') {
|
|
17
|
+
return parsed;
|
|
50
18
|
}
|
|
51
19
|
}
|
|
52
20
|
catch {
|
|
53
|
-
|
|
21
|
+
/* ignore malformed lines */
|
|
54
22
|
}
|
|
55
23
|
return null;
|
|
56
24
|
}
|
|
@@ -76,11 +44,7 @@ function sendTaskDelta(taskId, content, meta) {
|
|
|
76
44
|
send({
|
|
77
45
|
type: 'fwd',
|
|
78
46
|
agentId: config.agentId,
|
|
79
|
-
data: {
|
|
80
|
-
type: 'event',
|
|
81
|
-
event: 'task.delta',
|
|
82
|
-
payload,
|
|
83
|
-
},
|
|
47
|
+
data: { type: 'event', event: 'task.delta', payload },
|
|
84
48
|
});
|
|
85
49
|
}
|
|
86
50
|
function sendTaskComplete(taskId, result, sessionId, usage) {
|
|
@@ -94,579 +58,9 @@ function sendTaskComplete(taskId, result, sessionId, usage) {
|
|
|
94
58
|
send({
|
|
95
59
|
type: 'fwd',
|
|
96
60
|
agentId: config.agentId,
|
|
97
|
-
data: {
|
|
98
|
-
type: 'event',
|
|
99
|
-
event: 'task.complete',
|
|
100
|
-
payload,
|
|
101
|
-
},
|
|
61
|
+
data: { type: 'event', event: 'task.complete', payload },
|
|
102
62
|
});
|
|
103
63
|
}
|
|
104
|
-
function cleanupTask(taskId) {
|
|
105
|
-
const entry = runningTasks.get(taskId);
|
|
106
|
-
if (!entry)
|
|
107
|
-
return;
|
|
108
|
-
runningTasks.delete(taskId);
|
|
109
|
-
}
|
|
110
|
-
/* ── Task Routing ── */
|
|
111
|
-
function routeTask(task) {
|
|
112
|
-
if (!config)
|
|
113
|
-
return;
|
|
114
|
-
const taskId = task.taskId;
|
|
115
|
-
if (runningTasks.has(taskId)) {
|
|
116
|
-
sendTaskError(taskId, 'Task already running');
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
switch (config.agentType) {
|
|
120
|
-
case 'claude-code':
|
|
121
|
-
spawnClaudeCodeTask(task);
|
|
122
|
-
break;
|
|
123
|
-
case 'hermes':
|
|
124
|
-
spawnHermesTask(task);
|
|
125
|
-
break;
|
|
126
|
-
case 'openclaw':
|
|
127
|
-
spawnOpenClawTask(task);
|
|
128
|
-
break;
|
|
129
|
-
default:
|
|
130
|
-
spawnGenericTask(task);
|
|
131
|
-
break;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
/* ── Claude Code Task ── */
|
|
135
|
-
/**
|
|
136
|
-
* 在 workDir 下生成 .claude/mcp.json 配置,让 Claude Code 能通过 MCP Bridge 调用 WinMatrix 工具。
|
|
137
|
-
* 如果 .claude/mcp.json 已存在,使用 mcp.winmatrix.json 并通过 --mcp-config 指定。
|
|
138
|
-
*/
|
|
139
|
-
function generateClaudeMcpConfig(mcpBridgeUrl, mcpApiKey, workDir) {
|
|
140
|
-
const mcpConfig = {
|
|
141
|
-
mcpServers: {
|
|
142
|
-
winmatrix: {
|
|
143
|
-
type: 'http',
|
|
144
|
-
url: mcpBridgeUrl,
|
|
145
|
-
...(mcpApiKey ? { headers: { Authorization: `Bearer ${mcpApiKey}` } } : {}),
|
|
146
|
-
},
|
|
147
|
-
},
|
|
148
|
-
};
|
|
149
|
-
const configDir = workDir ? join(workDir, '.claude') : join(homedir(), '.claude');
|
|
150
|
-
const primaryPath = join(configDir, 'mcp.json');
|
|
151
|
-
const fallbackPath = join(configDir, 'mcp.winmatrix.json');
|
|
152
|
-
try {
|
|
153
|
-
if (!existsSync(configDir)) {
|
|
154
|
-
mkdirSync(configDir, { recursive: true });
|
|
155
|
-
}
|
|
156
|
-
if (existsSync(primaryPath)) {
|
|
157
|
-
writeFileSync(fallbackPath, JSON.stringify(mcpConfig, null, 2), 'utf-8');
|
|
158
|
-
return ['--mcp-config', fallbackPath];
|
|
159
|
-
}
|
|
160
|
-
writeFileSync(primaryPath, JSON.stringify(mcpConfig, null, 2), 'utf-8');
|
|
161
|
-
return null;
|
|
162
|
-
}
|
|
163
|
-
catch (err) {
|
|
164
|
-
console.warn(`MCP 配置写入失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
const CLAUDE_BASE_ARGS = [
|
|
169
|
-
'--print',
|
|
170
|
-
'--verbose',
|
|
171
|
-
'--input-format', 'stream-json',
|
|
172
|
-
'--output-format', 'stream-json',
|
|
173
|
-
'--include-partial-messages',
|
|
174
|
-
];
|
|
175
|
-
function spawnClaudeCodeTask(task) {
|
|
176
|
-
if (!config)
|
|
177
|
-
return;
|
|
178
|
-
const taskId = task.taskId;
|
|
179
|
-
const workDir = task.workDir ?? config.workDir;
|
|
180
|
-
const claude = task.claude ?? {};
|
|
181
|
-
// Validate workDir
|
|
182
|
-
if (workDir && !existsSync(workDir)) {
|
|
183
|
-
sendTaskError(taskId, `Work directory does not exist: ${workDir}`);
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
// Assemble CLI args
|
|
187
|
-
const args = [...CLAUDE_BASE_ARGS];
|
|
188
|
-
// Permission mode (from task config, default 'auto')
|
|
189
|
-
const permissionMode = claude.permissionMode ?? 'bypassPermissions';
|
|
190
|
-
args.push('--permission-mode', permissionMode);
|
|
191
|
-
// workDir → --add-dir
|
|
192
|
-
if (workDir) {
|
|
193
|
-
args.push('--add-dir', workDir);
|
|
194
|
-
}
|
|
195
|
-
// Session management: oneshot skips --session-id entirely
|
|
196
|
-
const isOneshot = task.mode === 'oneshot';
|
|
197
|
-
let sessionId;
|
|
198
|
-
if (!isOneshot) {
|
|
199
|
-
if (claude.forkSession) {
|
|
200
|
-
sessionId = randomUUID();
|
|
201
|
-
args.push('--session-id', sessionId);
|
|
202
|
-
}
|
|
203
|
-
else if (claude.resumeSession && claude.sessionId) {
|
|
204
|
-
sessionId = claude.sessionId;
|
|
205
|
-
args.push('--resume', sessionId);
|
|
206
|
-
}
|
|
207
|
-
else if (claude.sessionId) {
|
|
208
|
-
sessionId = claude.sessionId;
|
|
209
|
-
args.push('--session-id', sessionId);
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
sessionId = randomUUID();
|
|
213
|
-
args.push('--session-id', sessionId);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
// Tool restriction
|
|
217
|
-
if (claude.tools && claude.tools.length > 0) {
|
|
218
|
-
args.push('--allowedTools', claude.tools.join(','));
|
|
219
|
-
}
|
|
220
|
-
if (claude.disallowedTools && claude.disallowedTools.length > 0) {
|
|
221
|
-
args.push('--disallowedTools', claude.disallowedTools.join(','));
|
|
222
|
-
}
|
|
223
|
-
// Budget
|
|
224
|
-
if (claude.maxBudgetUsd !== undefined) {
|
|
225
|
-
args.push('--max-budget-usd', String(claude.maxBudgetUsd));
|
|
226
|
-
}
|
|
227
|
-
// System prompt: task.systemPrompt 优先于 claude.systemPrompt
|
|
228
|
-
const effectiveSystemPrompt = task.systemPrompt ?? claude.systemPrompt;
|
|
229
|
-
if (effectiveSystemPrompt) {
|
|
230
|
-
args.push('--append-system-prompt', effectiveSystemPrompt);
|
|
231
|
-
}
|
|
232
|
-
// MCP Bridge 配置:在 workDir 下生成 .claude/mcp.json 或 mcp.winmatrix.json
|
|
233
|
-
if (config.mcpBridgeUrl) {
|
|
234
|
-
const effectiveMcpKey = task.mcpToken ?? config.mcpApiKey;
|
|
235
|
-
const mcpArgs = generateClaudeMcpConfig(config.mcpBridgeUrl, effectiveMcpKey, workDir);
|
|
236
|
-
if (mcpArgs) {
|
|
237
|
-
args.push(...mcpArgs);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
// Build env
|
|
241
|
-
const env = {
|
|
242
|
-
...process.env,
|
|
243
|
-
WINMATRIX_AGENT_ID: config.agentId,
|
|
244
|
-
WINMATRIX_API_KEY: config.apiKey,
|
|
245
|
-
WINMATRIX_AGENT_NAME: config.name,
|
|
246
|
-
WINMATRIX_AGENT_TYPE: config.agentType,
|
|
247
|
-
WINMATRIX_TASK_ID: taskId,
|
|
248
|
-
...task.env,
|
|
249
|
-
};
|
|
250
|
-
let proc;
|
|
251
|
-
try {
|
|
252
|
-
const spawnSpec = resolveClaudeRuntimeCommand(config.runtime);
|
|
253
|
-
proc = spawn(spawnSpec.command, args, {
|
|
254
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
255
|
-
env,
|
|
256
|
-
cwd: workDir ?? process.cwd(),
|
|
257
|
-
shell: spawnSpec.shell,
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
catch (err) {
|
|
261
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
262
|
-
sendTaskError(taskId, `Failed to spawn Claude Code: ${message}`);
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
const entry = {
|
|
266
|
-
taskId,
|
|
267
|
-
process: proc,
|
|
268
|
-
abortController: null,
|
|
269
|
-
startedAt: Date.now(),
|
|
270
|
-
stdoutChunks: [],
|
|
271
|
-
completed: false,
|
|
272
|
-
};
|
|
273
|
-
runningTasks.set(taskId, entry);
|
|
274
|
-
proc.on('error', (err) => {
|
|
275
|
-
if (entry.completed)
|
|
276
|
-
return;
|
|
277
|
-
entry.completed = true;
|
|
278
|
-
sendTaskError(taskId, err.message);
|
|
279
|
-
cleanupTask(taskId);
|
|
280
|
-
});
|
|
281
|
-
// Send user message via stdin (stream-json protocol)
|
|
282
|
-
const userMessage = JSON.stringify({
|
|
283
|
-
type: 'user',
|
|
284
|
-
message: { role: 'user', content: task.input },
|
|
285
|
-
}) + '\n';
|
|
286
|
-
proc.stdin?.write(userMessage);
|
|
287
|
-
proc.stdin?.end();
|
|
288
|
-
// Track extracted fields from result event for task.complete
|
|
289
|
-
let extractedSessionId;
|
|
290
|
-
let extractedResultText;
|
|
291
|
-
let extractedUsage;
|
|
292
|
-
// Parse stdout stream-json events
|
|
293
|
-
let stdoutBuffer = '';
|
|
294
|
-
proc.stdout?.on('data', (data) => {
|
|
295
|
-
const chunk = data.toString('utf-8');
|
|
296
|
-
const { lines, remainder } = splitLines(stdoutBuffer, chunk);
|
|
297
|
-
stdoutBuffer = remainder;
|
|
298
|
-
for (const line of lines) {
|
|
299
|
-
entry.stdoutChunks.push(line + '\n');
|
|
300
|
-
const action = parseClaudeLine(line);
|
|
301
|
-
switch (action.type) {
|
|
302
|
-
case 'delta':
|
|
303
|
-
sendTaskDelta(taskId, action.text);
|
|
304
|
-
break;
|
|
305
|
-
case 'thinking':
|
|
306
|
-
sendTaskDelta(taskId, action.text, { stream: 'thinking' });
|
|
307
|
-
break;
|
|
308
|
-
case 'tool_use':
|
|
309
|
-
sendTaskDelta(taskId, `[调用工具: ${action.toolName}]`, { toolUse: { name: action.toolName, input: action.toolInput } });
|
|
310
|
-
break;
|
|
311
|
-
case 'result':
|
|
312
|
-
if (action.sessionId) {
|
|
313
|
-
extractedSessionId = action.sessionId;
|
|
314
|
-
}
|
|
315
|
-
if (action.result) {
|
|
316
|
-
extractedResultText = action.result;
|
|
317
|
-
}
|
|
318
|
-
if (action.usage) {
|
|
319
|
-
extractedUsage = action.usage;
|
|
320
|
-
}
|
|
321
|
-
break;
|
|
322
|
-
case 'error':
|
|
323
|
-
entry.completed = true;
|
|
324
|
-
sendTaskError(taskId, action.message);
|
|
325
|
-
return;
|
|
326
|
-
case 'skip':
|
|
327
|
-
break;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
// Forward stderr as task.delta with meta marker to distinguish from stdout content
|
|
332
|
-
proc.stderr?.on('data', (data) => {
|
|
333
|
-
const text = data.toString('utf-8').trimEnd();
|
|
334
|
-
if (!text)
|
|
335
|
-
return;
|
|
336
|
-
sendTaskDelta(taskId, text, { source: 'stderr' });
|
|
337
|
-
});
|
|
338
|
-
proc.on('exit', (code, signal) => {
|
|
339
|
-
if (entry.completed)
|
|
340
|
-
return;
|
|
341
|
-
entry.completed = true;
|
|
342
|
-
if (extractedSessionId || code === 0) {
|
|
343
|
-
// Use result text extracted from stream-json result event
|
|
344
|
-
sendTaskComplete(taskId, { result: extractedResultText ?? null, exitCode: code ?? 0 }, isOneshot ? undefined : extractedSessionId, extractedUsage);
|
|
345
|
-
}
|
|
346
|
-
else {
|
|
347
|
-
sendTaskError(taskId, signal
|
|
348
|
-
? `Claude Code killed by signal ${signal}`
|
|
349
|
-
: `Claude Code exited with code ${code ?? 'null'}`);
|
|
350
|
-
}
|
|
351
|
-
cleanupTask(taskId);
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
/* ── HTTP Task (shared by hermes / openclaw) ── */
|
|
355
|
-
const DEFAULT_HTTP_TIMEOUT_MS = 180000;
|
|
356
|
-
/**
|
|
357
|
-
* Shared HTTP POST task execution.
|
|
358
|
-
* Hermes and OpenClaw both POST to an endpoint; OpenClaw additionally unwraps the payload.
|
|
359
|
-
*
|
|
360
|
-
* Tool Proxy Fallback: when the HTTP response contains "unsupported mcp biz type" or
|
|
361
|
-
* errcode 846610, the Wrapper sends tool_call IPC frames to the daemon for each
|
|
362
|
-
* fallback tool call, aggregates results, and completes the task with mode: 'fallback'.
|
|
363
|
-
*/
|
|
364
|
-
function spawnHttpTask(task, options) {
|
|
365
|
-
if (!config)
|
|
366
|
-
return;
|
|
367
|
-
const taskId = task.taskId;
|
|
368
|
-
// Task-level endpoint overrides config-level; fallback to deprecated hermesEndpoint
|
|
369
|
-
const endpoint = task.endpoint ?? config.endpoint ?? config.hermesEndpoint;
|
|
370
|
-
if (!endpoint) {
|
|
371
|
-
sendTaskError(taskId, 'HTTP endpoint not configured');
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
const timeoutMs = task.hermes?.timeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS;
|
|
375
|
-
const abortController = new AbortController();
|
|
376
|
-
const timeoutId = setTimeout(() => abortController.abort(), timeoutMs);
|
|
377
|
-
const entry = {
|
|
378
|
-
taskId,
|
|
379
|
-
process: null,
|
|
380
|
-
abortController,
|
|
381
|
-
startedAt: Date.now(),
|
|
382
|
-
stdoutChunks: [],
|
|
383
|
-
completed: false,
|
|
384
|
-
};
|
|
385
|
-
runningTasks.set(taskId, entry);
|
|
386
|
-
// Build request headers
|
|
387
|
-
const headers = { 'Content-Type': 'application/json' };
|
|
388
|
-
const token = task.endpointToken ?? config.endpointToken;
|
|
389
|
-
if (token) {
|
|
390
|
-
headers['Authorization'] = `Bearer ${token}`;
|
|
391
|
-
}
|
|
392
|
-
// systemPrompt 拼接到 input 前方(Hermes/OpenClaw Gateway 不支持独立 system prompt 参数)
|
|
393
|
-
const effectiveInput = task.systemPrompt
|
|
394
|
-
? `${task.systemPrompt}\n\n---\n\n${task.input}`
|
|
395
|
-
: task.input;
|
|
396
|
-
entry.completionPromise = fetch(endpoint, {
|
|
397
|
-
method: 'POST',
|
|
398
|
-
headers,
|
|
399
|
-
body: JSON.stringify({
|
|
400
|
-
taskId: task.taskId,
|
|
401
|
-
input: effectiveInput,
|
|
402
|
-
context: task.context,
|
|
403
|
-
...(task.systemPrompt ? { systemPrompt: task.systemPrompt } : {}),
|
|
404
|
-
...(config.mcpBridgeUrl ? { mcpBridgeUrl: config.mcpBridgeUrl } : {}),
|
|
405
|
-
...((task.mcpToken ?? config.mcpApiKey) ? { mcpToken: (task.mcpToken ?? config.mcpApiKey) } : {}),
|
|
406
|
-
}),
|
|
407
|
-
signal: abortController.signal,
|
|
408
|
-
})
|
|
409
|
-
.then(async (res) => {
|
|
410
|
-
clearTimeout(timeoutId);
|
|
411
|
-
if (entry.completed)
|
|
412
|
-
return;
|
|
413
|
-
entry.completed = true;
|
|
414
|
-
if (!res.ok) {
|
|
415
|
-
sendTaskError(taskId, `HTTP ${res.status}: ${res.statusText}`);
|
|
416
|
-
cleanupTask(taskId);
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
const bodyText = await res.text();
|
|
420
|
-
let parsed = bodyText;
|
|
421
|
-
try {
|
|
422
|
-
parsed = JSON.parse(bodyText);
|
|
423
|
-
}
|
|
424
|
-
catch {
|
|
425
|
-
// Non-JSON response, keep raw text
|
|
426
|
-
}
|
|
427
|
-
// Apply unwrap if configured (openclaw) — unwrap expects raw string, internally JSON.parses
|
|
428
|
-
if (options.unwrap && typeof parsed === 'object' && parsed !== null) {
|
|
429
|
-
try {
|
|
430
|
-
parsed = options.unwrap(bodyText);
|
|
431
|
-
}
|
|
432
|
-
catch {
|
|
433
|
-
// unwrap failed, keep original
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
// Tool Proxy Fallback detection
|
|
437
|
-
const responseStr = typeof parsed === 'string' ? parsed : JSON.stringify(parsed);
|
|
438
|
-
if (/unsupported mcp biz type|errcode.*846610/i.test(responseStr)) {
|
|
439
|
-
const fallbackCalls = extractFallbackToolCalls(parsed);
|
|
440
|
-
if (fallbackCalls.length > 0) {
|
|
441
|
-
await executeFallbackToolCalls(taskId, fallbackCalls);
|
|
442
|
-
cleanupTask(taskId);
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
sendTaskComplete(taskId, parsed);
|
|
447
|
-
cleanupTask(taskId);
|
|
448
|
-
})
|
|
449
|
-
.catch((err) => {
|
|
450
|
-
clearTimeout(timeoutId);
|
|
451
|
-
if (entry.completed)
|
|
452
|
-
return;
|
|
453
|
-
entry.completed = true;
|
|
454
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
455
|
-
if (err.name === 'AbortError') {
|
|
456
|
-
sendTaskError(taskId, `HTTP request timed out after ${timeoutMs}ms`);
|
|
457
|
-
}
|
|
458
|
-
else {
|
|
459
|
-
sendTaskError(taskId, `HTTP request failed: ${message}`);
|
|
460
|
-
}
|
|
461
|
-
cleanupTask(taskId);
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
function spawnHermesTask(task) {
|
|
465
|
-
spawnHttpTask(task, {});
|
|
466
|
-
}
|
|
467
|
-
function spawnOpenClawTask(task) {
|
|
468
|
-
spawnHttpTask(task, { unwrap: unwrapOpenClawPayloads });
|
|
469
|
-
}
|
|
470
|
-
function extractFallbackToolCalls(parsed) {
|
|
471
|
-
const calls = [];
|
|
472
|
-
// Try to extract from common response shapes
|
|
473
|
-
let data = parsed;
|
|
474
|
-
if (typeof data === 'string') {
|
|
475
|
-
try {
|
|
476
|
-
data = JSON.parse(data);
|
|
477
|
-
}
|
|
478
|
-
catch {
|
|
479
|
-
return calls;
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
if (!data || typeof data !== 'object')
|
|
483
|
-
return calls;
|
|
484
|
-
// Shape: { tool_calls: [{ name, arguments }, ...] }
|
|
485
|
-
const rawCalls = data.tool_calls ?? data.toolCalls ?? data.calls;
|
|
486
|
-
if (Array.isArray(rawCalls)) {
|
|
487
|
-
for (const c of rawCalls) {
|
|
488
|
-
if (typeof c !== 'object' || c === null)
|
|
489
|
-
continue;
|
|
490
|
-
const name = c.name ?? c.tool_name ?? c.function;
|
|
491
|
-
if (typeof name !== 'string')
|
|
492
|
-
continue;
|
|
493
|
-
let args = {};
|
|
494
|
-
const rawArgs = c.arguments ?? c.args ?? c.parameters;
|
|
495
|
-
if (typeof rawArgs === 'string') {
|
|
496
|
-
try {
|
|
497
|
-
args = JSON.parse(rawArgs);
|
|
498
|
-
}
|
|
499
|
-
catch {
|
|
500
|
-
args = { raw: rawArgs };
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
else if (typeof rawArgs === 'object' && rawArgs !== null) {
|
|
504
|
-
args = rawArgs;
|
|
505
|
-
}
|
|
506
|
-
calls.push({ toolName: name, args });
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
// Shape: { tool_query: "..." } → single tool call
|
|
510
|
-
if (calls.length === 0 && typeof data.tool_query === 'string') {
|
|
511
|
-
calls.push({ toolName: 'tool_query', args: { query: data.tool_query } });
|
|
512
|
-
}
|
|
513
|
-
return calls;
|
|
514
|
-
}
|
|
515
|
-
async function executeFallbackToolCalls(taskId, calls) {
|
|
516
|
-
const results = [];
|
|
517
|
-
for (const call of calls) {
|
|
518
|
-
const callId = randomUUID();
|
|
519
|
-
// Send tool_call IPC to daemon
|
|
520
|
-
process.stdout.write(JSON.stringify({
|
|
521
|
-
type: 'tool_call',
|
|
522
|
-
callId,
|
|
523
|
-
taskId,
|
|
524
|
-
toolName: call.toolName,
|
|
525
|
-
args: call.args,
|
|
526
|
-
}) + '\n');
|
|
527
|
-
// Wait for tool_result from daemon via stdin
|
|
528
|
-
try {
|
|
529
|
-
const result = await waitForToolResult(callId, 30000);
|
|
530
|
-
results.push({ toolName: call.toolName, ok: true, result });
|
|
531
|
-
}
|
|
532
|
-
catch (err) {
|
|
533
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
534
|
-
results.push({ toolName: call.toolName, ok: false, error: message });
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
sendTaskComplete(taskId, { mode: 'fallback', calls: results });
|
|
538
|
-
}
|
|
539
|
-
/** Pending tool_result resolvers keyed by callId */
|
|
540
|
-
const toolResultPending = new Map();
|
|
541
|
-
function waitForToolResult(callId, timeoutMs) {
|
|
542
|
-
return new Promise((resolve, reject) => {
|
|
543
|
-
const timer = setTimeout(() => {
|
|
544
|
-
toolResultPending.delete(callId);
|
|
545
|
-
reject(new Error('Tool call timed out'));
|
|
546
|
-
}, timeoutMs);
|
|
547
|
-
toolResultPending.set(callId, { resolve, reject, timer });
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
/** Called from stdin handler when a tool_result frame arrives from daemon */
|
|
551
|
-
function handleToolResult(frame) {
|
|
552
|
-
const pending = toolResultPending.get(frame.callId);
|
|
553
|
-
if (!pending)
|
|
554
|
-
return;
|
|
555
|
-
clearTimeout(pending.timer);
|
|
556
|
-
toolResultPending.delete(frame.callId);
|
|
557
|
-
if (frame.ok) {
|
|
558
|
-
pending.resolve(frame.result);
|
|
559
|
-
}
|
|
560
|
-
else {
|
|
561
|
-
pending.reject(new Error(frame.error ?? 'Tool call failed'));
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
/* ── Generic Task (fallback) ── */
|
|
565
|
-
function spawnGenericTask(task) {
|
|
566
|
-
if (!config)
|
|
567
|
-
return;
|
|
568
|
-
const taskId = task.taskId;
|
|
569
|
-
const workDir = task.workDir ?? config.workDir;
|
|
570
|
-
const args = ['--print', '--output-format', 'json', task.input];
|
|
571
|
-
let proc;
|
|
572
|
-
try {
|
|
573
|
-
proc = spawn(config.runtime, args, {
|
|
574
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
575
|
-
env: {
|
|
576
|
-
...process.env,
|
|
577
|
-
WINMATRIX_AGENT_ID: config.agentId,
|
|
578
|
-
WINMATRIX_API_KEY: config.apiKey,
|
|
579
|
-
WINMATRIX_AGENT_NAME: config.name,
|
|
580
|
-
WINMATRIX_AGENT_TYPE: config.agentType,
|
|
581
|
-
WINMATRIX_TASK_ID: taskId,
|
|
582
|
-
...task.env,
|
|
583
|
-
},
|
|
584
|
-
cwd: workDir ?? process.cwd(),
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
catch (err) {
|
|
588
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
589
|
-
sendTaskError(taskId, `Failed to spawn runtime: ${message}`);
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
const entry = {
|
|
593
|
-
taskId,
|
|
594
|
-
process: proc,
|
|
595
|
-
abortController: null,
|
|
596
|
-
startedAt: Date.now(),
|
|
597
|
-
stdoutChunks: [],
|
|
598
|
-
completed: false,
|
|
599
|
-
};
|
|
600
|
-
runningTasks.set(taskId, entry);
|
|
601
|
-
proc.stdout?.on('data', (data) => {
|
|
602
|
-
const text = data.toString('utf-8');
|
|
603
|
-
entry.stdoutChunks.push(text);
|
|
604
|
-
const trimmed = text.trimEnd();
|
|
605
|
-
if (!trimmed)
|
|
606
|
-
return;
|
|
607
|
-
sendTaskDelta(taskId, trimmed);
|
|
608
|
-
});
|
|
609
|
-
proc.stderr?.on('data', (data) => {
|
|
610
|
-
const text = data.toString('utf-8').trimEnd();
|
|
611
|
-
if (!text)
|
|
612
|
-
return;
|
|
613
|
-
sendTaskDelta(taskId, text, { source: 'stderr' });
|
|
614
|
-
});
|
|
615
|
-
proc.on('exit', (code, signal) => {
|
|
616
|
-
if (entry.completed)
|
|
617
|
-
return;
|
|
618
|
-
entry.completed = true;
|
|
619
|
-
if (code === 0) {
|
|
620
|
-
const rawOutput = entry.stdoutChunks.join('');
|
|
621
|
-
let parsedResult = rawOutput;
|
|
622
|
-
try {
|
|
623
|
-
parsedResult = JSON.parse(rawOutput);
|
|
624
|
-
}
|
|
625
|
-
catch {
|
|
626
|
-
// Non-JSON output, keep raw
|
|
627
|
-
}
|
|
628
|
-
sendTaskComplete(taskId, parsedResult);
|
|
629
|
-
}
|
|
630
|
-
else {
|
|
631
|
-
sendTaskError(taskId, signal
|
|
632
|
-
? `Process killed by signal ${signal}`
|
|
633
|
-
: `Process exited with code ${code ?? 'null'}`);
|
|
634
|
-
}
|
|
635
|
-
cleanupTask(taskId);
|
|
636
|
-
});
|
|
637
|
-
proc.on('error', (err) => {
|
|
638
|
-
if (entry.completed)
|
|
639
|
-
return;
|
|
640
|
-
entry.completed = true;
|
|
641
|
-
sendTaskError(taskId, err.message);
|
|
642
|
-
cleanupTask(taskId);
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
/* ── Task Cancellation ── */
|
|
646
|
-
function cancelTask(taskId) {
|
|
647
|
-
const entry = runningTasks.get(taskId);
|
|
648
|
-
if (!entry)
|
|
649
|
-
return;
|
|
650
|
-
if (entry.abortController) {
|
|
651
|
-
entry.abortController.abort();
|
|
652
|
-
}
|
|
653
|
-
if (entry.process) {
|
|
654
|
-
try {
|
|
655
|
-
entry.process.kill('SIGTERM');
|
|
656
|
-
setTimeout(() => {
|
|
657
|
-
try {
|
|
658
|
-
entry.process?.kill('SIGKILL');
|
|
659
|
-
}
|
|
660
|
-
catch { /* already dead */ }
|
|
661
|
-
}, 10000);
|
|
662
|
-
}
|
|
663
|
-
catch {
|
|
664
|
-
// process already exited
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
cleanupTask(taskId);
|
|
668
|
-
}
|
|
669
|
-
/* ── Register Flow ── */
|
|
670
64
|
function sendRegister() {
|
|
671
65
|
if (!config)
|
|
672
66
|
return;
|
|
@@ -691,13 +85,11 @@ function sendRegister() {
|
|
|
691
85
|
},
|
|
692
86
|
});
|
|
693
87
|
}
|
|
694
|
-
/* ── Stdin Event Handling ── */
|
|
695
88
|
async function sendWorkspaceReport() {
|
|
696
89
|
if (!config?.workDir)
|
|
697
90
|
return;
|
|
698
91
|
try {
|
|
699
92
|
const workdir = await scanWorkDir(config.workDir);
|
|
700
|
-
// Detect key runtime versions
|
|
701
93
|
const runtimes = {};
|
|
702
94
|
const runtimeBinaries = config.runtime ? [config.runtime] : [];
|
|
703
95
|
for (const bin of runtimeBinaries) {
|
|
@@ -711,30 +103,24 @@ async function sendWorkspaceReport() {
|
|
|
711
103
|
send({
|
|
712
104
|
type: 'fwd',
|
|
713
105
|
agentId: config.agentId,
|
|
714
|
-
data: {
|
|
715
|
-
type: 'event',
|
|
716
|
-
event: 'workspace.report',
|
|
717
|
-
payload,
|
|
718
|
-
},
|
|
106
|
+
data: { type: 'event', event: 'workspace.report', payload },
|
|
719
107
|
});
|
|
720
108
|
}
|
|
721
109
|
catch {
|
|
722
|
-
|
|
110
|
+
/* workspace scan failed silently */
|
|
723
111
|
}
|
|
724
112
|
}
|
|
725
113
|
function handleServerFrame(data) {
|
|
726
|
-
if (!config)
|
|
114
|
+
if (!config || !processManager)
|
|
727
115
|
return;
|
|
728
|
-
// Handle register response
|
|
729
116
|
if (data.type === 'res' && data.ok === true) {
|
|
730
117
|
send({
|
|
731
118
|
type: 'event',
|
|
732
119
|
event: 'agent.ready',
|
|
733
120
|
payload: { agentId: config.agentId, pid: process.pid },
|
|
734
121
|
});
|
|
735
|
-
// Fire-and-forget workspace scan after agent is ready
|
|
736
122
|
if (config.workDir) {
|
|
737
|
-
sendWorkspaceReport();
|
|
123
|
+
void sendWorkspaceReport();
|
|
738
124
|
}
|
|
739
125
|
return;
|
|
740
126
|
}
|
|
@@ -750,29 +136,24 @@ function handleServerFrame(data) {
|
|
|
750
136
|
process.exit(1);
|
|
751
137
|
return;
|
|
752
138
|
}
|
|
753
|
-
// Handle task.assign event
|
|
754
139
|
if (data.type === 'event' && data.event === 'task.assign') {
|
|
755
140
|
const payload = (data.payload ?? {});
|
|
756
141
|
if (payload.taskId && payload.input) {
|
|
757
|
-
routeTask(payload);
|
|
142
|
+
processManager.routeTask(payload);
|
|
758
143
|
}
|
|
759
144
|
return;
|
|
760
145
|
}
|
|
761
|
-
// Handle task.cancel event
|
|
762
146
|
if (data.type === 'event' && data.event === 'task.cancel') {
|
|
763
147
|
const payload = data.payload;
|
|
764
148
|
if (payload?.taskId) {
|
|
765
|
-
cancelTask(payload.taskId);
|
|
149
|
+
processManager.cancelTask(payload.taskId);
|
|
766
150
|
}
|
|
767
151
|
return;
|
|
768
152
|
}
|
|
769
|
-
// Handle reconnect event (Daemon reconnected, re-register)
|
|
770
153
|
if (data.type === 'event' && data.event === 'reconnect') {
|
|
771
154
|
sendRegister();
|
|
772
|
-
return;
|
|
773
155
|
}
|
|
774
156
|
}
|
|
775
|
-
/* ── Main ── */
|
|
776
157
|
function main() {
|
|
777
158
|
const args = process.argv.slice(2);
|
|
778
159
|
const parsed = {};
|
|
@@ -800,16 +181,17 @@ function main() {
|
|
|
800
181
|
send({
|
|
801
182
|
type: 'event',
|
|
802
183
|
event: 'agent.error',
|
|
803
|
-
payload: {
|
|
804
|
-
error: 'Missing required --agent-id or WINMATRIX_API_KEY env',
|
|
805
|
-
},
|
|
184
|
+
payload: { error: 'Missing required --agent-id or WINMATRIX_API_KEY env' },
|
|
806
185
|
});
|
|
807
186
|
process.exit(1);
|
|
808
187
|
return;
|
|
809
188
|
}
|
|
810
|
-
|
|
189
|
+
processManager = new WrapperProcessManager(config, {
|
|
190
|
+
onDelta: sendTaskDelta,
|
|
191
|
+
onComplete: sendTaskComplete,
|
|
192
|
+
onError: sendTaskError,
|
|
193
|
+
});
|
|
811
194
|
sendRegister();
|
|
812
|
-
// Step 2: Wait for register response and task events via stdin
|
|
813
195
|
process.stdin.setEncoding('utf-8');
|
|
814
196
|
let buffer = '';
|
|
815
197
|
process.stdin.on('data', (chunk) => {
|
|
@@ -827,53 +209,21 @@ function main() {
|
|
|
827
209
|
}
|
|
828
210
|
});
|
|
829
211
|
process.stdin.on('end', () => {
|
|
830
|
-
shutdown();
|
|
212
|
+
void shutdown();
|
|
831
213
|
});
|
|
832
214
|
process.stdin.resume();
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
for (const [, entry] of runningTasks) {
|
|
837
|
-
if (entry.abortController) {
|
|
838
|
-
entry.abortController.abort();
|
|
839
|
-
}
|
|
840
|
-
if (entry.process) {
|
|
841
|
-
try {
|
|
842
|
-
entry.process.kill('SIGTERM');
|
|
843
|
-
}
|
|
844
|
-
catch { /* already dead */ }
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
// Wait for in-flight fetch tasks to settle (with 5s timeout),
|
|
848
|
-
// then SIGKILL any remaining child processes.
|
|
849
|
-
const fetchPromises = [...runningTasks.values()]
|
|
850
|
-
.filter((e) => e.completionPromise)
|
|
851
|
-
.map((e) => e.completionPromise);
|
|
852
|
-
const finalize = () => {
|
|
853
|
-
for (const [, entry] of runningTasks) {
|
|
854
|
-
if (entry.process) {
|
|
855
|
-
try {
|
|
856
|
-
entry.process.kill('SIGKILL');
|
|
857
|
-
}
|
|
858
|
-
catch { /* already dead */ }
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
runningTasks.clear();
|
|
862
|
-
process.exit(0);
|
|
863
|
-
};
|
|
864
|
-
if (fetchPromises.length > 0) {
|
|
865
|
-
Promise.race([
|
|
866
|
-
Promise.allSettled(fetchPromises),
|
|
867
|
-
new Promise((resolve) => setTimeout(resolve, 5000)),
|
|
868
|
-
]).finally(finalize);
|
|
869
|
-
}
|
|
870
|
-
else {
|
|
871
|
-
// No fetch tasks: 10s grace for child process SIGTERM → SIGKILL
|
|
872
|
-
setTimeout(finalize, 10000);
|
|
215
|
+
async function shutdown() {
|
|
216
|
+
if (processManager) {
|
|
217
|
+
await processManager.shutdown();
|
|
873
218
|
}
|
|
219
|
+
process.exit(0);
|
|
874
220
|
}
|
|
875
|
-
process.on('SIGTERM',
|
|
876
|
-
|
|
221
|
+
process.on('SIGTERM', () => {
|
|
222
|
+
void shutdown();
|
|
223
|
+
});
|
|
224
|
+
process.on('SIGINT', () => {
|
|
225
|
+
void shutdown();
|
|
226
|
+
});
|
|
877
227
|
}
|
|
878
228
|
main();
|
|
879
229
|
//# sourceMappingURL=AgentWrapper.js.map
|