@cydm/magic-shell-agent-node 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/pty-adapter.d.ts +18 -0
- package/dist/adapters/pty-adapter.js +99 -0
- package/dist/adapters/registry.d.ts +28 -0
- package/dist/adapters/registry.js +64 -0
- package/dist/adapters/rpc-adapter.d.ts +19 -0
- package/dist/adapters/rpc-adapter.js +182 -0
- package/dist/adapters/stdio-adapter.d.ts +17 -0
- package/dist/adapters/stdio-adapter.js +107 -0
- package/dist/adapters/types.d.ts +17 -0
- package/dist/adapters/types.js +2 -0
- package/dist/claude-exec.d.ts +11 -0
- package/dist/claude-exec.js +54 -0
- package/dist/claude-worker.d.ts +12 -0
- package/dist/claude-worker.js +163 -0
- package/dist/codex-exec.d.ts +12 -0
- package/dist/codex-exec.js +84 -0
- package/dist/codex-worker.d.ts +12 -0
- package/dist/codex-worker.js +179 -0
- package/dist/directory-browser.d.ts +3 -0
- package/dist/directory-browser.js +48 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/local-direct-server.d.ts +38 -0
- package/dist/local-direct-server.js +266 -0
- package/dist/node-conversation.d.ts +21 -0
- package/dist/node-conversation.js +28 -0
- package/dist/node-intent.d.ts +2 -0
- package/dist/node-intent.js +40 -0
- package/dist/node-reply.d.ts +30 -0
- package/dist/node-reply.js +77 -0
- package/dist/node.d.ts +132 -0
- package/dist/node.js +1954 -0
- package/dist/pie-session-control.d.ts +21 -0
- package/dist/pie-session-control.js +28 -0
- package/dist/plugin-loader.d.ts +19 -0
- package/dist/plugin-loader.js +144 -0
- package/dist/plugins/pie.json +7 -0
- package/dist/primary-agent-bridge.d.ts +69 -0
- package/dist/primary-agent-bridge.js +282 -0
- package/dist/session-manager.d.ts +66 -0
- package/dist/session-manager.js +197 -0
- package/dist/terminal-metadata.d.ts +7 -0
- package/dist/terminal-metadata.js +52 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.js +1 -0
- package/dist/worker-control.d.ts +15 -0
- package/dist/worker-control.js +89 -0
- package/dist/worker-narration.d.ts +25 -0
- package/dist/worker-narration.js +90 -0
- package/dist/worker-output.d.ts +6 -0
- package/dist/worker-output.js +72 -0
- package/dist/worker-registry.d.ts +45 -0
- package/dist/worker-registry.js +501 -0
- package/dist/worker-runtime.d.ts +18 -0
- package/dist/worker-runtime.js +69 -0
- package/dist/ws-client.d.ts +68 -0
- package/dist/ws-client.js +193 -0
- package/package.json +38 -0
package/dist/node.js
ADDED
|
@@ -0,0 +1,1954 @@
|
|
|
1
|
+
import { WebSocketClient } from "./ws-client.js";
|
|
2
|
+
import { SessionManager } from "./session-manager.js";
|
|
3
|
+
import { LocalDirectServer } from "./local-direct-server.js";
|
|
4
|
+
import { loadPlugins, getDefaultPluginDir } from "./plugin-loader.js";
|
|
5
|
+
import { adapterRegistry } from "./adapters/registry.js";
|
|
6
|
+
import { WorkerRegistry } from "./worker-registry.js";
|
|
7
|
+
import { buildPrimarySessionPrompt, readPieSessionSnapshot, withPrimaryPieExtensionPath, } from "./primary-agent-bridge.js";
|
|
8
|
+
import { readPieSessionControlState, writePieSessionControlCommand, } from "./pie-session-control.js";
|
|
9
|
+
import { buildDirectoryList } from "./directory-browser.js";
|
|
10
|
+
import { attachWorkerSession, inspectWorkerSession, restartWorkerSession, stopWorkerSession, } from "./worker-control.js";
|
|
11
|
+
import { spawnManagedWorker } from "./worker-runtime.js";
|
|
12
|
+
import { createNodeConversationState, recordConversationTurn, rememberNodeSpawn, } from "./node-conversation.js";
|
|
13
|
+
import { getLastMeaningfulWorkerMessage, stripAnsi, summarizeRecentWorkerOutput, } from "./worker-output.js";
|
|
14
|
+
import { claudeNeedsTrustConfirmation, getLastClaudeWorkerMessage, isClaudeReadyForTask, isClaudeCodeWorker, summarizeClaudeWorkerOutput, } from "./claude-worker.js";
|
|
15
|
+
import { runClaudeExec } from "./claude-exec.js";
|
|
16
|
+
import { codexNeedsTrustConfirmation, getLastCodexWorkerMessage, isCodexReadyForTask, isCodexWorker, summarizeCodexWorkerOutput, } from "./codex-worker.js";
|
|
17
|
+
import { runCodexExec } from "./codex-exec.js";
|
|
18
|
+
import { extractTerminalMetadata, normalizeWorkerTitleCandidate, } from "./terminal-metadata.js";
|
|
19
|
+
import { existsSync } from "node:fs";
|
|
20
|
+
import os from "node:os";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
function generateSessionId() {
|
|
23
|
+
return `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
24
|
+
}
|
|
25
|
+
export class AgentNode {
|
|
26
|
+
options;
|
|
27
|
+
wsClient;
|
|
28
|
+
localDirectServer;
|
|
29
|
+
localDirectInfo;
|
|
30
|
+
sessionManager;
|
|
31
|
+
workerRegistry;
|
|
32
|
+
plugins;
|
|
33
|
+
running = false;
|
|
34
|
+
sessionTitleBuffers = new Map();
|
|
35
|
+
primaryAgent;
|
|
36
|
+
nodeConversation = createNodeConversationState();
|
|
37
|
+
lastFailedNamedWorker;
|
|
38
|
+
preferredWorkerPluginName = "pie";
|
|
39
|
+
syntheticSessionResults = new Map();
|
|
40
|
+
liveAttachedSessions = new Set();
|
|
41
|
+
remoteAttachedSessions = new Set();
|
|
42
|
+
localAttachedSessions = new Set();
|
|
43
|
+
relayConnectedOnce = false;
|
|
44
|
+
constructor(options) {
|
|
45
|
+
this.options = options;
|
|
46
|
+
this.plugins = new Map();
|
|
47
|
+
this.sessionManager = new SessionManager(adapterRegistry);
|
|
48
|
+
this.workerRegistry = new WorkerRegistry();
|
|
49
|
+
const primaryPluginName = options.primaryPluginName || "pie";
|
|
50
|
+
const primaryLaunchMode = options.primaryLaunchMode || (primaryPluginName === "pie" ? "session" : "headless");
|
|
51
|
+
this.primaryAgent = {
|
|
52
|
+
pluginName: primaryPluginName,
|
|
53
|
+
sessionId: `primary-${options.nodeId}`,
|
|
54
|
+
launchMode: primaryLaunchMode,
|
|
55
|
+
status: "starting",
|
|
56
|
+
};
|
|
57
|
+
this.nodeConversation = createNodeConversationState();
|
|
58
|
+
this.wsClient = new WebSocketClient({
|
|
59
|
+
url: options.relayUrl,
|
|
60
|
+
nodeId: options.nodeId,
|
|
61
|
+
password: options.password,
|
|
62
|
+
autoReconnect: options.autoReconnect ?? true,
|
|
63
|
+
});
|
|
64
|
+
this.wsClient.onStatus((status, error) => {
|
|
65
|
+
if (status === "connected") {
|
|
66
|
+
void this.onRelayConnected();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (status === "error" && error) {
|
|
70
|
+
console.warn(`[AgentNode] Relay transport error: ${error.message}`);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
getOptions() {
|
|
75
|
+
return this.options;
|
|
76
|
+
}
|
|
77
|
+
getLocalDirectInfo() {
|
|
78
|
+
return this.localDirectInfo;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 启动 AgentNode
|
|
82
|
+
*/
|
|
83
|
+
async start() {
|
|
84
|
+
console.log(`[AgentNode] Starting...`);
|
|
85
|
+
console.log(`[AgentNode] Node ID: ${this.options.nodeId}`);
|
|
86
|
+
console.log(`[AgentNode] Relay: ${this.options.relayUrl}`);
|
|
87
|
+
// 1. 加载插件
|
|
88
|
+
this.plugins = loadPlugins({ dir: this.options.pluginDir });
|
|
89
|
+
if (this.plugins.size === 0) {
|
|
90
|
+
console.warn("[AgentNode] No plugins loaded!");
|
|
91
|
+
}
|
|
92
|
+
// 2. 设置消息处理器
|
|
93
|
+
this.setupMessageHandlers();
|
|
94
|
+
// 3. 启动本地直连入口
|
|
95
|
+
if (this.options.enableLocalDirect !== false) {
|
|
96
|
+
await this.startLocalDirectServer();
|
|
97
|
+
}
|
|
98
|
+
// 4. 连接 Relay
|
|
99
|
+
try {
|
|
100
|
+
await this.wsClient.connect();
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
104
|
+
console.warn(`[AgentNode] Relay connection unavailable at startup: ${message}`);
|
|
105
|
+
if (this.options.autoReconnect === false && !this.localDirectServer) {
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// 5. 设置会话输出转发
|
|
110
|
+
this.sessionManager.onOutput((sessionId, output) => {
|
|
111
|
+
this.handleSessionOutput(sessionId, output);
|
|
112
|
+
});
|
|
113
|
+
this.sessionManager.onExit((sessionId, agentId, code) => {
|
|
114
|
+
if (sessionId === this.primaryAgent.sessionId) {
|
|
115
|
+
this.primaryAgent = {
|
|
116
|
+
...this.primaryAgent,
|
|
117
|
+
status: code === 0 ? "stopped" : "failed",
|
|
118
|
+
lastError: code === 0 ? undefined : `Exited with code ${code}`,
|
|
119
|
+
};
|
|
120
|
+
this.broadcastWorkerList();
|
|
121
|
+
}
|
|
122
|
+
const worker = this.workerRegistry.getWorkerBySessionId(sessionId);
|
|
123
|
+
if (!worker)
|
|
124
|
+
return;
|
|
125
|
+
if (worker.agentId !== agentId) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (code !== 0) {
|
|
129
|
+
this.workerRegistry.recordEvent(worker.agentId, {
|
|
130
|
+
type: "error",
|
|
131
|
+
timestamp: Date.now(),
|
|
132
|
+
message: `process exited with code ${code}`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const previousWorker = { ...worker };
|
|
136
|
+
const updatedWorker = this.workerRegistry.updateWorkerStatus(worker.agentId, code === 0 ? "stopped" : "failed", { exitCode: code, lastError: code === 0 ? undefined : `Exited with code ${code}` });
|
|
137
|
+
if (updatedWorker) {
|
|
138
|
+
this.maybeNarrateWorkerStateChange(previousWorker, updatedWorker);
|
|
139
|
+
}
|
|
140
|
+
this.broadcastWorkerList();
|
|
141
|
+
});
|
|
142
|
+
this.running = true;
|
|
143
|
+
await this.ensurePrimaryAgent();
|
|
144
|
+
console.log("[AgentNode] Started successfully");
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* 停止 AgentNode
|
|
148
|
+
*/
|
|
149
|
+
async stop() {
|
|
150
|
+
this.running = false;
|
|
151
|
+
console.log("[AgentNode] Stopping...");
|
|
152
|
+
// 停止所有会话
|
|
153
|
+
await this.sessionManager.stopAll();
|
|
154
|
+
if (this.localDirectServer) {
|
|
155
|
+
await this.localDirectServer.stop();
|
|
156
|
+
this.localDirectServer = undefined;
|
|
157
|
+
}
|
|
158
|
+
// 断开 WebSocket
|
|
159
|
+
this.wsClient.disconnect();
|
|
160
|
+
console.log("[AgentNode] Stopped");
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* 设置消息处理器
|
|
164
|
+
*/
|
|
165
|
+
setupMessageHandlers() {
|
|
166
|
+
this.wsClient.onMessage(async (message) => {
|
|
167
|
+
await this.handleIncomingMessage(message, "relay");
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
async handleIncomingMessage(message, source = "relay") {
|
|
171
|
+
switch (message.type) {
|
|
172
|
+
case "auth":
|
|
173
|
+
return;
|
|
174
|
+
case "input":
|
|
175
|
+
if (message.sessionId && message.data) {
|
|
176
|
+
await this.ensureSessionForInteractiveMessage(message.sessionId, "input");
|
|
177
|
+
this.captureWorkerTitleFromInput(message.sessionId, message.data);
|
|
178
|
+
this.sessionManager.sendInput(message.sessionId, message.data);
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
case "resize":
|
|
182
|
+
if (message.sessionId && message.cols && message.rows) {
|
|
183
|
+
await this.ensureSessionForInteractiveMessage(message.sessionId, "resize");
|
|
184
|
+
this.sessionManager.resize(message.sessionId, message.cols, message.rows);
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
case "list_workers":
|
|
188
|
+
this.broadcastWorkerList();
|
|
189
|
+
return;
|
|
190
|
+
case "spawn_worker": {
|
|
191
|
+
const pluginName = message.pluginName || this.preferredWorkerPluginName || "pie";
|
|
192
|
+
const sessionId = message.sessionId || generateSessionId();
|
|
193
|
+
try {
|
|
194
|
+
await this.spawnWorker({
|
|
195
|
+
sessionId,
|
|
196
|
+
pluginName,
|
|
197
|
+
cwd: message.cwd,
|
|
198
|
+
taskSummary: message.taskSummary,
|
|
199
|
+
});
|
|
200
|
+
this.rememberNodeSpawn(message.taskSummary, sessionId);
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
this.sendToSource(source, {
|
|
204
|
+
type: "error",
|
|
205
|
+
error: err instanceof Error ? err.message : String(err),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
case "attach_worker":
|
|
211
|
+
if (message.sessionId) {
|
|
212
|
+
const responses = attachWorkerSession(message.sessionId, {
|
|
213
|
+
workerRegistry: this.workerRegistry,
|
|
214
|
+
sessionManager: this.sessionManager,
|
|
215
|
+
});
|
|
216
|
+
for (const response of responses) {
|
|
217
|
+
this.sendToSource(source, response);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return;
|
|
221
|
+
case "stop_worker":
|
|
222
|
+
if (message.sessionId) {
|
|
223
|
+
await stopWorkerSession(message.sessionId, {
|
|
224
|
+
workerRegistry: this.workerRegistry,
|
|
225
|
+
sessionManager: this.sessionManager,
|
|
226
|
+
clearSessionTitle: (sessionId) => {
|
|
227
|
+
this.sessionTitleBuffers.delete(sessionId);
|
|
228
|
+
},
|
|
229
|
+
broadcastWorkerList: () => {
|
|
230
|
+
this.broadcastWorkerList();
|
|
231
|
+
},
|
|
232
|
+
spawnWorker: (options) => this.spawnWorker(options),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
case "restart_worker":
|
|
237
|
+
if (message.sessionId) {
|
|
238
|
+
const error = await restartWorkerSession(message.sessionId, {
|
|
239
|
+
workerRegistry: this.workerRegistry,
|
|
240
|
+
sessionManager: this.sessionManager,
|
|
241
|
+
clearSessionTitle: (sessionId) => {
|
|
242
|
+
this.sessionTitleBuffers.delete(sessionId);
|
|
243
|
+
},
|
|
244
|
+
broadcastWorkerList: () => {
|
|
245
|
+
this.broadcastWorkerList();
|
|
246
|
+
},
|
|
247
|
+
spawnWorker: (options) => this.spawnWorker(options),
|
|
248
|
+
});
|
|
249
|
+
if (error) {
|
|
250
|
+
this.sendToSource(source, {
|
|
251
|
+
type: "error",
|
|
252
|
+
error,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
case "inspect_worker":
|
|
258
|
+
if (message.sessionId) {
|
|
259
|
+
const response = inspectWorkerSession(message.sessionId, {
|
|
260
|
+
workerRegistry: this.workerRegistry,
|
|
261
|
+
sessionManager: this.sessionManager,
|
|
262
|
+
});
|
|
263
|
+
if (response) {
|
|
264
|
+
this.sendToSource(source, response);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
case "browse_dirs":
|
|
269
|
+
this.sendServerMessage(await buildDirectoryList(message.path));
|
|
270
|
+
return;
|
|
271
|
+
case "node_message":
|
|
272
|
+
if (message.text?.trim()) {
|
|
273
|
+
const resolvedReply = await this.handlePrimaryMessage(message.text);
|
|
274
|
+
this.sendServerMessage({
|
|
275
|
+
type: "node_reply",
|
|
276
|
+
text: resolvedReply.text,
|
|
277
|
+
actionType: resolvedReply.actionType,
|
|
278
|
+
actionLabel: resolvedReply.actionLabel,
|
|
279
|
+
actionTaskSummary: resolvedReply.actionTaskSummary,
|
|
280
|
+
actionSessionId: resolvedReply.actionSessionId,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
case "control":
|
|
285
|
+
await this.handleControlEnvelope(message, source);
|
|
286
|
+
return;
|
|
287
|
+
case "auth_success":
|
|
288
|
+
console.log(`[AgentNode] Browser connected: ${message.sessionId}`);
|
|
289
|
+
return;
|
|
290
|
+
case "ping":
|
|
291
|
+
this.sendToSource(source, { type: "pong" });
|
|
292
|
+
return;
|
|
293
|
+
default:
|
|
294
|
+
console.warn(`[AgentNode] Unknown message type: ${message.type}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* 创建新会话
|
|
299
|
+
*/
|
|
300
|
+
async spawnWorker(options) {
|
|
301
|
+
const sessionMessage = await spawnManagedWorker(options, {
|
|
302
|
+
nodeId: this.options.nodeId,
|
|
303
|
+
plugins: this.plugins,
|
|
304
|
+
sessionManager: this.sessionManager,
|
|
305
|
+
workerRegistry: this.workerRegistry,
|
|
306
|
+
});
|
|
307
|
+
if (!options.taskSummary) {
|
|
308
|
+
void this.primeInteractiveWorkerSession(options.sessionId).catch(() => { });
|
|
309
|
+
}
|
|
310
|
+
if (options.taskSummary) {
|
|
311
|
+
await this.dispatchInitialWorkerTask(options.sessionId, options.taskSummary);
|
|
312
|
+
}
|
|
313
|
+
this.broadcastWorkerList();
|
|
314
|
+
this.sendServerMessage(sessionMessage);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Backward-compatible wrapper while the transport still speaks in session terms.
|
|
318
|
+
*/
|
|
319
|
+
async createSession(sessionId, pluginName) {
|
|
320
|
+
await this.spawnWorker({
|
|
321
|
+
sessionId,
|
|
322
|
+
pluginName,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
async waitForPrimaryAgentSessionId(timeoutMs = 6000) {
|
|
326
|
+
const deadline = Date.now() + timeoutMs;
|
|
327
|
+
while (Date.now() <= deadline) {
|
|
328
|
+
if (this.primaryAgent.agentSessionId) {
|
|
329
|
+
return this.primaryAgent.agentSessionId;
|
|
330
|
+
}
|
|
331
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
332
|
+
}
|
|
333
|
+
return this.primaryAgent.agentSessionId || null;
|
|
334
|
+
}
|
|
335
|
+
async sendPromptToPrimarySession(prompt) {
|
|
336
|
+
const sessionId = this.primaryAgent.sessionId;
|
|
337
|
+
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
338
|
+
const session = this.sessionManager.getSession(sessionId);
|
|
339
|
+
if (!session) {
|
|
340
|
+
throw new Error("Primary session is not available");
|
|
341
|
+
}
|
|
342
|
+
const bufferedOutput = this.sessionManager.getBufferedOutput(sessionId);
|
|
343
|
+
if (bufferedOutput.length > 0 || attempt >= 3) {
|
|
344
|
+
this.sessionManager.sendInput(sessionId, `${prompt}\r`);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
348
|
+
}
|
|
349
|
+
this.sessionManager.sendInput(sessionId, `${prompt}\r`);
|
|
350
|
+
}
|
|
351
|
+
formatSessionControlInput(message) {
|
|
352
|
+
if (!message) {
|
|
353
|
+
return "\r";
|
|
354
|
+
}
|
|
355
|
+
if (message.endsWith("\r") || message.endsWith("\n")) {
|
|
356
|
+
return message;
|
|
357
|
+
}
|
|
358
|
+
return `${message}\r`;
|
|
359
|
+
}
|
|
360
|
+
async queryPrimarySessionText(text) {
|
|
361
|
+
await this.ensurePrimaryAgent();
|
|
362
|
+
const session = this.sessionManager.getSession(this.primaryAgent.sessionId);
|
|
363
|
+
if (!session || this.primaryAgent.pluginName !== "pie" || this.primaryAgent.launchMode !== "session") {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
const agentSessionId = await this.waitForPrimaryAgentSessionId();
|
|
367
|
+
if (!agentSessionId) {
|
|
368
|
+
throw new Error("Primary session is running but its Pie session ID is still unavailable");
|
|
369
|
+
}
|
|
370
|
+
const before = readPieSessionSnapshot(agentSessionId);
|
|
371
|
+
const prompt = buildPrimarySessionPrompt(text, {
|
|
372
|
+
pluginName: this.primaryAgent.pluginName,
|
|
373
|
+
preferredWorkerPluginName: this.preferredWorkerPluginName,
|
|
374
|
+
lastTaskSummary: this.nodeConversation.lastTaskSummary,
|
|
375
|
+
lastWorkerSessionId: this.nodeConversation.lastWorkerSessionId,
|
|
376
|
+
history: this.nodeConversation.history,
|
|
377
|
+
liveWorkers: this.workerRegistry
|
|
378
|
+
.listWorkers()
|
|
379
|
+
.filter((worker) => worker.status !== "stopped" && worker.status !== "failed"),
|
|
380
|
+
});
|
|
381
|
+
this.primaryAgent = {
|
|
382
|
+
...this.primaryAgent,
|
|
383
|
+
activityState: "busy",
|
|
384
|
+
lastError: undefined,
|
|
385
|
+
};
|
|
386
|
+
this.broadcastWorkerList();
|
|
387
|
+
try {
|
|
388
|
+
await this.sendPromptToPrimarySession(prompt);
|
|
389
|
+
const deadline = Date.now() + 90_000;
|
|
390
|
+
while (Date.now() <= deadline) {
|
|
391
|
+
const after = readPieSessionSnapshot(agentSessionId);
|
|
392
|
+
if (after
|
|
393
|
+
&& after.messageCount > (before?.messageCount || 0)
|
|
394
|
+
&& after.lastAssistantText
|
|
395
|
+
&& after.lastAssistantText !== before?.lastAssistantText
|
|
396
|
+
&& after.lastMessageRole === "assistant"
|
|
397
|
+
&& !after.lastAssistantHasToolCall
|
|
398
|
+
&& Date.now() - after.updatedAt >= 750) {
|
|
399
|
+
return after.lastAssistantText;
|
|
400
|
+
}
|
|
401
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
402
|
+
}
|
|
403
|
+
throw new Error("Primary session did not produce a new assistant message in time");
|
|
404
|
+
}
|
|
405
|
+
finally {
|
|
406
|
+
this.primaryAgent = {
|
|
407
|
+
...this.primaryAgent,
|
|
408
|
+
activityState: "ready",
|
|
409
|
+
};
|
|
410
|
+
this.broadcastWorkerList();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async dispatchInitialWorkerTask(sessionId, taskSummary) {
|
|
414
|
+
const session = this.sessionManager.getSession(sessionId);
|
|
415
|
+
const pluginName = session?.plugin.name;
|
|
416
|
+
const worker = this.workerRegistry.getWorkerBySessionId(sessionId);
|
|
417
|
+
if (worker) {
|
|
418
|
+
this.workerRegistry.updateWorker(worker.agentId, {
|
|
419
|
+
coordinationState: "delegating",
|
|
420
|
+
coordinationTaskSummary: taskSummary,
|
|
421
|
+
activityState: "busy",
|
|
422
|
+
activityUpdatedAt: Date.now(),
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
try {
|
|
426
|
+
if (pluginName && isClaudeCodeWorker(pluginName)) {
|
|
427
|
+
await this.seedClaudeWorkerTask(sessionId, taskSummary);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (pluginName && isCodexWorker(pluginName)) {
|
|
431
|
+
await this.seedCodexWorkerTask(sessionId, taskSummary);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
await this.seedWorkerTask(sessionId, taskSummary);
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
if (worker) {
|
|
438
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
439
|
+
this.workerRegistry.updateWorker(worker.agentId, {
|
|
440
|
+
status: "blocked",
|
|
441
|
+
lastError: message,
|
|
442
|
+
coordinationState: "idle",
|
|
443
|
+
});
|
|
444
|
+
this.workerRegistry.recordEvent(worker.agentId, {
|
|
445
|
+
type: "error",
|
|
446
|
+
timestamp: Date.now(),
|
|
447
|
+
message,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
this.broadcastWorkerList();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
async seedWorkerTask(sessionId, taskSummary) {
|
|
454
|
+
const prompt = `${taskSummary}\r`;
|
|
455
|
+
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
456
|
+
const session = this.sessionManager.getSession(sessionId);
|
|
457
|
+
if (!session) {
|
|
458
|
+
throw new Error(`Worker session disappeared before task dispatch: ${sessionId}`);
|
|
459
|
+
}
|
|
460
|
+
const bufferedOutput = this.sessionManager.getBufferedOutput(sessionId);
|
|
461
|
+
if (bufferedOutput.length > 0 || attempt >= 3) {
|
|
462
|
+
this.sessionManager.sendInput(sessionId, prompt);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
466
|
+
}
|
|
467
|
+
this.sessionManager.sendInput(sessionId, prompt);
|
|
468
|
+
}
|
|
469
|
+
async primeInteractiveWorkerSession(sessionId) {
|
|
470
|
+
const session = this.sessionManager.getSession(sessionId);
|
|
471
|
+
const pluginName = session?.plugin.name;
|
|
472
|
+
if (!pluginName || (!isClaudeCodeWorker(pluginName) && !isCodexWorker(pluginName))) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
for (let attempt = 0; attempt < 24; attempt += 1) {
|
|
476
|
+
const currentSession = this.sessionManager.getSession(sessionId);
|
|
477
|
+
if (!currentSession)
|
|
478
|
+
return;
|
|
479
|
+
const bufferedOutput = this.sessionManager.getBufferedOutput(sessionId);
|
|
480
|
+
if (isClaudeCodeWorker(pluginName)) {
|
|
481
|
+
if (claudeNeedsTrustConfirmation(bufferedOutput)) {
|
|
482
|
+
this.sessionManager.sendInput(sessionId, "\r");
|
|
483
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (isClaudeReadyForTask(bufferedOutput)) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (isCodexWorker(pluginName)) {
|
|
491
|
+
if (codexNeedsTrustConfirmation(bufferedOutput)) {
|
|
492
|
+
this.sessionManager.sendInput(sessionId, "\r");
|
|
493
|
+
await new Promise((resolve) => setTimeout(resolve, 350));
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (isCodexReadyForTask(bufferedOutput)) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
async seedClaudeWorkerTask(sessionId, taskSummary) {
|
|
504
|
+
this.captureWorkerTitleFromInput(sessionId, taskSummary);
|
|
505
|
+
const result = await this.runClaudeWorkerTurn(sessionId, taskSummary, 90_000);
|
|
506
|
+
const worker = this.workerRegistry.getWorkerBySessionId(sessionId);
|
|
507
|
+
if (worker) {
|
|
508
|
+
this.workerRegistry.updateWorker(worker.agentId, {
|
|
509
|
+
coordinationState: result.changed ? "result_received" : "waiting_for_result",
|
|
510
|
+
coordinationTaskSummary: taskSummary,
|
|
511
|
+
activityState: result.changed ? "ready" : "busy",
|
|
512
|
+
activityUpdatedAt: Date.now(),
|
|
513
|
+
lastError: result.changed ? undefined : worker.lastError,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
if (!result.changed) {
|
|
517
|
+
throw new Error("Claude worker did not produce a fresh result for the initial task");
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
async seedCodexWorkerTask(sessionId, taskSummary) {
|
|
521
|
+
this.captureWorkerTitleFromInput(sessionId, taskSummary);
|
|
522
|
+
const result = await this.runCodexWorkerTurn(sessionId, taskSummary, 90_000);
|
|
523
|
+
const worker = this.workerRegistry.getWorkerBySessionId(sessionId);
|
|
524
|
+
if (worker) {
|
|
525
|
+
this.workerRegistry.updateWorker(worker.agentId, {
|
|
526
|
+
coordinationState: result.changed ? "result_received" : "waiting_for_result",
|
|
527
|
+
coordinationTaskSummary: taskSummary,
|
|
528
|
+
activityState: result.changed ? "ready" : "busy",
|
|
529
|
+
activityUpdatedAt: Date.now(),
|
|
530
|
+
lastError: result.changed ? undefined : worker.lastError,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
if (!result.changed) {
|
|
534
|
+
throw new Error("Codex worker did not produce a fresh result for the initial task");
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
async ensurePrimaryAgent() {
|
|
538
|
+
const pluginName = this.primaryAgent.pluginName;
|
|
539
|
+
if (!pluginName) {
|
|
540
|
+
this.primaryAgent = { ...this.primaryAgent, status: "disabled" };
|
|
541
|
+
this.broadcastWorkerList();
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
const plugin = this.plugins.get(pluginName);
|
|
545
|
+
if (!plugin) {
|
|
546
|
+
this.primaryAgent = { ...this.primaryAgent, status: "missing", lastError: `Plugin not found: ${pluginName}` };
|
|
547
|
+
this.broadcastWorkerList();
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (this.sessionManager.getSession(this.primaryAgent.sessionId)) {
|
|
551
|
+
this.primaryAgent = {
|
|
552
|
+
...this.primaryAgent,
|
|
553
|
+
status: "running",
|
|
554
|
+
capabilities: [...plugin.capabilities],
|
|
555
|
+
};
|
|
556
|
+
this.broadcastWorkerList();
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
try {
|
|
560
|
+
const pieSessionId = plugin.name === "pie" ? this.primaryAgent.sessionId : undefined;
|
|
561
|
+
const primaryPluginBase = {
|
|
562
|
+
...plugin,
|
|
563
|
+
args: pieSessionId
|
|
564
|
+
? [...(plugin.args || []), "--session-id", pieSessionId]
|
|
565
|
+
: plugin.args,
|
|
566
|
+
env: {
|
|
567
|
+
...(plugin.env || {}),
|
|
568
|
+
MAGIC_SHELL_RELAY_URL: this.options.relayUrl,
|
|
569
|
+
MAGIC_SHELL_NODE_ID: this.options.nodeId,
|
|
570
|
+
MAGIC_SHELL_PASSWORD: this.options.password,
|
|
571
|
+
},
|
|
572
|
+
};
|
|
573
|
+
const primaryPlugin = plugin.name === "pie"
|
|
574
|
+
? withPrimaryPieExtensionPath(primaryPluginBase)
|
|
575
|
+
: primaryPluginBase;
|
|
576
|
+
this.primaryAgent = {
|
|
577
|
+
...this.primaryAgent,
|
|
578
|
+
status: "starting",
|
|
579
|
+
capabilities: [...plugin.capabilities],
|
|
580
|
+
lastError: undefined,
|
|
581
|
+
agentSessionId: pieSessionId || this.primaryAgent.agentSessionId,
|
|
582
|
+
};
|
|
583
|
+
if (this.primaryAgent.launchMode === "headless") {
|
|
584
|
+
this.primaryAgent = {
|
|
585
|
+
...this.primaryAgent,
|
|
586
|
+
status: "running",
|
|
587
|
+
};
|
|
588
|
+
this.broadcastWorkerList();
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
await this.sessionManager.createSession(this.primaryAgent.sessionId, primaryPlugin);
|
|
592
|
+
this.primaryAgent = {
|
|
593
|
+
...this.primaryAgent,
|
|
594
|
+
status: "running",
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
catch (err) {
|
|
598
|
+
this.primaryAgent = {
|
|
599
|
+
...this.primaryAgent,
|
|
600
|
+
status: "failed",
|
|
601
|
+
lastError: err instanceof Error ? err.message : String(err),
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
this.broadcastWorkerList();
|
|
605
|
+
}
|
|
606
|
+
async handleControlEnvelope(message, source = "relay") {
|
|
607
|
+
if (message.controlKind === "event" && String(message.controlName || "") === "session.subscription_changed") {
|
|
608
|
+
const controlMessage = message;
|
|
609
|
+
const payload = this.readPayloadObject(controlMessage.payload);
|
|
610
|
+
const sessionId = this.readString(payload.sessionId) || controlMessage.sessionId;
|
|
611
|
+
const attachedBrowserCount = this.readNumber(payload.attachedBrowserCount) || 0;
|
|
612
|
+
if (sessionId) {
|
|
613
|
+
this.updateSessionSubscription(source, sessionId, attachedBrowserCount);
|
|
614
|
+
}
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (message.controlKind === "event") {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
await this.handleControlMessage(message, source);
|
|
621
|
+
}
|
|
622
|
+
async handleControlMessage(message, source = "relay") {
|
|
623
|
+
if (message.controlKind === "command" && message.controlName) {
|
|
624
|
+
await this.handleControlCommand(message.controlName, message, source);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (message.controlKind === "query" && message.controlName) {
|
|
628
|
+
await this.handleControlQuery(message.controlName, message, source);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
this.sendControlResult(source, message.requestId, message.controlName, false, {
|
|
632
|
+
error: "Unsupported control message",
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
async handleControlCommand(name, message, source = "relay") {
|
|
636
|
+
const payload = this.readPayloadObject(message.payload);
|
|
637
|
+
switch (name) {
|
|
638
|
+
case "spawn_worker": {
|
|
639
|
+
const pluginName = this.readString(payload.pluginName) || message.pluginName || this.preferredWorkerPluginName || "pie";
|
|
640
|
+
const taskSummary = this.readString(payload.taskSummary) || message.taskSummary;
|
|
641
|
+
const cwd = this.readString(payload.cwd) || message.cwd;
|
|
642
|
+
const displayName = this.readString(payload.displayName) || message.displayName;
|
|
643
|
+
const sessionId = this.readString(payload.sessionId) || message.target?.sessionId || generateSessionId();
|
|
644
|
+
try {
|
|
645
|
+
await this.spawnWorker({ sessionId, pluginName, cwd, displayName, taskSummary });
|
|
646
|
+
if (taskSummary) {
|
|
647
|
+
this.rememberNodeSpawn(taskSummary, sessionId);
|
|
648
|
+
}
|
|
649
|
+
this.sendControlResult(source, message.requestId, name, true, {
|
|
650
|
+
sessionId,
|
|
651
|
+
pluginName,
|
|
652
|
+
displayName: displayName || null,
|
|
653
|
+
taskSummary: taskSummary || null,
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
catch (err) {
|
|
657
|
+
this.sendControlResult(source, message.requestId, name, false, {
|
|
658
|
+
error: err instanceof Error ? err.message : String(err),
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
case "set_worker_plugin_preference": {
|
|
664
|
+
const pluginName = this.readString(payload.pluginName) || message.pluginName;
|
|
665
|
+
if (!pluginName) {
|
|
666
|
+
this.sendControlResult(source, message.requestId, name, false, { error: "Missing pluginName" });
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (!this.plugins.has(pluginName)) {
|
|
670
|
+
this.sendControlResult(source, message.requestId, name, false, { error: `Unknown plugin: ${pluginName}` });
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
this.preferredWorkerPluginName = pluginName;
|
|
674
|
+
this.broadcastWorkerList();
|
|
675
|
+
this.sendControlResult(source, message.requestId, name, true, { pluginName });
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
case "attach_session": {
|
|
679
|
+
const sessionId = message.target?.sessionId || this.readString(payload.sessionId);
|
|
680
|
+
if (!sessionId) {
|
|
681
|
+
this.sendControlResult(source, message.requestId, name, false, { error: "Missing sessionId" });
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (this.isProtectedPrimarySession(sessionId)) {
|
|
685
|
+
this.sendControlResult(source, message.requestId, name, false, {
|
|
686
|
+
error: "Primary session is protected. Use send_primary_message instead of attach_session.",
|
|
687
|
+
});
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
const responses = attachWorkerSession(sessionId, {
|
|
691
|
+
workerRegistry: this.workerRegistry,
|
|
692
|
+
sessionManager: this.sessionManager,
|
|
693
|
+
});
|
|
694
|
+
for (const response of responses) {
|
|
695
|
+
this.sendToSource(source, response);
|
|
696
|
+
}
|
|
697
|
+
this.sendControlResult(source, message.requestId, name, true, { sessionId });
|
|
698
|
+
this.sendToSource(source, {
|
|
699
|
+
type: "control",
|
|
700
|
+
controlKind: "event",
|
|
701
|
+
controlName: "session.attached",
|
|
702
|
+
payload: { sessionId },
|
|
703
|
+
sessionId,
|
|
704
|
+
});
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
case "stop_worker": {
|
|
708
|
+
const sessionId = message.target?.sessionId || this.readString(payload.sessionId);
|
|
709
|
+
if (!sessionId) {
|
|
710
|
+
this.sendControlResult(source, message.requestId, name, false, { error: "Missing sessionId" });
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
await stopWorkerSession(sessionId, {
|
|
714
|
+
workerRegistry: this.workerRegistry,
|
|
715
|
+
sessionManager: this.sessionManager,
|
|
716
|
+
clearSessionTitle: (nextSessionId) => {
|
|
717
|
+
this.sessionTitleBuffers.delete(nextSessionId);
|
|
718
|
+
},
|
|
719
|
+
broadcastWorkerList: () => {
|
|
720
|
+
this.broadcastWorkerList();
|
|
721
|
+
},
|
|
722
|
+
spawnWorker: (options) => this.spawnWorker(options),
|
|
723
|
+
});
|
|
724
|
+
this.sendControlResult(source, message.requestId, name, true, { sessionId });
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
case "restart_worker": {
|
|
728
|
+
const sessionId = message.target?.sessionId || this.readString(payload.sessionId);
|
|
729
|
+
if (!sessionId) {
|
|
730
|
+
this.sendControlResult(source, message.requestId, name, false, { error: "Missing sessionId" });
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const error = await restartWorkerSession(sessionId, {
|
|
734
|
+
workerRegistry: this.workerRegistry,
|
|
735
|
+
sessionManager: this.sessionManager,
|
|
736
|
+
clearSessionTitle: (nextSessionId) => {
|
|
737
|
+
this.sessionTitleBuffers.delete(nextSessionId);
|
|
738
|
+
},
|
|
739
|
+
broadcastWorkerList: () => {
|
|
740
|
+
this.broadcastWorkerList();
|
|
741
|
+
},
|
|
742
|
+
spawnWorker: (options) => this.spawnWorker(options),
|
|
743
|
+
});
|
|
744
|
+
if (error) {
|
|
745
|
+
this.sendControlResult(source, message.requestId, name, false, { error });
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
this.sendControlResult(source, message.requestId, name, true, { sessionId });
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
case "send_input": {
|
|
752
|
+
const sessionId = message.target?.sessionId || this.readString(payload.sessionId) || message.sessionId;
|
|
753
|
+
const data = this.readString(payload.data) || message.data;
|
|
754
|
+
if (!sessionId || !data) {
|
|
755
|
+
this.sendControlResult(source, message.requestId, name, false, { error: "Missing sessionId or data" });
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
if (this.isProtectedPrimarySession(sessionId)) {
|
|
759
|
+
this.sendControlResult(source, message.requestId, name, false, {
|
|
760
|
+
error: "Primary session is protected. Use send_primary_message instead of send_input.",
|
|
761
|
+
});
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
this.captureWorkerTitleFromInput(sessionId, data);
|
|
765
|
+
this.sessionManager.sendInput(sessionId, data);
|
|
766
|
+
this.sendControlResult(source, message.requestId, name, true, { sessionId });
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
case "send_to_session": {
|
|
770
|
+
const sessionId = message.target?.sessionId || this.readString(payload.sessionId) || message.sessionId;
|
|
771
|
+
const data = this.readString(payload.message) || this.readString(payload.data) || message.data;
|
|
772
|
+
const mode = this.readString(payload.mode) || "follow_up";
|
|
773
|
+
if (!sessionId || !data) {
|
|
774
|
+
this.sendControlResult(source, message.requestId, name, false, { error: "Missing sessionId or message" });
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
if (this.isProtectedPrimarySession(sessionId)) {
|
|
778
|
+
this.sendControlResult(source, message.requestId, name, false, {
|
|
779
|
+
error: "Primary session is protected. Use send_primary_message instead of send_to_session.",
|
|
780
|
+
});
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
this.captureWorkerTitleFromInput(sessionId, data);
|
|
784
|
+
this.sessionManager.sendInput(sessionId, this.formatSessionControlInput(data));
|
|
785
|
+
this.sendControlResult(source, message.requestId, name, true, { sessionId, mode, message: data });
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
case "session_turn": {
|
|
789
|
+
const sessionId = message.target?.sessionId || this.readString(payload.sessionId) || message.sessionId;
|
|
790
|
+
const data = this.readString(payload.message) || this.readString(payload.data) || message.data;
|
|
791
|
+
const mode = this.readString(payload.mode) || "follow_up";
|
|
792
|
+
const timeoutMs = this.readNumber(payload.timeoutMs) || 8000;
|
|
793
|
+
const pollIntervalMs = this.readNumber(payload.pollIntervalMs) || 250;
|
|
794
|
+
if (!sessionId || !data) {
|
|
795
|
+
this.sendControlResult(source, message.requestId, name, false, { error: "Missing sessionId or message" });
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
if (this.isProtectedPrimarySession(sessionId)) {
|
|
799
|
+
this.sendControlResult(source, message.requestId, name, false, {
|
|
800
|
+
error: "Primary session is protected. Use send_primary_message instead of session_turn.",
|
|
801
|
+
});
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
const result = await this.runSessionTurn(sessionId, data, {
|
|
805
|
+
mode,
|
|
806
|
+
timeoutMs,
|
|
807
|
+
pollIntervalMs,
|
|
808
|
+
});
|
|
809
|
+
this.sendControlResult(source, message.requestId, name, true, result);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
case "send_primary_message": {
|
|
813
|
+
const text = this.readString(payload.text) || message.text;
|
|
814
|
+
if (!text) {
|
|
815
|
+
this.sendControlResult(source, message.requestId, name, false, { error: "Missing text" });
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
const response = await this.handlePrimaryMessage(text);
|
|
819
|
+
this.sendControlResult(source, message.requestId, name, true, response);
|
|
820
|
+
this.sendServerMessage({
|
|
821
|
+
type: "control",
|
|
822
|
+
controlKind: "event",
|
|
823
|
+
controlName: "node.reply",
|
|
824
|
+
payload: response,
|
|
825
|
+
});
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
case "resize_session": {
|
|
829
|
+
const sessionId = message.target?.sessionId || this.readString(payload.sessionId) || message.sessionId;
|
|
830
|
+
const cols = this.readNumber(payload.cols) || message.cols;
|
|
831
|
+
const rows = this.readNumber(payload.rows) || message.rows;
|
|
832
|
+
if (!sessionId || !cols || !rows) {
|
|
833
|
+
this.sendControlResult(source, message.requestId, name, false, { error: "Missing sessionId, cols, or rows" });
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
this.sessionManager.resize(sessionId, cols, rows);
|
|
837
|
+
this.sendControlResult(source, message.requestId, name, true, { sessionId, cols, rows });
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
async handleControlQuery(name, message, source = "relay") {
|
|
843
|
+
const payload = this.readPayloadObject(message.payload);
|
|
844
|
+
switch (name) {
|
|
845
|
+
case "get_runtime_summary":
|
|
846
|
+
case "list_workers":
|
|
847
|
+
this.sendControlResult(source, message.requestId, name, true, this.buildRuntimeSnapshot());
|
|
848
|
+
return;
|
|
849
|
+
case "get_worker_detail": {
|
|
850
|
+
const sessionId = message.target?.sessionId || this.readString(payload.sessionId);
|
|
851
|
+
const agentId = message.target?.agentId || this.readString(payload.agentId);
|
|
852
|
+
const worker = sessionId
|
|
853
|
+
? this.workerRegistry.getWorkerBySessionId(sessionId)
|
|
854
|
+
: agentId
|
|
855
|
+
? this.workerRegistry.getWorkerByAgentId(agentId)
|
|
856
|
+
: undefined;
|
|
857
|
+
if (!worker) {
|
|
858
|
+
this.sendControlResult(source, message.requestId, name, false, { error: "Worker not found" });
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
const detail = this.workerRegistry.getWorkerDetail(worker.agentId, this.getWorkerOutputSummary(worker.sessionId));
|
|
862
|
+
if (!detail) {
|
|
863
|
+
this.sendControlResult(source, message.requestId, name, false, { error: "Worker detail unavailable" });
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
this.sendControlResult(source, message.requestId, name, true, { worker: detail });
|
|
867
|
+
this.sendServerMessage({
|
|
868
|
+
type: "control",
|
|
869
|
+
controlKind: "event",
|
|
870
|
+
controlName: "worker.detail",
|
|
871
|
+
payload: { worker: detail },
|
|
872
|
+
sessionId: worker.sessionId,
|
|
873
|
+
});
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
case "get_session_message": {
|
|
877
|
+
const sessionId = message.target?.sessionId || this.readString(payload.sessionId);
|
|
878
|
+
if (!sessionId) {
|
|
879
|
+
this.sendControlResult(source, message.requestId, name, false, { error: "Missing sessionId" });
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
if (this.isProtectedPrimarySession(sessionId)) {
|
|
883
|
+
this.sendControlResult(source, message.requestId, name, false, {
|
|
884
|
+
error: "Primary session is protected. Use get_primary_agent instead of get_session_message.",
|
|
885
|
+
});
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
const pieSnapshot = this.sessionManager.getSession(sessionId)?.plugin.name === "pie"
|
|
889
|
+
? readPieSessionSnapshot(sessionId)
|
|
890
|
+
: null;
|
|
891
|
+
const lastMessage = pieSnapshot?.lastAssistantText || this.getWorkerOutputMessage(sessionId);
|
|
892
|
+
this.sendControlResult(source, message.requestId, name, true, {
|
|
893
|
+
sessionId,
|
|
894
|
+
message: lastMessage || null,
|
|
895
|
+
});
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
case "get_session_summary": {
|
|
899
|
+
const sessionId = message.target?.sessionId || this.readString(payload.sessionId);
|
|
900
|
+
if (!sessionId) {
|
|
901
|
+
this.sendControlResult(source, message.requestId, name, false, { error: "Missing sessionId" });
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
if (this.isProtectedPrimarySession(sessionId)) {
|
|
905
|
+
this.sendControlResult(source, message.requestId, name, false, {
|
|
906
|
+
error: "Primary session is protected. Use get_primary_agent instead of get_session_summary.",
|
|
907
|
+
});
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
const pieSnapshot = this.sessionManager.getSession(sessionId)?.plugin.name === "pie"
|
|
911
|
+
? readPieSessionSnapshot(sessionId)
|
|
912
|
+
: null;
|
|
913
|
+
const summary = pieSnapshot?.lastAssistantText || this.getWorkerOutputSummary(sessionId);
|
|
914
|
+
this.sendControlResult(source, message.requestId, name, true, {
|
|
915
|
+
sessionId,
|
|
916
|
+
summary: summary || null,
|
|
917
|
+
});
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
case "get_primary_agent": {
|
|
921
|
+
this.sendControlResult(source, message.requestId, name, true, {
|
|
922
|
+
primary: this.buildPrimaryAgentStatus(),
|
|
923
|
+
});
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
case "browse_directories": {
|
|
927
|
+
const response = await buildDirectoryList(message.target?.path || this.readString(payload.path) || message.path);
|
|
928
|
+
this.sendControlResult(source, message.requestId, name, true, {
|
|
929
|
+
path: response.path || null,
|
|
930
|
+
parentPath: response.parentPath || null,
|
|
931
|
+
repoRoot: response.repoRoot || null,
|
|
932
|
+
entries: response.entries || [],
|
|
933
|
+
});
|
|
934
|
+
this.sendServerMessage({
|
|
935
|
+
type: "control",
|
|
936
|
+
controlKind: "event",
|
|
937
|
+
controlName: "directory.list",
|
|
938
|
+
payload: {
|
|
939
|
+
path: response.path || null,
|
|
940
|
+
parentPath: response.parentPath || null,
|
|
941
|
+
repoRoot: response.repoRoot || null,
|
|
942
|
+
entries: response.entries || [],
|
|
943
|
+
},
|
|
944
|
+
});
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
sendControlResult(source = "relay", requestId, controlName, ok, payload) {
|
|
950
|
+
this.sendToSource(source, {
|
|
951
|
+
type: "control",
|
|
952
|
+
controlKind: "result",
|
|
953
|
+
controlName,
|
|
954
|
+
requestId,
|
|
955
|
+
ok,
|
|
956
|
+
payload,
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
buildRuntimeSnapshot() {
|
|
960
|
+
return {
|
|
961
|
+
nodeId: this.options.nodeId,
|
|
962
|
+
workers: this.workerRegistry.listWorkers(),
|
|
963
|
+
runtime: this.workerRegistry.getRuntimeSummary(),
|
|
964
|
+
focus: this.workerRegistry.getRuntimeFocus(),
|
|
965
|
+
primary: this.buildPrimaryAgentStatus(),
|
|
966
|
+
nodeHistory: this.nodeConversation.history,
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
buildPrimaryAgentStatus() {
|
|
970
|
+
return {
|
|
971
|
+
pluginName: this.primaryAgent.pluginName,
|
|
972
|
+
preferredWorkerPluginName: this.preferredWorkerPluginName,
|
|
973
|
+
sessionId: this.primaryAgent.sessionId,
|
|
974
|
+
status: this.primaryAgent.status,
|
|
975
|
+
activityState: this.primaryAgent.activityState,
|
|
976
|
+
lastError: this.primaryAgent.lastError,
|
|
977
|
+
capabilities: this.primaryAgent.capabilities || [],
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
isProtectedPrimarySession(sessionId) {
|
|
981
|
+
return !!sessionId && sessionId === this.primaryAgent.sessionId;
|
|
982
|
+
}
|
|
983
|
+
async runSessionTurn(sessionId, message, options) {
|
|
984
|
+
const session = this.sessionManager.getSession(sessionId);
|
|
985
|
+
const isPieSession = session?.plugin.name === "pie";
|
|
986
|
+
const isClaudeSession = isClaudeCodeWorker(session?.plugin.name);
|
|
987
|
+
const isCodexSession = isCodexWorker(session?.plugin.name);
|
|
988
|
+
let usePieSessionControl = isPieSession;
|
|
989
|
+
const beforeOutput = this.sessionManager.getBufferedOutput(sessionId);
|
|
990
|
+
const previousMessage = getLastMeaningfulWorkerMessage(beforeOutput) || "";
|
|
991
|
+
const previousSummary = summarizeRecentWorkerOutput(beforeOutput) || "";
|
|
992
|
+
const pieSessionBefore = isPieSession ? readPieSessionSnapshot(sessionId) : null;
|
|
993
|
+
const pieControlBefore = isPieSession ? readPieSessionControlState(sessionId) : null;
|
|
994
|
+
const terminalInput = this.formatSessionControlInput(message);
|
|
995
|
+
const startedAt = Date.now();
|
|
996
|
+
const requestId = `ms-${startedAt}-${Math.random().toString(36).slice(2, 8)}`;
|
|
997
|
+
const worker = this.workerRegistry.getWorkerBySessionId(sessionId);
|
|
998
|
+
this.captureWorkerTitleFromInput(sessionId, message);
|
|
999
|
+
if (worker) {
|
|
1000
|
+
this.workerRegistry.updateWorker(worker.agentId, {
|
|
1001
|
+
coordinationState: "delegating",
|
|
1002
|
+
coordinationTaskSummary: message,
|
|
1003
|
+
activityState: "busy",
|
|
1004
|
+
activityUpdatedAt: startedAt,
|
|
1005
|
+
});
|
|
1006
|
+
this.broadcastWorkerList();
|
|
1007
|
+
}
|
|
1008
|
+
if (isPieSession) {
|
|
1009
|
+
writePieSessionControlCommand(sessionId, {
|
|
1010
|
+
requestId,
|
|
1011
|
+
message,
|
|
1012
|
+
mode: options.mode === "steer" ? "steer" : "follow_up",
|
|
1013
|
+
createdAt: startedAt,
|
|
1014
|
+
status: "pending",
|
|
1015
|
+
});
|
|
1016
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1017
|
+
if (!readPieSessionControlState(sessionId)) {
|
|
1018
|
+
usePieSessionControl = false;
|
|
1019
|
+
this.sessionManager.sendInput(sessionId, terminalInput);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
else if (isClaudeSession) {
|
|
1023
|
+
const claudeResult = await this.runClaudeWorkerTurn(sessionId, message, options.timeoutMs);
|
|
1024
|
+
if (worker) {
|
|
1025
|
+
this.workerRegistry.updateWorker(worker.agentId, {
|
|
1026
|
+
coordinationState: claudeResult.changed ? "result_received" : "waiting_for_result",
|
|
1027
|
+
coordinationTaskSummary: message,
|
|
1028
|
+
activityState: claudeResult.changed ? "ready" : "busy",
|
|
1029
|
+
activityUpdatedAt: Date.now(),
|
|
1030
|
+
lastError: claudeResult.changed ? undefined : worker.lastError,
|
|
1031
|
+
});
|
|
1032
|
+
this.broadcastWorkerList();
|
|
1033
|
+
}
|
|
1034
|
+
return {
|
|
1035
|
+
sessionId,
|
|
1036
|
+
mode: options.mode,
|
|
1037
|
+
message: claudeResult.message || null,
|
|
1038
|
+
summary: claudeResult.summary || null,
|
|
1039
|
+
changed: claudeResult.changed,
|
|
1040
|
+
timedOut: claudeResult.timedOut,
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
else if (isCodexSession) {
|
|
1044
|
+
const codexResult = await this.runCodexWorkerTurn(sessionId, message, options.timeoutMs);
|
|
1045
|
+
if (worker) {
|
|
1046
|
+
this.workerRegistry.updateWorker(worker.agentId, {
|
|
1047
|
+
coordinationState: codexResult.changed ? "result_received" : "waiting_for_result",
|
|
1048
|
+
coordinationTaskSummary: message,
|
|
1049
|
+
activityState: codexResult.changed ? "ready" : "busy",
|
|
1050
|
+
activityUpdatedAt: Date.now(),
|
|
1051
|
+
lastError: codexResult.changed ? undefined : worker.lastError,
|
|
1052
|
+
});
|
|
1053
|
+
this.broadcastWorkerList();
|
|
1054
|
+
}
|
|
1055
|
+
return {
|
|
1056
|
+
sessionId,
|
|
1057
|
+
mode: options.mode,
|
|
1058
|
+
message: codexResult.message || null,
|
|
1059
|
+
summary: codexResult.summary || null,
|
|
1060
|
+
changed: codexResult.changed,
|
|
1061
|
+
timedOut: codexResult.timedOut,
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
this.sessionManager.sendInput(sessionId, terminalInput);
|
|
1066
|
+
}
|
|
1067
|
+
if (worker && !isPieSession) {
|
|
1068
|
+
this.workerRegistry.updateWorker(worker.agentId, {
|
|
1069
|
+
coordinationState: "waiting_for_result",
|
|
1070
|
+
coordinationTaskSummary: message,
|
|
1071
|
+
activityState: "busy",
|
|
1072
|
+
activityUpdatedAt: Date.now(),
|
|
1073
|
+
});
|
|
1074
|
+
this.broadcastWorkerList();
|
|
1075
|
+
}
|
|
1076
|
+
const deadline = startedAt + Math.max(options.timeoutMs, 250);
|
|
1077
|
+
const pollIntervalMs = Math.max(options.pollIntervalMs, 25);
|
|
1078
|
+
let latestMessage = previousMessage;
|
|
1079
|
+
let latestSummary = previousSummary;
|
|
1080
|
+
let changed = false;
|
|
1081
|
+
while (Date.now() <= deadline) {
|
|
1082
|
+
if (usePieSessionControl) {
|
|
1083
|
+
const pieControlAfter = readPieSessionControlState(sessionId);
|
|
1084
|
+
if (pieControlAfter
|
|
1085
|
+
&& pieControlAfter.lastCompletedRequestId === requestId
|
|
1086
|
+
&& pieControlAfter.lastAssistantText
|
|
1087
|
+
&& pieControlAfter.lastAssistantText !== pieControlBefore?.lastAssistantText
|
|
1088
|
+
&& this.isUsablePieSessionTurnResult(pieControlAfter.lastAssistantText, message)) {
|
|
1089
|
+
latestMessage = pieControlAfter.lastAssistantText;
|
|
1090
|
+
latestSummary = pieControlAfter.lastAssistantText;
|
|
1091
|
+
changed = true;
|
|
1092
|
+
break;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
const pieSessionAfter = isPieSession ? readPieSessionSnapshot(sessionId) : null;
|
|
1096
|
+
if (pieSessionAfter
|
|
1097
|
+
&& pieSessionAfter.messageCount > (pieSessionBefore?.messageCount || 0)
|
|
1098
|
+
&& pieSessionAfter.lastAssistantText
|
|
1099
|
+
&& pieSessionAfter.lastAssistantText !== pieSessionBefore?.lastAssistantText
|
|
1100
|
+
&& pieSessionAfter.lastMessageRole === "assistant"
|
|
1101
|
+
&& !pieSessionAfter.lastAssistantHasToolCall
|
|
1102
|
+
&& Date.now() - pieSessionAfter.updatedAt >= 500) {
|
|
1103
|
+
if (this.isUsablePieSessionTurnResult(pieSessionAfter.lastAssistantText, message)) {
|
|
1104
|
+
latestMessage = pieSessionAfter.lastAssistantText;
|
|
1105
|
+
latestSummary = pieSessionAfter.lastAssistantText;
|
|
1106
|
+
changed = true;
|
|
1107
|
+
break;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
if (!isPieSession) {
|
|
1111
|
+
const bufferedOutput = this.sessionManager.getBufferedOutput(sessionId);
|
|
1112
|
+
latestMessage = getLastMeaningfulWorkerMessage(bufferedOutput) || "";
|
|
1113
|
+
latestSummary = summarizeRecentWorkerOutput(bufferedOutput) || "";
|
|
1114
|
+
changed = (!!latestMessage && latestMessage !== previousMessage)
|
|
1115
|
+
|| (!!latestSummary && latestSummary !== previousSummary);
|
|
1116
|
+
if (changed
|
|
1117
|
+
&& this.isLikelyEchoedSessionInput(latestMessage, latestSummary, message)) {
|
|
1118
|
+
changed = false;
|
|
1119
|
+
}
|
|
1120
|
+
if (changed) {
|
|
1121
|
+
break;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
1125
|
+
}
|
|
1126
|
+
if (worker) {
|
|
1127
|
+
this.workerRegistry.updateWorker(worker.agentId, {
|
|
1128
|
+
coordinationState: changed ? "result_received" : "waiting_for_result",
|
|
1129
|
+
coordinationTaskSummary: message,
|
|
1130
|
+
activityState: changed ? "ready" : "busy",
|
|
1131
|
+
activityUpdatedAt: Date.now(),
|
|
1132
|
+
lastError: changed ? undefined : worker.lastError,
|
|
1133
|
+
});
|
|
1134
|
+
this.broadcastWorkerList();
|
|
1135
|
+
}
|
|
1136
|
+
return {
|
|
1137
|
+
sessionId,
|
|
1138
|
+
mode: options.mode,
|
|
1139
|
+
message: latestMessage || null,
|
|
1140
|
+
summary: latestSummary || null,
|
|
1141
|
+
changed,
|
|
1142
|
+
timedOut: !changed,
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
async runClaudeWorkerTurn(sessionId, message, timeoutMs) {
|
|
1146
|
+
const session = this.sessionManager.getSession(sessionId);
|
|
1147
|
+
if (!session) {
|
|
1148
|
+
throw new Error(`Claude worker session not found: ${sessionId}`);
|
|
1149
|
+
}
|
|
1150
|
+
const cwd = session.plugin.cwd || this.workerRegistry.getWorkerBySessionId(sessionId)?.cwd || process.cwd();
|
|
1151
|
+
const beforeOutput = this.sessionManager.getBufferedOutput(sessionId);
|
|
1152
|
+
const previousSummary = summarizeClaudeWorkerOutput(beforeOutput, message);
|
|
1153
|
+
try {
|
|
1154
|
+
const result = await runClaudeExec({
|
|
1155
|
+
cwd,
|
|
1156
|
+
message,
|
|
1157
|
+
timeoutMs,
|
|
1158
|
+
});
|
|
1159
|
+
this.syntheticSessionResults.set(sessionId, {
|
|
1160
|
+
message: result.message,
|
|
1161
|
+
summary: result.summary,
|
|
1162
|
+
updatedAt: Date.now(),
|
|
1163
|
+
});
|
|
1164
|
+
if (result.summary) {
|
|
1165
|
+
this.sessionManager.appendOutput(sessionId, `${result.summary}\n`);
|
|
1166
|
+
}
|
|
1167
|
+
return {
|
|
1168
|
+
message: result.message,
|
|
1169
|
+
summary: result.summary,
|
|
1170
|
+
changed: !!result.summary,
|
|
1171
|
+
timedOut: false,
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
catch (error) {
|
|
1175
|
+
const code = typeof error === "object" && error && "code" in error ? String(error.code || "") : "";
|
|
1176
|
+
if (code === "CLAUDE_EXEC_TIMEOUT") {
|
|
1177
|
+
return {
|
|
1178
|
+
message: "",
|
|
1179
|
+
summary: previousSummary,
|
|
1180
|
+
changed: false,
|
|
1181
|
+
timedOut: true,
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
throw error;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
async runCodexWorkerTurn(sessionId, message, timeoutMs) {
|
|
1188
|
+
const session = this.sessionManager.getSession(sessionId);
|
|
1189
|
+
if (!session) {
|
|
1190
|
+
throw new Error(`Codex worker session not found: ${sessionId}`);
|
|
1191
|
+
}
|
|
1192
|
+
const cwd = session.plugin.cwd || this.workerRegistry.getWorkerBySessionId(sessionId)?.cwd || process.cwd();
|
|
1193
|
+
const beforeOutput = this.sessionManager.getBufferedOutput(sessionId);
|
|
1194
|
+
const previousSummary = summarizeCodexWorkerOutput(beforeOutput, message);
|
|
1195
|
+
try {
|
|
1196
|
+
const result = await runCodexExec({
|
|
1197
|
+
cwd,
|
|
1198
|
+
message,
|
|
1199
|
+
timeoutMs,
|
|
1200
|
+
});
|
|
1201
|
+
this.syntheticSessionResults.set(sessionId, {
|
|
1202
|
+
message: result.message,
|
|
1203
|
+
summary: result.summary,
|
|
1204
|
+
updatedAt: Date.now(),
|
|
1205
|
+
});
|
|
1206
|
+
if (result.summary) {
|
|
1207
|
+
this.sessionManager.appendOutput(sessionId, `${result.summary}\n`);
|
|
1208
|
+
}
|
|
1209
|
+
return {
|
|
1210
|
+
message: result.message,
|
|
1211
|
+
summary: result.summary,
|
|
1212
|
+
changed: !!result.summary,
|
|
1213
|
+
timedOut: false,
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
catch (error) {
|
|
1217
|
+
const code = typeof error === "object" && error && "code" in error ? String(error.code || "") : "";
|
|
1218
|
+
if (code === "CODEX_EXEC_TIMEOUT") {
|
|
1219
|
+
return {
|
|
1220
|
+
message: "",
|
|
1221
|
+
summary: previousSummary,
|
|
1222
|
+
changed: false,
|
|
1223
|
+
timedOut: true,
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
throw error;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
isLikelyEchoedSessionInput(message, summary, input) {
|
|
1230
|
+
const normalize = (value) => stripAnsi(value || "").replace(/\s+/g, " ").trim();
|
|
1231
|
+
const normalizedInput = normalize(input);
|
|
1232
|
+
if (!normalizedInput)
|
|
1233
|
+
return false;
|
|
1234
|
+
const candidates = [normalize(message), normalize(summary)].filter(Boolean);
|
|
1235
|
+
if (!candidates.length)
|
|
1236
|
+
return false;
|
|
1237
|
+
return candidates.every((candidate) => {
|
|
1238
|
+
if (!candidate)
|
|
1239
|
+
return true;
|
|
1240
|
+
if (normalizedInput.includes(candidate) || candidate.includes(normalizedInput)) {
|
|
1241
|
+
return true;
|
|
1242
|
+
}
|
|
1243
|
+
const prefix = normalizedInput.slice(0, Math.min(normalizedInput.length, 80));
|
|
1244
|
+
return prefix.length >= 24 && candidate.includes(prefix.slice(0, 24));
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
isUsablePieSessionTurnResult(text, input) {
|
|
1248
|
+
const normalized = stripAnsi(text || "").replace(/\s+/g, " ").trim();
|
|
1249
|
+
if (!normalized) {
|
|
1250
|
+
return false;
|
|
1251
|
+
}
|
|
1252
|
+
if (this.isLikelyEchoedSessionInput(normalized, normalized, input)) {
|
|
1253
|
+
return false;
|
|
1254
|
+
}
|
|
1255
|
+
const lowered = normalized.toLowerCase();
|
|
1256
|
+
if (lowered.includes("queued message for")
|
|
1257
|
+
|| lowered.includes("message_update")
|
|
1258
|
+
|| lowered.includes("looks ready for input")
|
|
1259
|
+
|| lowered.includes("work in your own session")
|
|
1260
|
+
|| lowered.includes("reply to the primary agent now")
|
|
1261
|
+
|| lowered.startsWith("the user asked me")
|
|
1262
|
+
|| normalized.startsWith("用户要求我")
|
|
1263
|
+
|| lowered.includes("please tell me the specific task")
|
|
1264
|
+
|| lowered.includes("tell me the specific task")
|
|
1265
|
+
|| lowered.includes("what would you like me to do")
|
|
1266
|
+
|| lowered.includes("what do you want me to do")
|
|
1267
|
+
|| normalized.includes("请告诉我具体任务")
|
|
1268
|
+
|| normalized.includes("请告诉我具体想做什么")
|
|
1269
|
+
|| normalized.includes("你想派 worker 去干什么")) {
|
|
1270
|
+
return false;
|
|
1271
|
+
}
|
|
1272
|
+
return true;
|
|
1273
|
+
}
|
|
1274
|
+
getWorkerOutputSummary(sessionId) {
|
|
1275
|
+
const worker = this.workerRegistry.getWorkerBySessionId(sessionId);
|
|
1276
|
+
const syntheticResult = this.syntheticSessionResults.get(sessionId);
|
|
1277
|
+
if (worker && (isClaudeCodeWorker(worker.agentType) || isCodexWorker(worker.agentType)) && syntheticResult?.summary) {
|
|
1278
|
+
return syntheticResult.summary;
|
|
1279
|
+
}
|
|
1280
|
+
const bufferedOutput = this.sessionManager.getBufferedOutput(sessionId);
|
|
1281
|
+
if (worker && isClaudeCodeWorker(worker.agentType)) {
|
|
1282
|
+
return summarizeClaudeWorkerOutput(bufferedOutput, worker.coordinationTaskSummary || worker.taskSummary || "");
|
|
1283
|
+
}
|
|
1284
|
+
if (worker && isCodexWorker(worker.agentType)) {
|
|
1285
|
+
return summarizeCodexWorkerOutput(bufferedOutput, worker.coordinationTaskSummary || worker.taskSummary || "");
|
|
1286
|
+
}
|
|
1287
|
+
return summarizeRecentWorkerOutput(bufferedOutput);
|
|
1288
|
+
}
|
|
1289
|
+
getWorkerOutputMessage(sessionId) {
|
|
1290
|
+
const worker = this.workerRegistry.getWorkerBySessionId(sessionId);
|
|
1291
|
+
const syntheticResult = this.syntheticSessionResults.get(sessionId);
|
|
1292
|
+
if (worker && (isClaudeCodeWorker(worker.agentType) || isCodexWorker(worker.agentType)) && syntheticResult?.message) {
|
|
1293
|
+
return syntheticResult.message;
|
|
1294
|
+
}
|
|
1295
|
+
const bufferedOutput = this.sessionManager.getBufferedOutput(sessionId);
|
|
1296
|
+
if (worker && isClaudeCodeWorker(worker.agentType)) {
|
|
1297
|
+
return getLastClaudeWorkerMessage(bufferedOutput, worker.coordinationTaskSummary || worker.taskSummary || "");
|
|
1298
|
+
}
|
|
1299
|
+
if (worker && isCodexWorker(worker.agentType)) {
|
|
1300
|
+
return getLastCodexWorkerMessage(bufferedOutput, worker.coordinationTaskSummary || worker.taskSummary || "");
|
|
1301
|
+
}
|
|
1302
|
+
return getLastMeaningfulWorkerMessage(bufferedOutput);
|
|
1303
|
+
}
|
|
1304
|
+
readString(value) {
|
|
1305
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
1306
|
+
}
|
|
1307
|
+
readNumber(value) {
|
|
1308
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
1309
|
+
}
|
|
1310
|
+
readPayloadObject(value) {
|
|
1311
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
1312
|
+
? value
|
|
1313
|
+
: {};
|
|
1314
|
+
}
|
|
1315
|
+
recordPrimaryOutput(content) {
|
|
1316
|
+
const metadata = extractTerminalMetadata(content, this.primaryAgent.pluginName);
|
|
1317
|
+
this.primaryAgent = {
|
|
1318
|
+
...this.primaryAgent,
|
|
1319
|
+
status: "running",
|
|
1320
|
+
lastOutputAt: Date.now(),
|
|
1321
|
+
title: metadata?.title || this.primaryAgent.title,
|
|
1322
|
+
activityState: metadata?.activityState || this.primaryAgent.activityState,
|
|
1323
|
+
agentSessionId: metadata?.agentSessionId || this.primaryAgent.agentSessionId,
|
|
1324
|
+
};
|
|
1325
|
+
if (metadata) {
|
|
1326
|
+
this.broadcastWorkerList();
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
captureWorkerTitleFromInput(sessionId, data) {
|
|
1330
|
+
const worker = this.workerRegistry.getWorkerBySessionId(sessionId);
|
|
1331
|
+
if (!worker || worker.taskSummary) {
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
let buffer = this.sessionTitleBuffers.get(sessionId) || "";
|
|
1335
|
+
for (const char of data) {
|
|
1336
|
+
if (char === "\u001b") {
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
if (char === "\u007f" || char === "\b") {
|
|
1340
|
+
buffer = buffer.slice(0, -1);
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
if (char === "\r" || char === "\n") {
|
|
1344
|
+
const title = normalizeWorkerTitleCandidate(buffer);
|
|
1345
|
+
if (title) {
|
|
1346
|
+
this.workerRegistry.updateWorker(worker.agentId, { taskSummary: title });
|
|
1347
|
+
this.broadcastWorkerList();
|
|
1348
|
+
this.sessionTitleBuffers.delete(sessionId);
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
buffer = "";
|
|
1352
|
+
continue;
|
|
1353
|
+
}
|
|
1354
|
+
if (char >= " " && char !== "\u007f") {
|
|
1355
|
+
buffer += char;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
this.sessionTitleBuffers.set(sessionId, buffer.slice(-256));
|
|
1359
|
+
}
|
|
1360
|
+
broadcastWorkerList() {
|
|
1361
|
+
const snapshot = this.buildRuntimeSnapshot();
|
|
1362
|
+
this.sendServerMessage({
|
|
1363
|
+
type: "worker_list",
|
|
1364
|
+
workers: snapshot.workers,
|
|
1365
|
+
runtime: snapshot.runtime,
|
|
1366
|
+
focus: snapshot.focus,
|
|
1367
|
+
primary: snapshot.primary,
|
|
1368
|
+
nodeHistory: snapshot.nodeHistory,
|
|
1369
|
+
});
|
|
1370
|
+
this.sendServerMessage({
|
|
1371
|
+
type: "control",
|
|
1372
|
+
controlKind: "event",
|
|
1373
|
+
controlName: "runtime.snapshot",
|
|
1374
|
+
payload: snapshot,
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
rememberNodeSpawn(taskSummary, sessionId) {
|
|
1378
|
+
const worker = this.workerRegistry.getWorkerBySessionId(sessionId);
|
|
1379
|
+
this.nodeConversation = rememberNodeSpawn(this.nodeConversation, taskSummary, sessionId, worker);
|
|
1380
|
+
}
|
|
1381
|
+
async handlePrimaryMessage(text) {
|
|
1382
|
+
const userText = text.trim();
|
|
1383
|
+
this.nodeConversation.lastUserMessage = userText;
|
|
1384
|
+
this.recordConversationTurn("user", userText);
|
|
1385
|
+
const orchestrated = await this.tryHandlePrimaryOrchestration(userText);
|
|
1386
|
+
if (orchestrated) {
|
|
1387
|
+
this.recordConversationTurn("assistant", orchestrated.text, {
|
|
1388
|
+
actionType: orchestrated.actionType,
|
|
1389
|
+
actionLabel: orchestrated.actionLabel,
|
|
1390
|
+
actionTaskSummary: orchestrated.actionTaskSummary,
|
|
1391
|
+
actionSessionId: orchestrated.actionSessionId,
|
|
1392
|
+
});
|
|
1393
|
+
return orchestrated;
|
|
1394
|
+
}
|
|
1395
|
+
try {
|
|
1396
|
+
const primaryText = await this.queryPrimarySessionText(userText);
|
|
1397
|
+
if (primaryText?.trim()) {
|
|
1398
|
+
const response = {
|
|
1399
|
+
text: primaryText.trim(),
|
|
1400
|
+
};
|
|
1401
|
+
this.recordConversationTurn("assistant", response.text);
|
|
1402
|
+
return response;
|
|
1403
|
+
}
|
|
1404
|
+
throw new Error("Primary session did not return a visible assistant message");
|
|
1405
|
+
}
|
|
1406
|
+
catch (err) {
|
|
1407
|
+
this.primaryAgent = {
|
|
1408
|
+
...this.primaryAgent,
|
|
1409
|
+
lastError: err instanceof Error ? err.message : String(err),
|
|
1410
|
+
};
|
|
1411
|
+
this.broadcastWorkerList();
|
|
1412
|
+
const response = {
|
|
1413
|
+
text: `Primary agent unavailable: ${this.primaryAgent.lastError}`,
|
|
1414
|
+
};
|
|
1415
|
+
this.recordConversationTurn("assistant", response.text);
|
|
1416
|
+
return response;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
async tryHandlePrimaryOrchestration(userText) {
|
|
1420
|
+
const spawnIntent = this.parseSpawnWorkerIntent(userText);
|
|
1421
|
+
if (spawnIntent) {
|
|
1422
|
+
return this.handleSpawnWorkerIntent(spawnIntent);
|
|
1423
|
+
}
|
|
1424
|
+
const stopName = this.parseNamedWorkerAction(userText, /(停止|停掉|stop)\s*([^\s,。,.]+)/i);
|
|
1425
|
+
if (stopName) {
|
|
1426
|
+
return this.handleStopWorkerIntent(stopName);
|
|
1427
|
+
}
|
|
1428
|
+
const restartName = this.parseNamedWorkerAction(userText, /(重启|restart)\s*([^\s,。,.]+)/i);
|
|
1429
|
+
if (restartName) {
|
|
1430
|
+
return this.handleRestartWorkerIntent(restartName, userText);
|
|
1431
|
+
}
|
|
1432
|
+
if (/现在有哪些\s*worker|who.*alive|谁还活着|谁停了/u.test(userText)) {
|
|
1433
|
+
return this.handleWorkerRosterIntent();
|
|
1434
|
+
}
|
|
1435
|
+
if (/第一个\s*worker.*README.*第二个\s*worker.*git commit/u.test(userText)) {
|
|
1436
|
+
return this.handleOrdinalMultiWorkerIntent();
|
|
1437
|
+
}
|
|
1438
|
+
if (/小明和小美.*git.*汇总/u.test(userText)) {
|
|
1439
|
+
return this.handleNamedMultiWorkerCommitIntent(["小明", "小美"]);
|
|
1440
|
+
}
|
|
1441
|
+
const readmeTarget = this.parseNamedWorkerTaskIntent(userText, /让\s*([^\s,。,.]+)\s*(?:读一下|检查|看看).*(README|readme)/u);
|
|
1442
|
+
if (readmeTarget) {
|
|
1443
|
+
return this.handleWorkerTaskIntent(readmeTarget.workerName, "请在当前仓库读取 README,并用中文给出简洁但具体的摘要。");
|
|
1444
|
+
}
|
|
1445
|
+
const inspectTarget = this.parseNamedWorkerTaskIntent(userText, /让\s*([^\s,。,.]+)\s*看一下当前目录有什么/u);
|
|
1446
|
+
if (inspectTarget) {
|
|
1447
|
+
return this.handleWorkerTaskIntent(inspectTarget.workerName, "请列出当前目录里最重要的文件和目录,并用中文简洁总结它们是做什么的。");
|
|
1448
|
+
}
|
|
1449
|
+
return null;
|
|
1450
|
+
}
|
|
1451
|
+
parseSpawnWorkerIntent(userText) {
|
|
1452
|
+
const workerName = this.parseWorkerName(userText) || this.lastFailedNamedWorker;
|
|
1453
|
+
if (/(spawn 一个 worker|来一个|创建.*worker|新建.*worker)/u.test(userText)
|
|
1454
|
+
|| (/去\s*.+目录/u.test(userText) && /worker/u.test(userText))) {
|
|
1455
|
+
const cwdText = this.extractPathFragment(userText);
|
|
1456
|
+
if (cwdText) {
|
|
1457
|
+
return { cwdText, workerName: workerName || undefined };
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
if (workerName && /(在|去).*(codex|token-manager|not-exist-project|AIProjects)/iu.test(userText)) {
|
|
1461
|
+
const cwdText = this.extractPathFragment(userText);
|
|
1462
|
+
if (cwdText) {
|
|
1463
|
+
return { cwdText, workerName };
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
return null;
|
|
1467
|
+
}
|
|
1468
|
+
parseWorkerName(userText) {
|
|
1469
|
+
const explicit = userText.match(/叫\s*([^,。,.]+)/u)?.[1];
|
|
1470
|
+
if (explicit)
|
|
1471
|
+
return explicit.trim();
|
|
1472
|
+
const implied = userText.match(/(?:再?来一个|来个)\s*([^\s,在去让把帮给,。,.]+)/u)?.[1];
|
|
1473
|
+
if (implied)
|
|
1474
|
+
return implied.trim();
|
|
1475
|
+
return null;
|
|
1476
|
+
}
|
|
1477
|
+
extractPathFragment(userText) {
|
|
1478
|
+
const absolute = userText.match(/(\/Users\/[^\s,。]+)/u)?.[1];
|
|
1479
|
+
if (absolute)
|
|
1480
|
+
return absolute;
|
|
1481
|
+
const homeNamed = userText.match(/\b(Documents|Desktop|Downloads)\b/i)?.[1];
|
|
1482
|
+
if (homeNamed)
|
|
1483
|
+
return homeNamed;
|
|
1484
|
+
const named = userText.match(/\b(codex|token-manager|not-exist-project)\b/i)?.[1];
|
|
1485
|
+
if (named)
|
|
1486
|
+
return named;
|
|
1487
|
+
if (/AIProjects.*token-manager/i.test(userText))
|
|
1488
|
+
return "token-manager";
|
|
1489
|
+
if (/AIProjects.*codex/i.test(userText))
|
|
1490
|
+
return "codex";
|
|
1491
|
+
return null;
|
|
1492
|
+
}
|
|
1493
|
+
resolveWorkerCwd(cwdText) {
|
|
1494
|
+
if (!cwdText)
|
|
1495
|
+
return null;
|
|
1496
|
+
if (cwdText.startsWith("/")) {
|
|
1497
|
+
return existsSync(cwdText) ? cwdText : null;
|
|
1498
|
+
}
|
|
1499
|
+
const homeCandidate = path.join(os.homedir(), cwdText);
|
|
1500
|
+
if (existsSync(homeCandidate)) {
|
|
1501
|
+
return homeCandidate;
|
|
1502
|
+
}
|
|
1503
|
+
const aiProjectsRoot = path.resolve(process.cwd(), "..");
|
|
1504
|
+
const candidate = path.join(aiProjectsRoot, cwdText);
|
|
1505
|
+
return existsSync(candidate) ? candidate : null;
|
|
1506
|
+
}
|
|
1507
|
+
async handleSpawnWorkerIntent(intent) {
|
|
1508
|
+
const resolvedCwd = this.resolveWorkerCwd(intent.cwdText);
|
|
1509
|
+
if (!resolvedCwd) {
|
|
1510
|
+
this.lastFailedNamedWorker = intent.workerName;
|
|
1511
|
+
return {
|
|
1512
|
+
text: `${intent.workerName ? `${intent.workerName} ` : ""}目标目录不存在或当前节点无法访问:${intent.cwdText}`,
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
const sessionId = generateSessionId();
|
|
1516
|
+
await this.spawnWorker({
|
|
1517
|
+
sessionId,
|
|
1518
|
+
pluginName: this.preferredWorkerPluginName,
|
|
1519
|
+
cwd: resolvedCwd,
|
|
1520
|
+
displayName: intent.workerName,
|
|
1521
|
+
});
|
|
1522
|
+
this.lastFailedNamedWorker = undefined;
|
|
1523
|
+
this.rememberNodeSpawn(`Inspect ${path.basename(resolvedCwd)}`, sessionId);
|
|
1524
|
+
this.broadcastWorkerList();
|
|
1525
|
+
return {
|
|
1526
|
+
text: `${intent.workerName || "新 worker"} 已创建,目录是 \`${resolvedCwd}\`。`,
|
|
1527
|
+
actionType: "attach",
|
|
1528
|
+
actionLabel: "ATTACH WORKER",
|
|
1529
|
+
actionSessionId: sessionId,
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
parseNamedWorkerAction(userText, pattern) {
|
|
1533
|
+
const match = userText.match(pattern);
|
|
1534
|
+
const raw = match?.[2]?.trim();
|
|
1535
|
+
if (!raw)
|
|
1536
|
+
return null;
|
|
1537
|
+
return raw.split(/(?:并|然后|再|,|,|。|\s)/u)[0]?.trim() || null;
|
|
1538
|
+
}
|
|
1539
|
+
parseNamedWorkerTaskIntent(userText, pattern) {
|
|
1540
|
+
const match = userText.match(pattern);
|
|
1541
|
+
if (!match?.[1])
|
|
1542
|
+
return null;
|
|
1543
|
+
return { workerName: match[1].trim() };
|
|
1544
|
+
}
|
|
1545
|
+
findWorkerByName(name) {
|
|
1546
|
+
const normalized = name.trim().toLowerCase();
|
|
1547
|
+
return this.workerRegistry.listWorkers().find((worker) => (worker.displayName || "").trim().toLowerCase() === normalized);
|
|
1548
|
+
}
|
|
1549
|
+
getOrderedLiveWorkers() {
|
|
1550
|
+
return this.workerRegistry
|
|
1551
|
+
.listWorkers()
|
|
1552
|
+
.filter((worker) => worker.status !== "stopped" && worker.status !== "failed")
|
|
1553
|
+
.sort((a, b) => a.createdAt - b.createdAt);
|
|
1554
|
+
}
|
|
1555
|
+
async handleStopWorkerIntent(workerName) {
|
|
1556
|
+
const worker = this.findWorkerByName(workerName);
|
|
1557
|
+
if (!worker) {
|
|
1558
|
+
return { text: `没有找到名为 ${workerName} 的 worker。` };
|
|
1559
|
+
}
|
|
1560
|
+
await stopWorkerSession(worker.sessionId, {
|
|
1561
|
+
workerRegistry: this.workerRegistry,
|
|
1562
|
+
sessionManager: this.sessionManager,
|
|
1563
|
+
clearSessionTitle: (sessionId) => this.sessionTitleBuffers.delete(sessionId),
|
|
1564
|
+
broadcastWorkerList: () => this.broadcastWorkerList(),
|
|
1565
|
+
spawnWorker: (options) => this.spawnWorker(options),
|
|
1566
|
+
});
|
|
1567
|
+
return { text: `${workerName} 已停止。` };
|
|
1568
|
+
}
|
|
1569
|
+
async handleRestartWorkerIntent(workerName, userText) {
|
|
1570
|
+
const worker = this.findWorkerByName(workerName);
|
|
1571
|
+
if (!worker) {
|
|
1572
|
+
return { text: `没有找到名为 ${workerName} 的 worker。` };
|
|
1573
|
+
}
|
|
1574
|
+
const error = await restartWorkerSession(worker.sessionId, {
|
|
1575
|
+
workerRegistry: this.workerRegistry,
|
|
1576
|
+
sessionManager: this.sessionManager,
|
|
1577
|
+
clearSessionTitle: (sessionId) => this.sessionTitleBuffers.delete(sessionId),
|
|
1578
|
+
broadcastWorkerList: () => this.broadcastWorkerList(),
|
|
1579
|
+
spawnWorker: (options) => this.spawnWorker(options),
|
|
1580
|
+
});
|
|
1581
|
+
if (error) {
|
|
1582
|
+
return { text: `${workerName} 重启失败:${error}` };
|
|
1583
|
+
}
|
|
1584
|
+
if (/README|readme/.test(userText)) {
|
|
1585
|
+
return this.handleWorkerTaskIntent(workerName, "请在当前仓库读取 README,并用中文给出简洁但具体的摘要。");
|
|
1586
|
+
}
|
|
1587
|
+
return { text: `${workerName} 已重启。` };
|
|
1588
|
+
}
|
|
1589
|
+
async handleWorkerTaskIntent(workerName, task) {
|
|
1590
|
+
const worker = this.findWorkerByName(workerName);
|
|
1591
|
+
if (!worker) {
|
|
1592
|
+
return { text: `没有找到名为 ${workerName} 的 worker。` };
|
|
1593
|
+
}
|
|
1594
|
+
const result = await this.runSessionTurn(worker.sessionId, task, {
|
|
1595
|
+
mode: "steer",
|
|
1596
|
+
timeoutMs: 90_000,
|
|
1597
|
+
pollIntervalMs: 500,
|
|
1598
|
+
});
|
|
1599
|
+
const answer = result.message || result.summary || "(worker 没有返回可用结果)";
|
|
1600
|
+
return {
|
|
1601
|
+
text: `${workerName} 的结果:\n${answer}`,
|
|
1602
|
+
actionType: "attach",
|
|
1603
|
+
actionLabel: "ATTACH WORKER",
|
|
1604
|
+
actionSessionId: worker.sessionId,
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
async handleOrdinalMultiWorkerIntent() {
|
|
1608
|
+
const workers = this.getOrderedLiveWorkers();
|
|
1609
|
+
if (workers.length < 2) {
|
|
1610
|
+
return { text: "当前没有足够的 live workers 来完成这个双 worker 协作请求。" };
|
|
1611
|
+
}
|
|
1612
|
+
const first = workers[0];
|
|
1613
|
+
const second = workers[1];
|
|
1614
|
+
const readme = await this.runSessionTurn(first.sessionId, "请读取当前仓库的 README,并用中文给出简洁摘要。", {
|
|
1615
|
+
mode: "steer",
|
|
1616
|
+
timeoutMs: 90_000,
|
|
1617
|
+
pollIntervalMs: 500,
|
|
1618
|
+
});
|
|
1619
|
+
const commits = await this.runSessionTurn(second.sessionId, "请统计当前 git 仓库一共有多少次 commit,只给出数字和一句简短说明。", {
|
|
1620
|
+
mode: "steer",
|
|
1621
|
+
timeoutMs: 90_000,
|
|
1622
|
+
pollIntervalMs: 500,
|
|
1623
|
+
});
|
|
1624
|
+
return {
|
|
1625
|
+
text: [
|
|
1626
|
+
`${first.displayName || first.sessionId} (${path.basename(first.cwd)}):`,
|
|
1627
|
+
readme.message || readme.summary || "(没有拿到 README 结果)",
|
|
1628
|
+
"",
|
|
1629
|
+
`${second.displayName || second.sessionId} (${path.basename(second.cwd)}):`,
|
|
1630
|
+
commits.message || commits.summary || "(没有拿到 commit 统计结果)",
|
|
1631
|
+
].join("\n"),
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
async handleNamedMultiWorkerCommitIntent(names) {
|
|
1635
|
+
const parts = [];
|
|
1636
|
+
for (const name of names) {
|
|
1637
|
+
const worker = this.findWorkerByName(name);
|
|
1638
|
+
if (!worker) {
|
|
1639
|
+
parts.push(`${name}: 没找到对应 worker。`);
|
|
1640
|
+
continue;
|
|
1641
|
+
}
|
|
1642
|
+
const result = await this.runSessionTurn(worker.sessionId, "请统计当前 git 仓库一共有多少次 commit,只给出数字和一句简短说明。", {
|
|
1643
|
+
mode: "steer",
|
|
1644
|
+
timeoutMs: 90_000,
|
|
1645
|
+
pollIntervalMs: 500,
|
|
1646
|
+
});
|
|
1647
|
+
parts.push(`${name} (${path.basename(worker.cwd)}):\n${result.message || result.summary || "(没有拿到结果)"}`);
|
|
1648
|
+
}
|
|
1649
|
+
return { text: parts.join("\n\n") };
|
|
1650
|
+
}
|
|
1651
|
+
handleWorkerRosterIntent() {
|
|
1652
|
+
const workers = this.workerRegistry.listWorkers().sort((a, b) => a.createdAt - b.createdAt);
|
|
1653
|
+
if (!workers.length) {
|
|
1654
|
+
return { text: "当前没有 workers。" };
|
|
1655
|
+
}
|
|
1656
|
+
return {
|
|
1657
|
+
text: workers.map((worker) => {
|
|
1658
|
+
const status = worker.status === "stopped" || worker.status === "failed" ? "已停止" : "运行中";
|
|
1659
|
+
return `${worker.displayName || worker.sessionId}: ${worker.cwd} · ${status}`;
|
|
1660
|
+
}).join("\n"),
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
async ensureSessionForInteractiveMessage(sessionId, reason) {
|
|
1664
|
+
if (this.sessionManager.getSession(sessionId)) {
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
if (this.workerRegistry.getWorkerBySessionId(sessionId)) {
|
|
1668
|
+
console.warn(`[AgentNode] Ignoring ${reason} for inactive worker session: ${sessionId}`);
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
if (!this.plugins.has("pie")) {
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
try {
|
|
1675
|
+
await this.createSession(sessionId, "pie");
|
|
1676
|
+
console.log(`[AgentNode] Created pie session for ${reason}: ${sessionId}`);
|
|
1677
|
+
}
|
|
1678
|
+
catch (err) {
|
|
1679
|
+
console.error(`[AgentNode] Failed to create session for ${reason}:`, err);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
handleSessionOutput(sessionId, output) {
|
|
1683
|
+
if (sessionId === this.primaryAgent.sessionId) {
|
|
1684
|
+
this.recordPrimaryOutput(output.content);
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
const existingWorker = this.workerRegistry.getWorkerBySessionId(sessionId);
|
|
1688
|
+
const previousWorker = existingWorker ? { ...existingWorker } : undefined;
|
|
1689
|
+
const worker = existingWorker
|
|
1690
|
+
? this.workerRegistry.recordOutputActivity(existingWorker.agentId, output.content.length)
|
|
1691
|
+
: undefined;
|
|
1692
|
+
let sawTerminalMetadata = false;
|
|
1693
|
+
if (worker) {
|
|
1694
|
+
if (output.content.trim()) {
|
|
1695
|
+
this.workerRegistry.recordEvent(worker.agentId, {
|
|
1696
|
+
type: "output",
|
|
1697
|
+
timestamp: Date.now(),
|
|
1698
|
+
message: `output ${output.content.length} chars`,
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
console.log(`[AgentNode] Worker ${worker.agentId} output at ${worker.lastOutputAt}`);
|
|
1702
|
+
const terminalMetadata = extractTerminalMetadata(output.content, worker.agentType);
|
|
1703
|
+
if (terminalMetadata) {
|
|
1704
|
+
sawTerminalMetadata = true;
|
|
1705
|
+
const updates = {};
|
|
1706
|
+
if (terminalMetadata.title && !worker.taskSummary) {
|
|
1707
|
+
updates.taskSummary = terminalMetadata.title;
|
|
1708
|
+
}
|
|
1709
|
+
if (terminalMetadata.activityState && terminalMetadata.activityState !== worker.activityState) {
|
|
1710
|
+
updates.activityState = terminalMetadata.activityState;
|
|
1711
|
+
updates.activityUpdatedAt = Date.now();
|
|
1712
|
+
}
|
|
1713
|
+
if (terminalMetadata.agentSessionId && terminalMetadata.agentSessionId !== worker.agentSessionId) {
|
|
1714
|
+
updates.agentSessionId = terminalMetadata.agentSessionId;
|
|
1715
|
+
}
|
|
1716
|
+
if (Object.keys(updates).length > 0) {
|
|
1717
|
+
const updatedWorker = this.workerRegistry.updateWorker(worker.agentId, updates);
|
|
1718
|
+
if (updatedWorker) {
|
|
1719
|
+
if (updatedWorker.taskSummary
|
|
1720
|
+
&& (updatedWorker.sessionId === this.nodeConversation.lastWorkerSessionId
|
|
1721
|
+
|| updatedWorker.agentId === this.nodeConversation.lastWorkerAgentId)) {
|
|
1722
|
+
this.nodeConversation.lastTaskSummary = updatedWorker.taskSummary;
|
|
1723
|
+
}
|
|
1724
|
+
this.maybeNarrateWorkerStateChange(previousWorker || worker, updatedWorker);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
this.maybeNarrateWorkerStateChange(previousWorker || worker, this.workerRegistry.getWorkerByAgentId(worker.agentId) || worker);
|
|
1729
|
+
if (sawTerminalMetadata) {
|
|
1730
|
+
this.broadcastWorkerList();
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
const remoteAttached = this.remoteAttachedSessions.has(sessionId);
|
|
1734
|
+
const localAttached = this.localAttachedSessions.has(sessionId);
|
|
1735
|
+
if (!remoteAttached && !localAttached) {
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
console.log(`[AgentNode] Forwarding live output: ${sessionId}, ${output.content.length} chars`);
|
|
1739
|
+
if (remoteAttached) {
|
|
1740
|
+
this.wsClient.send({
|
|
1741
|
+
type: "output",
|
|
1742
|
+
sessionId,
|
|
1743
|
+
data: output.content,
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
if (localAttached) {
|
|
1747
|
+
this.localDirectServer?.handleNodeMessage({
|
|
1748
|
+
type: "output",
|
|
1749
|
+
sessionId,
|
|
1750
|
+
data: output.content,
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
sendServerMessage(message) {
|
|
1755
|
+
this.wsClient.send(message);
|
|
1756
|
+
this.localDirectServer?.handleNodeMessage(message);
|
|
1757
|
+
}
|
|
1758
|
+
sendToSource(source, message) {
|
|
1759
|
+
if (source === "relay") {
|
|
1760
|
+
this.wsClient.send(message);
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
this.localDirectServer?.handleNodeMessage(message);
|
|
1764
|
+
}
|
|
1765
|
+
async startLocalDirectServer() {
|
|
1766
|
+
this.localDirectServer = new LocalDirectServer({
|
|
1767
|
+
host: this.options.localControlHost || "127.0.0.1",
|
|
1768
|
+
port: this.options.localControlPort || 0,
|
|
1769
|
+
nodeId: this.options.nodeId,
|
|
1770
|
+
password: this.options.password,
|
|
1771
|
+
}, {
|
|
1772
|
+
handleIncomingMessage: async (message) => {
|
|
1773
|
+
await this.handleIncomingMessage(message, "local");
|
|
1774
|
+
},
|
|
1775
|
+
handleSubscriptionChange: async (sessionId, attachedBrowserCount) => {
|
|
1776
|
+
this.updateSessionSubscription("local", sessionId, attachedBrowserCount);
|
|
1777
|
+
},
|
|
1778
|
+
});
|
|
1779
|
+
this.localDirectInfo = await this.localDirectServer.start();
|
|
1780
|
+
console.log(`[AgentNode] Local direct web: ${this.localDirectInfo.webUrl}?relay=${encodeURIComponent(this.localDirectInfo.wsUrl)}&node=${encodeURIComponent(this.options.nodeId)}&pwd=${encodeURIComponent(this.options.password)}&autoconnect=1`);
|
|
1781
|
+
console.log(`[AgentNode] Local direct ws: ${this.localDirectInfo.wsUrl}`);
|
|
1782
|
+
}
|
|
1783
|
+
updateSessionSubscription(source, sessionId, attachedBrowserCount) {
|
|
1784
|
+
const targetSet = source === "relay" ? this.remoteAttachedSessions : this.localAttachedSessions;
|
|
1785
|
+
if (attachedBrowserCount > 0) {
|
|
1786
|
+
targetSet.add(sessionId);
|
|
1787
|
+
this.liveAttachedSessions.add(sessionId);
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
targetSet.delete(sessionId);
|
|
1791
|
+
if (!this.remoteAttachedSessions.has(sessionId) && !this.localAttachedSessions.has(sessionId)) {
|
|
1792
|
+
this.liveAttachedSessions.delete(sessionId);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
async onRelayConnected() {
|
|
1796
|
+
const isReconnect = this.relayConnectedOnce;
|
|
1797
|
+
this.relayConnectedOnce = true;
|
|
1798
|
+
for (const sessionId of this.remoteAttachedSessions) {
|
|
1799
|
+
if (!this.localAttachedSessions.has(sessionId)) {
|
|
1800
|
+
this.liveAttachedSessions.delete(sessionId);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
this.remoteAttachedSessions.clear();
|
|
1804
|
+
if (!this.running && !isReconnect) {
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
console.log(isReconnect ? "[AgentNode] Relay reconnected, resyncing runtime snapshot" : "[AgentNode] Relay connected");
|
|
1808
|
+
this.broadcastWorkerList();
|
|
1809
|
+
}
|
|
1810
|
+
maybeNarrateWorkerStateChange(previous, next) {
|
|
1811
|
+
void previous;
|
|
1812
|
+
void next;
|
|
1813
|
+
// Chat replies should come only from the primary agent. Runtime/workbench updates
|
|
1814
|
+
// continue to flow through worker lists and control-plane events instead.
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
recordConversationTurn(role, text, extra = {}) {
|
|
1818
|
+
this.nodeConversation = recordConversationTurn(this.nodeConversation, role, text, extra);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
// CLI 入口
|
|
1822
|
+
function parseArgs() {
|
|
1823
|
+
const args = process.argv.slice(2);
|
|
1824
|
+
const options = {};
|
|
1825
|
+
for (let i = 0; i < args.length; i++) {
|
|
1826
|
+
switch (args[i]) {
|
|
1827
|
+
case "--relay":
|
|
1828
|
+
case "-r":
|
|
1829
|
+
options.relayUrl = args[++i];
|
|
1830
|
+
break;
|
|
1831
|
+
case "--node-id":
|
|
1832
|
+
case "-n":
|
|
1833
|
+
options.nodeId = args[++i];
|
|
1834
|
+
break;
|
|
1835
|
+
case "--password":
|
|
1836
|
+
case "-p":
|
|
1837
|
+
options.password = args[++i];
|
|
1838
|
+
break;
|
|
1839
|
+
case "--plugins":
|
|
1840
|
+
options.pluginDir = args[++i];
|
|
1841
|
+
break;
|
|
1842
|
+
case "--primary-plugin":
|
|
1843
|
+
options.primaryPluginName = args[++i];
|
|
1844
|
+
break;
|
|
1845
|
+
case "--local-control-port":
|
|
1846
|
+
options.localControlPort = Number(args[++i]);
|
|
1847
|
+
break;
|
|
1848
|
+
case "--local-bind":
|
|
1849
|
+
options.localControlHost = args[++i];
|
|
1850
|
+
break;
|
|
1851
|
+
case "--disable-local-direct":
|
|
1852
|
+
options.enableLocalDirect = false;
|
|
1853
|
+
break;
|
|
1854
|
+
case "--no-reconnect":
|
|
1855
|
+
options.autoReconnect = false;
|
|
1856
|
+
break;
|
|
1857
|
+
case "--help":
|
|
1858
|
+
case "-h":
|
|
1859
|
+
showHelp();
|
|
1860
|
+
process.exit(0);
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
// 环境变量覆盖
|
|
1864
|
+
if (process.env.MAGIC_SHELL_RELAY)
|
|
1865
|
+
options.relayUrl = process.env.MAGIC_SHELL_RELAY;
|
|
1866
|
+
if (process.env.MAGIC_SHELL_NODE_ID)
|
|
1867
|
+
options.nodeId = process.env.MAGIC_SHELL_NODE_ID;
|
|
1868
|
+
if (process.env.MAGIC_SHELL_PASSWORD)
|
|
1869
|
+
options.password = process.env.MAGIC_SHELL_PASSWORD;
|
|
1870
|
+
if (process.env.MAGIC_SHELL_PLUGINS_DIR)
|
|
1871
|
+
options.pluginDir = process.env.MAGIC_SHELL_PLUGINS_DIR;
|
|
1872
|
+
if (process.env.MAGIC_SHELL_PRIMARY_PLUGIN)
|
|
1873
|
+
options.primaryPluginName = process.env.MAGIC_SHELL_PRIMARY_PLUGIN;
|
|
1874
|
+
if (process.env.MAGIC_SHELL_LOCAL_CONTROL_HOST)
|
|
1875
|
+
options.localControlHost = process.env.MAGIC_SHELL_LOCAL_CONTROL_HOST;
|
|
1876
|
+
if (process.env.MAGIC_SHELL_LOCAL_CONTROL_PORT)
|
|
1877
|
+
options.localControlPort = Number(process.env.MAGIC_SHELL_LOCAL_CONTROL_PORT);
|
|
1878
|
+
if (process.env.MAGIC_SHELL_DISABLE_LOCAL_DIRECT === "1")
|
|
1879
|
+
options.enableLocalDirect = false;
|
|
1880
|
+
// 默认值
|
|
1881
|
+
if (!options.relayUrl)
|
|
1882
|
+
options.relayUrl = "ws://localhost:8080";
|
|
1883
|
+
if (!options.nodeId)
|
|
1884
|
+
options.nodeId = generateId(8);
|
|
1885
|
+
if (!options.password)
|
|
1886
|
+
options.password = generateId(12);
|
|
1887
|
+
if (!options.pluginDir)
|
|
1888
|
+
options.pluginDir = getDefaultPluginDir();
|
|
1889
|
+
if (!options.primaryPluginName)
|
|
1890
|
+
options.primaryPluginName = "pie";
|
|
1891
|
+
if (!options.localControlHost)
|
|
1892
|
+
options.localControlHost = "127.0.0.1";
|
|
1893
|
+
return options;
|
|
1894
|
+
}
|
|
1895
|
+
function generateId(length = 8) {
|
|
1896
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
1897
|
+
let result = "";
|
|
1898
|
+
for (let i = 0; i < length; i++) {
|
|
1899
|
+
result += chars[Math.floor(Math.random() * chars.length)];
|
|
1900
|
+
}
|
|
1901
|
+
return result;
|
|
1902
|
+
}
|
|
1903
|
+
function showHelp() {
|
|
1904
|
+
console.log(`
|
|
1905
|
+
AgentNode - Magic Shell 本地 Agent 连接器
|
|
1906
|
+
|
|
1907
|
+
Usage: node node.js [options]
|
|
1908
|
+
|
|
1909
|
+
Options:
|
|
1910
|
+
-r, --relay <url> Relay Server URL (default: ws://localhost:8080)
|
|
1911
|
+
-n, --node-id <id> Node ID (auto-generated if not provided)
|
|
1912
|
+
-p, --password <pwd> Connection password (auto-generated if not provided)
|
|
1913
|
+
--plugins <dir> Plugin directory (default: ./plugins)
|
|
1914
|
+
--primary-plugin <name> Primary agent plugin (default: pie)
|
|
1915
|
+
--local-control-port Local direct workbench port (default: random)
|
|
1916
|
+
--local-bind <host> Local direct bind host (default: 127.0.0.1)
|
|
1917
|
+
--disable-local-direct Disable the local direct workbench server
|
|
1918
|
+
--no-reconnect Disable auto-reconnect
|
|
1919
|
+
-h, --help Show this help
|
|
1920
|
+
|
|
1921
|
+
Environment Variables:
|
|
1922
|
+
MAGIC_SHELL_RELAY Relay URL
|
|
1923
|
+
MAGIC_SHELL_NODE_ID Node ID
|
|
1924
|
+
MAGIC_SHELL_PASSWORD Password
|
|
1925
|
+
MAGIC_SHELL_PLUGINS_DIR Plugin directory
|
|
1926
|
+
MAGIC_SHELL_PRIMARY_PLUGIN Primary agent plugin
|
|
1927
|
+
MAGIC_SHELL_LOCAL_CONTROL_HOST Local direct bind host
|
|
1928
|
+
MAGIC_SHELL_LOCAL_CONTROL_PORT Local direct port
|
|
1929
|
+
MAGIC_SHELL_DISABLE_LOCAL_DIRECT Set to 1 to disable the local direct server
|
|
1930
|
+
`);
|
|
1931
|
+
}
|
|
1932
|
+
// 主函数
|
|
1933
|
+
async function main() {
|
|
1934
|
+
const options = parseArgs();
|
|
1935
|
+
const node = new AgentNode(options);
|
|
1936
|
+
// 优雅退出
|
|
1937
|
+
process.on("SIGINT", async () => {
|
|
1938
|
+
await node.stop();
|
|
1939
|
+
process.exit(0);
|
|
1940
|
+
});
|
|
1941
|
+
process.on("SIGTERM", async () => {
|
|
1942
|
+
await node.stop();
|
|
1943
|
+
process.exit(0);
|
|
1944
|
+
});
|
|
1945
|
+
await node.start();
|
|
1946
|
+
}
|
|
1947
|
+
// 如果直接运行此文件
|
|
1948
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
1949
|
+
main().catch((err) => {
|
|
1950
|
+
console.error("[AgentNode] Fatal error:", err);
|
|
1951
|
+
process.exit(1);
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
export { main };
|