openfox 2.0.12 → 2.0.14
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/{chat-handler-6NUNVXIU.js → chat-handler-ITXCBFJK.js} +4 -4
- package/dist/{chunk-A52FXWJX.js → chunk-3SGGOBCL.js} +10 -387
- package/dist/{chunk-WF3MA65W.js → chunk-4U45A7B3.js} +386 -2072
- package/dist/chunk-4W2Z4B5U.js +2077 -0
- package/dist/{chunk-2VDLXCLO.js → chunk-4ZTQO5JP.js} +31 -25
- package/dist/{chunk-EZJUU54W.js → chunk-7GJ6XYAF.js} +10 -8
- package/dist/{chunk-HYYFVYDV.js → chunk-LNEBVOH6.js} +6 -6
- package/dist/chunk-NWO6GRYE.js +39 -0
- package/dist/{chunk-HPCGVAS4.js → chunk-PP6VQXQW.js} +17 -1
- package/dist/chunk-PQ56PX7L.js +397 -0
- package/dist/cli/dev.js +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/{config-BU66P4KX.js → config-YFHA5MWV.js} +2 -2
- package/dist/{orchestrator-WXEZHUVQ.js → orchestrator-VP74KOXI.js} +4 -4
- package/dist/package.json +3 -2
- package/dist/{processor-GA5NG3T7.js → processor-CCXZYLX6.js} +4 -4
- package/dist/{protocol-CaLuIetw.d.ts → protocol-BE5dRtYw.d.ts} +9 -0
- package/dist/{provider-QPECLUZ7.js → provider-QUJQLVOM.js} +7 -5
- package/dist/{provider-manager-YA2WALTF.js → provider-manager-DETXLDKW.js} +3 -2
- package/dist/{serve-5BSOEK54.js → serve-GA2UIHHB.js} +11 -7
- package/dist/server/index.d.ts +3 -2
- package/dist/server/index.js +8 -5
- package/dist/server-EA6W7YIC.js +29 -0
- package/dist/shared/index.d.ts +2 -2
- package/dist/tool-adapter-B7QP6NLA.js +7 -0
- package/dist/{tools-THXBQJ7A.js → tools-DUC54GEH.js} +5 -3
- package/dist/web/assets/{index-CQw-9GC9.js → index-D0qVcBva.js} +76 -75
- package/dist/web/assets/{index-Db3yUKBI.css → index-Ho_tVUbw.css} +1 -1
- package/dist/web/index.html +2 -2
- package/dist/web/sw.js +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,2077 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendCompactionPrompt
|
|
3
|
+
} from "./chunk-FWJ6YXGA.js";
|
|
4
|
+
import {
|
|
5
|
+
computeDynamicContextHash,
|
|
6
|
+
injectWorkflowKickoffIfNeeded,
|
|
7
|
+
runAgentTurn,
|
|
8
|
+
runChatTurn
|
|
9
|
+
} from "./chunk-7GJ6XYAF.js";
|
|
10
|
+
import {
|
|
11
|
+
checkAborted,
|
|
12
|
+
deleteItemFromDir,
|
|
13
|
+
devServerManager,
|
|
14
|
+
executeSubAgent,
|
|
15
|
+
findAgentById,
|
|
16
|
+
getAllInstructions,
|
|
17
|
+
getEnabledSkillMetadata,
|
|
18
|
+
getSubAgents,
|
|
19
|
+
getToolRegistryForAgent,
|
|
20
|
+
jsonSerializer,
|
|
21
|
+
loadAllAgentsDefault,
|
|
22
|
+
saveItemToDir,
|
|
23
|
+
spawnShellProcess
|
|
24
|
+
} from "./chunk-4ZTQO5JP.js";
|
|
25
|
+
import {
|
|
26
|
+
TurnMetrics,
|
|
27
|
+
buildTopLevelSystemPrompt,
|
|
28
|
+
createMessageStartEvent
|
|
29
|
+
} from "./chunk-L7TDUIQY.js";
|
|
30
|
+
import {
|
|
31
|
+
getPlatformShell,
|
|
32
|
+
onProcessEvent
|
|
33
|
+
} from "./chunk-PBGOZMVY.js";
|
|
34
|
+
import {
|
|
35
|
+
getCurrentContextWindowId,
|
|
36
|
+
getEventStore,
|
|
37
|
+
getRuntimeConfig
|
|
38
|
+
} from "./chunk-SYG2ENUQ.js";
|
|
39
|
+
import {
|
|
40
|
+
createChatErrorMessage,
|
|
41
|
+
createChatMessageMessage,
|
|
42
|
+
createContextStateMessage,
|
|
43
|
+
createErrorMessage,
|
|
44
|
+
createGitStatusMessage,
|
|
45
|
+
createQueueStateMessage,
|
|
46
|
+
createSessionRunningMessage,
|
|
47
|
+
isAskAnswerPayload,
|
|
48
|
+
isSessionLoadPayload,
|
|
49
|
+
parseClientMessage,
|
|
50
|
+
serializeServerMessage,
|
|
51
|
+
storedEventToServerMessage
|
|
52
|
+
} from "./chunk-3QB2RMX2.js";
|
|
53
|
+
import {
|
|
54
|
+
provideAnswer
|
|
55
|
+
} from "./chunk-EU3WWTFH.js";
|
|
56
|
+
import {
|
|
57
|
+
computeSessionStats
|
|
58
|
+
} from "./chunk-VUQCQXXJ.js";
|
|
59
|
+
import {
|
|
60
|
+
createServerMessage
|
|
61
|
+
} from "./chunk-5BDVM6YI.js";
|
|
62
|
+
import {
|
|
63
|
+
getGlobalConfigDir
|
|
64
|
+
} from "./chunk-CQGTEGKL.js";
|
|
65
|
+
import {
|
|
66
|
+
createLLMClient,
|
|
67
|
+
ensureVersionPrefix
|
|
68
|
+
} from "./chunk-PQ56PX7L.js";
|
|
69
|
+
import {
|
|
70
|
+
logger
|
|
71
|
+
} from "./chunk-K44MW7JJ.js";
|
|
72
|
+
|
|
73
|
+
// src/server/ws/server.ts
|
|
74
|
+
import { WebSocketServer, WebSocket as WebSocket2 } from "ws";
|
|
75
|
+
import { spawn as spawn3 } from "child_process";
|
|
76
|
+
import { createHash as createHash2 } from "crypto";
|
|
77
|
+
|
|
78
|
+
// src/server/ws/terminal.ts
|
|
79
|
+
import WebSocket from "ws";
|
|
80
|
+
|
|
81
|
+
// src/server/terminal/manager.ts
|
|
82
|
+
import * as pty from "node-pty";
|
|
83
|
+
import os from "os";
|
|
84
|
+
var TerminalManager = class {
|
|
85
|
+
sessions = /* @__PURE__ */ new Map();
|
|
86
|
+
outputCallbacks = /* @__PURE__ */ new Set();
|
|
87
|
+
exitCallbacks = /* @__PURE__ */ new Set();
|
|
88
|
+
outputHistory = /* @__PURE__ */ new Map();
|
|
89
|
+
generateId() {
|
|
90
|
+
return `term_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
91
|
+
}
|
|
92
|
+
getShell() {
|
|
93
|
+
return getPlatformShell().command;
|
|
94
|
+
}
|
|
95
|
+
resolveWorkdir(workdir) {
|
|
96
|
+
if (workdir && workdir.length > 0) {
|
|
97
|
+
return workdir;
|
|
98
|
+
}
|
|
99
|
+
return os.homedir();
|
|
100
|
+
}
|
|
101
|
+
create(workdir, projectId) {
|
|
102
|
+
const id = this.generateId();
|
|
103
|
+
const shell = this.getShell();
|
|
104
|
+
const cwd = this.resolveWorkdir(workdir);
|
|
105
|
+
const ptyProcess = pty.spawn(shell, [], {
|
|
106
|
+
name: "xterm-256color",
|
|
107
|
+
cols: 80,
|
|
108
|
+
rows: 24,
|
|
109
|
+
cwd,
|
|
110
|
+
env: process.env
|
|
111
|
+
});
|
|
112
|
+
const session = {
|
|
113
|
+
id,
|
|
114
|
+
pty: ptyProcess,
|
|
115
|
+
workdir: cwd,
|
|
116
|
+
projectId: projectId ?? ""
|
|
117
|
+
};
|
|
118
|
+
ptyProcess.onData((data) => {
|
|
119
|
+
const output = { sessionId: id, data };
|
|
120
|
+
for (const cb of this.outputCallbacks) {
|
|
121
|
+
cb(output);
|
|
122
|
+
}
|
|
123
|
+
const history = this.outputHistory.get(id) || [];
|
|
124
|
+
history.push(data);
|
|
125
|
+
if (history.length > 1e4) history.shift();
|
|
126
|
+
this.outputHistory.set(id, history);
|
|
127
|
+
});
|
|
128
|
+
ptyProcess.onExit(({ exitCode }) => {
|
|
129
|
+
logger.info("Terminal session exited", { id, exitCode });
|
|
130
|
+
this.sessions.delete(id);
|
|
131
|
+
this.outputHistory.delete(id);
|
|
132
|
+
for (const cb of this.exitCallbacks) {
|
|
133
|
+
cb(id, exitCode);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
this.sessions.set(id, session);
|
|
137
|
+
logger.info("Terminal session created", { id, shell, cwd });
|
|
138
|
+
return session;
|
|
139
|
+
}
|
|
140
|
+
write(sessionId, data) {
|
|
141
|
+
const session = this.sessions.get(sessionId);
|
|
142
|
+
if (!session) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
session.pty.write(data);
|
|
147
|
+
return true;
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
resize(sessionId, cols, rows) {
|
|
153
|
+
const session = this.sessions.get(sessionId);
|
|
154
|
+
if (!session) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
session.pty.resize(cols, rows);
|
|
159
|
+
return true;
|
|
160
|
+
} catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
kill(sessionId) {
|
|
165
|
+
const session = this.sessions.get(sessionId);
|
|
166
|
+
if (!session) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
session.pty.kill();
|
|
170
|
+
this.sessions.delete(sessionId);
|
|
171
|
+
logger.info("Terminal session killed", { id: sessionId });
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
get(sessionId) {
|
|
175
|
+
return this.sessions.get(sessionId);
|
|
176
|
+
}
|
|
177
|
+
getAll() {
|
|
178
|
+
return Array.from(this.sessions.values());
|
|
179
|
+
}
|
|
180
|
+
getByProject(projectId) {
|
|
181
|
+
if (!projectId) return [];
|
|
182
|
+
return Array.from(this.sessions.values()).filter((s) => s.projectId === projectId);
|
|
183
|
+
}
|
|
184
|
+
getOutputHistory(sessionId) {
|
|
185
|
+
return (this.outputHistory.get(sessionId) || []).join("");
|
|
186
|
+
}
|
|
187
|
+
onOutput(callback) {
|
|
188
|
+
this.outputCallbacks.add(callback);
|
|
189
|
+
return () => {
|
|
190
|
+
this.outputCallbacks.delete(callback);
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
onExit(callback) {
|
|
194
|
+
this.exitCallbacks.add(callback);
|
|
195
|
+
return () => {
|
|
196
|
+
this.exitCallbacks.delete(callback);
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
killAll() {
|
|
200
|
+
for (const session of this.sessions.values()) {
|
|
201
|
+
session.pty.kill();
|
|
202
|
+
}
|
|
203
|
+
this.sessions.clear();
|
|
204
|
+
logger.info("All terminal sessions killed");
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
var terminalManager = new TerminalManager();
|
|
208
|
+
|
|
209
|
+
// src/server/ws/terminal.ts
|
|
210
|
+
var subscriptions = /* @__PURE__ */ new Set();
|
|
211
|
+
var outputHandlerRegistered = false;
|
|
212
|
+
var exitHandlerRegistered = false;
|
|
213
|
+
function registerOutputHandler() {
|
|
214
|
+
if (outputHandlerRegistered) return;
|
|
215
|
+
outputHandlerRegistered = true;
|
|
216
|
+
terminalManager.onOutput((output) => {
|
|
217
|
+
const subs = Array.from(subscriptions).filter(
|
|
218
|
+
(s) => s.sessionId === output.sessionId && s.ws.readyState === WebSocket.OPEN
|
|
219
|
+
);
|
|
220
|
+
for (const sub of subs) {
|
|
221
|
+
sub.ws.send(
|
|
222
|
+
JSON.stringify({
|
|
223
|
+
type: "terminal.output",
|
|
224
|
+
payload: { sessionId: output.sessionId, data: output.data }
|
|
225
|
+
})
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
function registerExitHandler() {
|
|
231
|
+
if (exitHandlerRegistered) return;
|
|
232
|
+
exitHandlerRegistered = true;
|
|
233
|
+
terminalManager.onExit((sessionId, exitCode) => {
|
|
234
|
+
const subs = Array.from(subscriptions).filter(
|
|
235
|
+
(s) => s.sessionId === sessionId && s.ws.readyState === WebSocket.OPEN
|
|
236
|
+
);
|
|
237
|
+
for (const sub of subs) {
|
|
238
|
+
sub.ws.send(
|
|
239
|
+
JSON.stringify({
|
|
240
|
+
type: "terminal.exit",
|
|
241
|
+
payload: { sessionId, exitCode }
|
|
242
|
+
})
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
for (const sub of subscriptions) {
|
|
246
|
+
if (sub.sessionId === sessionId) {
|
|
247
|
+
subscriptions.delete(sub);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
function handleTerminalMessage(ws, message) {
|
|
253
|
+
switch (message.type) {
|
|
254
|
+
case "terminal.subscribe": {
|
|
255
|
+
if (message.payload.sessionId) {
|
|
256
|
+
const exists = Array.from(subscriptions).some(
|
|
257
|
+
(sub) => sub.ws === ws && sub.sessionId === message.payload.sessionId
|
|
258
|
+
);
|
|
259
|
+
if (!exists) {
|
|
260
|
+
subscriptions.add({ ws, sessionId: message.payload.sessionId });
|
|
261
|
+
}
|
|
262
|
+
registerOutputHandler();
|
|
263
|
+
registerExitHandler();
|
|
264
|
+
const sessionId = message.payload.sessionId;
|
|
265
|
+
const history = terminalManager.getOutputHistory(sessionId);
|
|
266
|
+
if (history) {
|
|
267
|
+
setTimeout(() => {
|
|
268
|
+
ws.send(
|
|
269
|
+
JSON.stringify({
|
|
270
|
+
type: "terminal.output",
|
|
271
|
+
payload: { sessionId, data: history }
|
|
272
|
+
})
|
|
273
|
+
);
|
|
274
|
+
}, 100);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
case "terminal.write": {
|
|
280
|
+
if (message.payload.sessionId && message.payload.data) {
|
|
281
|
+
terminalManager.write(message.payload.sessionId, message.payload.data);
|
|
282
|
+
}
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
case "terminal.resize": {
|
|
286
|
+
if (message.payload.sessionId && typeof message.payload.cols === "number" && typeof message.payload.rows === "number") {
|
|
287
|
+
terminalManager.resize(message.payload.sessionId, message.payload.cols, message.payload.rows);
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
case "terminal.kill": {
|
|
292
|
+
if (message.payload.sessionId) {
|
|
293
|
+
terminalManager.kill(message.payload.sessionId);
|
|
294
|
+
for (const sub of subscriptions) {
|
|
295
|
+
if (sub.sessionId === message.payload.sessionId) {
|
|
296
|
+
subscriptions.delete(sub);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
ws.send(
|
|
300
|
+
JSON.stringify({
|
|
301
|
+
type: "terminal.killed",
|
|
302
|
+
payload: { sessionId: message.payload.sessionId }
|
|
303
|
+
})
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function unsubscribeAllFromTerminal(ws) {
|
|
311
|
+
for (const sub of subscriptions) {
|
|
312
|
+
if (sub.ws === ws) {
|
|
313
|
+
subscriptions.delete(sub);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/server/workflows/registry.ts
|
|
319
|
+
import { readdir, readFile, writeFile, mkdir, access, unlink } from "fs/promises";
|
|
320
|
+
import { join, dirname } from "path";
|
|
321
|
+
import { constants } from "fs";
|
|
322
|
+
import { fileURLToPath } from "url";
|
|
323
|
+
var __bundleDir = dirname(fileURLToPath(import.meta.url));
|
|
324
|
+
var DEFAULTS_DIR = join(__bundleDir, "defaults");
|
|
325
|
+
var DEFAULTS_DIR_ALT = join(__bundleDir, "workflow-defaults");
|
|
326
|
+
var WORKFLOW_EXTENSION = ".workflow.json";
|
|
327
|
+
function getWorkflowsDir(configDir) {
|
|
328
|
+
return join(configDir, "workflows");
|
|
329
|
+
}
|
|
330
|
+
function getProjectWorkflowsDir(projectDir) {
|
|
331
|
+
return join(projectDir, ".openfox", "workflows");
|
|
332
|
+
}
|
|
333
|
+
async function pathExists(path) {
|
|
334
|
+
try {
|
|
335
|
+
await access(path, constants.R_OK);
|
|
336
|
+
return true;
|
|
337
|
+
} catch {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
async function loadWorkflowsFromDir(dir) {
|
|
342
|
+
if (!await pathExists(dir)) {
|
|
343
|
+
return [];
|
|
344
|
+
}
|
|
345
|
+
let files;
|
|
346
|
+
try {
|
|
347
|
+
files = (await readdir(dir)).filter((f) => f.endsWith(WORKFLOW_EXTENSION));
|
|
348
|
+
} catch {
|
|
349
|
+
return [];
|
|
350
|
+
}
|
|
351
|
+
const workflows = [];
|
|
352
|
+
for (const file of files) {
|
|
353
|
+
try {
|
|
354
|
+
const raw = await readFile(join(dir, file), "utf-8");
|
|
355
|
+
const parsed = JSON.parse(raw);
|
|
356
|
+
if (parsed.metadata?.id && parsed.steps?.length > 0) {
|
|
357
|
+
workflows.push(parsed);
|
|
358
|
+
} else {
|
|
359
|
+
logger.warn("Skipping invalid workflow file", { file });
|
|
360
|
+
}
|
|
361
|
+
} catch (err) {
|
|
362
|
+
logger.warn("Failed to parse workflow file", { file, error: err instanceof Error ? err.message : String(err) });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return workflows;
|
|
366
|
+
}
|
|
367
|
+
async function loadDefaultWorkflows() {
|
|
368
|
+
let defaults = await loadWorkflowsFromDir(DEFAULTS_DIR);
|
|
369
|
+
if (!defaults.length) {
|
|
370
|
+
defaults = await loadWorkflowsFromDir(DEFAULTS_DIR_ALT);
|
|
371
|
+
}
|
|
372
|
+
return defaults;
|
|
373
|
+
}
|
|
374
|
+
async function loadUserWorkflows(configDir) {
|
|
375
|
+
return loadWorkflowsFromDir(getWorkflowsDir(configDir));
|
|
376
|
+
}
|
|
377
|
+
async function loadProjectWorkflows(projectDir) {
|
|
378
|
+
return loadWorkflowsFromDir(getProjectWorkflowsDir(projectDir));
|
|
379
|
+
}
|
|
380
|
+
async function loadAllWorkflows(configDir, projectDir) {
|
|
381
|
+
const [defaultWorkflows, userWorkflows] = await Promise.all([loadDefaultWorkflows(), loadUserWorkflows(configDir)]);
|
|
382
|
+
const workflowMap = /* @__PURE__ */ new Map();
|
|
383
|
+
for (const workflow of defaultWorkflows) {
|
|
384
|
+
workflowMap.set(workflow.metadata.id, workflow);
|
|
385
|
+
}
|
|
386
|
+
for (const workflow of userWorkflows) {
|
|
387
|
+
workflowMap.set(workflow.metadata.id, workflow);
|
|
388
|
+
}
|
|
389
|
+
if (projectDir) {
|
|
390
|
+
const projectWorkflows = await loadProjectWorkflows(projectDir);
|
|
391
|
+
for (const workflow of projectWorkflows) {
|
|
392
|
+
workflowMap.set(workflow.metadata.id, workflow);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return Array.from(workflowMap.values());
|
|
396
|
+
}
|
|
397
|
+
async function saveWorkflowToProject(projectDir, workflow) {
|
|
398
|
+
await saveItemToDir(getProjectWorkflowsDir(projectDir), workflow, WORKFLOW_EXTENSION, jsonSerializer);
|
|
399
|
+
}
|
|
400
|
+
async function deleteProjectWorkflow(projectDir, workflowId) {
|
|
401
|
+
return deleteItemFromDir(getProjectWorkflowsDir(projectDir), workflowId, WORKFLOW_EXTENSION);
|
|
402
|
+
}
|
|
403
|
+
async function getDefaultWorkflowIds() {
|
|
404
|
+
for (const dir of [DEFAULTS_DIR, DEFAULTS_DIR_ALT]) {
|
|
405
|
+
try {
|
|
406
|
+
const files = (await readdir(dir)).filter((f) => f.endsWith(WORKFLOW_EXTENSION));
|
|
407
|
+
return files.map((f) => f.replace(WORKFLOW_EXTENSION, ""));
|
|
408
|
+
} catch {
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return [];
|
|
412
|
+
}
|
|
413
|
+
async function isDefaultWorkflow(workflowId) {
|
|
414
|
+
const defaultIds = await getDefaultWorkflowIds();
|
|
415
|
+
return defaultIds.includes(workflowId);
|
|
416
|
+
}
|
|
417
|
+
function findWorkflowById(workflowId, workflows) {
|
|
418
|
+
return workflows.find((p) => p.metadata.id === workflowId);
|
|
419
|
+
}
|
|
420
|
+
async function workflowExists(configDir, workflowId, projectDir) {
|
|
421
|
+
if (await pathExists(join(getWorkflowsDir(configDir), `${workflowId}${WORKFLOW_EXTENSION}`))) return true;
|
|
422
|
+
if (projectDir && await pathExists(join(getProjectWorkflowsDir(projectDir), `${workflowId}${WORKFLOW_EXTENSION}`)))
|
|
423
|
+
return true;
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
async function saveWorkflow(configDir, workflow) {
|
|
427
|
+
const workflowsDir = getWorkflowsDir(configDir);
|
|
428
|
+
if (!await pathExists(workflowsDir)) {
|
|
429
|
+
await mkdir(workflowsDir, { recursive: true });
|
|
430
|
+
}
|
|
431
|
+
const filePath = join(workflowsDir, `${workflow.metadata.id}${WORKFLOW_EXTENSION}`);
|
|
432
|
+
await writeFile(filePath, JSON.stringify(workflow, null, 2) + "\n", "utf-8");
|
|
433
|
+
}
|
|
434
|
+
async function deleteWorkflow(configDir, workflowId) {
|
|
435
|
+
const isDefault = await isDefaultWorkflow(workflowId);
|
|
436
|
+
if (isDefault) {
|
|
437
|
+
return { success: false, reason: "Cannot delete built-in defaults" };
|
|
438
|
+
}
|
|
439
|
+
const filePath = join(getWorkflowsDir(configDir), `${workflowId}${WORKFLOW_EXTENSION}`);
|
|
440
|
+
try {
|
|
441
|
+
await unlink(filePath);
|
|
442
|
+
return { success: true };
|
|
443
|
+
} catch {
|
|
444
|
+
return { success: false };
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// src/server/workflows/types.ts
|
|
449
|
+
var TERMINAL_DONE = "$done";
|
|
450
|
+
var TERMINAL_BLOCKED = "$blocked";
|
|
451
|
+
|
|
452
|
+
// src/server/git/diff.ts
|
|
453
|
+
import { spawn as spawn2 } from "child_process";
|
|
454
|
+
function getGitDiffFiles(cwd) {
|
|
455
|
+
return new Promise((resolve) => {
|
|
456
|
+
const diffProc = spawn2("git", ["diff", "--name-status", "HEAD"], { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
457
|
+
const statusProc = spawn2("git", ["status", "--porcelain"], { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
458
|
+
let diffStdout = "";
|
|
459
|
+
let statusStdout = "";
|
|
460
|
+
let diffExited = false;
|
|
461
|
+
let statusExited = false;
|
|
462
|
+
let diffCode = null;
|
|
463
|
+
let statusCode = null;
|
|
464
|
+
const collect = () => {
|
|
465
|
+
if (!diffExited || !statusExited) return;
|
|
466
|
+
const files = [];
|
|
467
|
+
if (diffCode === 0) {
|
|
468
|
+
for (const line of diffStdout.split("\n")) {
|
|
469
|
+
if (!line.trim()) continue;
|
|
470
|
+
const [statusChar, ...pathParts] = line.split(" ");
|
|
471
|
+
const path = pathParts.join(" ") || statusChar || "";
|
|
472
|
+
if (!path) continue;
|
|
473
|
+
const status = statusChar === "A" ? "added" : statusChar === "D" ? "deleted" : "modified";
|
|
474
|
+
files.push({ path, status, additions: 0, deletions: 0 });
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (statusCode === 0) {
|
|
478
|
+
for (const line of statusStdout.split("\n")) {
|
|
479
|
+
if (!line.startsWith("?? ")) continue;
|
|
480
|
+
const path = line.slice(3).trim();
|
|
481
|
+
if (!path) continue;
|
|
482
|
+
files.push({ path, status: "added", additions: 0, deletions: 0 });
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
resolve(files);
|
|
486
|
+
};
|
|
487
|
+
diffProc.stdout.on("data", (data) => {
|
|
488
|
+
diffStdout += data.toString();
|
|
489
|
+
});
|
|
490
|
+
statusProc.stdout.on("data", (data) => {
|
|
491
|
+
statusStdout += data.toString();
|
|
492
|
+
});
|
|
493
|
+
diffProc.on("close", (code) => {
|
|
494
|
+
diffExited = true;
|
|
495
|
+
diffCode = code;
|
|
496
|
+
collect();
|
|
497
|
+
});
|
|
498
|
+
statusProc.on("close", (code) => {
|
|
499
|
+
statusExited = true;
|
|
500
|
+
statusCode = code;
|
|
501
|
+
collect();
|
|
502
|
+
});
|
|
503
|
+
diffProc.on("error", () => {
|
|
504
|
+
diffExited = true;
|
|
505
|
+
diffCode = 1;
|
|
506
|
+
collect();
|
|
507
|
+
});
|
|
508
|
+
statusProc.on("error", () => {
|
|
509
|
+
statusExited = true;
|
|
510
|
+
statusCode = 1;
|
|
511
|
+
collect();
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
async function formatGitDiffFiles(cwd) {
|
|
516
|
+
const files = await getGitDiffFiles(cwd);
|
|
517
|
+
if (files.length === 0) return "(none)";
|
|
518
|
+
return files.map((f) => `- ${f.path} (${f.status})`).join("\n");
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// src/server/workflows/shell.ts
|
|
522
|
+
function executeShellCommand(command, cwd, timeout, signal) {
|
|
523
|
+
return new Promise((resolve, reject) => {
|
|
524
|
+
if (checkAborted(signal)) {
|
|
525
|
+
reject(new Error("Aborted"));
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const proc = spawnShellProcess(command, cwd, signal);
|
|
529
|
+
let stdout = "";
|
|
530
|
+
let stderr = "";
|
|
531
|
+
let killed = false;
|
|
532
|
+
const timer = setTimeout(() => {
|
|
533
|
+
killed = true;
|
|
534
|
+
proc.kill("SIGTERM");
|
|
535
|
+
resolve({ exitCode: 1, stdout, stderr: stderr + "\nCommand timed out", success: false });
|
|
536
|
+
}, timeout);
|
|
537
|
+
const onAbort = () => {
|
|
538
|
+
if (!killed) {
|
|
539
|
+
killed = true;
|
|
540
|
+
proc.kill("SIGTERM");
|
|
541
|
+
clearTimeout(timer);
|
|
542
|
+
reject(new Error("Aborted"));
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
signal?.addEventListener("abort", onAbort);
|
|
546
|
+
proc.stdout?.on("data", (data) => {
|
|
547
|
+
stdout += data.toString();
|
|
548
|
+
});
|
|
549
|
+
proc.stderr?.on("data", (data) => {
|
|
550
|
+
stderr += data.toString();
|
|
551
|
+
});
|
|
552
|
+
proc.on("close", (code) => {
|
|
553
|
+
clearTimeout(timer);
|
|
554
|
+
signal?.removeEventListener("abort", onAbort);
|
|
555
|
+
if (killed) return;
|
|
556
|
+
const exitCode = code ?? 1;
|
|
557
|
+
resolve({ exitCode, stdout, stderr, success: exitCode === 0 });
|
|
558
|
+
});
|
|
559
|
+
proc.on("error", (err) => {
|
|
560
|
+
clearTimeout(timer);
|
|
561
|
+
signal?.removeEventListener("abort", onAbort);
|
|
562
|
+
if (killed) return;
|
|
563
|
+
resolve({ exitCode: 1, stdout, stderr: err.message, success: false });
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// src/server/workflows/executor.ts
|
|
569
|
+
var TEMPLATE_VARIABLES = [
|
|
570
|
+
{ name: "workdir", description: "Working directory of the session" },
|
|
571
|
+
{ name: "reason", description: 'Human-readable reason (e.g. "2 criteria remaining")' },
|
|
572
|
+
{
|
|
573
|
+
name: "stepOutput",
|
|
574
|
+
description: "Structured output from the previous step (content, stdout, stderr, exitCode, etc.)"
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
name: "verifierFindings",
|
|
578
|
+
description: "@deprecated Use stepOutput.content instead. Output from the last sub-agent step"
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
name: "previousStepOutput",
|
|
582
|
+
description: "@deprecated Use stepOutput.stdout instead. Output from the last shell step"
|
|
583
|
+
},
|
|
584
|
+
{ name: "criteriaCount", description: "Total number of criteria" },
|
|
585
|
+
{ name: "pendingCount", description: "Number of pending/failed criteria" },
|
|
586
|
+
{ name: "criteriaList", description: "Formatted list of all criteria with status" },
|
|
587
|
+
{ name: "modifiedFiles", description: "List of modified files" }
|
|
588
|
+
];
|
|
589
|
+
function formatCriteriaList(entries) {
|
|
590
|
+
if (entries.length === 0) return "(none)";
|
|
591
|
+
return entries.map((e) => {
|
|
592
|
+
const status = e.status === "passed" ? "[PASSED]" : e.status === "completed" ? "[NEEDS VERIFICATION]" : e.status === "failed" ? "[FAILED]" : "[NOT COMPLETED]";
|
|
593
|
+
return `- **${e.id}** ${status}: ${e.description}`;
|
|
594
|
+
}).join("\n");
|
|
595
|
+
}
|
|
596
|
+
async function formatModifiedFiles(session) {
|
|
597
|
+
return formatGitDiffFiles(session.workdir);
|
|
598
|
+
}
|
|
599
|
+
function resolveTemplate(template, ctx) {
|
|
600
|
+
let result = template;
|
|
601
|
+
for (const { name } of TEMPLATE_VARIABLES) {
|
|
602
|
+
if (name === "stepOutput" || name === "verifierFindings" || name === "previousStepOutput") continue;
|
|
603
|
+
const value = String(ctx[name]);
|
|
604
|
+
result = result.replace(new RegExp(`\\{\\{${name}\\}\\}`, "g"), value);
|
|
605
|
+
}
|
|
606
|
+
result = result.replace(/\{\{stepOutput\.(\w+)\}\}/g, (_, key) => ctx.stepOutput[key] ?? "");
|
|
607
|
+
result = result.replace(/\{\{verifierFindings\}\}/g, ctx.stepOutput["content"] ?? "");
|
|
608
|
+
result = result.replace(/\{\{previousStepOutput\}\}/g, ctx.stepOutput["stdout"] ?? "");
|
|
609
|
+
return result;
|
|
610
|
+
}
|
|
611
|
+
function evaluateCondition(condition, stepOutcome, metadataEntries) {
|
|
612
|
+
switch (condition.type) {
|
|
613
|
+
case "step_result":
|
|
614
|
+
if (!stepOutcome) return false;
|
|
615
|
+
return stepOutcome.result === condition.result;
|
|
616
|
+
case "metadata_all_match": {
|
|
617
|
+
if (!metadataEntries) return false;
|
|
618
|
+
const entries = metadataEntries[condition.key];
|
|
619
|
+
if (!entries || entries.length === 0) return true;
|
|
620
|
+
return entries.every((e) => e[condition.field] === condition.value);
|
|
621
|
+
}
|
|
622
|
+
case "metadata_all_in": {
|
|
623
|
+
if (!metadataEntries) return false;
|
|
624
|
+
const entries = metadataEntries[condition.key];
|
|
625
|
+
if (!entries || entries.length === 0) return true;
|
|
626
|
+
return entries.every((e) => condition.values.includes(e[condition.field]));
|
|
627
|
+
}
|
|
628
|
+
case "always":
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
function evaluateTransitions(transitions, stepOutcome, metadataEntries) {
|
|
633
|
+
for (const transition of transitions) {
|
|
634
|
+
if (evaluateCondition(transition.when, stepOutcome, metadataEntries)) {
|
|
635
|
+
return transition.goto;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return TERMINAL_BLOCKED;
|
|
639
|
+
}
|
|
640
|
+
function emitWorkflowMessage(eventStore, sessionId, content, windowOptions, onMessage) {
|
|
641
|
+
const msgId = crypto.randomUUID();
|
|
642
|
+
eventStore.append(
|
|
643
|
+
sessionId,
|
|
644
|
+
createMessageStartEvent(msgId, "user", content, {
|
|
645
|
+
...windowOptions ?? {},
|
|
646
|
+
isSystemGenerated: true,
|
|
647
|
+
messageKind: "correction",
|
|
648
|
+
metadata: { type: "workflow", name: "Workflow", color: "#f59e0b" }
|
|
649
|
+
})
|
|
650
|
+
);
|
|
651
|
+
eventStore.append(sessionId, { type: "message.done", data: { messageId: msgId } });
|
|
652
|
+
if (onMessage) {
|
|
653
|
+
onMessage(
|
|
654
|
+
createChatMessageMessage({
|
|
655
|
+
id: msgId,
|
|
656
|
+
role: "user",
|
|
657
|
+
content,
|
|
658
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
659
|
+
isSystemGenerated: true,
|
|
660
|
+
messageKind: "correction",
|
|
661
|
+
metadata: { type: "workflow", name: "Workflow", color: "#f59e0b" }
|
|
662
|
+
})
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
return msgId;
|
|
666
|
+
}
|
|
667
|
+
function getCurrentWindowMessageOptions(sessionId) {
|
|
668
|
+
const contextWindowId = getCurrentContextWindowId(sessionId);
|
|
669
|
+
return contextWindowId ? { contextWindowId } : void 0;
|
|
670
|
+
}
|
|
671
|
+
function buildReason(metadataEntries) {
|
|
672
|
+
const entries = metadataEntries?.["criteria"] ?? [];
|
|
673
|
+
const remaining = entries.filter((e) => e.status !== "passed");
|
|
674
|
+
return `${remaining.length} criteria remaining`;
|
|
675
|
+
}
|
|
676
|
+
async function executeWorkflow(workflow, options, subGroup) {
|
|
677
|
+
const { sessionManager, sessionId, llmClient, signal, onMessage } = options;
|
|
678
|
+
const eventStore = getEventStore();
|
|
679
|
+
const startTime = performance.now();
|
|
680
|
+
let iterations = 0;
|
|
681
|
+
const activeSteps = subGroup ? workflow.steps.filter((s) => s.subGroup === subGroup) : workflow.steps;
|
|
682
|
+
let currentStepId = subGroup ? activeSteps[0]?.id ?? workflow.entryStep : workflow.entryStep;
|
|
683
|
+
let lastStepOutput = {};
|
|
684
|
+
const firstEntryForStep = /* @__PURE__ */ new Set();
|
|
685
|
+
const messagesBeforeWorkflow = sessionManager.requireSession(sessionId).messages.length;
|
|
686
|
+
const activeStepIds = new Set(activeSteps.map((s) => s.id));
|
|
687
|
+
const stepsById = /* @__PURE__ */ new Map();
|
|
688
|
+
for (const step of activeSteps) {
|
|
689
|
+
stepsById.set(step.id, step);
|
|
690
|
+
}
|
|
691
|
+
logger.debug("Workflow executor starting", { sessionId, workflow: workflow.metadata.id });
|
|
692
|
+
if (workflow.startCondition && workflow.startCondition.type !== "always") {
|
|
693
|
+
const session = sessionManager.requireSession(sessionId);
|
|
694
|
+
const conditionMet = evaluateCondition(
|
|
695
|
+
workflow.startCondition,
|
|
696
|
+
null,
|
|
697
|
+
session.metadataEntries
|
|
698
|
+
);
|
|
699
|
+
if (!conditionMet) {
|
|
700
|
+
logger.debug("Workflow start condition not met", { sessionId, condition: workflow.startCondition.type });
|
|
701
|
+
return {
|
|
702
|
+
finalAction: {
|
|
703
|
+
type: "BLOCKED",
|
|
704
|
+
reason: `Start condition not met: ${workflow.startCondition.type}`,
|
|
705
|
+
blockedCriteria: []
|
|
706
|
+
},
|
|
707
|
+
iterations: 0,
|
|
708
|
+
totalTime: (performance.now() - startTime) / 1e3
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
const startMsgId = crypto.randomUUID();
|
|
713
|
+
const startWindowOpts = getCurrentWindowMessageOptions(sessionId);
|
|
714
|
+
eventStore.append(
|
|
715
|
+
sessionId,
|
|
716
|
+
createMessageStartEvent(
|
|
717
|
+
startMsgId,
|
|
718
|
+
"user",
|
|
719
|
+
JSON.stringify({
|
|
720
|
+
workflowName: workflow.metadata.name,
|
|
721
|
+
workflowId: workflow.metadata.id,
|
|
722
|
+
workflowColor: workflow.metadata.color
|
|
723
|
+
}),
|
|
724
|
+
{ ...startWindowOpts ?? {}, isSystemGenerated: true, messageKind: "workflow-started" }
|
|
725
|
+
)
|
|
726
|
+
);
|
|
727
|
+
eventStore.append(sessionId, { type: "message.done", data: { messageId: startMsgId } });
|
|
728
|
+
if (options.userMessage) {
|
|
729
|
+
sessionManager.addMessage(sessionId, {
|
|
730
|
+
role: "user",
|
|
731
|
+
content: options.userMessage.content,
|
|
732
|
+
...options.userMessage.attachments ? { attachments: options.userMessage.attachments } : {}
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
while (iterations < workflow.settings.maxIterations) {
|
|
736
|
+
if (signal?.aborted) {
|
|
737
|
+
logger.debug("Workflow executor aborted", { sessionId, iterations });
|
|
738
|
+
return {
|
|
739
|
+
finalAction: { type: "RUN_BUILDER", reason: "Aborted" },
|
|
740
|
+
iterations,
|
|
741
|
+
totalTime: (performance.now() - startTime) / 1e3
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
iterations++;
|
|
745
|
+
const step = stepsById.get(currentStepId);
|
|
746
|
+
if (!step) {
|
|
747
|
+
logger.error("Workflow step not found", { sessionId, stepId: currentStepId });
|
|
748
|
+
return {
|
|
749
|
+
finalAction: { type: "BLOCKED", reason: `Step "${currentStepId}" not found in workflow`, blockedCriteria: [] },
|
|
750
|
+
iterations,
|
|
751
|
+
totalTime: (performance.now() - startTime) / 1e3
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
const session = sessionManager.requireSession(sessionId);
|
|
755
|
+
const currentWindowMessageOptions = getCurrentWindowMessageOptions(sessionId);
|
|
756
|
+
const criteriaEntries = session.metadataEntries["criteria"] ?? [];
|
|
757
|
+
const templateCtx = {
|
|
758
|
+
workdir: session.workdir,
|
|
759
|
+
reason: buildReason(session.metadataEntries),
|
|
760
|
+
verifierFindings: lastStepOutput["content"] ?? "",
|
|
761
|
+
previousStepOutput: lastStepOutput["stdout"] ?? "",
|
|
762
|
+
criteriaCount: criteriaEntries.length,
|
|
763
|
+
pendingCount: criteriaEntries.filter((e) => e.status !== "passed").length,
|
|
764
|
+
criteriaList: formatCriteriaList(criteriaEntries),
|
|
765
|
+
modifiedFiles: await formatModifiedFiles(session),
|
|
766
|
+
stepOutput: lastStepOutput
|
|
767
|
+
};
|
|
768
|
+
sessionManager.setPhase(sessionId, step.phase);
|
|
769
|
+
logger.debug("Workflow step executing", { sessionId, iteration: iterations, stepId: step.id, stepType: step.type });
|
|
770
|
+
let stepOutcome = null;
|
|
771
|
+
switch (step.type) {
|
|
772
|
+
case "agent": {
|
|
773
|
+
const agentStep = step;
|
|
774
|
+
const STEP_DONE_PROMPT = "\n\nOnce you're done, call step_done()";
|
|
775
|
+
const STEP_DONE_NUDGE = "You haven't called step_done(). If you haven't finished the task, continue and when you're finished call step_done()";
|
|
776
|
+
let promptContent;
|
|
777
|
+
let nudgeContent;
|
|
778
|
+
if (!firstEntryForStep.has(step.id) && agentStep.prompt) {
|
|
779
|
+
const resolvedPrompt = resolveTemplate(agentStep.prompt, templateCtx);
|
|
780
|
+
promptContent = resolvedPrompt + STEP_DONE_PROMPT;
|
|
781
|
+
const promptMsgId = crypto.randomUUID();
|
|
782
|
+
const msgMetadata = { type: "workflow", name: "Workflow", color: "#f59e0b" };
|
|
783
|
+
eventStore.append(
|
|
784
|
+
sessionId,
|
|
785
|
+
createMessageStartEvent(promptMsgId, "user", promptContent, {
|
|
786
|
+
...currentWindowMessageOptions ?? {},
|
|
787
|
+
isSystemGenerated: true,
|
|
788
|
+
messageKind: "auto-prompt",
|
|
789
|
+
metadata: msgMetadata
|
|
790
|
+
})
|
|
791
|
+
);
|
|
792
|
+
eventStore.append(sessionId, { type: "message.done", data: { messageId: promptMsgId } });
|
|
793
|
+
if (onMessage) {
|
|
794
|
+
onMessage(
|
|
795
|
+
createChatMessageMessage({
|
|
796
|
+
id: promptMsgId,
|
|
797
|
+
role: "user",
|
|
798
|
+
content: promptContent,
|
|
799
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
800
|
+
isSystemGenerated: true,
|
|
801
|
+
messageKind: "auto-prompt",
|
|
802
|
+
metadata: { type: "workflow", name: "Workflow", color: "#f59e0b" }
|
|
803
|
+
})
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
} else if (firstEntryForStep.has(step.id)) {
|
|
807
|
+
const parts = [];
|
|
808
|
+
if (agentStep.nudgePrompt) {
|
|
809
|
+
const resolvedNudge = resolveTemplate(agentStep.nudgePrompt, templateCtx);
|
|
810
|
+
parts.push(resolvedNudge);
|
|
811
|
+
}
|
|
812
|
+
parts.push(STEP_DONE_NUDGE);
|
|
813
|
+
nudgeContent = parts.join("\n\n");
|
|
814
|
+
emitWorkflowMessage(eventStore, sessionId, nudgeContent, currentWindowMessageOptions, onMessage);
|
|
815
|
+
}
|
|
816
|
+
const turnMetrics = new TurnMetrics();
|
|
817
|
+
const es = getEventStore();
|
|
818
|
+
const append = (event) => es.append(sessionId, event);
|
|
819
|
+
let stepDoneCalled = false;
|
|
820
|
+
const agentResult = await runAgentTurn(
|
|
821
|
+
{
|
|
822
|
+
sessionManager,
|
|
823
|
+
sessionId,
|
|
824
|
+
llmClient,
|
|
825
|
+
...options.statsIdentity ? { statsIdentity: options.statsIdentity } : {},
|
|
826
|
+
...signal ? { signal } : {},
|
|
827
|
+
...onMessage ? { onMessage } : {}
|
|
828
|
+
},
|
|
829
|
+
turnMetrics,
|
|
830
|
+
agentStep.agentId ?? "planner",
|
|
831
|
+
append,
|
|
832
|
+
{
|
|
833
|
+
...!firstEntryForStep.has(step.id) && !agentStep.prompt && options.injectWorkflowKickoff === true ? { injectKickoff: () => injectWorkflowKickoffIfNeeded(sessionManager, sessionId, es) } : {},
|
|
834
|
+
onToolExecuted: (toolCall, toolResult) => {
|
|
835
|
+
if (toolCall.name === "step_done" && toolResult.success) {
|
|
836
|
+
stepDoneCalled = true;
|
|
837
|
+
}
|
|
838
|
+
if (toolResult.success && ["write_file", "edit_file"].includes(toolCall.name)) {
|
|
839
|
+
const path = toolCall.arguments["path"];
|
|
840
|
+
sessionManager.addModifiedFile(sessionId, path);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
);
|
|
845
|
+
firstEntryForStep.add(step.id);
|
|
846
|
+
const agentReturnValue = agentResult.returnValueResult ?? "completed";
|
|
847
|
+
lastStepOutput = {
|
|
848
|
+
...agentResult.returnValueContent ? { content: agentResult.returnValueContent } : {},
|
|
849
|
+
...agentResult.returnValueResult ? { result: agentResult.returnValueResult } : {},
|
|
850
|
+
stepDoneCalled: String(stepDoneCalled)
|
|
851
|
+
};
|
|
852
|
+
stepOutcome = { result: agentReturnValue, output: lastStepOutput };
|
|
853
|
+
if (!stepDoneCalled) {
|
|
854
|
+
logger.debug("step_done not called, looping agent step", {
|
|
855
|
+
sessionId,
|
|
856
|
+
stepId: step.id,
|
|
857
|
+
iteration: iterations
|
|
858
|
+
});
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
break;
|
|
862
|
+
}
|
|
863
|
+
case "sub_agent": {
|
|
864
|
+
const subStep = step;
|
|
865
|
+
const turnMetrics = new TurnMetrics();
|
|
866
|
+
const promptTemplate = subStep.prompt ?? "Perform your task.";
|
|
867
|
+
const resolvedPrompt = resolveTemplate(promptTemplate, templateCtx);
|
|
868
|
+
const allAgents = await loadAllAgentsDefault();
|
|
869
|
+
const agentDef = findAgentById(subStep.subAgentType, allAgents);
|
|
870
|
+
if (!agentDef) {
|
|
871
|
+
logger.error("Sub-agent definition not found", { subAgentType: subStep.subAgentType });
|
|
872
|
+
stepOutcome = { result: "error", output: {} };
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
875
|
+
const toolRegistry = getToolRegistryForAgent(agentDef);
|
|
876
|
+
const filteredToolRegistry = {
|
|
877
|
+
tools: toolRegistry.tools.filter((t) => t.name !== "step_done"),
|
|
878
|
+
definitions: toolRegistry.definitions.filter((d) => d.type === "function" && d.function.name !== "step_done"),
|
|
879
|
+
execute: toolRegistry.execute
|
|
880
|
+
};
|
|
881
|
+
const result = await executeSubAgent({
|
|
882
|
+
subAgentType: subStep.subAgentType,
|
|
883
|
+
prompt: resolvedPrompt,
|
|
884
|
+
sessionManager,
|
|
885
|
+
sessionId,
|
|
886
|
+
llmClient,
|
|
887
|
+
toolRegistry: filteredToolRegistry,
|
|
888
|
+
turnMetrics,
|
|
889
|
+
statsIdentity: options.statsIdentity ?? {
|
|
890
|
+
providerId: "",
|
|
891
|
+
providerName: "",
|
|
892
|
+
backend: "unknown",
|
|
893
|
+
model: llmClient.getModel()
|
|
894
|
+
},
|
|
895
|
+
...signal ? { signal } : {},
|
|
896
|
+
...onMessage ? { onMessage } : {}
|
|
897
|
+
});
|
|
898
|
+
lastStepOutput = { content: result.content ?? "", ...result.result ? { result: result.result } : {} };
|
|
899
|
+
stepOutcome = { result: result.result ?? "success", output: lastStepOutput };
|
|
900
|
+
break;
|
|
901
|
+
}
|
|
902
|
+
case "shell": {
|
|
903
|
+
const shellStep = step;
|
|
904
|
+
const command = resolveTemplate(shellStep.command, templateCtx);
|
|
905
|
+
const timeout = shellStep.timeout ?? 6e4;
|
|
906
|
+
const successCodes = shellStep.successExitCodes ?? [0];
|
|
907
|
+
const shellMsgId = crypto.randomUUID();
|
|
908
|
+
eventStore.append(
|
|
909
|
+
sessionId,
|
|
910
|
+
createMessageStartEvent(shellMsgId, "user", `Running: \`${command}\``, {
|
|
911
|
+
...currentWindowMessageOptions ?? {},
|
|
912
|
+
isSystemGenerated: true,
|
|
913
|
+
messageKind: "auto-prompt",
|
|
914
|
+
metadata: { type: "workflow", name: "Workflow", color: "#f59e0b" }
|
|
915
|
+
})
|
|
916
|
+
);
|
|
917
|
+
if (onMessage) {
|
|
918
|
+
onMessage(
|
|
919
|
+
createChatMessageMessage({
|
|
920
|
+
id: shellMsgId,
|
|
921
|
+
role: "user",
|
|
922
|
+
content: `Running: \`${command}\``,
|
|
923
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
924
|
+
isSystemGenerated: true,
|
|
925
|
+
messageKind: "auto-prompt",
|
|
926
|
+
metadata: { type: "workflow", name: "Workflow", color: "#f59e0b" }
|
|
927
|
+
})
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
const result = await executeShellCommand(command, session.workdir, timeout, signal);
|
|
931
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
932
|
+
lastStepOutput = { stdout: result.stdout ?? "", stderr: result.stderr ?? "", exitCode: String(result.exitCode) };
|
|
933
|
+
const outputContent = output ? `Exit code: ${result.exitCode}
|
|
934
|
+
\`\`\`
|
|
935
|
+
${output.slice(0, 1e4)}
|
|
936
|
+
\`\`\`` : `Exit code: ${result.exitCode}`;
|
|
937
|
+
eventStore.append(sessionId, { type: "message.done", data: { messageId: shellMsgId } });
|
|
938
|
+
const outputMsgId = crypto.randomUUID();
|
|
939
|
+
eventStore.append(
|
|
940
|
+
sessionId,
|
|
941
|
+
createMessageStartEvent(outputMsgId, "user", outputContent, {
|
|
942
|
+
...currentWindowMessageOptions ?? {},
|
|
943
|
+
isSystemGenerated: true,
|
|
944
|
+
messageKind: "correction"
|
|
945
|
+
})
|
|
946
|
+
);
|
|
947
|
+
eventStore.append(sessionId, { type: "message.done", data: { messageId: outputMsgId } });
|
|
948
|
+
if (onMessage) {
|
|
949
|
+
onMessage(
|
|
950
|
+
createChatMessageMessage({
|
|
951
|
+
id: outputMsgId,
|
|
952
|
+
role: "user",
|
|
953
|
+
content: outputContent,
|
|
954
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
955
|
+
isSystemGenerated: true,
|
|
956
|
+
messageKind: "correction"
|
|
957
|
+
})
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
stepOutcome = { result: successCodes.includes(result.exitCode) ? "success" : "failure", output: lastStepOutput };
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
const refreshedSession = sessionManager.requireSession(sessionId);
|
|
965
|
+
const effectiveTransitions = subGroup ? step.transitions.filter((t) => !t.subGroup || t.subGroup === subGroup) : step.transitions;
|
|
966
|
+
const rawNextStepId = evaluateTransitions(effectiveTransitions, stepOutcome, refreshedSession.metadataEntries);
|
|
967
|
+
const nextStepId = subGroup && rawNextStepId !== TERMINAL_DONE && rawNextStepId !== TERMINAL_BLOCKED && !activeStepIds.has(rawNextStepId) ? TERMINAL_DONE : rawNextStepId;
|
|
968
|
+
if (nextStepId === TERMINAL_DONE) {
|
|
969
|
+
sessionManager.setPhase(sessionId, "done");
|
|
970
|
+
const totalTimeSeconds = Math.round((performance.now() - startTime) / 100) / 10;
|
|
971
|
+
const completedSession = sessionManager.requireSession(sessionId);
|
|
972
|
+
const workflowMessages = completedSession.messages.slice(messagesBeforeWorkflow);
|
|
973
|
+
const workflowStats = computeSessionStats(workflowMessages);
|
|
974
|
+
const totalToolCalls = workflowMessages.reduce((sum, m) => sum + (m.toolCalls?.length ?? 0), 0);
|
|
975
|
+
const taskCompletedData = {
|
|
976
|
+
summary: null,
|
|
977
|
+
iterations,
|
|
978
|
+
totalTimeSeconds,
|
|
979
|
+
totalToolCalls,
|
|
980
|
+
totalTokensGenerated: workflowStats?.generationTokens ?? 0,
|
|
981
|
+
avgGenerationSpeed: workflowStats?.avgGenerationSpeed ?? 0,
|
|
982
|
+
responseCount: workflowStats?.responseCount ?? 0,
|
|
983
|
+
llmCallCount: workflowStats?.llmCallCount ?? 0,
|
|
984
|
+
criteria: [],
|
|
985
|
+
workflowName: workflow.metadata.name,
|
|
986
|
+
workflowId: workflow.metadata.id,
|
|
987
|
+
...workflow.metadata.color ? { workflowColor: workflow.metadata.color } : {}
|
|
988
|
+
};
|
|
989
|
+
eventStore.append(sessionId, { type: "task.completed", data: taskCompletedData });
|
|
990
|
+
const markerMsgId = crypto.randomUUID();
|
|
991
|
+
eventStore.append(
|
|
992
|
+
sessionId,
|
|
993
|
+
createMessageStartEvent(markerMsgId, "user", JSON.stringify(taskCompletedData), {
|
|
994
|
+
...currentWindowMessageOptions ?? {},
|
|
995
|
+
isSystemGenerated: true,
|
|
996
|
+
messageKind: "task-completed"
|
|
997
|
+
})
|
|
998
|
+
);
|
|
999
|
+
eventStore.append(sessionId, { type: "message.done", data: { messageId: markerMsgId } });
|
|
1000
|
+
logger.debug("Workflow executor complete", { sessionId, iterations });
|
|
1001
|
+
const doneAction = { type: "DONE" };
|
|
1002
|
+
return {
|
|
1003
|
+
finalAction: doneAction,
|
|
1004
|
+
iterations,
|
|
1005
|
+
totalTime: totalTimeSeconds
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
if (nextStepId === TERMINAL_BLOCKED) {
|
|
1009
|
+
sessionManager.setPhase(sessionId, "blocked");
|
|
1010
|
+
const reason = "No matching transition";
|
|
1011
|
+
emitWorkflowMessage(eventStore, sessionId, `Runner blocked: ${reason}`, currentWindowMessageOptions, onMessage);
|
|
1012
|
+
logger.warn("Workflow executor blocked", { sessionId, iterations, reason });
|
|
1013
|
+
const blockedAction = { type: "BLOCKED", reason, blockedCriteria: [] };
|
|
1014
|
+
return {
|
|
1015
|
+
finalAction: blockedAction,
|
|
1016
|
+
iterations,
|
|
1017
|
+
totalTime: (performance.now() - startTime) / 1e3
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
currentStepId = nextStepId;
|
|
1021
|
+
}
|
|
1022
|
+
logger.warn("Workflow executor max iterations reached", { sessionId, iterations });
|
|
1023
|
+
return {
|
|
1024
|
+
finalAction: {
|
|
1025
|
+
type: "BLOCKED",
|
|
1026
|
+
reason: `Max iterations (${workflow.settings.maxIterations}) reached`,
|
|
1027
|
+
blockedCriteria: []
|
|
1028
|
+
},
|
|
1029
|
+
iterations,
|
|
1030
|
+
totalTime: (performance.now() - startTime) / 1e3
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// src/server/runner/orchestrator.ts
|
|
1035
|
+
async function runOrchestrator(options) {
|
|
1036
|
+
const runtimeConfig = getRuntimeConfig();
|
|
1037
|
+
const workflowId = options.workflowId ?? runtimeConfig.activeWorkflowId ?? "default";
|
|
1038
|
+
const configDir = getGlobalConfigDir(runtimeConfig.mode ?? "production");
|
|
1039
|
+
const workflows = await loadAllWorkflows(configDir);
|
|
1040
|
+
const workflow = findWorkflowById(workflowId, workflows);
|
|
1041
|
+
if (!workflow) {
|
|
1042
|
+
throw new Error(`Workflow "${workflowId}" not found`);
|
|
1043
|
+
}
|
|
1044
|
+
logger.debug("Using workflow executor", {
|
|
1045
|
+
sessionId: options.sessionId,
|
|
1046
|
+
workflow: workflow.metadata.id,
|
|
1047
|
+
subGroup: options.subGroup
|
|
1048
|
+
});
|
|
1049
|
+
return executeWorkflow(workflow, options, options.subGroup);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// src/server/auth.ts
|
|
1053
|
+
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
1054
|
+
import { join as join2, dirname as dirname2 } from "path";
|
|
1055
|
+
import { createHash, privateDecrypt, createPublicKey } from "crypto";
|
|
1056
|
+
function getAuthConfigPath() {
|
|
1057
|
+
const configDir = getRuntimeConfig();
|
|
1058
|
+
const mode = configDir.mode === "development" ? "development" : configDir.mode === "test" ? "test" : "production";
|
|
1059
|
+
if (mode === "test") {
|
|
1060
|
+
const cwd = process.cwd();
|
|
1061
|
+
const base = cwd.endsWith("/e2e") ? cwd : join2(cwd, "e2e");
|
|
1062
|
+
const testAuthPath = join2(base, ".openfox-test", "auth.json");
|
|
1063
|
+
return testAuthPath;
|
|
1064
|
+
}
|
|
1065
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || "";
|
|
1066
|
+
const basePath = process.env["XDG_CONFIG_HOME"] || `${home}/.config`;
|
|
1067
|
+
const suffix = mode === "development" ? "-dev" : "";
|
|
1068
|
+
return `${basePath}/openfox${suffix}/auth.json`;
|
|
1069
|
+
}
|
|
1070
|
+
function getKeyPath() {
|
|
1071
|
+
const authPath = getAuthConfigPath();
|
|
1072
|
+
const dir = dirname2(authPath);
|
|
1073
|
+
return join2(dir, "auth.key");
|
|
1074
|
+
}
|
|
1075
|
+
var cachedAuth = null;
|
|
1076
|
+
var cachedPrivateKey = null;
|
|
1077
|
+
async function loadPrivateKey() {
|
|
1078
|
+
if (cachedPrivateKey) {
|
|
1079
|
+
return cachedPrivateKey;
|
|
1080
|
+
}
|
|
1081
|
+
const keyPath = getKeyPath();
|
|
1082
|
+
const keyDir = dirname2(keyPath);
|
|
1083
|
+
try {
|
|
1084
|
+
cachedPrivateKey = await readFile2(keyPath, "utf-8");
|
|
1085
|
+
return cachedPrivateKey;
|
|
1086
|
+
} catch {
|
|
1087
|
+
const { privateKey } = await import("crypto").then(
|
|
1088
|
+
(c) => c.generateKeyPairSync("rsa", {
|
|
1089
|
+
modulusLength: 2048,
|
|
1090
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
1091
|
+
publicKeyEncoding: { type: "spki", format: "pem" }
|
|
1092
|
+
})
|
|
1093
|
+
);
|
|
1094
|
+
await mkdir2(keyDir, { recursive: true });
|
|
1095
|
+
await writeFile2(keyPath, privateKey, { mode: 384 });
|
|
1096
|
+
cachedPrivateKey = privateKey;
|
|
1097
|
+
return privateKey;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
async function loadServerAuthConfig() {
|
|
1101
|
+
const configDir = getRuntimeConfig();
|
|
1102
|
+
const isTestMode = configDir.mode === "test";
|
|
1103
|
+
if (!isTestMode) {
|
|
1104
|
+
if (cachedAuth) {
|
|
1105
|
+
return cachedAuth;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
try {
|
|
1109
|
+
const authPath = getAuthConfigPath();
|
|
1110
|
+
const data = await readFile2(authPath, "utf-8");
|
|
1111
|
+
const authConfig = JSON.parse(data);
|
|
1112
|
+
cachedAuth = authConfig;
|
|
1113
|
+
return authConfig;
|
|
1114
|
+
} catch {
|
|
1115
|
+
return null;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
function getAuthConfig() {
|
|
1119
|
+
return cachedAuth;
|
|
1120
|
+
}
|
|
1121
|
+
function hashPassword(password) {
|
|
1122
|
+
return createHash("sha256").update(password).digest("hex");
|
|
1123
|
+
}
|
|
1124
|
+
function requiresAuth() {
|
|
1125
|
+
return cachedAuth?.strategy === "network";
|
|
1126
|
+
}
|
|
1127
|
+
function hasPassword() {
|
|
1128
|
+
return cachedAuth?.encryptedPassword != null && cachedAuth.encryptedPassword.length > 0;
|
|
1129
|
+
}
|
|
1130
|
+
async function verifyPassword(password) {
|
|
1131
|
+
const encryptedPassword = cachedAuth?.encryptedPassword;
|
|
1132
|
+
if (!encryptedPassword) return false;
|
|
1133
|
+
const privateKey = await loadPrivateKey();
|
|
1134
|
+
try {
|
|
1135
|
+
const decrypted = privateDecrypt({ key: privateKey, padding: 1 }, Buffer.from(encryptedPassword, "base64"));
|
|
1136
|
+
return decrypted.toString() === password;
|
|
1137
|
+
} catch {
|
|
1138
|
+
return false;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
async function tokenFromPassword(password) {
|
|
1142
|
+
const privateKey = await loadPrivateKey();
|
|
1143
|
+
const passwordHash = hashPassword(password);
|
|
1144
|
+
const sign = await import("crypto").then((c) => {
|
|
1145
|
+
const s = c.createSign("SHA256");
|
|
1146
|
+
s.update(passwordHash);
|
|
1147
|
+
s.end();
|
|
1148
|
+
return s.sign(privateKey, "base64");
|
|
1149
|
+
});
|
|
1150
|
+
return sign;
|
|
1151
|
+
}
|
|
1152
|
+
async function isValidToken(token) {
|
|
1153
|
+
if (!cachedAuth?.encryptedPassword) return false;
|
|
1154
|
+
const privateKey = await loadPrivateKey();
|
|
1155
|
+
try {
|
|
1156
|
+
const decrypted = privateDecrypt(
|
|
1157
|
+
{ key: privateKey, padding: 1 },
|
|
1158
|
+
Buffer.from(cachedAuth.encryptedPassword, "base64")
|
|
1159
|
+
);
|
|
1160
|
+
const storedPassword = decrypted.toString();
|
|
1161
|
+
const storedHash = hashPassword(storedPassword);
|
|
1162
|
+
const verify = await import("crypto").then((c) => {
|
|
1163
|
+
const v = c.createVerify("SHA256");
|
|
1164
|
+
v.update(storedHash);
|
|
1165
|
+
v.end();
|
|
1166
|
+
return v;
|
|
1167
|
+
});
|
|
1168
|
+
const publicKey = createPublicKey(privateKey).export({ type: "spki", format: "pem" });
|
|
1169
|
+
return verify.verify(publicKey, token, "base64");
|
|
1170
|
+
} catch {
|
|
1171
|
+
return false;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// src/server/ws/server.ts
|
|
1176
|
+
var _mcpManager;
|
|
1177
|
+
function setMcpManagerForWs(mgr) {
|
|
1178
|
+
_mcpManager = mgr;
|
|
1179
|
+
}
|
|
1180
|
+
function moduleGitBranch(cwd) {
|
|
1181
|
+
return new Promise((resolve) => {
|
|
1182
|
+
const proc = spawn3("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
1183
|
+
cwd,
|
|
1184
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1185
|
+
});
|
|
1186
|
+
let stdout = "";
|
|
1187
|
+
proc.stdout.on("data", (data) => {
|
|
1188
|
+
stdout += data.toString();
|
|
1189
|
+
});
|
|
1190
|
+
proc.on("close", (code) => {
|
|
1191
|
+
if (code === 0 && stdout.trim()) {
|
|
1192
|
+
resolve(stdout.trim());
|
|
1193
|
+
} else {
|
|
1194
|
+
resolve(null);
|
|
1195
|
+
}
|
|
1196
|
+
});
|
|
1197
|
+
proc.on("error", () => resolve(null));
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
function hashContent(content) {
|
|
1201
|
+
return createHash2("sha256").update(content).digest("hex");
|
|
1202
|
+
}
|
|
1203
|
+
function moduleGitDiff(cwd) {
|
|
1204
|
+
return new Promise((resolve) => {
|
|
1205
|
+
const diffProc = spawn3("git", ["diff", "--name-status", "HEAD"], {
|
|
1206
|
+
cwd,
|
|
1207
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1208
|
+
});
|
|
1209
|
+
const statusProc = spawn3("git", ["status", "--porcelain"], {
|
|
1210
|
+
cwd,
|
|
1211
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1212
|
+
});
|
|
1213
|
+
let diffStdout = "";
|
|
1214
|
+
let statusStdout = "";
|
|
1215
|
+
let diffExited = false;
|
|
1216
|
+
let statusExited = false;
|
|
1217
|
+
let diffCode = null;
|
|
1218
|
+
let statusCode = null;
|
|
1219
|
+
const processResults = () => {
|
|
1220
|
+
if (!diffExited || !statusExited) return;
|
|
1221
|
+
const raw = diffStdout + statusStdout;
|
|
1222
|
+
const hash = raw ? hashContent(raw) : "";
|
|
1223
|
+
const files = [];
|
|
1224
|
+
if (diffCode === 0) {
|
|
1225
|
+
for (const line of diffStdout.split("\n")) {
|
|
1226
|
+
if (!line.trim()) continue;
|
|
1227
|
+
const [statusChar, ...pathParts] = line.split(" ");
|
|
1228
|
+
const path = pathParts.join(" ") || statusChar || "";
|
|
1229
|
+
if (!path) continue;
|
|
1230
|
+
const status = statusChar === "A" ? "added" : statusChar === "D" ? "deleted" : "modified";
|
|
1231
|
+
files.push({ path, status, additions: 0, deletions: 0 });
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
if (statusCode === 0) {
|
|
1235
|
+
for (const line of statusStdout.split("\n")) {
|
|
1236
|
+
if (!line.startsWith("?? ")) continue;
|
|
1237
|
+
const path = line.slice(3).trim();
|
|
1238
|
+
if (!path) continue;
|
|
1239
|
+
files.push({ path, status: "added", additions: 0, deletions: 0 });
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
resolve({ hash, files });
|
|
1243
|
+
};
|
|
1244
|
+
diffProc.stdout.on("data", (data) => {
|
|
1245
|
+
diffStdout += data.toString();
|
|
1246
|
+
});
|
|
1247
|
+
statusProc.stdout.on("data", (data) => {
|
|
1248
|
+
statusStdout += data.toString();
|
|
1249
|
+
});
|
|
1250
|
+
diffProc.on("close", (code) => {
|
|
1251
|
+
diffExited = true;
|
|
1252
|
+
diffCode = code;
|
|
1253
|
+
processResults();
|
|
1254
|
+
});
|
|
1255
|
+
statusProc.on("close", (code) => {
|
|
1256
|
+
statusExited = true;
|
|
1257
|
+
statusCode = code;
|
|
1258
|
+
processResults();
|
|
1259
|
+
});
|
|
1260
|
+
diffProc.on("error", () => {
|
|
1261
|
+
diffExited = true;
|
|
1262
|
+
diffCode = 1;
|
|
1263
|
+
processResults();
|
|
1264
|
+
});
|
|
1265
|
+
statusProc.on("error", () => {
|
|
1266
|
+
statusExited = true;
|
|
1267
|
+
statusCode = 1;
|
|
1268
|
+
processResults();
|
|
1269
|
+
});
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
var moduleWorkdirLastHash = /* @__PURE__ */ new Map();
|
|
1273
|
+
var moduleWorkdirInterval = /* @__PURE__ */ new Map();
|
|
1274
|
+
var gitPollInterval = parseInt(process.env["OPENFOX_GIT_POLL_INTERVAL"] ?? "", 10) || 1e4;
|
|
1275
|
+
var moduleClients = null;
|
|
1276
|
+
var moduleEnqueueSend = null;
|
|
1277
|
+
function moduleGitPoll(workdir) {
|
|
1278
|
+
;
|
|
1279
|
+
(async () => {
|
|
1280
|
+
try {
|
|
1281
|
+
const branch = await moduleGitBranch(workdir);
|
|
1282
|
+
const { hash, files } = await moduleGitDiff(workdir);
|
|
1283
|
+
const lastHash = moduleWorkdirLastHash.get(workdir);
|
|
1284
|
+
if (hash !== lastHash) {
|
|
1285
|
+
moduleWorkdirLastHash.set(workdir, hash);
|
|
1286
|
+
const msg = createGitStatusMessage(branch, files);
|
|
1287
|
+
const activeClients = moduleClients;
|
|
1288
|
+
const sendFn = moduleEnqueueSend;
|
|
1289
|
+
if (!activeClients || !sendFn) return;
|
|
1290
|
+
for (const [ws, client] of activeClients) {
|
|
1291
|
+
if (client.activeWorkdir === workdir && ws.readyState === WebSocket2.OPEN) {
|
|
1292
|
+
const seq = client.lastSentSeq + 1;
|
|
1293
|
+
sendFn(client, serializeServerMessage({ ...msg, sessionId: client.activeSessionId ?? "" }), seq);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
} catch {
|
|
1298
|
+
}
|
|
1299
|
+
})();
|
|
1300
|
+
}
|
|
1301
|
+
function moduleStartGitPolling(workdir) {
|
|
1302
|
+
if (moduleWorkdirInterval.has(workdir)) return;
|
|
1303
|
+
const interval = setInterval(() => moduleGitPoll(workdir), gitPollInterval);
|
|
1304
|
+
moduleWorkdirInterval.set(workdir, interval);
|
|
1305
|
+
}
|
|
1306
|
+
function moduleStopGitPolling(workdir) {
|
|
1307
|
+
const interval = moduleWorkdirInterval.get(workdir);
|
|
1308
|
+
if (interval !== void 0) {
|
|
1309
|
+
clearInterval(interval);
|
|
1310
|
+
moduleWorkdirInterval.delete(workdir);
|
|
1311
|
+
moduleWorkdirLastHash.delete(workdir);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
function resolveStatsIdentity(llmClient, getActiveProvider) {
|
|
1315
|
+
const provider = getActiveProvider?.();
|
|
1316
|
+
const model = llmClient.getModel();
|
|
1317
|
+
const backend = provider?.backend ?? (llmClient.getBackend() === "unknown" ? "unknown" : llmClient.getBackend());
|
|
1318
|
+
return {
|
|
1319
|
+
providerId: provider?.id ?? `provider:${model}`,
|
|
1320
|
+
providerName: provider?.name ?? "Unknown Provider",
|
|
1321
|
+
backend,
|
|
1322
|
+
model
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
function addUserMessageAndBroadcast(sessionManager, sessionId, message, broadcastFn) {
|
|
1326
|
+
const userMessage = sessionManager.addMessage(sessionId, {
|
|
1327
|
+
role: "user",
|
|
1328
|
+
content: message.content,
|
|
1329
|
+
...message.attachments ? { attachments: message.attachments } : {},
|
|
1330
|
+
...message.messageKind ? { messageKind: message.messageKind } : {}
|
|
1331
|
+
});
|
|
1332
|
+
broadcastFn(sessionId, createChatMessageMessage(userMessage));
|
|
1333
|
+
return userMessage;
|
|
1334
|
+
}
|
|
1335
|
+
function processQueueAndRestartTurn(sessionManager, sessionId, drainFn, queueMode, broadcastFn, activeAgents2, startTurnFn, queueMessageFn) {
|
|
1336
|
+
const messages = drainFn(sessionId);
|
|
1337
|
+
const next = messages[0];
|
|
1338
|
+
if (!next) return false;
|
|
1339
|
+
for (const remaining of messages.slice(1)) {
|
|
1340
|
+
if (queueMessageFn) {
|
|
1341
|
+
queueMessageFn(
|
|
1342
|
+
sessionId,
|
|
1343
|
+
queueMode,
|
|
1344
|
+
remaining.content,
|
|
1345
|
+
remaining.attachments,
|
|
1346
|
+
remaining.messageKind
|
|
1347
|
+
);
|
|
1348
|
+
} else {
|
|
1349
|
+
sessionManager.queueMessage(sessionId, queueMode, remaining.content, remaining.attachments);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
broadcastFn(sessionId, createQueueStateMessage(sessionManager.getQueueState(sessionId)));
|
|
1353
|
+
addUserMessageAndBroadcast(
|
|
1354
|
+
sessionManager,
|
|
1355
|
+
sessionId,
|
|
1356
|
+
{
|
|
1357
|
+
content: next.content,
|
|
1358
|
+
...next.attachments ? { attachments: next.attachments } : {},
|
|
1359
|
+
...next.messageKind ? { messageKind: next.messageKind } : {}
|
|
1360
|
+
},
|
|
1361
|
+
broadcastFn
|
|
1362
|
+
);
|
|
1363
|
+
const newController = new AbortController();
|
|
1364
|
+
activeAgents2.set(sessionId, newController);
|
|
1365
|
+
startTurnFn(sessionId, newController);
|
|
1366
|
+
return true;
|
|
1367
|
+
}
|
|
1368
|
+
var activeAgents = /* @__PURE__ */ new Map();
|
|
1369
|
+
var MAX_SEND_QUEUE_SIZE = 1e3;
|
|
1370
|
+
function createWebSocketServer(httpServer, config, getLLMClient, getActiveProvider, sessionManager, providerManager) {
|
|
1371
|
+
const wss = new WebSocketServer({ server: httpServer });
|
|
1372
|
+
const clients = /* @__PURE__ */ new Map();
|
|
1373
|
+
moduleClients = clients;
|
|
1374
|
+
const sessionLLMClients = /* @__PURE__ */ new Map();
|
|
1375
|
+
function getSessionLLMClient(sessionId) {
|
|
1376
|
+
const session = sessionManager.getSession(sessionId);
|
|
1377
|
+
if (!session?.providerId || !session?.providerModel || !providerManager) {
|
|
1378
|
+
return getLLMClient();
|
|
1379
|
+
}
|
|
1380
|
+
const cacheKey = `${session.providerId}:${session.providerModel}`;
|
|
1381
|
+
const cached = sessionLLMClients.get(sessionId);
|
|
1382
|
+
if (cached && cached.key === cacheKey) {
|
|
1383
|
+
return cached.client;
|
|
1384
|
+
}
|
|
1385
|
+
const provider = providerManager.getProviders().find((p) => p.id === session.providerId);
|
|
1386
|
+
if (!provider) {
|
|
1387
|
+
logger.warn("Session references deleted provider, falling back to global", {
|
|
1388
|
+
sessionId,
|
|
1389
|
+
providerId: session.providerId
|
|
1390
|
+
});
|
|
1391
|
+
sessionManager.setSessionProvider(sessionId, null, null);
|
|
1392
|
+
sessionLLMClients.delete(sessionId);
|
|
1393
|
+
return getLLMClient();
|
|
1394
|
+
}
|
|
1395
|
+
const modelConfig = provider.models.find((m) => m.id === session.providerModel);
|
|
1396
|
+
const reasoningEffort = modelConfig?.thinkingEnabled && modelConfig?.thinkingLevel ? modelConfig.thinkingLevel : void 0;
|
|
1397
|
+
const baseUrl = ensureVersionPrefix(provider.url);
|
|
1398
|
+
const sessionConfig = {
|
|
1399
|
+
...config,
|
|
1400
|
+
llm: {
|
|
1401
|
+
...config.llm,
|
|
1402
|
+
baseUrl,
|
|
1403
|
+
model: session.providerModel,
|
|
1404
|
+
...provider.apiKey && { apiKey: provider.apiKey },
|
|
1405
|
+
...provider.thinkingField && { thinkingField: provider.thinkingField },
|
|
1406
|
+
...reasoningEffort && { reasoningEffort }
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
const client = createLLMClient(sessionConfig);
|
|
1410
|
+
if (provider.backend !== "unknown") {
|
|
1411
|
+
client.setBackend(provider.backend);
|
|
1412
|
+
}
|
|
1413
|
+
client.setModel(session.providerModel);
|
|
1414
|
+
sessionLLMClients.set(sessionId, { key: cacheKey, client });
|
|
1415
|
+
return client;
|
|
1416
|
+
}
|
|
1417
|
+
function getSessionStatsIdentity(sessionId) {
|
|
1418
|
+
const session = sessionManager.getSession(sessionId);
|
|
1419
|
+
if (!session?.providerId || !providerManager) {
|
|
1420
|
+
return resolveStatsIdentity(getLLMClient(), getActiveProvider);
|
|
1421
|
+
}
|
|
1422
|
+
const provider = providerManager.getProviders().find((p) => p.id === session.providerId);
|
|
1423
|
+
const client = getSessionLLMClient(sessionId);
|
|
1424
|
+
return {
|
|
1425
|
+
providerId: provider?.id ?? session.providerId,
|
|
1426
|
+
providerName: provider?.name ?? "Unknown Provider",
|
|
1427
|
+
backend: provider?.backend ?? client.getBackend(),
|
|
1428
|
+
model: client.getModel()
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
const isSubscribedToSession = (client, sessionId) => {
|
|
1432
|
+
return client.activeSessionId === sessionId;
|
|
1433
|
+
};
|
|
1434
|
+
const broadcastForSession = (sessionId, msg) => {
|
|
1435
|
+
for (const [clientWs, client] of clients) {
|
|
1436
|
+
if (isSubscribedToSession(client, sessionId) && clientWs.readyState === WebSocket2.OPEN) {
|
|
1437
|
+
clientWs.send(serializeServerMessage({ ...msg, sessionId }));
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
function enqueueSend(client, data, seq) {
|
|
1442
|
+
if (client.sendQueue.length >= MAX_SEND_QUEUE_SIZE) {
|
|
1443
|
+
logger.warn("WebSocket send queue full, dropping message", {
|
|
1444
|
+
queueSize: client.sendQueue.length,
|
|
1445
|
+
sessionId: client.activeSessionId
|
|
1446
|
+
});
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
client.sendQueue.push({ data, seq });
|
|
1450
|
+
processSendQueue(client);
|
|
1451
|
+
}
|
|
1452
|
+
moduleEnqueueSend = enqueueSend;
|
|
1453
|
+
function processSendQueue(client) {
|
|
1454
|
+
if (client.isSending || client.sendQueue.length === 0) {
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
client.isSending = true;
|
|
1458
|
+
const item = client.sendQueue.shift();
|
|
1459
|
+
if (client.ws.readyState === WebSocket2.OPEN) {
|
|
1460
|
+
client.ws.send(item.data, (err) => {
|
|
1461
|
+
if (err) {
|
|
1462
|
+
logger.debug("WebSocket send error", { error: err });
|
|
1463
|
+
}
|
|
1464
|
+
client.isSending = false;
|
|
1465
|
+
client.lastSentSeq = item.seq;
|
|
1466
|
+
processSendQueue(client);
|
|
1467
|
+
});
|
|
1468
|
+
} else {
|
|
1469
|
+
client.isSending = false;
|
|
1470
|
+
processSendQueue(client);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
const llmForSession = (sessionId) => getSessionLLMClient?.(sessionId) ?? getLLMClient();
|
|
1474
|
+
const statsForSession = (sessionId) => getSessionStatsIdentity?.(sessionId) ?? resolveStatsIdentity(getLLMClient(), getActiveProvider);
|
|
1475
|
+
function cleanupAfterTurn(sessionId, controller, sendFn, setRunningOnEarlyReturn) {
|
|
1476
|
+
if (activeAgents.get(sessionId) !== controller) {
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
activeAgents.delete(sessionId);
|
|
1480
|
+
const processed = processQueueAndRestartTurn(
|
|
1481
|
+
sessionManager,
|
|
1482
|
+
sessionId,
|
|
1483
|
+
(sid) => sessionManager.drainAsapMessages(sid),
|
|
1484
|
+
"asap",
|
|
1485
|
+
sendFn,
|
|
1486
|
+
activeAgents,
|
|
1487
|
+
startTurnWithCompletionChain,
|
|
1488
|
+
(sid, mode, content, attachments, messageKind) => sessionManager.queueMessage(sid, mode, content, attachments, messageKind)
|
|
1489
|
+
);
|
|
1490
|
+
if (processed) {
|
|
1491
|
+
if (setRunningOnEarlyReturn) sessionManager.setRunning(sessionId, false);
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
const processedCompletion = processQueueAndRestartTurn(
|
|
1495
|
+
sessionManager,
|
|
1496
|
+
sessionId,
|
|
1497
|
+
(sid) => sessionManager.drainCompletionMessages(sid),
|
|
1498
|
+
"completion",
|
|
1499
|
+
sendFn,
|
|
1500
|
+
activeAgents,
|
|
1501
|
+
startTurnWithCompletionChain
|
|
1502
|
+
);
|
|
1503
|
+
if (processedCompletion) {
|
|
1504
|
+
if (setRunningOnEarlyReturn) sessionManager.setRunning(sessionId, false);
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
sessionManager.clearMessageQueue(sessionId);
|
|
1508
|
+
const contextState = sessionManager.getContextState(sessionId);
|
|
1509
|
+
sendFn(sessionId, createContextStateMessage(contextState));
|
|
1510
|
+
}
|
|
1511
|
+
function startTurnWithCompletionChain(sessionId, controller) {
|
|
1512
|
+
runChatTurn({
|
|
1513
|
+
sessionManager,
|
|
1514
|
+
sessionId,
|
|
1515
|
+
llmClient: llmForSession(sessionId),
|
|
1516
|
+
statsIdentity: statsForSession(sessionId),
|
|
1517
|
+
signal: controller.signal,
|
|
1518
|
+
onMessage: (msg) => broadcastForSession(sessionId, msg)
|
|
1519
|
+
}).catch((error) => {
|
|
1520
|
+
if (error instanceof Error && error.message === "Aborted") {
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
logger.error("Chat turn error", { error });
|
|
1524
|
+
}).finally(() => {
|
|
1525
|
+
cleanupAfterTurn(sessionId, controller, broadcastForSession, false);
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
const broadcastAll = (msg) => {
|
|
1529
|
+
const serialized = serializeServerMessage(msg);
|
|
1530
|
+
for (const [clientWs, client] of clients) {
|
|
1531
|
+
if (clientWs.readyState === WebSocket2.OPEN) {
|
|
1532
|
+
const seq = client.lastSentSeq + 1;
|
|
1533
|
+
enqueueSend(client, serialized, seq);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
};
|
|
1537
|
+
devServerManager.onOutput((workdir, chunk) => {
|
|
1538
|
+
broadcastAll(
|
|
1539
|
+
createServerMessage("devServer.output", {
|
|
1540
|
+
workdir,
|
|
1541
|
+
stream: chunk.stream,
|
|
1542
|
+
content: chunk.content
|
|
1543
|
+
})
|
|
1544
|
+
);
|
|
1545
|
+
});
|
|
1546
|
+
devServerManager.onStateChange((workdir, state, errorMessage) => {
|
|
1547
|
+
broadcastAll(
|
|
1548
|
+
createServerMessage("devServer.state", {
|
|
1549
|
+
workdir,
|
|
1550
|
+
state,
|
|
1551
|
+
errorMessage
|
|
1552
|
+
})
|
|
1553
|
+
);
|
|
1554
|
+
});
|
|
1555
|
+
onProcessEvent((_processId, msg) => {
|
|
1556
|
+
const sessionId = msg.sessionId;
|
|
1557
|
+
if (!sessionId) return;
|
|
1558
|
+
for (const [clientWs, client] of clients) {
|
|
1559
|
+
if (client.activeSessionId === sessionId) {
|
|
1560
|
+
if (clientWs.readyState === WebSocket2.OPEN) {
|
|
1561
|
+
const seq = client.lastSentSeq + 1;
|
|
1562
|
+
enqueueSend(client, serializeServerMessage(msg), seq);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
});
|
|
1567
|
+
wss.on("connection", async (ws, req) => {
|
|
1568
|
+
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
1569
|
+
const token = url.searchParams.get("token");
|
|
1570
|
+
const authConfig = getAuthConfig();
|
|
1571
|
+
if (authConfig?.strategy === "network" && authConfig.encryptedPassword) {
|
|
1572
|
+
if (!token || !await isValidToken(token)) {
|
|
1573
|
+
setTimeout(() => {
|
|
1574
|
+
ws.close(4e3, "Unauthorized");
|
|
1575
|
+
}, 100);
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
logger.debug("WebSocket client connected");
|
|
1580
|
+
clients.set(ws, {
|
|
1581
|
+
ws,
|
|
1582
|
+
activeSessionId: null,
|
|
1583
|
+
activeWorkdir: null,
|
|
1584
|
+
globalSubscription: null,
|
|
1585
|
+
sendQueue: [],
|
|
1586
|
+
isSending: false,
|
|
1587
|
+
lastSentSeq: 0
|
|
1588
|
+
});
|
|
1589
|
+
const eventStore = getEventStore();
|
|
1590
|
+
const { iterator: globalIterator, unsubscribe: globalUnsubscribe } = eventStore.subscribeAll();
|
|
1591
|
+
clients.get(ws).globalSubscription = globalUnsubscribe;
|
|
1592
|
+
(async () => {
|
|
1593
|
+
try {
|
|
1594
|
+
for await (const storedEvent of globalIterator) {
|
|
1595
|
+
if (ws.readyState !== WebSocket2.OPEN) break;
|
|
1596
|
+
const serverMsg = storedEventToServerMessage(storedEvent);
|
|
1597
|
+
if (serverMsg) {
|
|
1598
|
+
const client = clients.get(ws);
|
|
1599
|
+
enqueueSend(
|
|
1600
|
+
client,
|
|
1601
|
+
serializeServerMessage({ ...serverMsg, seq: storedEvent.seq, sessionId: storedEvent.sessionId }),
|
|
1602
|
+
storedEvent.seq
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
} catch (error) {
|
|
1607
|
+
logger.debug("Global event subscription ended", { error });
|
|
1608
|
+
}
|
|
1609
|
+
})();
|
|
1610
|
+
ws.on("message", async (data) => {
|
|
1611
|
+
const message = parseClientMessage(data.toString());
|
|
1612
|
+
if (!message) {
|
|
1613
|
+
const client2 = clients.get(ws);
|
|
1614
|
+
const seq = client2.lastSentSeq + 1;
|
|
1615
|
+
enqueueSend(
|
|
1616
|
+
client2,
|
|
1617
|
+
serializeServerMessage(createErrorMessage("INVALID_MESSAGE", "Invalid message format")),
|
|
1618
|
+
seq
|
|
1619
|
+
);
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
if (message.type.startsWith("terminal.")) {
|
|
1623
|
+
handleTerminalMessage(ws, message);
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
const client = clients.get(ws);
|
|
1627
|
+
try {
|
|
1628
|
+
await handleClientMessage(
|
|
1629
|
+
ws,
|
|
1630
|
+
client,
|
|
1631
|
+
message,
|
|
1632
|
+
getLLMClient,
|
|
1633
|
+
getActiveProvider,
|
|
1634
|
+
sessionManager,
|
|
1635
|
+
broadcastForSession,
|
|
1636
|
+
providerManager,
|
|
1637
|
+
getSessionLLMClient,
|
|
1638
|
+
getSessionStatsIdentity,
|
|
1639
|
+
llmForSession,
|
|
1640
|
+
statsForSession,
|
|
1641
|
+
startTurnWithCompletionChain,
|
|
1642
|
+
cleanupAfterTurn,
|
|
1643
|
+
enqueueSend
|
|
1644
|
+
);
|
|
1645
|
+
} catch (error) {
|
|
1646
|
+
logger.error("Error handling client message", { error, type: message.type });
|
|
1647
|
+
const client2 = clients.get(ws);
|
|
1648
|
+
const seq = client2.lastSentSeq + 1;
|
|
1649
|
+
enqueueSend(
|
|
1650
|
+
client2,
|
|
1651
|
+
serializeServerMessage(
|
|
1652
|
+
createErrorMessage("INTERNAL_ERROR", error instanceof Error ? error.message : "Unknown error", message.id)
|
|
1653
|
+
),
|
|
1654
|
+
seq
|
|
1655
|
+
);
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
ws.on("close", () => {
|
|
1659
|
+
logger.debug("WebSocket client disconnected");
|
|
1660
|
+
const client = clients.get(ws);
|
|
1661
|
+
const disconnectedWorkdir = client?.activeWorkdir ?? null;
|
|
1662
|
+
if (client?.globalSubscription) {
|
|
1663
|
+
client.globalSubscription();
|
|
1664
|
+
}
|
|
1665
|
+
unsubscribeAllFromTerminal(ws);
|
|
1666
|
+
clients.delete(ws);
|
|
1667
|
+
if (disconnectedWorkdir) {
|
|
1668
|
+
const hasRemaining = [...clients.values()].some((c) => c.activeWorkdir === disconnectedWorkdir);
|
|
1669
|
+
if (!hasRemaining) {
|
|
1670
|
+
moduleStopGitPolling(disconnectedWorkdir);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
});
|
|
1674
|
+
ws.on("error", (error) => {
|
|
1675
|
+
logger.error("WebSocket error", { error });
|
|
1676
|
+
});
|
|
1677
|
+
});
|
|
1678
|
+
return {
|
|
1679
|
+
wss,
|
|
1680
|
+
abortSession: (sessionId) => {
|
|
1681
|
+
const controller = activeAgents.get(sessionId);
|
|
1682
|
+
if (controller) {
|
|
1683
|
+
activeAgents.delete(sessionId);
|
|
1684
|
+
controller.abort();
|
|
1685
|
+
return true;
|
|
1686
|
+
}
|
|
1687
|
+
return false;
|
|
1688
|
+
},
|
|
1689
|
+
close: (cb) => wss.close(cb),
|
|
1690
|
+
broadcastForSession
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
async function handleClientMessage(ws, client, message, _getLLMClient, _getActiveProvider, sessionManager, _broadcastForSession, _providerManager, _getSessionLLMClient, _getSessionStatsIdentity, llmForSession, statsForSession, _startTurnWithCompletionChain, cleanupAfterTurn, enqueueSendFn) {
|
|
1694
|
+
const send = (msg) => {
|
|
1695
|
+
if (ws.readyState === WebSocket2.OPEN) {
|
|
1696
|
+
const seq = client.lastSentSeq + 1;
|
|
1697
|
+
enqueueSendFn(client, serializeServerMessage(msg), seq);
|
|
1698
|
+
}
|
|
1699
|
+
};
|
|
1700
|
+
const sendForSession = (sessionId, msg) => {
|
|
1701
|
+
send({ ...msg, sessionId });
|
|
1702
|
+
};
|
|
1703
|
+
const ensureEventStoreSubscription = (_sessionId) => {
|
|
1704
|
+
};
|
|
1705
|
+
switch (message.type) {
|
|
1706
|
+
// =========================================================================
|
|
1707
|
+
// DEPRECATED: All CRUD operations moved to REST API
|
|
1708
|
+
// If you see this error, update your code to use REST endpoints instead.
|
|
1709
|
+
// See docs/REST-API.md for details.
|
|
1710
|
+
// =========================================================================
|
|
1711
|
+
case "project.create":
|
|
1712
|
+
case "project.create-with-dir":
|
|
1713
|
+
case "project.list":
|
|
1714
|
+
case "project.load":
|
|
1715
|
+
case "project.update":
|
|
1716
|
+
case "project.delete":
|
|
1717
|
+
case "settings.get":
|
|
1718
|
+
case "settings.set":
|
|
1719
|
+
case "session.create":
|
|
1720
|
+
case "session.list":
|
|
1721
|
+
case "session.delete":
|
|
1722
|
+
case "session.deleteAll":
|
|
1723
|
+
case "session.setProvider":
|
|
1724
|
+
send(
|
|
1725
|
+
createErrorMessage(
|
|
1726
|
+
"DEPRECATED_MESSAGE_TYPE",
|
|
1727
|
+
`${message.type} removed. Use REST API instead. See docs/REST-API.md`,
|
|
1728
|
+
message.id
|
|
1729
|
+
)
|
|
1730
|
+
);
|
|
1731
|
+
return;
|
|
1732
|
+
// =========================================================================
|
|
1733
|
+
// Session Load - Sets active session for event routing
|
|
1734
|
+
// Note: Data loading is done via REST API: GET /api/sessions/:id
|
|
1735
|
+
// This WebSocket message is ONLY to tell the server which session is active
|
|
1736
|
+
// so it can route real-time events correctly.
|
|
1737
|
+
// =========================================================================
|
|
1738
|
+
case "session.load": {
|
|
1739
|
+
if (!isSessionLoadPayload(message.payload)) {
|
|
1740
|
+
send(createErrorMessage("INVALID_PAYLOAD", "Invalid session.load payload", message.id));
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
const session = sessionManager.getSession(message.payload.sessionId);
|
|
1744
|
+
if (!session) {
|
|
1745
|
+
send(createErrorMessage("NOT_FOUND", "Session not found", message.id));
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
client.activeSessionId = session.id;
|
|
1749
|
+
client.activeWorkdir = session.workdir;
|
|
1750
|
+
if (session.workdir) {
|
|
1751
|
+
const branch = await moduleGitBranch(session.workdir);
|
|
1752
|
+
const { files } = await moduleGitDiff(session.workdir);
|
|
1753
|
+
const msg = createGitStatusMessage(branch, files);
|
|
1754
|
+
send(msg);
|
|
1755
|
+
if (branch) moduleStartGitPolling(session.workdir);
|
|
1756
|
+
}
|
|
1757
|
+
ensureEventStoreSubscription(session.id);
|
|
1758
|
+
send({ type: "ack", payload: { sessionId: session.id }, id: message.id });
|
|
1759
|
+
const sendContextState = () => {
|
|
1760
|
+
const contextState = sessionManager.getContextState(session.id);
|
|
1761
|
+
send(createContextStateMessage(contextState));
|
|
1762
|
+
};
|
|
1763
|
+
sendContextState();
|
|
1764
|
+
const cachedHash = sessionManager.getCachedPrompt(session.id)?.hash;
|
|
1765
|
+
if (cachedHash) {
|
|
1766
|
+
;
|
|
1767
|
+
(async () => {
|
|
1768
|
+
try {
|
|
1769
|
+
const { content: instructionContent } = await getAllInstructions(session.workdir, session.projectId);
|
|
1770
|
+
const runtimeConfig = getRuntimeConfig();
|
|
1771
|
+
const configDir = getGlobalConfigDir(runtimeConfig.mode ?? "production");
|
|
1772
|
+
const skills = await getEnabledSkillMetadata(configDir, runtimeConfig.workdir);
|
|
1773
|
+
const dynamicInputs = JSON.stringify({
|
|
1774
|
+
instructions: instructionContent,
|
|
1775
|
+
skills: skills.map((s) => s.id).sort()
|
|
1776
|
+
});
|
|
1777
|
+
const currentHash = createHash2("sha256").update(dynamicInputs).digest("hex");
|
|
1778
|
+
if (currentHash !== cachedHash) {
|
|
1779
|
+
sessionManager.setDynamicContextChanged(session.id, true);
|
|
1780
|
+
sendContextState();
|
|
1781
|
+
}
|
|
1782
|
+
} catch {
|
|
1783
|
+
}
|
|
1784
|
+
})();
|
|
1785
|
+
}
|
|
1786
|
+
break;
|
|
1787
|
+
}
|
|
1788
|
+
// =========================================================================
|
|
1789
|
+
// Context Management
|
|
1790
|
+
// =========================================================================
|
|
1791
|
+
case "context.compact": {
|
|
1792
|
+
if (!client.activeSessionId) {
|
|
1793
|
+
send(createErrorMessage("NO_SESSION", "No active session", message.id));
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
const session = sessionManager.requireSession(client.activeSessionId);
|
|
1797
|
+
const sessionId = client.activeSessionId;
|
|
1798
|
+
if (session.isRunning) {
|
|
1799
|
+
send(createErrorMessage("SESSION_RUNNING", "Cannot compact while session is running", message.id));
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
send({ type: "ack", payload: {}, id: message.id });
|
|
1803
|
+
appendCompactionPrompt(sessionId, (event) => getEventStore().append(sessionId, event));
|
|
1804
|
+
runChatTurn({
|
|
1805
|
+
sessionManager,
|
|
1806
|
+
sessionId,
|
|
1807
|
+
llmClient: llmForSession(sessionId),
|
|
1808
|
+
statsIdentity: statsForSession(sessionId),
|
|
1809
|
+
onMessage: (msg) => _broadcastForSession(sessionId, msg),
|
|
1810
|
+
initialCompacting: true
|
|
1811
|
+
}).then(() => {
|
|
1812
|
+
const newContextState = sessionManager.getContextState(sessionId);
|
|
1813
|
+
sendForSession(sessionId, createContextStateMessage(newContextState));
|
|
1814
|
+
}).catch((error) => {
|
|
1815
|
+
if (error instanceof Error && error.message === "Aborted") return;
|
|
1816
|
+
logger.error("Compaction failed", { error, sessionId });
|
|
1817
|
+
sendForSession(
|
|
1818
|
+
sessionId,
|
|
1819
|
+
createChatErrorMessage(
|
|
1820
|
+
`Compaction failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
1821
|
+
true
|
|
1822
|
+
)
|
|
1823
|
+
);
|
|
1824
|
+
}).finally(() => {
|
|
1825
|
+
sessionManager.setRunning(sessionId, false);
|
|
1826
|
+
sendForSession(sessionId, createSessionRunningMessage(false));
|
|
1827
|
+
});
|
|
1828
|
+
break;
|
|
1829
|
+
}
|
|
1830
|
+
case "context.checkDynamic": {
|
|
1831
|
+
if (!client.activeSessionId) {
|
|
1832
|
+
send(createErrorMessage("NO_SESSION", "No active session", message.id));
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
const sessionId = client.activeSessionId;
|
|
1836
|
+
const session = sessionManager.requireSession(sessionId);
|
|
1837
|
+
(async () => {
|
|
1838
|
+
try {
|
|
1839
|
+
const { content: instructionContent } = await getAllInstructions(session.workdir, session.projectId);
|
|
1840
|
+
const runtimeConfig = getRuntimeConfig();
|
|
1841
|
+
const configDir = getGlobalConfigDir(runtimeConfig.mode ?? "production");
|
|
1842
|
+
const skills = await getEnabledSkillMetadata(configDir, runtimeConfig.workdir);
|
|
1843
|
+
const mcpTools = _mcpManager ? _mcpManager.getToolDefinitions() : [];
|
|
1844
|
+
const toolFingerprint = mcpTools.map(
|
|
1845
|
+
(t) => `${t.function.name}:${JSON.stringify(t.function.parameters)}`
|
|
1846
|
+
).sort().join("|");
|
|
1847
|
+
const currentHash = computeDynamicContextHash(instructionContent, skills, toolFingerprint);
|
|
1848
|
+
const cachedHash = sessionManager.getCachedPrompt(sessionId)?.hash;
|
|
1849
|
+
if (cachedHash) {
|
|
1850
|
+
if (currentHash !== cachedHash) {
|
|
1851
|
+
if (!sessionManager.getDynamicContextChanged(sessionId)) {
|
|
1852
|
+
sessionManager.setDynamicContextChanged(sessionId, true);
|
|
1853
|
+
const newContextState = sessionManager.getContextState(sessionId);
|
|
1854
|
+
sendForSession(sessionId, createContextStateMessage(newContextState));
|
|
1855
|
+
}
|
|
1856
|
+
} else if (sessionManager.getDynamicContextChanged(sessionId)) {
|
|
1857
|
+
sessionManager.setDynamicContextChanged(sessionId, false);
|
|
1858
|
+
const newContextState = sessionManager.getContextState(sessionId);
|
|
1859
|
+
sendForSession(sessionId, createContextStateMessage(newContextState));
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
send({ type: "ack", payload: {}, id: message.id });
|
|
1863
|
+
} catch (error) {
|
|
1864
|
+
logger.error("Failed to check dynamic context", { error, sessionId });
|
|
1865
|
+
send({ type: "ack", payload: {}, id: message.id });
|
|
1866
|
+
}
|
|
1867
|
+
})();
|
|
1868
|
+
break;
|
|
1869
|
+
}
|
|
1870
|
+
case "context.applyDynamic": {
|
|
1871
|
+
if (!client.activeSessionId) {
|
|
1872
|
+
send(createErrorMessage("NO_SESSION", "No active session", message.id));
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
const sessionId = client.activeSessionId;
|
|
1876
|
+
const session = sessionManager.requireSession(sessionId);
|
|
1877
|
+
if (session.isRunning) {
|
|
1878
|
+
send(createErrorMessage("SESSION_RUNNING", "Cannot apply dynamic context while session is running", message.id));
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
;
|
|
1882
|
+
(async () => {
|
|
1883
|
+
try {
|
|
1884
|
+
const { content: instructionContent } = await getAllInstructions(session.workdir, session.projectId);
|
|
1885
|
+
const runtimeConfig = getRuntimeConfig();
|
|
1886
|
+
const configDir = getGlobalConfigDir(runtimeConfig.mode ?? "production");
|
|
1887
|
+
const skills = await getEnabledSkillMetadata(configDir, runtimeConfig.workdir);
|
|
1888
|
+
const mcpTools = _mcpManager ? _mcpManager.getToolDefinitions() : [];
|
|
1889
|
+
const toolFingerprint = mcpTools.map(
|
|
1890
|
+
(t) => `${t.function.name}:${JSON.stringify(t.function.parameters)}`
|
|
1891
|
+
).sort().join("|");
|
|
1892
|
+
const dynamicHash = computeDynamicContextHash(instructionContent, skills, toolFingerprint);
|
|
1893
|
+
const allAgents = await loadAllAgentsDefault();
|
|
1894
|
+
const subAgentDefs = getSubAgents(allAgents);
|
|
1895
|
+
const systemPrompt = buildTopLevelSystemPrompt(
|
|
1896
|
+
session.workdir,
|
|
1897
|
+
instructionContent || void 0,
|
|
1898
|
+
skills,
|
|
1899
|
+
subAgentDefs
|
|
1900
|
+
);
|
|
1901
|
+
sessionManager.setCachedPrompt(sessionId, systemPrompt, mcpTools, dynamicHash);
|
|
1902
|
+
sessionManager.setDynamicContextChanged(sessionId, false);
|
|
1903
|
+
const newContextState = sessionManager.getContextState(sessionId);
|
|
1904
|
+
sendForSession(sessionId, createContextStateMessage(newContextState));
|
|
1905
|
+
send({ type: "ack", payload: {}, id: message.id });
|
|
1906
|
+
} catch (error) {
|
|
1907
|
+
logger.error("Failed to apply dynamic context", { error, sessionId });
|
|
1908
|
+
sendForSession(
|
|
1909
|
+
sessionId,
|
|
1910
|
+
createChatErrorMessage(
|
|
1911
|
+
`Failed to apply dynamic context: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
1912
|
+
true
|
|
1913
|
+
)
|
|
1914
|
+
);
|
|
1915
|
+
send({ type: "ack", payload: {}, id: message.id });
|
|
1916
|
+
}
|
|
1917
|
+
})();
|
|
1918
|
+
break;
|
|
1919
|
+
}
|
|
1920
|
+
// =========================================================================
|
|
1921
|
+
// Runner (Auto-Loop)
|
|
1922
|
+
// =========================================================================
|
|
1923
|
+
case "runner.launch": {
|
|
1924
|
+
if (!client.activeSessionId) {
|
|
1925
|
+
send(createErrorMessage("NO_SESSION", "No active session", message.id));
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
const session = sessionManager.requireSession(client.activeSessionId);
|
|
1929
|
+
if (session.isRunning) {
|
|
1930
|
+
const launchPayload2 = message.payload;
|
|
1931
|
+
const content = launchPayload2?.content ?? "";
|
|
1932
|
+
const attachments = launchPayload2?.attachments;
|
|
1933
|
+
const workflowId = launchPayload2?.workflowId;
|
|
1934
|
+
let fullContent = content;
|
|
1935
|
+
if (workflowId) {
|
|
1936
|
+
const workflowInfo = `// Workflow: ${workflowId}`;
|
|
1937
|
+
fullContent = content ? `${workflowInfo}
|
|
1938
|
+
|
|
1939
|
+
${content}` : workflowInfo;
|
|
1940
|
+
}
|
|
1941
|
+
sessionManager.queueMessage(client.activeSessionId, "asap", fullContent, attachments, "workflow-launch");
|
|
1942
|
+
const queueState = sessionManager.getQueueState(client.activeSessionId);
|
|
1943
|
+
send({
|
|
1944
|
+
type: "queue.state",
|
|
1945
|
+
payload: { success: true, queueState },
|
|
1946
|
+
id: message.id
|
|
1947
|
+
});
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
const launchPayloadEarly = message.payload;
|
|
1951
|
+
const pendingCriteria = session.criteria.filter((c) => c.status.type !== "passed");
|
|
1952
|
+
if (!launchPayloadEarly?.workflowId && pendingCriteria.length === 0) {
|
|
1953
|
+
send(createErrorMessage("NO_WORK", "No pending criteria to work on", message.id));
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
const sessionId = client.activeSessionId;
|
|
1957
|
+
if (session.phase === "blocked") {
|
|
1958
|
+
logger.info("User launched runner - resetting blocked state", { sessionId });
|
|
1959
|
+
sessionManager.setPhase(sessionId, "build");
|
|
1960
|
+
}
|
|
1961
|
+
const launchPayload = message.payload;
|
|
1962
|
+
const launchAttachments = launchPayload?.attachments;
|
|
1963
|
+
const hasUserContent = launchPayload?.content && typeof launchPayload.content === "string" && launchPayload.content.trim();
|
|
1964
|
+
const hasUserAttachments = launchAttachments && launchAttachments.length > 0;
|
|
1965
|
+
const hasUserMessage = hasUserContent || hasUserAttachments;
|
|
1966
|
+
sessionManager.setRunning(sessionId, true);
|
|
1967
|
+
sendForSession(sessionId, createSessionRunningMessage(true));
|
|
1968
|
+
const controller = new AbortController();
|
|
1969
|
+
const existingController = activeAgents.get(sessionId);
|
|
1970
|
+
if (existingController) {
|
|
1971
|
+
logger.warn("Aborting existing agent before starting new one", { sessionId });
|
|
1972
|
+
existingController.abort();
|
|
1973
|
+
}
|
|
1974
|
+
activeAgents.set(sessionId, controller);
|
|
1975
|
+
send({ type: "ack", payload: {}, id: message.id });
|
|
1976
|
+
ensureEventStoreSubscription(sessionId);
|
|
1977
|
+
logger.info("Runner launching", { sessionId, pendingCriteria: pendingCriteria.length });
|
|
1978
|
+
runOrchestrator({
|
|
1979
|
+
sessionManager,
|
|
1980
|
+
sessionId,
|
|
1981
|
+
llmClient: llmForSession(sessionId),
|
|
1982
|
+
statsIdentity: statsForSession(sessionId),
|
|
1983
|
+
injectWorkflowKickoff: !hasUserMessage,
|
|
1984
|
+
...launchPayload?.workflowId ? { workflowId: launchPayload.workflowId } : {},
|
|
1985
|
+
...launchPayload?.subGroup ? { subGroup: launchPayload.subGroup } : {},
|
|
1986
|
+
...hasUserMessage ? {
|
|
1987
|
+
userMessage: {
|
|
1988
|
+
content: hasUserContent ? launchPayload.content : "",
|
|
1989
|
+
...hasUserAttachments ? { attachments: launchAttachments } : {}
|
|
1990
|
+
}
|
|
1991
|
+
} : {},
|
|
1992
|
+
signal: controller.signal,
|
|
1993
|
+
onMessage: (msg) => sendForSession(sessionId, msg)
|
|
1994
|
+
// For path confirmation dialogs
|
|
1995
|
+
}).catch((error) => {
|
|
1996
|
+
if (error instanceof Error && error.message === "Aborted") {
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
logger.error("Runner error", { error, sessionId });
|
|
2000
|
+
}).finally(() => {
|
|
2001
|
+
sessionManager.setRunning(sessionId, false);
|
|
2002
|
+
sendForSession(sessionId, createSessionRunningMessage(false));
|
|
2003
|
+
cleanupAfterTurn(sessionId, controller, sendForSession, true);
|
|
2004
|
+
});
|
|
2005
|
+
break;
|
|
2006
|
+
}
|
|
2007
|
+
// =========================================================================
|
|
2008
|
+
// Path Confirmation
|
|
2009
|
+
// =========================================================================
|
|
2010
|
+
case "path.confirm": {
|
|
2011
|
+
send(
|
|
2012
|
+
createErrorMessage(
|
|
2013
|
+
"DEPRECATED",
|
|
2014
|
+
"path.confirm removed. Use REST API: POST /api/sessions/:id/confirm-path",
|
|
2015
|
+
message.id
|
|
2016
|
+
)
|
|
2017
|
+
);
|
|
2018
|
+
break;
|
|
2019
|
+
}
|
|
2020
|
+
// =========================================================================
|
|
2021
|
+
// Ask User
|
|
2022
|
+
// =========================================================================
|
|
2023
|
+
case "ask.answer": {
|
|
2024
|
+
if (!client.activeSessionId) {
|
|
2025
|
+
send(createErrorMessage("NO_SESSION", "No active session", message.id));
|
|
2026
|
+
return;
|
|
2027
|
+
}
|
|
2028
|
+
if (!isAskAnswerPayload(message.payload)) {
|
|
2029
|
+
send(createErrorMessage("INVALID_PAYLOAD", "Invalid ask.answer payload", message.id));
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
const { callId, answer, skip } = message.payload;
|
|
2033
|
+
const found = provideAnswer(callId, answer, skip);
|
|
2034
|
+
if (!found) {
|
|
2035
|
+
send(createErrorMessage("NOT_FOUND", "No pending question with that ID", message.id));
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
logger.debug("Ask user answer received", {
|
|
2039
|
+
sessionId: client.activeSessionId,
|
|
2040
|
+
callId,
|
|
2041
|
+
answerLength: answer.length
|
|
2042
|
+
});
|
|
2043
|
+
send({ type: "ack", payload: {}, id: message.id });
|
|
2044
|
+
break;
|
|
2045
|
+
}
|
|
2046
|
+
default: {
|
|
2047
|
+
send(createErrorMessage("UNKNOWN_MESSAGE", `Unknown message type: ${message.type}`, message.id));
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
export {
|
|
2053
|
+
terminalManager,
|
|
2054
|
+
loadDefaultWorkflows,
|
|
2055
|
+
loadUserWorkflows,
|
|
2056
|
+
loadProjectWorkflows,
|
|
2057
|
+
loadAllWorkflows,
|
|
2058
|
+
saveWorkflowToProject,
|
|
2059
|
+
deleteProjectWorkflow,
|
|
2060
|
+
getDefaultWorkflowIds,
|
|
2061
|
+
isDefaultWorkflow,
|
|
2062
|
+
findWorkflowById,
|
|
2063
|
+
workflowExists,
|
|
2064
|
+
saveWorkflow,
|
|
2065
|
+
deleteWorkflow,
|
|
2066
|
+
TEMPLATE_VARIABLES,
|
|
2067
|
+
loadServerAuthConfig,
|
|
2068
|
+
getAuthConfig,
|
|
2069
|
+
requiresAuth,
|
|
2070
|
+
hasPassword,
|
|
2071
|
+
verifyPassword,
|
|
2072
|
+
tokenFromPassword,
|
|
2073
|
+
isValidToken,
|
|
2074
|
+
setMcpManagerForWs,
|
|
2075
|
+
createWebSocketServer
|
|
2076
|
+
};
|
|
2077
|
+
//# sourceMappingURL=chunk-4W2Z4B5U.js.map
|