openfox 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-defaults/builder.agent.md +38 -0
- package/dist/agent-defaults/code-reviewer.agent.md +21 -0
- package/dist/agent-defaults/debugger.agent.md +21 -0
- package/dist/agent-defaults/planner.agent.md +35 -0
- package/dist/agent-defaults/test-generator.agent.md +22 -0
- package/dist/agent-defaults/verifier.agent.md +41 -0
- package/dist/{chunk-FF6Z2MC3.js → chunk-5ZUU23XF.js} +18 -1
- package/dist/{chunk-IVHCPQO2.js → chunk-ARFK3RZW.js} +1 -1
- package/dist/{chunk-IKAWIOXM.js → chunk-FUUYYDVG.js} +387 -73
- package/dist/{chunk-YQ36AK5A.js → chunk-JFQEATRC.js} +623 -327
- package/dist/{chunk-43SWNGYW.js → chunk-L475ND2Z.js} +9 -9
- package/dist/{chunk-6MOGZMGX.js → chunk-L4XDA3V7.js} +2 -2
- package/dist/{chunk-EI46G5WO.js → chunk-MKAHOQBI.js} +10 -10
- package/dist/cli/dev.js +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/{config-6GJ4SVFH.js → config-BMXSDOZB.js} +3 -3
- package/dist/{init-UVZDOHMX.js → init-PLRTRSDC.js} +6 -6
- package/dist/{protocol-BJsm88OW.d.ts → protocol-BFFz3WM0.d.ts} +14 -3
- package/dist/{provider-LCE6WZA6.js → provider-ETVY64A2.js} +5 -5
- package/dist/{serve-27DZK34K.js → serve-EOKIXTWM.js} +8 -8
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +5 -5
- package/dist/shared/index.d.ts +2 -2
- package/dist/shared/index.js +1 -1
- package/dist/{tools-R4YQAYUF.js → tools-QMIIDJTV.js} +7 -5
- package/dist/web/assets/index-CAD7Iv_d.js +122 -0
- package/dist/web/assets/{index-BSlSWZrM.css → index-DHmZmhnR.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +34 -16
- package/dist/branch.api-WDHKZJS6.js.map +0 -1
- package/dist/chunk-26NRL3HX.js.map +0 -1
- package/dist/chunk-43SWNGYW.js.map +0 -1
- package/dist/chunk-4Z2OZ6NX.js.map +0 -1
- package/dist/chunk-6MOGZMGX.js.map +0 -1
- package/dist/chunk-AF7XS4OI.js.map +0 -1
- package/dist/chunk-EI46G5WO.js.map +0 -1
- package/dist/chunk-FF6Z2MC3.js.map +0 -1
- package/dist/chunk-IKAWIOXM.js.map +0 -1
- package/dist/chunk-IVHCPQO2.js.map +0 -1
- package/dist/chunk-MTX5Y3V3.js.map +0 -1
- package/dist/chunk-N2XYX4ZA.js.map +0 -1
- package/dist/chunk-QY7BMXWT.js.map +0 -1
- package/dist/chunk-U62ZECSQ.js.map +0 -1
- package/dist/chunk-YQ36AK5A.js.map +0 -1
- package/dist/cli/dev.js.map +0 -1
- package/dist/cli/index.js.map +0 -1
- package/dist/config-6GJ4SVFH.js.map +0 -1
- package/dist/init-UVZDOHMX.js.map +0 -1
- package/dist/paths-4BPXWB2W.js.map +0 -1
- package/dist/project-creator-CWPKRIKI.js.map +0 -1
- package/dist/provider-LCE6WZA6.js.map +0 -1
- package/dist/serve-27DZK34K.js.map +0 -1
- package/dist/server/index.js.map +0 -1
- package/dist/settings-D5O3TMLD.js.map +0 -1
- package/dist/shared/index.js.map +0 -1
- package/dist/tools-R4YQAYUF.js.map +0 -1
- package/dist/web/assets/index-BMNDCsbJ.js +0 -120
- /package/dist/command-defaults/{defaults/commit-push.command.md → commit-push.command.md} +0 -0
- /package/dist/command-defaults/{defaults/init.command.md → init.command.md} +0 -0
- /package/dist/command-defaults/{defaults/test-ui.command.md → test-ui.command.md} +0 -0
- /package/dist/skill-defaults/{defaults/playwright-cli.skill.md → playwright-cli.skill.md} +0 -0
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
createToolRegistry,
|
|
32
32
|
deleteAgent,
|
|
33
33
|
deleteSkill,
|
|
34
|
+
devServerManager,
|
|
34
35
|
emitAssistantMessageStart,
|
|
35
36
|
emitContextCompacted,
|
|
36
37
|
emitContextState,
|
|
@@ -95,7 +96,7 @@ import {
|
|
|
95
96
|
setSkillEnabled,
|
|
96
97
|
skillExists,
|
|
97
98
|
storedEventToServerMessage
|
|
98
|
-
} from "./chunk-
|
|
99
|
+
} from "./chunk-FUUYYDVG.js";
|
|
99
100
|
import {
|
|
100
101
|
getSetting,
|
|
101
102
|
setSetting
|
|
@@ -114,9 +115,12 @@ import {
|
|
|
114
115
|
import {
|
|
115
116
|
computeSessionStats
|
|
116
117
|
} from "./chunk-QY7BMXWT.js";
|
|
118
|
+
import {
|
|
119
|
+
createServerMessage
|
|
120
|
+
} from "./chunk-ARFK3RZW.js";
|
|
117
121
|
import {
|
|
118
122
|
createProviderManager
|
|
119
|
-
} from "./chunk-
|
|
123
|
+
} from "./chunk-L4XDA3V7.js";
|
|
120
124
|
import {
|
|
121
125
|
SessionNotFoundError,
|
|
122
126
|
createLLMClient,
|
|
@@ -125,7 +129,7 @@ import {
|
|
|
125
129
|
getBackendDisplayName,
|
|
126
130
|
getLlmStatus,
|
|
127
131
|
getModelProfile
|
|
128
|
-
} from "./chunk-
|
|
132
|
+
} from "./chunk-5ZUU23XF.js";
|
|
129
133
|
import {
|
|
130
134
|
logger,
|
|
131
135
|
setLogLevel
|
|
@@ -749,7 +753,8 @@ function getPromptAwareToolResponse(prompt) {
|
|
|
749
753
|
tools: [
|
|
750
754
|
{ name: "get_criteria", arguments: {} },
|
|
751
755
|
{ name: "write_file", arguments: { path: "src/test.ts", content: "export const created = true" } },
|
|
752
|
-
{ name: "complete_criterion", arguments: { id: "test-file", reason: "Created the requested file" } }
|
|
756
|
+
{ name: "complete_criterion", arguments: { id: "test-file", reason: "Created the requested file" } },
|
|
757
|
+
{ name: "step_done", arguments: {} }
|
|
753
758
|
],
|
|
754
759
|
response: "Reviewed the criteria, created the file, and completed the criterion."
|
|
755
760
|
};
|
|
@@ -758,7 +763,8 @@ function getPromptAwareToolResponse(prompt) {
|
|
|
758
763
|
return {
|
|
759
764
|
tools: [
|
|
760
765
|
{ name: "write_file", arguments: { path: "src/utils.ts", content: "export const created = true" } },
|
|
761
|
-
{ name: "complete_criterion", arguments: { id: "file-created", reason: "Created the requested file" } }
|
|
766
|
+
{ name: "complete_criterion", arguments: { id: "file-created", reason: "Created the requested file" } },
|
|
767
|
+
{ name: "step_done", arguments: {} }
|
|
762
768
|
],
|
|
763
769
|
response: "Created the file and completed the criterion."
|
|
764
770
|
};
|
|
@@ -865,12 +871,48 @@ function getConversationAwareToolResponse(request) {
|
|
|
865
871
|
);
|
|
866
872
|
completedCriteria.push("file-created");
|
|
867
873
|
}
|
|
868
|
-
if (tools.length
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
response: `Completed builder work for: ${completedCriteria.join(", ")}.`
|
|
872
|
-
};
|
|
874
|
+
if (tools.length === 0) {
|
|
875
|
+
tools.push({ name: "complete_criterion", arguments: { id: "mock-crit", reason: "Completed for testing" } });
|
|
876
|
+
completedCriteria.push("mock-crit");
|
|
873
877
|
}
|
|
878
|
+
tools.push({ name: "step_done", arguments: {} });
|
|
879
|
+
return {
|
|
880
|
+
tools,
|
|
881
|
+
response: `Completed builder work for: ${completedCriteria.join(", ")}.`
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
if (/fulfil the \d+ criteria/i.test(prompt)) {
|
|
885
|
+
return {
|
|
886
|
+
tools: [
|
|
887
|
+
{ name: "complete_criterion", arguments: { id: "mock-crit", reason: "Completed for testing" } },
|
|
888
|
+
{ name: "step_done", arguments: {} }
|
|
889
|
+
],
|
|
890
|
+
response: "Completed criterion and finished step."
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
if (/Continue working on the acceptance criteria/i.test(prompt)) {
|
|
894
|
+
const tools = [];
|
|
895
|
+
if (conversationText.includes("file-created")) {
|
|
896
|
+
tools.push(
|
|
897
|
+
{ name: "write_file", arguments: { path: "src/utils.ts", content: "export const created = true" } },
|
|
898
|
+
{ name: "complete_criterion", arguments: { id: "file-created", reason: "Created the requested file" } }
|
|
899
|
+
);
|
|
900
|
+
} else if (conversationText.includes("trivial-pass")) {
|
|
901
|
+
tools.push({ name: "complete_criterion", arguments: { id: "trivial-pass", reason: "Trivial criterion passes immediately" } });
|
|
902
|
+
} else if (conversationText.includes("verify-fail")) {
|
|
903
|
+
tools.push({ name: "complete_criterion", arguments: { id: "verify-fail", reason: "Prepared criterion for verification" } });
|
|
904
|
+
} else {
|
|
905
|
+
if (/1 criteria remaining/i.test(prompt)) {
|
|
906
|
+
tools.push({ name: "complete_criterion", arguments: { id: "trivial-pass", reason: "Trivial criterion passes immediately" } });
|
|
907
|
+
} else {
|
|
908
|
+
tools.push({ name: "complete_criterion", arguments: { id: "mock-crit", reason: "Completed for testing" } });
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
tools.push({ name: "step_done", arguments: {} });
|
|
912
|
+
return {
|
|
913
|
+
tools,
|
|
914
|
+
response: "Completed builder work."
|
|
915
|
+
};
|
|
874
916
|
}
|
|
875
917
|
if (/Verify each criterion marked \[NEEDS VERIFICATION\]\./i.test(prompt)) {
|
|
876
918
|
const alreadyVerified = request.messages.some(
|
|
@@ -1199,43 +1241,60 @@ async function runBuilderTurn(options, turnMetrics) {
|
|
|
1199
1241
|
const allAgents = await loadAllAgentsDefault();
|
|
1200
1242
|
const builderDef = findAgentById("builder", allAgents);
|
|
1201
1243
|
const subAgentDefs = getSubAgents(allAgents);
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
const
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1244
|
+
let stepDoneCalled = false;
|
|
1245
|
+
return {
|
|
1246
|
+
...await runTopLevelAgentLoop({
|
|
1247
|
+
mode: "builder",
|
|
1248
|
+
sessionManager: sessionManager2,
|
|
1249
|
+
sessionId,
|
|
1250
|
+
llmClient: options.llmClient,
|
|
1251
|
+
statsIdentity,
|
|
1252
|
+
signal: options.signal,
|
|
1253
|
+
onMessage: options.onMessage,
|
|
1254
|
+
assembleRequest: (input) => assembleAgentRequest({ ...input, agentDef: builderDef, subAgentDefs }),
|
|
1255
|
+
getToolRegistry: () => {
|
|
1256
|
+
const baseRegistry = getToolRegistryForAgent(builderDef);
|
|
1257
|
+
if (options.injectStepDone === true) {
|
|
1258
|
+
return baseRegistry;
|
|
1259
|
+
}
|
|
1260
|
+
return {
|
|
1261
|
+
tools: baseRegistry.tools.filter((t) => t.name !== "step_done"),
|
|
1262
|
+
definitions: baseRegistry.definitions.filter((d) => d.type === "function" && d.function.name !== "step_done"),
|
|
1263
|
+
execute: baseRegistry.execute
|
|
1264
|
+
};
|
|
1265
|
+
},
|
|
1266
|
+
onToolExecuted: (toolCall, toolResult) => {
|
|
1267
|
+
if (toolCall.name === "step_done" && toolResult.success) {
|
|
1268
|
+
stepDoneCalled = true;
|
|
1269
|
+
}
|
|
1270
|
+
if (toolResult.success && ["write_file", "edit_file"].includes(toolCall.name)) {
|
|
1271
|
+
const path = toolCall.arguments["path"];
|
|
1272
|
+
sessionManager2.addModifiedFile(sessionId, path);
|
|
1273
|
+
}
|
|
1274
|
+
},
|
|
1275
|
+
injectKickoff: () => {
|
|
1276
|
+
if (options.injectBuilderKickoff !== true) return;
|
|
1277
|
+
const session = sessionManager2.requireSession(sessionId);
|
|
1278
|
+
const events = eventStore.getEvents(sessionId);
|
|
1279
|
+
const hasBuilderKickoff = events.some((e) => {
|
|
1280
|
+
if (e.type !== "message.start") return false;
|
|
1281
|
+
const data = e.data;
|
|
1282
|
+
return data.messageKind === "auto-prompt" && data.content?.includes("fulfil the");
|
|
1283
|
+
});
|
|
1284
|
+
if (!hasBuilderKickoff) {
|
|
1285
|
+
const kickoffMsgId = crypto.randomUUID();
|
|
1286
|
+
const kickoffContent = BUILDER_KICKOFF_PROMPT(session.criteria.length);
|
|
1287
|
+
eventStore.append(sessionId, createMessageStartEvent(kickoffMsgId, "user", kickoffContent, {
|
|
1288
|
+
...getCurrentWindowMessageOptions(sessionId) ?? {},
|
|
1289
|
+
isSystemGenerated: true,
|
|
1290
|
+
messageKind: "auto-prompt"
|
|
1291
|
+
}));
|
|
1292
|
+
eventStore.append(sessionId, { type: "message.done", data: { messageId: kickoffMsgId } });
|
|
1293
|
+
}
|
|
1236
1294
|
}
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1295
|
+
}, turnMetrics),
|
|
1296
|
+
stepDoneCalled
|
|
1297
|
+
};
|
|
1239
1298
|
}
|
|
1240
1299
|
function buildSnapshot(sessionManager2, sessionId, lastStats) {
|
|
1241
1300
|
const eventStore = getEventStore();
|
|
@@ -1467,6 +1526,18 @@ function executeShellCommand(command, cwd, timeout, signal) {
|
|
|
1467
1526
|
}
|
|
1468
1527
|
|
|
1469
1528
|
// src/server/workflows/executor.ts
|
|
1529
|
+
var TEMPLATE_VARIABLES = [
|
|
1530
|
+
{ name: "workdir", description: "Working directory of the session" },
|
|
1531
|
+
{ name: "reason", description: 'Human-readable reason (e.g. "2 criteria remaining")' },
|
|
1532
|
+
{ name: "stepOutput", description: "Structured output from the previous step (content, stdout, stderr, exitCode, etc.)" },
|
|
1533
|
+
{ name: "verifierFindings", description: "@deprecated Use stepOutput.content instead. Output from the last sub-agent step" },
|
|
1534
|
+
{ name: "previousStepOutput", description: "@deprecated Use stepOutput.stdout instead. Output from the last shell step" },
|
|
1535
|
+
{ name: "criteriaCount", description: "Total number of criteria" },
|
|
1536
|
+
{ name: "pendingCount", description: "Number of pending/failed criteria" },
|
|
1537
|
+
{ name: "summary", description: "Session summary generated by planner" },
|
|
1538
|
+
{ name: "criteriaList", description: "Formatted list of all criteria with status" },
|
|
1539
|
+
{ name: "modifiedFiles", description: "List of modified files" }
|
|
1540
|
+
];
|
|
1470
1541
|
function formatCriteriaList(criteria) {
|
|
1471
1542
|
if (criteria.length === 0) return "(none)";
|
|
1472
1543
|
return criteria.map((c) => {
|
|
@@ -1479,7 +1550,16 @@ function formatModifiedFiles(session) {
|
|
|
1479
1550
|
return files.length > 0 ? files.map((f) => `- ${f}`).join("\n") : "(none)";
|
|
1480
1551
|
}
|
|
1481
1552
|
function resolveTemplate(template, ctx) {
|
|
1482
|
-
|
|
1553
|
+
let result = template;
|
|
1554
|
+
for (const { name } of TEMPLATE_VARIABLES) {
|
|
1555
|
+
if (name === "stepOutput" || name === "verifierFindings" || name === "previousStepOutput") continue;
|
|
1556
|
+
const value = String(ctx[name]);
|
|
1557
|
+
result = result.replace(new RegExp(`\\{\\{${name}\\}\\}`, "g"), value);
|
|
1558
|
+
}
|
|
1559
|
+
result = result.replace(/\{\{stepOutput\.(\w+)\}\}/g, (_, key) => ctx.stepOutput[key] ?? "");
|
|
1560
|
+
result = result.replace(/\{\{verifierFindings\}\}/g, ctx.stepOutput["content"] ?? "");
|
|
1561
|
+
result = result.replace(/\{\{previousStepOutput\}\}/g, ctx.stepOutput["stdout"] ?? "");
|
|
1562
|
+
return result;
|
|
1483
1563
|
}
|
|
1484
1564
|
function evaluateCondition(condition, criteria, stepOutcome) {
|
|
1485
1565
|
switch (condition.type) {
|
|
@@ -1495,7 +1575,7 @@ function evaluateCondition(condition, criteria, stepOutcome) {
|
|
|
1495
1575
|
return criteria.some((c) => c.status.type !== "passed");
|
|
1496
1576
|
case "step_result":
|
|
1497
1577
|
if (!stepOutcome) return false;
|
|
1498
|
-
return
|
|
1578
|
+
return stepOutcome.result === condition.result;
|
|
1499
1579
|
case "always":
|
|
1500
1580
|
return true;
|
|
1501
1581
|
}
|
|
@@ -1522,8 +1602,7 @@ async function executeWorkflow(workflow, options) {
|
|
|
1522
1602
|
const startTime = performance.now();
|
|
1523
1603
|
let iterations = 0;
|
|
1524
1604
|
let currentStepId = workflow.entryStep;
|
|
1525
|
-
let
|
|
1526
|
-
let lastShellOutput = "";
|
|
1605
|
+
let lastStepOutput = {};
|
|
1527
1606
|
let isFirstBuilderEntry = true;
|
|
1528
1607
|
const messagesBeforeWorkflow = sessionManager2.requireSession(sessionId).messages.length;
|
|
1529
1608
|
const stepsById = /* @__PURE__ */ new Map();
|
|
@@ -1557,6 +1636,13 @@ async function executeWorkflow(workflow, options) {
|
|
|
1557
1636
|
{ ...startWindowOpts ?? {}, isSystemGenerated: true, messageKind: "workflow-started" }
|
|
1558
1637
|
));
|
|
1559
1638
|
eventStore.append(sessionId, { type: "message.done", data: { messageId: startMsgId } });
|
|
1639
|
+
if (options.userMessage) {
|
|
1640
|
+
sessionManager2.addMessage(sessionId, {
|
|
1641
|
+
role: "user",
|
|
1642
|
+
content: options.userMessage.content,
|
|
1643
|
+
...options.userMessage.attachments ? { attachments: options.userMessage.attachments } : {}
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1560
1646
|
while (iterations < workflow.settings.maxIterations) {
|
|
1561
1647
|
if (signal?.aborted) {
|
|
1562
1648
|
logger.debug("Workflow executor aborted", { sessionId, iterations });
|
|
@@ -1582,13 +1668,14 @@ async function executeWorkflow(workflow, options) {
|
|
|
1582
1668
|
const templateCtx = {
|
|
1583
1669
|
workdir: session.workdir,
|
|
1584
1670
|
reason: buildReason(criteria),
|
|
1585
|
-
verifierFindings:
|
|
1586
|
-
previousStepOutput:
|
|
1671
|
+
verifierFindings: lastStepOutput["content"] ?? "",
|
|
1672
|
+
previousStepOutput: lastStepOutput["stdout"] ?? "",
|
|
1587
1673
|
criteriaCount: criteria.length,
|
|
1588
1674
|
pendingCount: criteria.filter((c) => c.status.type !== "passed").length,
|
|
1589
1675
|
summary: session.summary ?? "No summary available",
|
|
1590
1676
|
criteriaList: formatCriteriaList(criteria),
|
|
1591
|
-
modifiedFiles: formatModifiedFiles(session)
|
|
1677
|
+
modifiedFiles: formatModifiedFiles(session),
|
|
1678
|
+
stepOutput: lastStepOutput
|
|
1592
1679
|
};
|
|
1593
1680
|
sessionManager2.setPhase(sessionId, step.phase);
|
|
1594
1681
|
logger.debug("Workflow step executing", { sessionId, iteration: iterations, stepId: step.id, stepType: step.type });
|
|
@@ -1596,8 +1683,13 @@ async function executeWorkflow(workflow, options) {
|
|
|
1596
1683
|
switch (step.type) {
|
|
1597
1684
|
case "agent": {
|
|
1598
1685
|
const agentStep = step;
|
|
1686
|
+
const STEP_DONE_PROMPT = "\n\nOnce you're done, call step_done()";
|
|
1687
|
+
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()";
|
|
1688
|
+
let promptContent = null;
|
|
1689
|
+
let nudgeContent = null;
|
|
1599
1690
|
if (isFirstBuilderEntry && agentStep.prompt) {
|
|
1600
|
-
const
|
|
1691
|
+
const resolvedPrompt = resolveTemplate(agentStep.prompt, templateCtx);
|
|
1692
|
+
promptContent = resolvedPrompt + STEP_DONE_PROMPT;
|
|
1601
1693
|
const promptMsgId = crypto.randomUUID();
|
|
1602
1694
|
eventStore.append(sessionId, createMessageStartEvent(promptMsgId, "user", promptContent, {
|
|
1603
1695
|
...currentWindowMessageOptions ?? {},
|
|
@@ -1605,8 +1697,14 @@ async function executeWorkflow(workflow, options) {
|
|
|
1605
1697
|
messageKind: "auto-prompt"
|
|
1606
1698
|
}));
|
|
1607
1699
|
eventStore.append(sessionId, { type: "message.done", data: { messageId: promptMsgId } });
|
|
1608
|
-
} else if (!isFirstBuilderEntry
|
|
1609
|
-
const
|
|
1700
|
+
} else if (!isFirstBuilderEntry) {
|
|
1701
|
+
const parts = [];
|
|
1702
|
+
if (agentStep.nudgePrompt) {
|
|
1703
|
+
const resolvedNudge = resolveTemplate(agentStep.nudgePrompt, templateCtx);
|
|
1704
|
+
parts.push(resolvedNudge);
|
|
1705
|
+
}
|
|
1706
|
+
parts.push(STEP_DONE_NUDGE);
|
|
1707
|
+
nudgeContent = parts.join("\n\n");
|
|
1610
1708
|
const nudgeMsgId = crypto.randomUUID();
|
|
1611
1709
|
eventStore.append(sessionId, createMessageStartEvent(nudgeMsgId, "user", nudgeContent, {
|
|
1612
1710
|
...currentWindowMessageOptions ?? {},
|
|
@@ -1616,17 +1714,25 @@ async function executeWorkflow(workflow, options) {
|
|
|
1616
1714
|
eventStore.append(sessionId, { type: "message.done", data: { messageId: nudgeMsgId } });
|
|
1617
1715
|
}
|
|
1618
1716
|
const turnMetrics = new TurnMetrics();
|
|
1619
|
-
await runBuilderTurn({
|
|
1717
|
+
const agentResult = await runBuilderTurn({
|
|
1620
1718
|
sessionManager: sessionManager2,
|
|
1621
1719
|
sessionId,
|
|
1622
1720
|
llmClient,
|
|
1721
|
+
injectStepDone: true,
|
|
1623
1722
|
...options.statsIdentity ? { statsIdentity: options.statsIdentity } : {},
|
|
1624
1723
|
...isFirstBuilderEntry && !agentStep.prompt && options.injectBuilderKickoff === true ? { injectBuilderKickoff: true } : {},
|
|
1625
1724
|
...signal ? { signal } : {},
|
|
1626
1725
|
...onMessage ? { onMessage } : {}
|
|
1627
1726
|
}, turnMetrics);
|
|
1628
1727
|
isFirstBuilderEntry = false;
|
|
1629
|
-
|
|
1728
|
+
const agentReturnValue = agentResult.returnValueResult ?? "completed";
|
|
1729
|
+
const stepDoneCalled = agentResult.stepDoneCalled ?? false;
|
|
1730
|
+
lastStepOutput = { ...agentResult.returnValueContent ? { content: agentResult.returnValueContent } : {}, ...agentResult.returnValueResult ? { result: agentResult.returnValueResult } : {}, stepDoneCalled: String(stepDoneCalled) };
|
|
1731
|
+
stepOutcome = { result: agentReturnValue, output: lastStepOutput };
|
|
1732
|
+
if (!stepDoneCalled) {
|
|
1733
|
+
logger.debug("step_done not called, looping agent step", { sessionId, stepId: step.id, iteration: iterations });
|
|
1734
|
+
continue;
|
|
1735
|
+
}
|
|
1630
1736
|
break;
|
|
1631
1737
|
}
|
|
1632
1738
|
case "sub_agent": {
|
|
@@ -1638,10 +1744,15 @@ async function executeWorkflow(workflow, options) {
|
|
|
1638
1744
|
const agentDef = findAgentById(subStep.subAgentType, allAgents);
|
|
1639
1745
|
if (!agentDef) {
|
|
1640
1746
|
logger.error("Sub-agent definition not found", { subAgentType: subStep.subAgentType });
|
|
1641
|
-
stepOutcome = {
|
|
1747
|
+
stepOutcome = { result: "error", output: {} };
|
|
1642
1748
|
break;
|
|
1643
1749
|
}
|
|
1644
1750
|
const toolRegistry = getToolRegistryForAgent(agentDef);
|
|
1751
|
+
const filteredToolRegistry = {
|
|
1752
|
+
tools: toolRegistry.tools.filter((t) => t.name !== "step_done"),
|
|
1753
|
+
definitions: toolRegistry.definitions.filter((d) => d.type === "function" && d.function.name !== "step_done"),
|
|
1754
|
+
execute: toolRegistry.execute
|
|
1755
|
+
};
|
|
1645
1756
|
const nudgeConfig = subStep.subAgentType === "verifier" && !subStep.nudgePrompt ? createVerifierNudgeConfig() : void 0;
|
|
1646
1757
|
const result = await executeSubAgent({
|
|
1647
1758
|
subAgentType: subStep.subAgentType,
|
|
@@ -1649,15 +1760,15 @@ async function executeWorkflow(workflow, options) {
|
|
|
1649
1760
|
sessionManager: sessionManager2,
|
|
1650
1761
|
sessionId,
|
|
1651
1762
|
llmClient,
|
|
1652
|
-
toolRegistry,
|
|
1763
|
+
toolRegistry: filteredToolRegistry,
|
|
1653
1764
|
turnMetrics,
|
|
1654
1765
|
statsIdentity: options.statsIdentity ?? { providerId: "", providerName: "", backend: "unknown", model: llmClient.getModel() },
|
|
1655
1766
|
...signal ? { signal } : {},
|
|
1656
1767
|
...onMessage ? { onMessage } : {},
|
|
1657
1768
|
nudgeConfig
|
|
1658
1769
|
});
|
|
1659
|
-
|
|
1660
|
-
stepOutcome = {
|
|
1770
|
+
lastStepOutput = { content: result.content ?? "", ...result.result ? { result: result.result } : {} };
|
|
1771
|
+
stepOutcome = { result: result.result ?? "success", output: lastStepOutput };
|
|
1661
1772
|
break;
|
|
1662
1773
|
}
|
|
1663
1774
|
case "shell": {
|
|
@@ -1673,7 +1784,7 @@ async function executeWorkflow(workflow, options) {
|
|
|
1673
1784
|
}));
|
|
1674
1785
|
const result = await executeShellCommand(command, session.workdir, timeout, signal);
|
|
1675
1786
|
const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
1676
|
-
|
|
1787
|
+
lastStepOutput = { stdout: result.stdout ?? "", stderr: result.stderr ?? "", exitCode: String(result.exitCode) };
|
|
1677
1788
|
const outputContent = output ? `Exit code: ${result.exitCode}
|
|
1678
1789
|
\`\`\`
|
|
1679
1790
|
${output.slice(0, 1e4)}
|
|
@@ -1686,7 +1797,7 @@ ${output.slice(0, 1e4)}
|
|
|
1686
1797
|
messageKind: "correction"
|
|
1687
1798
|
}));
|
|
1688
1799
|
eventStore.append(sessionId, { type: "message.done", data: { messageId: outputMsgId } });
|
|
1689
|
-
stepOutcome = {
|
|
1800
|
+
stepOutcome = { result: successCodes.includes(result.exitCode) ? "success" : "failure", output: lastStepOutput };
|
|
1690
1801
|
break;
|
|
1691
1802
|
}
|
|
1692
1803
|
}
|
|
@@ -1898,7 +2009,22 @@ function listSessions() {
|
|
|
1898
2009
|
s.updated_at,
|
|
1899
2010
|
s.title,
|
|
1900
2011
|
s.provider_id,
|
|
1901
|
-
s.provider_model
|
|
2012
|
+
s.provider_model,
|
|
2013
|
+
COALESCE(
|
|
2014
|
+
(SELECT COUNT(*) FROM events e
|
|
2015
|
+
WHERE e.session_id = s.id
|
|
2016
|
+
AND e.event_type = 'message.start'
|
|
2017
|
+
AND json_extract(e.payload, '$.role') IN ('user', 'assistant'))
|
|
2018
|
+
+
|
|
2019
|
+
COALESCE(
|
|
2020
|
+
(SELECT SUM(json_array_length(json_extract(e.payload, '$.messages'))) FROM events e
|
|
2021
|
+
WHERE e.session_id = s.id
|
|
2022
|
+
AND e.event_type = 'turn.snapshot'
|
|
2023
|
+
AND json_extract(e.payload, '$.messages') IS NOT NULL),
|
|
2024
|
+
0
|
|
2025
|
+
),
|
|
2026
|
+
0
|
|
2027
|
+
) as message_count
|
|
1902
2028
|
FROM sessions s
|
|
1903
2029
|
ORDER BY s.updated_at DESC
|
|
1904
2030
|
`).all();
|
|
@@ -1916,8 +2042,9 @@ function listSessions() {
|
|
|
1916
2042
|
updatedAt: row.updated_at,
|
|
1917
2043
|
criteriaCount: 0,
|
|
1918
2044
|
// Derived from events
|
|
1919
|
-
criteriaCompleted: 0
|
|
2045
|
+
criteriaCompleted: 0,
|
|
1920
2046
|
// Derived from events
|
|
2047
|
+
messageCount: row.message_count
|
|
1921
2048
|
}));
|
|
1922
2049
|
}
|
|
1923
2050
|
function listSessionsByProject(projectId) {
|
|
@@ -1934,7 +2061,22 @@ function listSessionsByProject(projectId) {
|
|
|
1934
2061
|
s.updated_at,
|
|
1935
2062
|
s.title,
|
|
1936
2063
|
s.provider_id,
|
|
1937
|
-
s.provider_model
|
|
2064
|
+
s.provider_model,
|
|
2065
|
+
COALESCE(
|
|
2066
|
+
(SELECT COUNT(*) FROM events e
|
|
2067
|
+
WHERE e.session_id = s.id
|
|
2068
|
+
AND e.event_type = 'message.start'
|
|
2069
|
+
AND json_extract(e.payload, '$.role') IN ('user', 'assistant'))
|
|
2070
|
+
+
|
|
2071
|
+
COALESCE(
|
|
2072
|
+
(SELECT SUM(json_array_length(json_extract(e.payload, '$.messages'))) FROM events e
|
|
2073
|
+
WHERE e.session_id = s.id
|
|
2074
|
+
AND e.event_type = 'turn.snapshot'
|
|
2075
|
+
AND json_extract(e.payload, '$.messages') IS NOT NULL),
|
|
2076
|
+
0
|
|
2077
|
+
),
|
|
2078
|
+
0
|
|
2079
|
+
) as message_count
|
|
1938
2080
|
FROM sessions s
|
|
1939
2081
|
WHERE s.project_id = ?
|
|
1940
2082
|
ORDER BY s.updated_at DESC
|
|
@@ -1953,8 +2095,9 @@ function listSessionsByProject(projectId) {
|
|
|
1953
2095
|
updatedAt: row.updated_at,
|
|
1954
2096
|
criteriaCount: 0,
|
|
1955
2097
|
// Derived from events
|
|
1956
|
-
criteriaCompleted: 0
|
|
2098
|
+
criteriaCompleted: 0,
|
|
1957
2099
|
// Derived from events
|
|
2100
|
+
messageCount: row.message_count
|
|
1958
2101
|
}));
|
|
1959
2102
|
}
|
|
1960
2103
|
function deleteSession(id) {
|
|
@@ -1973,7 +2116,7 @@ Example inputs and outputs:
|
|
|
1973
2116
|
|
|
1974
2117
|
User message: {message}`;
|
|
1975
2118
|
async function generateSessionName(options) {
|
|
1976
|
-
const { userMessage, llmClient } = options;
|
|
2119
|
+
const { userMessage, llmClient, signal } = options;
|
|
1977
2120
|
try {
|
|
1978
2121
|
const prompt = SESSION_NAME_PROMPT.replace("{message}", userMessage);
|
|
1979
2122
|
const messages = [
|
|
@@ -1982,10 +2125,12 @@ async function generateSessionName(options) {
|
|
|
1982
2125
|
content: prompt
|
|
1983
2126
|
}
|
|
1984
2127
|
];
|
|
2128
|
+
const timeoutSignal = AbortSignal.timeout(6e4);
|
|
2129
|
+
const composedSignal = signal ? AbortSignal.any([timeoutSignal, signal]) : timeoutSignal;
|
|
1985
2130
|
const response = await llmClient.complete({
|
|
1986
2131
|
messages,
|
|
1987
2132
|
tools: [],
|
|
1988
|
-
signal:
|
|
2133
|
+
signal: composedSignal,
|
|
1989
2134
|
disableThinking: true
|
|
1990
2135
|
});
|
|
1991
2136
|
let name = response.content.trim();
|
|
@@ -2187,6 +2332,28 @@ function createWebSocketServer(httpServer, config, getLLMClient, getActiveProvid
|
|
|
2187
2332
|
}
|
|
2188
2333
|
}
|
|
2189
2334
|
});
|
|
2335
|
+
const broadcastAll = (msg) => {
|
|
2336
|
+
const serialized = serializeServerMessage(msg);
|
|
2337
|
+
for (const [clientWs] of clients) {
|
|
2338
|
+
if (clientWs.readyState === WebSocket.OPEN) {
|
|
2339
|
+
clientWs.send(serialized);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
};
|
|
2343
|
+
devServerManager.onOutput((workdir, chunk) => {
|
|
2344
|
+
broadcastAll(createServerMessage("devServer.output", {
|
|
2345
|
+
workdir,
|
|
2346
|
+
stream: chunk.stream,
|
|
2347
|
+
content: chunk.content
|
|
2348
|
+
}));
|
|
2349
|
+
});
|
|
2350
|
+
devServerManager.onStateChange((workdir, state, errorMessage) => {
|
|
2351
|
+
broadcastAll(createServerMessage("devServer.state", {
|
|
2352
|
+
workdir,
|
|
2353
|
+
state,
|
|
2354
|
+
errorMessage
|
|
2355
|
+
}));
|
|
2356
|
+
});
|
|
2190
2357
|
wss.on("connection", (ws) => {
|
|
2191
2358
|
logger.debug("WebSocket client connected");
|
|
2192
2359
|
clients.set(ws, { ws, activeSessionId: null, subscribedSessions: /* @__PURE__ */ new Map(), eventStoreSubscriptions: /* @__PURE__ */ new Map() });
|
|
@@ -2247,6 +2414,7 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
|
|
|
2247
2414
|
controller.abort();
|
|
2248
2415
|
cancelQuestionsForSession(sessionId, reason);
|
|
2249
2416
|
cancelPathConfirmationsForSession(sessionId, reason);
|
|
2417
|
+
sessionManager2.clearMessageQueue(sessionId);
|
|
2250
2418
|
sessionManager2.setRunning(sessionId, false);
|
|
2251
2419
|
sendForSession(sessionId, createSessionRunningMessage(false));
|
|
2252
2420
|
const contextState = sessionManager2.getContextState(sessionId);
|
|
@@ -2461,8 +2629,7 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
|
|
|
2461
2629
|
ensureEventStoreSubscription(session.id);
|
|
2462
2630
|
const eventStore = getEventStore();
|
|
2463
2631
|
const events = eventStore.getEvents(session.id);
|
|
2464
|
-
|
|
2465
|
-
messages = buildMessagesFromStoredEvents(events);
|
|
2632
|
+
const messages = buildMessagesFromStoredEvents(events);
|
|
2466
2633
|
logger.debug("Loaded messages from EventStore", { sessionId: session.id, eventCount: events.length, messageCount: messages.length });
|
|
2467
2634
|
sendForSession(session.id, createSessionStateMessage(session, messages, message.id));
|
|
2468
2635
|
const contextState = sessionManager2.getContextState(session.id);
|
|
@@ -2558,46 +2725,6 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
|
|
|
2558
2725
|
const sessionId = client.activeSessionId;
|
|
2559
2726
|
const eventStore = getEventStore();
|
|
2560
2727
|
ensureEventStoreSubscription(sessionId);
|
|
2561
|
-
await maybeAutoCompactContext({
|
|
2562
|
-
sessionManager: sessionManager2,
|
|
2563
|
-
sessionId,
|
|
2564
|
-
llmClient: llmForSession(sessionId),
|
|
2565
|
-
statsIdentity: statsForSession(sessionId)
|
|
2566
|
-
});
|
|
2567
|
-
const userMessage = sessionManager2.addMessage(sessionId, {
|
|
2568
|
-
role: "user",
|
|
2569
|
-
content: message.payload.content,
|
|
2570
|
-
...message.payload.attachments && { attachments: message.payload.attachments },
|
|
2571
|
-
...message.payload.messageKind && { messageKind: message.payload.messageKind },
|
|
2572
|
-
...message.payload.isSystemGenerated && { isSystemGenerated: message.payload.isSystemGenerated }
|
|
2573
|
-
});
|
|
2574
|
-
const messageCount = getSessionMessageCount(sessionId);
|
|
2575
|
-
if (needsNameGeneration(currentSession.metadata.title, messageCount)) {
|
|
2576
|
-
generateSessionName({
|
|
2577
|
-
userMessage: message.payload.content,
|
|
2578
|
-
llmClient: llmForSession(sessionId)
|
|
2579
|
-
}).then(async (result) => {
|
|
2580
|
-
if (result.success && result.name) {
|
|
2581
|
-
updateSessionMetadata(sessionId, { title: result.name });
|
|
2582
|
-
eventStore.append(sessionId, {
|
|
2583
|
-
type: "session.name_generated",
|
|
2584
|
-
data: { name: result.name }
|
|
2585
|
-
});
|
|
2586
|
-
const updatedSession = sessionManager2.getSession(sessionId);
|
|
2587
|
-
if (updatedSession) {
|
|
2588
|
-
const events = eventStore.getEvents(sessionId);
|
|
2589
|
-
const messages = buildMessagesFromStoredEvents(events);
|
|
2590
|
-
broadcastForSession(sessionId, createSessionStateMessage(updatedSession, messages));
|
|
2591
|
-
}
|
|
2592
|
-
logger.info("Session name generated", { sessionId, name: result.name });
|
|
2593
|
-
}
|
|
2594
|
-
}).catch((error) => {
|
|
2595
|
-
logger.warn("Session name generation failed", { sessionId, error: error instanceof Error ? error.message : error });
|
|
2596
|
-
});
|
|
2597
|
-
}
|
|
2598
|
-
sessionManager2.setRunning(sessionId, true);
|
|
2599
|
-
sendForSession(sessionId, createChatMessageMessage(userMessage));
|
|
2600
|
-
sendForSession(sessionId, createSessionRunningMessage(true));
|
|
2601
2728
|
const controller = new AbortController();
|
|
2602
2729
|
const existingController = activeAgents.get(sessionId);
|
|
2603
2730
|
if (existingController) {
|
|
@@ -2605,8 +2732,66 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
|
|
|
2605
2732
|
existingController.abort();
|
|
2606
2733
|
}
|
|
2607
2734
|
activeAgents.set(sessionId, controller);
|
|
2735
|
+
sessionManager2.setRunning(sessionId, true);
|
|
2736
|
+
sendForSession(sessionId, createSessionRunningMessage(true));
|
|
2608
2737
|
send({ type: "ack", payload: {}, id: message.id });
|
|
2609
|
-
|
|
2738
|
+
try {
|
|
2739
|
+
await maybeAutoCompactContext({
|
|
2740
|
+
sessionManager: sessionManager2,
|
|
2741
|
+
sessionId,
|
|
2742
|
+
llmClient: llmForSession(sessionId),
|
|
2743
|
+
statsIdentity: statsForSession(sessionId),
|
|
2744
|
+
signal: controller.signal
|
|
2745
|
+
});
|
|
2746
|
+
if (controller.signal.aborted) {
|
|
2747
|
+
break;
|
|
2748
|
+
}
|
|
2749
|
+
const userMessage = sessionManager2.addMessage(sessionId, {
|
|
2750
|
+
role: "user",
|
|
2751
|
+
content: message.payload.content,
|
|
2752
|
+
...message.payload.attachments && { attachments: message.payload.attachments },
|
|
2753
|
+
...message.payload.messageKind && { messageKind: message.payload.messageKind },
|
|
2754
|
+
...message.payload.isSystemGenerated && { isSystemGenerated: message.payload.isSystemGenerated }
|
|
2755
|
+
});
|
|
2756
|
+
sendForSession(sessionId, createChatMessageMessage(userMessage));
|
|
2757
|
+
const messageCount = getSessionMessageCount(sessionId);
|
|
2758
|
+
if (needsNameGeneration(currentSession.metadata.title, messageCount)) {
|
|
2759
|
+
generateSessionName({
|
|
2760
|
+
userMessage: message.payload.content,
|
|
2761
|
+
llmClient: llmForSession(sessionId),
|
|
2762
|
+
signal: controller.signal
|
|
2763
|
+
}).then(async (result) => {
|
|
2764
|
+
if (result.success && result.name) {
|
|
2765
|
+
updateSessionMetadata(sessionId, { title: result.name });
|
|
2766
|
+
eventStore.append(sessionId, {
|
|
2767
|
+
type: "session.name_generated",
|
|
2768
|
+
data: { name: result.name }
|
|
2769
|
+
});
|
|
2770
|
+
const updatedSession = sessionManager2.getSession(sessionId);
|
|
2771
|
+
if (updatedSession) {
|
|
2772
|
+
const events = eventStore.getEvents(sessionId);
|
|
2773
|
+
const messages = buildMessagesFromStoredEvents(events);
|
|
2774
|
+
broadcastForSession(sessionId, createSessionStateMessage(updatedSession, messages));
|
|
2775
|
+
}
|
|
2776
|
+
logger.info("Session name generated", { sessionId, name: result.name });
|
|
2777
|
+
}
|
|
2778
|
+
}).catch((error) => {
|
|
2779
|
+
logger.warn("Session name generation failed", { sessionId, error: error instanceof Error ? error.message : error });
|
|
2780
|
+
});
|
|
2781
|
+
}
|
|
2782
|
+
startTurnWithCompletionChain(sessionId, controller);
|
|
2783
|
+
} catch (error) {
|
|
2784
|
+
if (activeAgents.get(sessionId) === controller) {
|
|
2785
|
+
activeAgents.delete(sessionId);
|
|
2786
|
+
}
|
|
2787
|
+
sessionManager2.setRunning(sessionId, false);
|
|
2788
|
+
sendForSession(sessionId, createSessionRunningMessage(false));
|
|
2789
|
+
const contextState = sessionManager2.getContextState(sessionId);
|
|
2790
|
+
sendForSession(sessionId, createContextStateMessage(contextState));
|
|
2791
|
+
if (!(error instanceof Error && error.message === "Aborted")) {
|
|
2792
|
+
logger.error("Chat send pre-turn error", { sessionId, error });
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2610
2795
|
break;
|
|
2611
2796
|
}
|
|
2612
2797
|
case "chat.stop": {
|
|
@@ -2750,7 +2935,8 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
|
|
|
2750
2935
|
send(createErrorMessage("ALREADY_RUNNING", "Session is already running", message.id));
|
|
2751
2936
|
return;
|
|
2752
2937
|
}
|
|
2753
|
-
|
|
2938
|
+
const acceptPayloadEarly = message.payload;
|
|
2939
|
+
if (!acceptPayloadEarly?.workflowId && session.criteria.length === 0) {
|
|
2754
2940
|
send(createErrorMessage("NO_CRITERIA", "Cannot accept: no criteria defined", message.id));
|
|
2755
2941
|
return;
|
|
2756
2942
|
}
|
|
@@ -2815,13 +3001,18 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
|
|
|
2815
3001
|
}
|
|
2816
3002
|
activeAgents.set(sessionId, controller);
|
|
2817
3003
|
const acceptPayload = message.payload;
|
|
3004
|
+
const acceptAttachments = acceptPayload?.attachments;
|
|
3005
|
+
const hasAcceptContent = acceptPayload?.content && typeof acceptPayload.content === "string" && acceptPayload.content.trim();
|
|
3006
|
+
const hasAcceptAttachments = acceptAttachments && acceptAttachments.length > 0;
|
|
3007
|
+
const hasAcceptMessage = hasAcceptContent || hasAcceptAttachments;
|
|
2818
3008
|
await runOrchestrator({
|
|
2819
3009
|
sessionManager: sessionManager2,
|
|
2820
3010
|
sessionId,
|
|
2821
3011
|
llmClient: llmForSession(sessionId),
|
|
2822
3012
|
statsIdentity: statsForSession(sessionId),
|
|
2823
|
-
injectBuilderKickoff:
|
|
3013
|
+
injectBuilderKickoff: !hasAcceptMessage,
|
|
2824
3014
|
...acceptPayload?.workflowId ? { workflowId: acceptPayload.workflowId } : {},
|
|
3015
|
+
...hasAcceptMessage ? { userMessage: { content: hasAcceptContent ? acceptPayload.content : "", ...hasAcceptAttachments ? { attachments: acceptAttachments } : {} } } : {},
|
|
2825
3016
|
signal: controller.signal,
|
|
2826
3017
|
onMessage: (msg) => sendForSession(sessionId, msg)
|
|
2827
3018
|
// For path confirmation dialogs
|
|
@@ -2911,7 +3102,6 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
|
|
|
2911
3102
|
}
|
|
2912
3103
|
const contextState = sessionManager2.getContextState(sessionId);
|
|
2913
3104
|
const tokensBefore = contextState.currentTokens;
|
|
2914
|
-
const eventStore = getEventStore();
|
|
2915
3105
|
send({ type: "ack", payload: {}, id: message.id });
|
|
2916
3106
|
(async () => {
|
|
2917
3107
|
try {
|
|
@@ -2956,28 +3146,23 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
|
|
|
2956
3146
|
send(createErrorMessage("INVALID_MODE", "Runner can only be launched in builder mode", message.id));
|
|
2957
3147
|
return;
|
|
2958
3148
|
}
|
|
3149
|
+
const launchPayloadEarly = message.payload;
|
|
2959
3150
|
const pendingCriteria = session.criteria.filter((c) => c.status.type !== "passed");
|
|
2960
|
-
if (pendingCriteria.length === 0) {
|
|
3151
|
+
if (!launchPayloadEarly?.workflowId && pendingCriteria.length === 0) {
|
|
2961
3152
|
send(createErrorMessage("NO_WORK", "No pending criteria to work on", message.id));
|
|
2962
3153
|
return;
|
|
2963
3154
|
}
|
|
2964
3155
|
const sessionId = client.activeSessionId;
|
|
2965
|
-
const eventStore = getEventStore();
|
|
2966
3156
|
if (session.phase === "blocked") {
|
|
2967
3157
|
logger.info("User launched runner - resetting blocked state", { sessionId });
|
|
2968
3158
|
sessionManager2.setPhase(sessionId, "build");
|
|
2969
3159
|
sessionManager2.resetAllCriteriaAttempts(sessionId);
|
|
2970
3160
|
}
|
|
2971
3161
|
const launchPayload = message.payload;
|
|
2972
|
-
const
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
role: "user",
|
|
2977
|
-
content: launchPayload.content,
|
|
2978
|
-
...attachments ? { attachments } : {}
|
|
2979
|
-
});
|
|
2980
|
-
}
|
|
3162
|
+
const launchAttachments = launchPayload?.attachments;
|
|
3163
|
+
const hasUserContent = launchPayload?.content && typeof launchPayload.content === "string" && launchPayload.content.trim();
|
|
3164
|
+
const hasUserAttachments = launchAttachments && launchAttachments.length > 0;
|
|
3165
|
+
const hasUserMessage = hasUserContent || hasUserAttachments;
|
|
2981
3166
|
sessionManager2.setRunning(sessionId, true);
|
|
2982
3167
|
sendForSession(sessionId, createSessionRunningMessage(true));
|
|
2983
3168
|
const controller = new AbortController();
|
|
@@ -2997,6 +3182,7 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
|
|
|
2997
3182
|
statsIdentity: statsForSession(sessionId),
|
|
2998
3183
|
injectBuilderKickoff: !hasUserMessage,
|
|
2999
3184
|
...launchPayload?.workflowId ? { workflowId: launchPayload.workflowId } : {},
|
|
3185
|
+
...hasUserMessage ? { userMessage: { content: hasUserContent ? launchPayload.content : "", ...hasUserAttachments ? { attachments: launchAttachments } : {} } } : {},
|
|
3000
3186
|
signal: controller.signal,
|
|
3001
3187
|
onMessage: (msg) => sendForSession(sessionId, msg)
|
|
3002
3188
|
// For path confirmation dialogs
|
|
@@ -4931,153 +5117,11 @@ async function deleteCommand(configDir, commandId) {
|
|
|
4931
5117
|
}
|
|
4932
5118
|
}
|
|
4933
5119
|
|
|
4934
|
-
// src/server/
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
const db = initDatabase(config);
|
|
4940
|
-
initEventStore(db);
|
|
4941
|
-
const configDir = getGlobalConfigDir(config.mode ?? "production");
|
|
4942
|
-
await ensureDefaultSkills(configDir);
|
|
4943
|
-
await ensureDefaultCommands(configDir);
|
|
4944
|
-
await ensureDefaultAgents(configDir);
|
|
4945
|
-
await ensureDefaultWorkflows(configDir);
|
|
4946
|
-
const sessionManager2 = new SessionManager();
|
|
4947
|
-
const providerManager = createProviderManager(config);
|
|
4948
|
-
const useMock = process.env["OPENFOX_MOCK_LLM"] === "true";
|
|
4949
|
-
const getMockClient = useMock ? createMockLLMClient : null;
|
|
4950
|
-
const getLLMClient = () => getMockClient ? getMockClient() : providerManager.getLLMClient();
|
|
4951
|
-
if (useMock) {
|
|
4952
|
-
logger.info("Using MOCK LLM client - deterministic responses for testing");
|
|
4953
|
-
}
|
|
4954
|
-
async function initLLM() {
|
|
4955
|
-
const llmClient = getLLMClient();
|
|
4956
|
-
let backend = "unknown";
|
|
4957
|
-
const useMock2 = process.env["OPENFOX_MOCK_LLM"] === "true";
|
|
4958
|
-
if (config.llm.backend === "auto") {
|
|
4959
|
-
backend = await detectBackend(config.llm.baseUrl, void 0, useMock2);
|
|
4960
|
-
llmClient.setBackend(backend);
|
|
4961
|
-
if (!useMock2) {
|
|
4962
|
-
logger.info("Auto-detected LLM backend", { backend: getBackendDisplayName(backend) });
|
|
4963
|
-
}
|
|
4964
|
-
} else {
|
|
4965
|
-
backend = config.llm.backend;
|
|
4966
|
-
llmClient.setBackend(backend);
|
|
4967
|
-
if (!useMock2) {
|
|
4968
|
-
logger.info("Using configured LLM backend", { backend: getBackendDisplayName(backend) });
|
|
4969
|
-
}
|
|
4970
|
-
}
|
|
4971
|
-
const detected = await detectModel(config.llm.baseUrl);
|
|
4972
|
-
if (detected) {
|
|
4973
|
-
llmClient.setModel(detected);
|
|
4974
|
-
if (!useMock2) {
|
|
4975
|
-
logger.info("Auto-detected LLM model", { model: detected, backend: getBackendDisplayName(backend) });
|
|
4976
|
-
}
|
|
4977
|
-
} else {
|
|
4978
|
-
if (!useMock2) {
|
|
4979
|
-
logger.warn("Could not auto-detect model, using config", { model: config.llm.model });
|
|
4980
|
-
}
|
|
4981
|
-
}
|
|
4982
|
-
}
|
|
4983
|
-
initLLM().catch((err) => logger.error("LLM initialization failed", { error: err instanceof Error ? err.message : String(err) }));
|
|
4984
|
-
const toolRegistry = createToolRegistry();
|
|
4985
|
-
const app = express();
|
|
4986
|
-
app.use(cors());
|
|
4987
|
-
app.use(express.json());
|
|
4988
|
-
app.get("/api/health", (_req, res) => {
|
|
4989
|
-
res.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
4990
|
-
});
|
|
4991
|
-
app.post("/api/sessions", async (req, res) => {
|
|
4992
|
-
const { workdir, title } = req.body;
|
|
4993
|
-
const session = sessionManager2.createSession(workdir, title);
|
|
4994
|
-
res.status(201).json({ session });
|
|
4995
|
-
});
|
|
4996
|
-
app.get("/api/sessions/:id", (req, res) => {
|
|
4997
|
-
const session = sessionManager2.getSession(req.params.id);
|
|
4998
|
-
if (!session) {
|
|
4999
|
-
return res.status(404).json({ error: "Session not found" });
|
|
5000
|
-
}
|
|
5001
|
-
res.json({ session });
|
|
5002
|
-
});
|
|
5003
|
-
app.delete("/api/sessions/:id", (req, res) => {
|
|
5004
|
-
sessionManager2.deleteSession(req.params["id"]);
|
|
5005
|
-
res.json({ success: true });
|
|
5006
|
-
});
|
|
5007
|
-
app.delete("/api/projects/:projectId/sessions", (req, res) => {
|
|
5008
|
-
const projectId = req.params["projectId"];
|
|
5009
|
-
const project = sessionManager2.getProject(projectId);
|
|
5010
|
-
if (!project) {
|
|
5011
|
-
return res.status(404).json({ error: "Project not found" });
|
|
5012
|
-
}
|
|
5013
|
-
sessionManager2.deleteAllSessions(projectId, project.workdir);
|
|
5014
|
-
res.json({ success: true });
|
|
5015
|
-
});
|
|
5016
|
-
app.get("/api/config", (_req, res) => {
|
|
5017
|
-
const llmClient = getLLMClient();
|
|
5018
|
-
const activeProvider = providerManager.getActiveProvider();
|
|
5019
|
-
res.json({
|
|
5020
|
-
model: llmClient.getModel(),
|
|
5021
|
-
maxContext: config.context.maxTokens,
|
|
5022
|
-
llmUrl: activeProvider?.url ?? config.llm.baseUrl,
|
|
5023
|
-
llmStatus: getLlmStatus(),
|
|
5024
|
-
backend: llmClient.getBackend(),
|
|
5025
|
-
workdir: config.workdir,
|
|
5026
|
-
// Include provider info
|
|
5027
|
-
providers: providerManager.getProviders(),
|
|
5028
|
-
activeProviderId: providerManager.getActiveProviderId(),
|
|
5029
|
-
defaultModelSelection: config.defaultModelSelection
|
|
5030
|
-
});
|
|
5031
|
-
});
|
|
5032
|
-
app.post("/api/model/refresh", async (_req, res) => {
|
|
5033
|
-
const llmClient = getLLMClient();
|
|
5034
|
-
const currentModel = providerManager.getCurrentModel();
|
|
5035
|
-
if (currentModel === "auto") {
|
|
5036
|
-
const activeProvider = providerManager.getActiveProvider();
|
|
5037
|
-
const baseUrl = activeProvider?.url ?? config.llm.baseUrl;
|
|
5038
|
-
const detected = await detectModel(baseUrl);
|
|
5039
|
-
if (detected) {
|
|
5040
|
-
llmClient.setModel(detected);
|
|
5041
|
-
return res.json({ model: detected, source: "detected", llmStatus: getLlmStatus(), backend: llmClient.getBackend() });
|
|
5042
|
-
}
|
|
5043
|
-
}
|
|
5044
|
-
res.json({ model: llmClient.getModel(), source: "cached", llmStatus: getLlmStatus(), backend: llmClient.getBackend() });
|
|
5045
|
-
});
|
|
5046
|
-
app.get("/api/providers", (_req, res) => {
|
|
5047
|
-
const providers = providerManager.getProviders().map((p) => ({
|
|
5048
|
-
...p,
|
|
5049
|
-
status: providerManager.getProviderStatus(p.id)
|
|
5050
|
-
}));
|
|
5051
|
-
res.json({
|
|
5052
|
-
providers,
|
|
5053
|
-
activeProviderId: providerManager.getActiveProviderId()
|
|
5054
|
-
});
|
|
5055
|
-
});
|
|
5056
|
-
app.get("/api/providers/:id/models", async (req, res) => {
|
|
5057
|
-
const { id } = req.params;
|
|
5058
|
-
const models = await providerManager.getProviderModels(id);
|
|
5059
|
-
res.json({ models });
|
|
5060
|
-
});
|
|
5061
|
-
app.post("/api/providers/:id/activate", async (req, res) => {
|
|
5062
|
-
const { id } = req.params;
|
|
5063
|
-
const body = req.body;
|
|
5064
|
-
const result = await providerManager.activateProvider(id, body.model ? { model: body.model } : void 0);
|
|
5065
|
-
if (!result.success) {
|
|
5066
|
-
return res.status(400).json({ error: result.error });
|
|
5067
|
-
}
|
|
5068
|
-
const llmClient = getLLMClient();
|
|
5069
|
-
const { loadGlobalConfig, saveGlobalConfig, setDefaultModelSelection } = await import("./config-6GJ4SVFH.js");
|
|
5070
|
-
const globalConfig = await loadGlobalConfig(config.mode ?? "production");
|
|
5071
|
-
const updatedConfig = setDefaultModelSelection(globalConfig, id, llmClient.getModel());
|
|
5072
|
-
await saveGlobalConfig(config.mode ?? "production", updatedConfig);
|
|
5073
|
-
res.json({
|
|
5074
|
-
success: true,
|
|
5075
|
-
activeProviderId: id,
|
|
5076
|
-
model: llmClient.getModel(),
|
|
5077
|
-
backend: llmClient.getBackend()
|
|
5078
|
-
});
|
|
5079
|
-
});
|
|
5080
|
-
app.get("/api/skills", async (_req, res) => {
|
|
5120
|
+
// src/server/routes/skills.ts
|
|
5121
|
+
import { Router } from "express";
|
|
5122
|
+
function createSkillRoutes(configDir) {
|
|
5123
|
+
const router = Router();
|
|
5124
|
+
router.get("/", async (_req, res) => {
|
|
5081
5125
|
const [skills, defaultIds, modifiedIds] = await Promise.all([
|
|
5082
5126
|
loadAllSkills(configDir),
|
|
5083
5127
|
getDefaultSkillIds(),
|
|
@@ -5092,15 +5136,15 @@ async function createServerHandle(config) {
|
|
|
5092
5136
|
modifiedIds
|
|
5093
5137
|
});
|
|
5094
5138
|
});
|
|
5095
|
-
|
|
5139
|
+
router.get("/default-ids", async (_req, res) => {
|
|
5096
5140
|
const ids = await getDefaultSkillIds();
|
|
5097
5141
|
res.json({ ids });
|
|
5098
5142
|
});
|
|
5099
|
-
|
|
5143
|
+
router.post("/restore-all-defaults", async (_req, res) => {
|
|
5100
5144
|
const count = await restoreAllDefaultSkills(configDir);
|
|
5101
5145
|
res.json({ success: true, count });
|
|
5102
5146
|
});
|
|
5103
|
-
|
|
5147
|
+
router.post("/:id/restore-default", async (req, res) => {
|
|
5104
5148
|
const { id } = req.params;
|
|
5105
5149
|
const restored = await restoreDefaultSkill(configDir, id);
|
|
5106
5150
|
if (!restored) {
|
|
@@ -5108,13 +5152,13 @@ async function createServerHandle(config) {
|
|
|
5108
5152
|
}
|
|
5109
5153
|
res.json({ success: true });
|
|
5110
5154
|
});
|
|
5111
|
-
|
|
5155
|
+
router.post("/:id/toggle", (req, res) => {
|
|
5112
5156
|
const { id } = req.params;
|
|
5113
5157
|
const currentlyEnabled = isSkillEnabled(id);
|
|
5114
5158
|
setSkillEnabled(id, !currentlyEnabled);
|
|
5115
5159
|
res.json({ id, enabled: !currentlyEnabled });
|
|
5116
5160
|
});
|
|
5117
|
-
|
|
5161
|
+
router.get("/:id", async (req, res) => {
|
|
5118
5162
|
const { id } = req.params;
|
|
5119
5163
|
const skills = await loadAllSkills(configDir);
|
|
5120
5164
|
const skill = findSkillById(id, skills);
|
|
@@ -5123,7 +5167,7 @@ async function createServerHandle(config) {
|
|
|
5123
5167
|
}
|
|
5124
5168
|
res.json(skill);
|
|
5125
5169
|
});
|
|
5126
|
-
|
|
5170
|
+
router.post("/", async (req, res) => {
|
|
5127
5171
|
const body = req.body;
|
|
5128
5172
|
if (!body.metadata?.id || !body.metadata?.name || !body.prompt) {
|
|
5129
5173
|
return res.status(400).json({ error: "Missing required fields: metadata.id, metadata.name, prompt" });
|
|
@@ -5137,7 +5181,7 @@ async function createServerHandle(config) {
|
|
|
5137
5181
|
await saveSkill(configDir, body);
|
|
5138
5182
|
res.status(201).json(body);
|
|
5139
5183
|
});
|
|
5140
|
-
|
|
5184
|
+
router.put("/:id", async (req, res) => {
|
|
5141
5185
|
const { id } = req.params;
|
|
5142
5186
|
if (!await skillExists(configDir, id)) {
|
|
5143
5187
|
return res.status(404).json({ error: "Skill not found" });
|
|
@@ -5155,7 +5199,7 @@ async function createServerHandle(config) {
|
|
|
5155
5199
|
await saveSkill(configDir, updated);
|
|
5156
5200
|
res.json(updated);
|
|
5157
5201
|
});
|
|
5158
|
-
|
|
5202
|
+
router.delete("/:id", async (req, res) => {
|
|
5159
5203
|
const { id } = req.params;
|
|
5160
5204
|
const deleted = await deleteSkill(configDir, id);
|
|
5161
5205
|
if (!deleted) {
|
|
@@ -5163,7 +5207,14 @@ async function createServerHandle(config) {
|
|
|
5163
5207
|
}
|
|
5164
5208
|
res.json({ success: true });
|
|
5165
5209
|
});
|
|
5166
|
-
|
|
5210
|
+
return router;
|
|
5211
|
+
}
|
|
5212
|
+
|
|
5213
|
+
// src/server/routes/commands.ts
|
|
5214
|
+
import { Router as Router2 } from "express";
|
|
5215
|
+
function createCommandRoutes(configDir) {
|
|
5216
|
+
const router = Router2();
|
|
5217
|
+
router.get("/", async (_req, res) => {
|
|
5167
5218
|
const [commands, defaultIds, modifiedIds] = await Promise.all([
|
|
5168
5219
|
loadAllCommands(configDir),
|
|
5169
5220
|
getDefaultCommandIds(),
|
|
@@ -5175,15 +5226,15 @@ async function createServerHandle(config) {
|
|
|
5175
5226
|
modifiedIds
|
|
5176
5227
|
});
|
|
5177
5228
|
});
|
|
5178
|
-
|
|
5229
|
+
router.get("/default-ids", async (_req, res) => {
|
|
5179
5230
|
const ids = await getDefaultCommandIds();
|
|
5180
5231
|
res.json({ ids });
|
|
5181
5232
|
});
|
|
5182
|
-
|
|
5233
|
+
router.post("/restore-all-defaults", async (_req, res) => {
|
|
5183
5234
|
const count = await restoreAllDefaultCommands(configDir);
|
|
5184
5235
|
res.json({ success: true, count });
|
|
5185
5236
|
});
|
|
5186
|
-
|
|
5237
|
+
router.post("/:id/restore-default", async (req, res) => {
|
|
5187
5238
|
const { id } = req.params;
|
|
5188
5239
|
const restored = await restoreDefaultCommand(configDir, id);
|
|
5189
5240
|
if (!restored) {
|
|
@@ -5191,7 +5242,7 @@ async function createServerHandle(config) {
|
|
|
5191
5242
|
}
|
|
5192
5243
|
res.json({ success: true });
|
|
5193
5244
|
});
|
|
5194
|
-
|
|
5245
|
+
router.get("/:id", async (req, res) => {
|
|
5195
5246
|
const { id } = req.params;
|
|
5196
5247
|
const commands = await loadAllCommands(configDir);
|
|
5197
5248
|
const command = findCommandById(id, commands);
|
|
@@ -5200,7 +5251,7 @@ async function createServerHandle(config) {
|
|
|
5200
5251
|
}
|
|
5201
5252
|
res.json(command);
|
|
5202
5253
|
});
|
|
5203
|
-
|
|
5254
|
+
router.post("/", async (req, res) => {
|
|
5204
5255
|
const body = req.body;
|
|
5205
5256
|
if (!body.metadata?.id || !body.metadata?.name || !body.prompt) {
|
|
5206
5257
|
return res.status(400).json({ error: "Missing required fields: metadata.id, metadata.name, prompt" });
|
|
@@ -5214,7 +5265,7 @@ async function createServerHandle(config) {
|
|
|
5214
5265
|
await saveCommand(configDir, body);
|
|
5215
5266
|
res.status(201).json(body);
|
|
5216
5267
|
});
|
|
5217
|
-
|
|
5268
|
+
router.put("/:id", async (req, res) => {
|
|
5218
5269
|
const { id } = req.params;
|
|
5219
5270
|
if (!await commandExists(configDir, id)) {
|
|
5220
5271
|
return res.status(404).json({ error: "Command not found" });
|
|
@@ -5232,7 +5283,7 @@ async function createServerHandle(config) {
|
|
|
5232
5283
|
await saveCommand(configDir, updated);
|
|
5233
5284
|
res.json(updated);
|
|
5234
5285
|
});
|
|
5235
|
-
|
|
5286
|
+
router.delete("/:id", async (req, res) => {
|
|
5236
5287
|
const { id } = req.params;
|
|
5237
5288
|
const deleted = await deleteCommand(configDir, id);
|
|
5238
5289
|
if (!deleted) {
|
|
@@ -5240,7 +5291,14 @@ async function createServerHandle(config) {
|
|
|
5240
5291
|
}
|
|
5241
5292
|
res.json({ success: true });
|
|
5242
5293
|
});
|
|
5243
|
-
|
|
5294
|
+
return router;
|
|
5295
|
+
}
|
|
5296
|
+
|
|
5297
|
+
// src/server/routes/agents.ts
|
|
5298
|
+
import { Router as Router3 } from "express";
|
|
5299
|
+
function createAgentRoutes(configDir) {
|
|
5300
|
+
const router = Router3();
|
|
5301
|
+
router.get("/", async (_req, res) => {
|
|
5244
5302
|
const [agents, defaultIds, modifiedIds] = await Promise.all([
|
|
5245
5303
|
loadAllAgents(configDir),
|
|
5246
5304
|
getDefaultAgentIds(),
|
|
@@ -5252,15 +5310,15 @@ async function createServerHandle(config) {
|
|
|
5252
5310
|
modifiedIds
|
|
5253
5311
|
});
|
|
5254
5312
|
});
|
|
5255
|
-
|
|
5313
|
+
router.get("/default-ids", async (_req, res) => {
|
|
5256
5314
|
const ids = await getDefaultAgentIds();
|
|
5257
5315
|
res.json({ ids });
|
|
5258
5316
|
});
|
|
5259
|
-
|
|
5317
|
+
router.post("/restore-all-defaults", async (_req, res) => {
|
|
5260
5318
|
const count = await restoreAllDefaultAgents(configDir);
|
|
5261
5319
|
res.json({ success: true, count });
|
|
5262
5320
|
});
|
|
5263
|
-
|
|
5321
|
+
router.post("/:id/restore-default", async (req, res) => {
|
|
5264
5322
|
const { id } = req.params;
|
|
5265
5323
|
const restored = await restoreDefaultAgent(configDir, id);
|
|
5266
5324
|
if (!restored) {
|
|
@@ -5268,7 +5326,7 @@ async function createServerHandle(config) {
|
|
|
5268
5326
|
}
|
|
5269
5327
|
res.json({ success: true });
|
|
5270
5328
|
});
|
|
5271
|
-
|
|
5329
|
+
router.get("/:id", async (req, res) => {
|
|
5272
5330
|
const { id } = req.params;
|
|
5273
5331
|
const agents = await loadAllAgents(configDir);
|
|
5274
5332
|
const agent = findAgentById(id, agents);
|
|
@@ -5277,7 +5335,7 @@ async function createServerHandle(config) {
|
|
|
5277
5335
|
}
|
|
5278
5336
|
res.json(agent);
|
|
5279
5337
|
});
|
|
5280
|
-
|
|
5338
|
+
router.post("/", async (req, res) => {
|
|
5281
5339
|
const body = req.body;
|
|
5282
5340
|
if (!body?.metadata?.id || !body?.prompt) {
|
|
5283
5341
|
return res.status(400).json({ error: "Missing required fields: metadata.id, prompt" });
|
|
@@ -5288,7 +5346,7 @@ async function createServerHandle(config) {
|
|
|
5288
5346
|
await saveAgent(configDir, body);
|
|
5289
5347
|
res.status(201).json(body);
|
|
5290
5348
|
});
|
|
5291
|
-
|
|
5349
|
+
router.put("/:id", async (req, res) => {
|
|
5292
5350
|
const { id } = req.params;
|
|
5293
5351
|
if (!id) {
|
|
5294
5352
|
return res.status(400).json({ error: "Missing agent ID" });
|
|
@@ -5306,7 +5364,7 @@ async function createServerHandle(config) {
|
|
|
5306
5364
|
await saveAgent(configDir, updated);
|
|
5307
5365
|
res.json(updated);
|
|
5308
5366
|
});
|
|
5309
|
-
|
|
5367
|
+
router.delete("/:id", async (req, res) => {
|
|
5310
5368
|
const { id } = req.params;
|
|
5311
5369
|
const deleted = await deleteAgent(configDir, id);
|
|
5312
5370
|
if (!deleted) {
|
|
@@ -5314,28 +5372,38 @@ async function createServerHandle(config) {
|
|
|
5314
5372
|
}
|
|
5315
5373
|
res.json({ success: true });
|
|
5316
5374
|
});
|
|
5317
|
-
|
|
5375
|
+
return router;
|
|
5376
|
+
}
|
|
5377
|
+
|
|
5378
|
+
// src/server/routes/workflows.ts
|
|
5379
|
+
import { Router as Router4 } from "express";
|
|
5380
|
+
function createWorkflowRoutes(configDir, config) {
|
|
5381
|
+
const router = Router4();
|
|
5382
|
+
router.get("/", async (_req, res) => {
|
|
5318
5383
|
const [workflows, defaultIds, modifiedIds] = await Promise.all([
|
|
5319
5384
|
loadAllWorkflows(configDir),
|
|
5320
5385
|
getDefaultWorkflowIds(),
|
|
5321
5386
|
getModifiedDefaultWorkflowIds(configDir)
|
|
5322
5387
|
]);
|
|
5323
5388
|
res.json({
|
|
5324
|
-
workflows: workflows.map((p) => p.metadata),
|
|
5389
|
+
workflows: workflows.map((p) => ({ ...p.metadata, startCondition: p.startCondition })),
|
|
5325
5390
|
activeWorkflowId: config.activeWorkflowId ?? "default",
|
|
5326
5391
|
defaultIds,
|
|
5327
5392
|
modifiedIds
|
|
5328
5393
|
});
|
|
5329
5394
|
});
|
|
5330
|
-
|
|
5395
|
+
router.get("/template-variables", (_req, res) => {
|
|
5396
|
+
res.json({ variables: TEMPLATE_VARIABLES });
|
|
5397
|
+
});
|
|
5398
|
+
router.get("/default-ids", async (_req, res) => {
|
|
5331
5399
|
const ids = await getDefaultWorkflowIds();
|
|
5332
5400
|
res.json({ ids });
|
|
5333
5401
|
});
|
|
5334
|
-
|
|
5402
|
+
router.post("/restore-all-defaults", async (_req, res) => {
|
|
5335
5403
|
const count = await restoreAllDefaultWorkflows(configDir);
|
|
5336
5404
|
res.json({ success: true, count });
|
|
5337
5405
|
});
|
|
5338
|
-
|
|
5406
|
+
router.post("/:id/restore-default", async (req, res) => {
|
|
5339
5407
|
const { id } = req.params;
|
|
5340
5408
|
const restored = await restoreDefaultWorkflow(configDir, id);
|
|
5341
5409
|
if (!restored) {
|
|
@@ -5343,7 +5411,7 @@ async function createServerHandle(config) {
|
|
|
5343
5411
|
}
|
|
5344
5412
|
res.json({ success: true });
|
|
5345
5413
|
});
|
|
5346
|
-
|
|
5414
|
+
router.get("/:id", async (req, res) => {
|
|
5347
5415
|
const { id } = req.params;
|
|
5348
5416
|
const workflows = await loadAllWorkflows(configDir);
|
|
5349
5417
|
const workflow = findWorkflowById(id, workflows);
|
|
@@ -5352,7 +5420,7 @@ async function createServerHandle(config) {
|
|
|
5352
5420
|
}
|
|
5353
5421
|
res.json(workflow);
|
|
5354
5422
|
});
|
|
5355
|
-
|
|
5423
|
+
router.post("/", async (req, res) => {
|
|
5356
5424
|
const body = req.body;
|
|
5357
5425
|
if (!body?.metadata?.id || !body?.steps?.length) {
|
|
5358
5426
|
return res.status(400).json({ error: "Missing required fields: metadata.id, steps" });
|
|
@@ -5363,7 +5431,7 @@ async function createServerHandle(config) {
|
|
|
5363
5431
|
await saveWorkflow(configDir, body);
|
|
5364
5432
|
res.status(201).json(body);
|
|
5365
5433
|
});
|
|
5366
|
-
|
|
5434
|
+
router.put("/:id", async (req, res) => {
|
|
5367
5435
|
const { id } = req.params;
|
|
5368
5436
|
if (!await workflowExists(configDir, id)) {
|
|
5369
5437
|
return res.status(404).json({ error: "Workflow not found" });
|
|
@@ -5376,7 +5444,7 @@ async function createServerHandle(config) {
|
|
|
5376
5444
|
await saveWorkflow(configDir, updated);
|
|
5377
5445
|
res.json(updated);
|
|
5378
5446
|
});
|
|
5379
|
-
|
|
5447
|
+
router.delete("/:id", async (req, res) => {
|
|
5380
5448
|
const { id } = req.params;
|
|
5381
5449
|
if (id === "default") {
|
|
5382
5450
|
return res.status(400).json({ error: "Cannot delete the default workflow" });
|
|
@@ -5387,6 +5455,241 @@ async function createServerHandle(config) {
|
|
|
5387
5455
|
}
|
|
5388
5456
|
res.json({ success: true });
|
|
5389
5457
|
});
|
|
5458
|
+
return router;
|
|
5459
|
+
}
|
|
5460
|
+
|
|
5461
|
+
// src/server/routes/dev-server.ts
|
|
5462
|
+
import { Router as Router5 } from "express";
|
|
5463
|
+
function createDevServerRoutes() {
|
|
5464
|
+
const router = Router5();
|
|
5465
|
+
router.get("/", async (req, res) => {
|
|
5466
|
+
const workdir = req.query["workdir"];
|
|
5467
|
+
if (!workdir) return res.status(400).json({ error: "workdir required" });
|
|
5468
|
+
const status = devServerManager.getStatus(workdir);
|
|
5469
|
+
if (!status.config) {
|
|
5470
|
+
const config = await devServerManager.loadConfig(workdir);
|
|
5471
|
+
if (config) {
|
|
5472
|
+
status.config = config;
|
|
5473
|
+
status.url = config.url;
|
|
5474
|
+
status.hotReload = config.hotReload;
|
|
5475
|
+
}
|
|
5476
|
+
}
|
|
5477
|
+
res.json(status);
|
|
5478
|
+
});
|
|
5479
|
+
router.post("/start", async (req, res) => {
|
|
5480
|
+
const workdir = req.query["workdir"];
|
|
5481
|
+
if (!workdir) return res.status(400).json({ error: "workdir required" });
|
|
5482
|
+
try {
|
|
5483
|
+
const status = await devServerManager.start(workdir);
|
|
5484
|
+
res.json(status);
|
|
5485
|
+
} catch (err) {
|
|
5486
|
+
res.status(500).json({ error: err instanceof Error ? err.message : "Failed to start" });
|
|
5487
|
+
}
|
|
5488
|
+
});
|
|
5489
|
+
router.post("/stop", async (req, res) => {
|
|
5490
|
+
const workdir = req.query["workdir"];
|
|
5491
|
+
if (!workdir) return res.status(400).json({ error: "workdir required" });
|
|
5492
|
+
try {
|
|
5493
|
+
const status = await devServerManager.stop(workdir);
|
|
5494
|
+
res.json(status);
|
|
5495
|
+
} catch (err) {
|
|
5496
|
+
res.status(500).json({ error: err instanceof Error ? err.message : "Failed to stop" });
|
|
5497
|
+
}
|
|
5498
|
+
});
|
|
5499
|
+
router.post("/restart", async (req, res) => {
|
|
5500
|
+
const workdir = req.query["workdir"];
|
|
5501
|
+
if (!workdir) return res.status(400).json({ error: "workdir required" });
|
|
5502
|
+
try {
|
|
5503
|
+
const status = await devServerManager.restart(workdir);
|
|
5504
|
+
res.json(status);
|
|
5505
|
+
} catch (err) {
|
|
5506
|
+
res.status(500).json({ error: err instanceof Error ? err.message : "Failed to restart" });
|
|
5507
|
+
}
|
|
5508
|
+
});
|
|
5509
|
+
router.get("/logs", (req, res) => {
|
|
5510
|
+
const workdir = req.query["workdir"];
|
|
5511
|
+
if (!workdir) return res.status(400).json({ error: "workdir required" });
|
|
5512
|
+
const logs = devServerManager.getLogs(workdir);
|
|
5513
|
+
res.json({ logs });
|
|
5514
|
+
});
|
|
5515
|
+
router.get("/config", async (req, res) => {
|
|
5516
|
+
const workdir = req.query["workdir"];
|
|
5517
|
+
if (!workdir) return res.status(400).json({ error: "workdir required" });
|
|
5518
|
+
const config = await devServerManager.loadConfig(workdir);
|
|
5519
|
+
res.json({ config });
|
|
5520
|
+
});
|
|
5521
|
+
router.post("/config", async (req, res) => {
|
|
5522
|
+
const workdir = req.query["workdir"];
|
|
5523
|
+
if (!workdir) return res.status(400).json({ error: "workdir required" });
|
|
5524
|
+
const { command, url, hotReload } = req.body;
|
|
5525
|
+
if (!command || !url) {
|
|
5526
|
+
return res.status(400).json({ error: "command and url are required" });
|
|
5527
|
+
}
|
|
5528
|
+
try {
|
|
5529
|
+
const config = { command, url, hotReload: hotReload ?? false };
|
|
5530
|
+
await devServerManager.saveConfig(workdir, config);
|
|
5531
|
+
res.json({ config });
|
|
5532
|
+
} catch (err) {
|
|
5533
|
+
res.status(500).json({ error: err instanceof Error ? err.message : "Failed to save config" });
|
|
5534
|
+
}
|
|
5535
|
+
});
|
|
5536
|
+
return router;
|
|
5537
|
+
}
|
|
5538
|
+
|
|
5539
|
+
// src/server/index.ts
|
|
5540
|
+
var __dirname2 = dirname4(fileURLToPath4(import.meta.url));
|
|
5541
|
+
async function createServerHandle(config) {
|
|
5542
|
+
setRuntimeConfig(config);
|
|
5543
|
+
setLogLevel(config.logging?.level ?? void 0, config.mode);
|
|
5544
|
+
const db = initDatabase(config);
|
|
5545
|
+
initEventStore(db);
|
|
5546
|
+
const configDir = getGlobalConfigDir(config.mode ?? "production");
|
|
5547
|
+
await ensureDefaultSkills(configDir);
|
|
5548
|
+
await ensureDefaultCommands(configDir);
|
|
5549
|
+
await ensureDefaultAgents(configDir);
|
|
5550
|
+
await ensureDefaultWorkflows(configDir);
|
|
5551
|
+
const sessionManager2 = new SessionManager();
|
|
5552
|
+
const providerManager = createProviderManager(config);
|
|
5553
|
+
const useMock = process.env["OPENFOX_MOCK_LLM"] === "true";
|
|
5554
|
+
const getMockClient = useMock ? createMockLLMClient : null;
|
|
5555
|
+
const getLLMClient = () => getMockClient ? getMockClient() : providerManager.getLLMClient();
|
|
5556
|
+
if (useMock) {
|
|
5557
|
+
logger.info("Using MOCK LLM client - deterministic responses for testing");
|
|
5558
|
+
}
|
|
5559
|
+
async function initLLM() {
|
|
5560
|
+
const llmClient = getLLMClient();
|
|
5561
|
+
let backend;
|
|
5562
|
+
const useMock2 = process.env["OPENFOX_MOCK_LLM"] === "true";
|
|
5563
|
+
if (config.llm.backend === "auto") {
|
|
5564
|
+
backend = await detectBackend(config.llm.baseUrl, void 0, useMock2);
|
|
5565
|
+
llmClient.setBackend(backend);
|
|
5566
|
+
if (!useMock2) {
|
|
5567
|
+
logger.info("Auto-detected LLM backend", { backend: getBackendDisplayName(backend) });
|
|
5568
|
+
}
|
|
5569
|
+
} else {
|
|
5570
|
+
backend = config.llm.backend;
|
|
5571
|
+
llmClient.setBackend(backend);
|
|
5572
|
+
if (!useMock2) {
|
|
5573
|
+
logger.info("Using configured LLM backend", { backend: getBackendDisplayName(backend) });
|
|
5574
|
+
}
|
|
5575
|
+
}
|
|
5576
|
+
const detected = await detectModel(config.llm.baseUrl);
|
|
5577
|
+
if (detected) {
|
|
5578
|
+
llmClient.setModel(detected);
|
|
5579
|
+
if (!useMock2) {
|
|
5580
|
+
logger.info("Auto-detected LLM model", { model: detected, backend: getBackendDisplayName(backend) });
|
|
5581
|
+
}
|
|
5582
|
+
} else {
|
|
5583
|
+
if (!useMock2) {
|
|
5584
|
+
logger.warn("Could not auto-detect model, using config", { model: config.llm.model });
|
|
5585
|
+
}
|
|
5586
|
+
}
|
|
5587
|
+
}
|
|
5588
|
+
initLLM().catch((err) => logger.error("LLM initialization failed", { error: err instanceof Error ? err.message : String(err) }));
|
|
5589
|
+
const toolRegistry = createToolRegistry();
|
|
5590
|
+
const app = express();
|
|
5591
|
+
app.use(cors());
|
|
5592
|
+
app.use(express.json());
|
|
5593
|
+
app.get("/api/health", (_req, res) => {
|
|
5594
|
+
res.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
5595
|
+
});
|
|
5596
|
+
app.get("/api/tools", (_req, res) => {
|
|
5597
|
+
res.json({ tools: toolRegistry.tools.map((t) => t.name) });
|
|
5598
|
+
});
|
|
5599
|
+
app.post("/api/sessions", async (req, res) => {
|
|
5600
|
+
const { workdir, title } = req.body;
|
|
5601
|
+
const session = sessionManager2.createSession(workdir, title);
|
|
5602
|
+
res.status(201).json({ session });
|
|
5603
|
+
});
|
|
5604
|
+
app.get("/api/sessions/:id", (req, res) => {
|
|
5605
|
+
const session = sessionManager2.getSession(req.params.id);
|
|
5606
|
+
if (!session) {
|
|
5607
|
+
return res.status(404).json({ error: "Session not found" });
|
|
5608
|
+
}
|
|
5609
|
+
res.json({ session });
|
|
5610
|
+
});
|
|
5611
|
+
app.delete("/api/sessions/:id", (req, res) => {
|
|
5612
|
+
sessionManager2.deleteSession(req.params["id"]);
|
|
5613
|
+
res.json({ success: true });
|
|
5614
|
+
});
|
|
5615
|
+
app.delete("/api/projects/:projectId/sessions", (req, res) => {
|
|
5616
|
+
const projectId = req.params["projectId"];
|
|
5617
|
+
const project = sessionManager2.getProject(projectId);
|
|
5618
|
+
if (!project) {
|
|
5619
|
+
return res.status(404).json({ error: "Project not found" });
|
|
5620
|
+
}
|
|
5621
|
+
sessionManager2.deleteAllSessions(projectId, project.workdir);
|
|
5622
|
+
res.json({ success: true });
|
|
5623
|
+
});
|
|
5624
|
+
app.get("/api/config", (_req, res) => {
|
|
5625
|
+
const llmClient = getLLMClient();
|
|
5626
|
+
const activeProvider = providerManager.getActiveProvider();
|
|
5627
|
+
res.json({
|
|
5628
|
+
model: llmClient.getModel(),
|
|
5629
|
+
maxContext: config.context.maxTokens,
|
|
5630
|
+
llmUrl: activeProvider?.url ?? config.llm.baseUrl,
|
|
5631
|
+
llmStatus: getLlmStatus(),
|
|
5632
|
+
backend: llmClient.getBackend(),
|
|
5633
|
+
workdir: config.workdir,
|
|
5634
|
+
// Include provider info
|
|
5635
|
+
providers: providerManager.getProviders(),
|
|
5636
|
+
activeProviderId: providerManager.getActiveProviderId(),
|
|
5637
|
+
defaultModelSelection: config.defaultModelSelection
|
|
5638
|
+
});
|
|
5639
|
+
});
|
|
5640
|
+
app.post("/api/model/refresh", async (_req, res) => {
|
|
5641
|
+
const llmClient = getLLMClient();
|
|
5642
|
+
const currentModel = providerManager.getCurrentModel();
|
|
5643
|
+
if (currentModel === "auto") {
|
|
5644
|
+
const activeProvider = providerManager.getActiveProvider();
|
|
5645
|
+
const baseUrl = activeProvider?.url ?? config.llm.baseUrl;
|
|
5646
|
+
const detected = await detectModel(baseUrl);
|
|
5647
|
+
if (detected) {
|
|
5648
|
+
llmClient.setModel(detected);
|
|
5649
|
+
return res.json({ model: detected, source: "detected", llmStatus: getLlmStatus(), backend: llmClient.getBackend() });
|
|
5650
|
+
}
|
|
5651
|
+
}
|
|
5652
|
+
res.json({ model: llmClient.getModel(), source: "cached", llmStatus: getLlmStatus(), backend: llmClient.getBackend() });
|
|
5653
|
+
});
|
|
5654
|
+
app.get("/api/providers", (_req, res) => {
|
|
5655
|
+
const providers = providerManager.getProviders().map((p) => ({
|
|
5656
|
+
...p,
|
|
5657
|
+
status: providerManager.getProviderStatus(p.id)
|
|
5658
|
+
}));
|
|
5659
|
+
res.json({
|
|
5660
|
+
providers,
|
|
5661
|
+
activeProviderId: providerManager.getActiveProviderId()
|
|
5662
|
+
});
|
|
5663
|
+
});
|
|
5664
|
+
app.get("/api/providers/:id/models", async (req, res) => {
|
|
5665
|
+
const { id } = req.params;
|
|
5666
|
+
const models = await providerManager.getProviderModels(id);
|
|
5667
|
+
res.json({ models });
|
|
5668
|
+
});
|
|
5669
|
+
app.post("/api/providers/:id/activate", async (req, res) => {
|
|
5670
|
+
const { id } = req.params;
|
|
5671
|
+
const body = req.body;
|
|
5672
|
+
const result = await providerManager.activateProvider(id, body.model ? { model: body.model } : void 0);
|
|
5673
|
+
if (!result.success) {
|
|
5674
|
+
return res.status(400).json({ error: result.error });
|
|
5675
|
+
}
|
|
5676
|
+
const llmClient = getLLMClient();
|
|
5677
|
+
const { loadGlobalConfig, saveGlobalConfig, setDefaultModelSelection } = await import("./config-BMXSDOZB.js");
|
|
5678
|
+
const globalConfig = await loadGlobalConfig(config.mode ?? "production");
|
|
5679
|
+
const updatedConfig = setDefaultModelSelection(globalConfig, id, llmClient.getModel());
|
|
5680
|
+
await saveGlobalConfig(config.mode ?? "production", updatedConfig);
|
|
5681
|
+
res.json({
|
|
5682
|
+
success: true,
|
|
5683
|
+
activeProviderId: id,
|
|
5684
|
+
model: llmClient.getModel(),
|
|
5685
|
+
backend: llmClient.getBackend()
|
|
5686
|
+
});
|
|
5687
|
+
});
|
|
5688
|
+
app.use("/api/skills", createSkillRoutes(configDir));
|
|
5689
|
+
app.use("/api/commands", createCommandRoutes(configDir));
|
|
5690
|
+
app.use("/api/agents", createAgentRoutes(configDir));
|
|
5691
|
+
app.use("/api/workflows", createWorkflowRoutes(configDir, config));
|
|
5692
|
+
app.use("/api/dev-server", createDevServerRoutes());
|
|
5390
5693
|
const { getCurrentBranch } = await import("./branch.api-WDHKZJS6.js");
|
|
5391
5694
|
app.get("/api/branch", async (req, res) => {
|
|
5392
5695
|
await getCurrentBranch(req, res);
|
|
@@ -5432,7 +5735,7 @@ async function createServerHandle(config) {
|
|
|
5432
5735
|
logLevel: "warn"
|
|
5433
5736
|
});
|
|
5434
5737
|
app.use(viteServer.middlewares);
|
|
5435
|
-
app.get("/src/styles
|
|
5738
|
+
app.get("/src/styles/{*path}", async (req, res) => {
|
|
5436
5739
|
try {
|
|
5437
5740
|
const result = await viteServer.transformRequest(req.path.substring(1));
|
|
5438
5741
|
if (!result) {
|
|
@@ -5453,16 +5756,12 @@ async function createServerHandle(config) {
|
|
|
5453
5756
|
res.status(404).send("Not found");
|
|
5454
5757
|
});
|
|
5455
5758
|
});
|
|
5456
|
-
app.
|
|
5457
|
-
|
|
5458
|
-
readFile3(fullPath).then((content) => {
|
|
5759
|
+
app.use("/sounds", express.static(join4(webDir, "public", "sounds"), {
|
|
5760
|
+
setHeaders: (res) => {
|
|
5459
5761
|
res.set("Content-Type", "audio/mpeg");
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
});
|
|
5464
|
-
});
|
|
5465
|
-
app.get("*", (req, res) => {
|
|
5762
|
+
}
|
|
5763
|
+
}));
|
|
5764
|
+
app.get("/{*path}", (req, res) => {
|
|
5466
5765
|
if (req.path.startsWith("/api/")) {
|
|
5467
5766
|
return;
|
|
5468
5767
|
}
|
|
@@ -5490,21 +5789,17 @@ async function createServerHandle(config) {
|
|
|
5490
5789
|
res.status(404).send("Not found");
|
|
5491
5790
|
});
|
|
5492
5791
|
});
|
|
5493
|
-
app.
|
|
5494
|
-
|
|
5495
|
-
readFile3(fullPath).then((content) => {
|
|
5792
|
+
app.use("/sounds", express.static(join4(distWebDir, "sounds"), {
|
|
5793
|
+
setHeaders: (res) => {
|
|
5496
5794
|
res.set("Content-Type", "audio/mpeg");
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
res.status(404).send("Not found");
|
|
5500
|
-
});
|
|
5501
|
-
});
|
|
5795
|
+
}
|
|
5796
|
+
}));
|
|
5502
5797
|
app.get("/", (_req, res) => {
|
|
5503
5798
|
readFile3(join4(distWebDir, "index.html"), "utf-8").then((content) => res.send(content)).catch(() => {
|
|
5504
5799
|
res.status(404).send("Web UI not built. Run `npm run build:web`");
|
|
5505
5800
|
});
|
|
5506
5801
|
});
|
|
5507
|
-
app.get("*", (req, res) => {
|
|
5802
|
+
app.get("(*)", (req, res) => {
|
|
5508
5803
|
if (req.path.startsWith("/api/") || req.path.startsWith("/assets/") || req.path.startsWith("/sounds/") || req.path === "/fox.svg") {
|
|
5509
5804
|
return;
|
|
5510
5805
|
}
|
|
@@ -5535,6 +5830,7 @@ async function createServerHandle(config) {
|
|
|
5535
5830
|
close: () => new Promise((resolve2) => {
|
|
5536
5831
|
logger.info("Shutting down...");
|
|
5537
5832
|
void (async () => {
|
|
5833
|
+
await devServerManager.stopAll();
|
|
5538
5834
|
viteServer?.close();
|
|
5539
5835
|
for (const client of wss.clients) {
|
|
5540
5836
|
client.terminate();
|
|
@@ -5567,4 +5863,4 @@ export {
|
|
|
5567
5863
|
createServerHandle,
|
|
5568
5864
|
createServer
|
|
5569
5865
|
};
|
|
5570
|
-
//# sourceMappingURL=chunk-
|
|
5866
|
+
//# sourceMappingURL=chunk-JFQEATRC.js.map
|