@frumu/tandem-panel 0.4.37 → 0.4.38
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/bin/setup.js +186 -75
- package/dist/assets/{index-C6soKUiJ.css → index-Cj42d_-Y.css} +1 -1
- package/dist/assets/index-DWi6ftxU.js +7376 -0
- package/dist/index.html +2 -2
- package/package.json +3 -3
- package/server/routes/aca.js +61 -0
- package/dist/assets/index-B4DAxlnB.js +0 -7376
package/bin/setup.js
CHANGED
|
@@ -22,7 +22,10 @@ import { resolveControlPanelPrincipalIdentity } from "../lib/setup/control-panel
|
|
|
22
22
|
import { resolveControlPanelPreferencesPath } from "../lib/setup/control-panel-preferences.js";
|
|
23
23
|
import { createSwarmApiHandler, getOrchestratorMetrics } from "../server/routes/swarm.js";
|
|
24
24
|
import { createAcaApiHandler } from "../server/routes/aca.js";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
createCapabilitiesHandler,
|
|
27
|
+
getCapabilitiesMetrics,
|
|
28
|
+
} from "../server/routes/capabilities.js";
|
|
26
29
|
import { createControlPanelConfigHandler } from "../server/routes/control-panel-config.js";
|
|
27
30
|
import { createKnowledgebaseApiHandler } from "../server/routes/knowledgebase.js";
|
|
28
31
|
import { createControlPanelPreferencesHandler } from "../server/routes/control-panel-preferences.js";
|
|
@@ -175,9 +178,7 @@ if (initRequested) {
|
|
|
175
178
|
console.log("[Tandem Control Panel] Environment initialized.");
|
|
176
179
|
console.log(`[Tandem Control Panel] .env: ${result.envPath}`);
|
|
177
180
|
console.log(`[Tandem Control Panel] Engine URL: ${result.engineUrl}`);
|
|
178
|
-
console.log(
|
|
179
|
-
`[Tandem Control Panel] Panel URL: http://${result.panelHost}:${result.panelPort}`
|
|
180
|
-
);
|
|
181
|
+
console.log(`[Tandem Control Panel] Panel URL: http://${result.panelHost}:${result.panelPort}`);
|
|
181
182
|
console.log(`[Tandem Control Panel] Token: ${result.token}`);
|
|
182
183
|
if (
|
|
183
184
|
process.argv.slice(2).length === 1 ||
|
|
@@ -804,7 +805,9 @@ async function installServices() {
|
|
|
804
805
|
const installEngine = serviceMode === "both" || serviceMode === "engine";
|
|
805
806
|
const installPanel = serviceMode === "both" || serviceMode === "panel";
|
|
806
807
|
const defaultStateDir = resolve(posixHomeForUser(serviceUser), ".local", "share", "tandem");
|
|
807
|
-
const stateDir = String(
|
|
808
|
+
const stateDir = String(
|
|
809
|
+
process.env.TANDEM_HOME || process.env.TANDEM_STATE_DIR || defaultStateDir
|
|
810
|
+
).trim();
|
|
808
811
|
const engineEnvPath = "/etc/tandem/engine.env";
|
|
809
812
|
const panelEnvPath = "/etc/tandem/control-panel.env";
|
|
810
813
|
const engineServiceName = "tandem-engine";
|
|
@@ -857,9 +860,7 @@ async function installServices() {
|
|
|
857
860
|
...(existingEngineEnv.TANDEM_EXA_API_KEY
|
|
858
861
|
? { TANDEM_EXA_API_KEY: existingEngineEnv.TANDEM_EXA_API_KEY }
|
|
859
862
|
: {}),
|
|
860
|
-
...(existingEngineEnv.EXA_API_KEY
|
|
861
|
-
? { EXA_API_KEY: existingEngineEnv.EXA_API_KEY }
|
|
862
|
-
: {}),
|
|
863
|
+
...(existingEngineEnv.EXA_API_KEY ? { EXA_API_KEY: existingEngineEnv.EXA_API_KEY } : {}),
|
|
863
864
|
...(existingEngineEnv.TANDEM_SEARXNG_URL
|
|
864
865
|
? { TANDEM_SEARXNG_URL: existingEngineEnv.TANDEM_SEARXNG_URL }
|
|
865
866
|
: {}),
|
|
@@ -1089,7 +1090,11 @@ async function readJsonBody(req) {
|
|
|
1089
1090
|
}
|
|
1090
1091
|
|
|
1091
1092
|
function normalizeSearchBackend(raw) {
|
|
1092
|
-
switch (
|
|
1093
|
+
switch (
|
|
1094
|
+
String(raw || "")
|
|
1095
|
+
.trim()
|
|
1096
|
+
.toLowerCase()
|
|
1097
|
+
) {
|
|
1093
1098
|
case "":
|
|
1094
1099
|
case "auto":
|
|
1095
1100
|
return "auto";
|
|
@@ -1107,7 +1112,9 @@ function normalizeSearchBackend(raw) {
|
|
|
1107
1112
|
}
|
|
1108
1113
|
|
|
1109
1114
|
function normalizeSearchUrl(raw) {
|
|
1110
|
-
const value = String(raw || "")
|
|
1115
|
+
const value = String(raw || "")
|
|
1116
|
+
.trim()
|
|
1117
|
+
.replace(/\/+$/, "");
|
|
1111
1118
|
return value || "";
|
|
1112
1119
|
}
|
|
1113
1120
|
|
|
@@ -1163,7 +1170,10 @@ async function writeManagedSearchSettings(payload = {}) {
|
|
|
1163
1170
|
const nextEnv = { ...existingEnv };
|
|
1164
1171
|
|
|
1165
1172
|
nextEnv.TANDEM_SEARCH_BACKEND = normalizeSearchBackend(payload.backend || "auto");
|
|
1166
|
-
const timeoutRaw = Number.parseInt(
|
|
1173
|
+
const timeoutRaw = Number.parseInt(
|
|
1174
|
+
String(payload.timeout_ms || payload.timeoutMs || "10000"),
|
|
1175
|
+
10
|
|
1176
|
+
);
|
|
1167
1177
|
nextEnv.TANDEM_SEARCH_TIMEOUT_MS = String(
|
|
1168
1178
|
Number.isFinite(timeoutRaw) ? Math.min(Math.max(timeoutRaw, 1000), 120000) : 10000
|
|
1169
1179
|
);
|
|
@@ -1187,8 +1197,7 @@ async function writeManagedSearchSettings(payload = {}) {
|
|
|
1187
1197
|
if (exaKey) {
|
|
1188
1198
|
nextEnv.TANDEM_EXA_API_KEY = exaKey;
|
|
1189
1199
|
delete nextEnv.TANDEM_EXA_SEARCH_API_KEY;
|
|
1190
|
-
}
|
|
1191
|
-
else if (payload.clear_exa_key || payload.clearExaKey) {
|
|
1200
|
+
} else if (payload.clear_exa_key || payload.clearExaKey) {
|
|
1192
1201
|
delete nextEnv.TANDEM_EXA_API_KEY;
|
|
1193
1202
|
delete nextEnv.TANDEM_EXA_SEARCH_API_KEY;
|
|
1194
1203
|
delete nextEnv.EXA_API_KEY;
|
|
@@ -1224,7 +1233,9 @@ function getManagedSchedulerSettings() {
|
|
|
1224
1233
|
const hostedManaged = isHostedManagedControlPanel();
|
|
1225
1234
|
const available = localEngine || hostedManaged;
|
|
1226
1235
|
const env = existsSync(envPath) ? parseDotEnv(readFileSync(envPath, "utf8")) : {};
|
|
1227
|
-
const modeRaw = String(env.TANDEM_SCHEDULER_MODE || "multi")
|
|
1236
|
+
const modeRaw = String(env.TANDEM_SCHEDULER_MODE || "multi")
|
|
1237
|
+
.trim()
|
|
1238
|
+
.toLowerCase();
|
|
1228
1239
|
const mode = modeRaw === "single" ? "single" : "multi";
|
|
1229
1240
|
const maxRaw = Number.parseInt(String(env.TANDEM_SCHEDULER_MAX_CONCURRENT_RUNS || ""), 10);
|
|
1230
1241
|
const maxConcurrentRuns = Number.isFinite(maxRaw) && maxRaw > 0 ? maxRaw : null;
|
|
@@ -1258,7 +1269,9 @@ async function writeManagedSchedulerSettings(payload = {}) {
|
|
|
1258
1269
|
const envPath = current.managed_env_path;
|
|
1259
1270
|
const existingEnv = existsSync(envPath) ? parseDotEnv(readFileSync(envPath, "utf8")) : {};
|
|
1260
1271
|
const nextEnv = { ...existingEnv };
|
|
1261
|
-
const modeRaw = String(payload.mode || "multi")
|
|
1272
|
+
const modeRaw = String(payload.mode || "multi")
|
|
1273
|
+
.trim()
|
|
1274
|
+
.toLowerCase();
|
|
1262
1275
|
nextEnv.TANDEM_SCHEDULER_MODE = modeRaw === "single" ? "single" : "multi";
|
|
1263
1276
|
if (payload.max_concurrent_runs != null && payload.max_concurrent_runs > 0) {
|
|
1264
1277
|
nextEnv.TANDEM_SCHEDULER_MAX_CONCURRENT_RUNS = String(payload.max_concurrent_runs);
|
|
@@ -1307,9 +1320,7 @@ function readOptionalTokenFile(pathname) {
|
|
|
1307
1320
|
|
|
1308
1321
|
function getAcaToken() {
|
|
1309
1322
|
return (
|
|
1310
|
-
String(process.env.ACA_API_TOKEN || "").trim() ||
|
|
1311
|
-
readOptionalTokenFile(ACA_TOKEN_FILE) ||
|
|
1312
|
-
""
|
|
1323
|
+
String(process.env.ACA_API_TOKEN || "").trim() || readOptionalTokenFile(ACA_TOKEN_FILE) || ""
|
|
1313
1324
|
);
|
|
1314
1325
|
}
|
|
1315
1326
|
|
|
@@ -1780,7 +1791,9 @@ async function handleFilesApi(req, res, _session) {
|
|
|
1780
1791
|
}
|
|
1781
1792
|
}
|
|
1782
1793
|
directories.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
|
1783
|
-
files.sort(
|
|
1794
|
+
files.sort(
|
|
1795
|
+
(a, b) => b.updatedAt - a.updatedAt || String(a.name).localeCompare(String(b.name))
|
|
1796
|
+
);
|
|
1784
1797
|
sendJson(res, 200, {
|
|
1785
1798
|
ok: true,
|
|
1786
1799
|
root: FILES_ROOT,
|
|
@@ -2069,14 +2082,39 @@ async function proxyEngineRequest(req, res, session) {
|
|
|
2069
2082
|
const targetPath = incoming.pathname.replace(/^\/api\/engine/, "") || "/";
|
|
2070
2083
|
const targetUrl = `${ENGINE_URL}${targetPath}${incoming.search}`;
|
|
2071
2084
|
const forwardedHost = String(req.headers.host || "").trim();
|
|
2072
|
-
const forwardedProto =
|
|
2073
|
-
|
|
2085
|
+
const forwardedProto =
|
|
2086
|
+
String(req.headers["x-forwarded-proto"] || "").trim() ||
|
|
2087
|
+
(req.socket && req.socket.encrypted ? "https" : "http");
|
|
2088
|
+
const requestedSource = String(req.headers["x-tandem-request-source"] || "").trim();
|
|
2089
|
+
const requestedAgentId = String(req.headers["x-tandem-agent-id"] || "").trim();
|
|
2090
|
+
const agentTestMode = (() => {
|
|
2091
|
+
const raw = String(
|
|
2092
|
+
req.headers["x-tandem-agent-test-mode"] || req.headers["x-tandem-control-panel-agent-mode"] || ""
|
|
2093
|
+
).trim()
|
|
2094
|
+
.toLowerCase();
|
|
2095
|
+
return ["1", "true", "yes", "on"].includes(raw);
|
|
2096
|
+
})();
|
|
2097
|
+
const requestSource = agentTestMode
|
|
2098
|
+
? requestedSource || "agent"
|
|
2099
|
+
: "control_panel";
|
|
2074
2100
|
|
|
2075
2101
|
const headers = new Headers();
|
|
2076
2102
|
for (const [key, value] of Object.entries(req.headers)) {
|
|
2077
2103
|
if (!value) continue;
|
|
2078
2104
|
const lower = key.toLowerCase();
|
|
2079
|
-
if (
|
|
2105
|
+
if (
|
|
2106
|
+
[
|
|
2107
|
+
"host",
|
|
2108
|
+
"content-length",
|
|
2109
|
+
"cookie",
|
|
2110
|
+
"authorization",
|
|
2111
|
+
"x-tandem-token",
|
|
2112
|
+
"x-tandem-agent-id",
|
|
2113
|
+
"x-tandem-agent-ancestor-ids",
|
|
2114
|
+
"x-tandem-control-panel-agent-mode",
|
|
2115
|
+
"x-tandem-agent-test-mode",
|
|
2116
|
+
].includes(lower)
|
|
2117
|
+
) {
|
|
2080
2118
|
continue;
|
|
2081
2119
|
}
|
|
2082
2120
|
if (Array.isArray(value)) headers.set(key, value.join(", "));
|
|
@@ -2084,6 +2122,10 @@ async function proxyEngineRequest(req, res, session) {
|
|
|
2084
2122
|
}
|
|
2085
2123
|
headers.set("authorization", `Bearer ${session.token}`);
|
|
2086
2124
|
headers.set("x-tandem-token", session.token);
|
|
2125
|
+
headers.set("x-tandem-request-source", requestSource);
|
|
2126
|
+
if (agentTestMode && requestedAgentId) {
|
|
2127
|
+
headers.set("x-tandem-agent-id", requestedAgentId);
|
|
2128
|
+
}
|
|
2087
2129
|
if (forwardedHost) headers.set("x-forwarded-host", forwardedHost);
|
|
2088
2130
|
if (forwardedProto) headers.set("x-forwarded-proto", forwardedProto);
|
|
2089
2131
|
|
|
@@ -2241,10 +2283,7 @@ async function engineRequestJson(session, path, options = {}) {
|
|
|
2241
2283
|
}
|
|
2242
2284
|
if (!response.ok) {
|
|
2243
2285
|
const rawText = await response.text().catch(() => "");
|
|
2244
|
-
if (
|
|
2245
|
-
(response.status === 503 || isEngineStarting(rawText)) &&
|
|
2246
|
-
attempt < maxNetworkRetries
|
|
2247
|
-
) {
|
|
2286
|
+
if ((response.status === 503 || isEngineStarting(rawText)) && attempt < maxNetworkRetries) {
|
|
2248
2287
|
lastError = new Error(rawText || `${method} ${path} failed: ${response.status}`);
|
|
2249
2288
|
response = null;
|
|
2250
2289
|
continue;
|
|
@@ -2455,16 +2494,26 @@ function normalizePlannerTasks(rawTasks, maxTasks = 8, options = {}) {
|
|
|
2455
2494
|
const linearFallback = options?.linearFallback === true;
|
|
2456
2495
|
const candidates = Array.isArray(rawTasks) ? rawTasks : [];
|
|
2457
2496
|
const normalizeTaskKind = (value, outputTarget) => {
|
|
2458
|
-
const raw = String(value || "")
|
|
2497
|
+
const raw = String(value || "")
|
|
2498
|
+
.trim()
|
|
2499
|
+
.toLowerCase();
|
|
2459
2500
|
if (["implementation", "inspection", "research", "validation"].includes(raw)) return raw;
|
|
2460
2501
|
return outputTarget?.path ? "implementation" : "inspection";
|
|
2461
2502
|
};
|
|
2462
2503
|
const normalizeOutputTarget = (value) => {
|
|
2463
2504
|
if (!value || typeof value !== "object") return null;
|
|
2464
|
-
const path = String(
|
|
2505
|
+
const path = String(
|
|
2506
|
+
value?.path || value?.file || value?.file_path || value?.target || ""
|
|
2507
|
+
).trim();
|
|
2465
2508
|
if (!path) return null;
|
|
2466
|
-
const kind =
|
|
2467
|
-
|
|
2509
|
+
const kind =
|
|
2510
|
+
String(value?.kind || value?.type || "artifact")
|
|
2511
|
+
.trim()
|
|
2512
|
+
.toLowerCase() || "artifact";
|
|
2513
|
+
const operation =
|
|
2514
|
+
String(value?.operation || value?.mode || "")
|
|
2515
|
+
.trim()
|
|
2516
|
+
.toLowerCase() || "create_or_update";
|
|
2468
2517
|
return { path, kind, operation };
|
|
2469
2518
|
};
|
|
2470
2519
|
const provisional = candidates
|
|
@@ -2567,8 +2616,12 @@ function inferOutputTargetFromText(text) {
|
|
|
2567
2616
|
function ensurePlannerTaskOutputTargets(tasks, objective) {
|
|
2568
2617
|
const list = Array.isArray(tasks) ? tasks : [];
|
|
2569
2618
|
return list.map((task) => {
|
|
2570
|
-
const taskKind =
|
|
2571
|
-
|
|
2619
|
+
const taskKind =
|
|
2620
|
+
String(task?.taskKind || "")
|
|
2621
|
+
.trim()
|
|
2622
|
+
.toLowerCase() || "inspection";
|
|
2623
|
+
const existing =
|
|
2624
|
+
task?.outputTarget && typeof task.outputTarget === "object" ? task.outputTarget : null;
|
|
2572
2625
|
return {
|
|
2573
2626
|
...task,
|
|
2574
2627
|
taskKind,
|
|
@@ -2580,12 +2633,21 @@ function ensurePlannerTaskOutputTargets(tasks, objective) {
|
|
|
2580
2633
|
function validateStrictPlannerTasks(tasks) {
|
|
2581
2634
|
const list = Array.isArray(tasks) ? tasks : [];
|
|
2582
2635
|
const invalidTaskKinds = list
|
|
2583
|
-
.filter(
|
|
2636
|
+
.filter(
|
|
2637
|
+
(task) =>
|
|
2638
|
+
!["implementation", "inspection", "research", "validation"].includes(
|
|
2639
|
+
String(task?.taskKind || "")
|
|
2640
|
+
.trim()
|
|
2641
|
+
.toLowerCase()
|
|
2642
|
+
)
|
|
2643
|
+
)
|
|
2584
2644
|
.map((task) => String(task?.id || task?.title || "task").trim())
|
|
2585
2645
|
.filter(Boolean);
|
|
2586
2646
|
const missing = list
|
|
2587
2647
|
.filter((task) => {
|
|
2588
|
-
const taskKind = String(task?.taskKind || "")
|
|
2648
|
+
const taskKind = String(task?.taskKind || "")
|
|
2649
|
+
.trim()
|
|
2650
|
+
.toLowerCase();
|
|
2589
2651
|
return taskKind === "implementation" && !String(task?.outputTarget?.path || "").trim();
|
|
2590
2652
|
})
|
|
2591
2653
|
.map((task) => String(task?.id || task?.title || "task").trim())
|
|
@@ -2888,7 +2950,9 @@ function workerWriteRetryToolAllowlist() {
|
|
|
2888
2950
|
}
|
|
2889
2951
|
|
|
2890
2952
|
function strictWriteRetryEnabled() {
|
|
2891
|
-
const raw = String(process.env.TANDEM_STRICT_WRITE_RETRY_ENABLED || "")
|
|
2953
|
+
const raw = String(process.env.TANDEM_STRICT_WRITE_RETRY_ENABLED || "")
|
|
2954
|
+
.trim()
|
|
2955
|
+
.toLowerCase();
|
|
2892
2956
|
if (!raw) return true;
|
|
2893
2957
|
return !["0", "false", "no", "off"].includes(raw);
|
|
2894
2958
|
}
|
|
@@ -2910,7 +2974,9 @@ function nonWritingRetryMaxAttempts() {
|
|
|
2910
2974
|
}
|
|
2911
2975
|
|
|
2912
2976
|
function classifyStrictWriteFailureReason(verification) {
|
|
2913
|
-
const reason = String(verification?.reason || "")
|
|
2977
|
+
const reason = String(verification?.reason || "")
|
|
2978
|
+
.trim()
|
|
2979
|
+
.toUpperCase();
|
|
2914
2980
|
if (!reason || reason === "VERIFIED") return "";
|
|
2915
2981
|
if (
|
|
2916
2982
|
reason === "WRITE_ARGS_EMPTY_FROM_PROVIDER" ||
|
|
@@ -2958,7 +3024,9 @@ function buildStrictWriteRetryRequest(prompt, verification, attemptIndex, maxAtt
|
|
|
2958
3024
|
recoveryLines.push("- Then create or modify the required file in the same turn.");
|
|
2959
3025
|
} else {
|
|
2960
3026
|
recoveryLines.push("- Do not inspect further with read/search/glob/ls/list.");
|
|
2961
|
-
recoveryLines.push(
|
|
3027
|
+
recoveryLines.push(
|
|
3028
|
+
"- Create or modify the required target directly with write/edit/apply_patch."
|
|
3029
|
+
);
|
|
2962
3030
|
}
|
|
2963
3031
|
return {
|
|
2964
3032
|
parts: [{ type: "text", text: recoveryLines.join("\n") }],
|
|
@@ -2971,7 +3039,9 @@ function buildStrictWriteRetryRequest(prompt, verification, attemptIndex, maxAtt
|
|
|
2971
3039
|
}
|
|
2972
3040
|
|
|
2973
3041
|
function classifyNonWritingFailureReason(verification) {
|
|
2974
|
-
const reason = String(verification?.reason || "")
|
|
3042
|
+
const reason = String(verification?.reason || "")
|
|
3043
|
+
.trim()
|
|
3044
|
+
.toUpperCase();
|
|
2975
3045
|
if (!reason || reason === "VERIFIED") return "";
|
|
2976
3046
|
if (reason === "NO_TOOL_ACTIVITY_NO_DECISION" || reason === "NO_TOOL_ACTIVITY") {
|
|
2977
3047
|
return "no_tool_activity";
|
|
@@ -3011,9 +3081,7 @@ function buildNonWritingRetryRequest(prompt, verification, attemptIndex, maxAtte
|
|
|
3011
3081
|
|
|
3012
3082
|
const WRITE_TOOL_NAMES = new Set(["write", "edit", "apply_patch"]);
|
|
3013
3083
|
const REQUIRED_TOOL_MODE_REASON = "TOOL_MODE_REQUIRED_NOT_SATISFIED";
|
|
3014
|
-
const REQUIRED_TOOL_REASON_PATTERN = new RegExp(
|
|
3015
|
-
`${REQUIRED_TOOL_MODE_REASON}:\\s*([A-Z_]+)\\b`
|
|
3016
|
-
);
|
|
3084
|
+
const REQUIRED_TOOL_REASON_PATTERN = new RegExp(`${REQUIRED_TOOL_MODE_REASON}:\\s*([A-Z_]+)\\b`);
|
|
3017
3085
|
|
|
3018
3086
|
function normalizeVerificationMode(mode) {
|
|
3019
3087
|
return String(mode || "")
|
|
@@ -3057,7 +3125,9 @@ function extractRequiredToolFailureReason(text) {
|
|
|
3057
3125
|
const raw = String(text || "").trim();
|
|
3058
3126
|
if (!raw) return "";
|
|
3059
3127
|
const match = raw.match(REQUIRED_TOOL_REASON_PATTERN);
|
|
3060
|
-
return String(match?.[1] || "")
|
|
3128
|
+
return String(match?.[1] || "")
|
|
3129
|
+
.trim()
|
|
3130
|
+
.toUpperCase();
|
|
3061
3131
|
}
|
|
3062
3132
|
|
|
3063
3133
|
function shouldTrackWorkspacePath(pathname) {
|
|
@@ -3484,7 +3554,9 @@ function summarizeExecutionRows(rows, limit = 12) {
|
|
|
3484
3554
|
for (const row of list) {
|
|
3485
3555
|
if (out.length >= limit) break;
|
|
3486
3556
|
const role = roleOfMessage(row);
|
|
3487
|
-
const type = String(row?.type || "")
|
|
3557
|
+
const type = String(row?.type || "")
|
|
3558
|
+
.trim()
|
|
3559
|
+
.toLowerCase();
|
|
3488
3560
|
const text = textOfMessage(row).trim();
|
|
3489
3561
|
const parts = Array.isArray(row?.parts) ? row.parts : [];
|
|
3490
3562
|
const tools = [];
|
|
@@ -3505,7 +3577,15 @@ function summarizeExecutionRows(rows, limit = 12) {
|
|
|
3505
3577
|
return out;
|
|
3506
3578
|
}
|
|
3507
3579
|
|
|
3508
|
-
function buildAttemptTelemetry(
|
|
3580
|
+
function buildAttemptTelemetry(
|
|
3581
|
+
name,
|
|
3582
|
+
request,
|
|
3583
|
+
rows,
|
|
3584
|
+
messages,
|
|
3585
|
+
startedAtMs,
|
|
3586
|
+
error = null,
|
|
3587
|
+
meta = {}
|
|
3588
|
+
) {
|
|
3509
3589
|
const syncAudit = collectToolActivity(rows, `${name}_prompt_sync`);
|
|
3510
3590
|
const sessionAudit = collectToolActivity(messages, `${name}_session_snapshot`);
|
|
3511
3591
|
const merged = mergeToolActivityAudits(syncAudit, sessionAudit);
|
|
@@ -3561,8 +3641,7 @@ function buildVerificationSummary(syncRows, messages, workspaceChanges, sessionI
|
|
|
3561
3641
|
else if (requiredToolModeUnsatisfied) reason = REQUIRED_TOOL_MODE_REASON;
|
|
3562
3642
|
else if (toolAudit.rejectedWriteToolCalls > 0)
|
|
3563
3643
|
reason = "WRITE_TOOL_ATTEMPT_REJECTED_NO_WORKSPACE_CHANGE";
|
|
3564
|
-
else if (toolAudit.rejectedToolCalls > 0)
|
|
3565
|
-
reason = "TOOL_ATTEMPT_REJECTED_NO_WORKSPACE_CHANGE";
|
|
3644
|
+
else if (toolAudit.rejectedToolCalls > 0) reason = "TOOL_ATTEMPT_REJECTED_NO_WORKSPACE_CHANGE";
|
|
3566
3645
|
else if (strictMode && toolAudit.totalToolCalls > 0)
|
|
3567
3646
|
reason = "NO_WRITE_ACTIVITY_NO_WORKSPACE_CHANGE";
|
|
3568
3647
|
else reason = "NO_TOOL_ACTIVITY_NO_WORKSPACE_CHANGE";
|
|
@@ -3668,10 +3747,9 @@ async function resolveExecutionModel(session, run) {
|
|
|
3668
3747
|
function normalizeSessionModelRef(value) {
|
|
3669
3748
|
if (typeof value === "string") return value.trim();
|
|
3670
3749
|
if (value && typeof value === "object") {
|
|
3671
|
-
const model =
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
).trim();
|
|
3750
|
+
const model = String(
|
|
3751
|
+
value?.model_id || value?.id || value?.name || value?.slug || value?.model || ""
|
|
3752
|
+
).trim();
|
|
3675
3753
|
if (model) return model;
|
|
3676
3754
|
}
|
|
3677
3755
|
return "";
|
|
@@ -3684,7 +3762,9 @@ function summarizeRunStepsForPrompt(run, currentStepId, limit = 12) {
|
|
|
3684
3762
|
.map((row, index) => {
|
|
3685
3763
|
const stepId = String(row?.step_id || `step-${index + 1}`).trim();
|
|
3686
3764
|
const title = String(row?.title || stepId).trim();
|
|
3687
|
-
const status = String(row?.status || "unknown")
|
|
3765
|
+
const status = String(row?.status || "unknown")
|
|
3766
|
+
.trim()
|
|
3767
|
+
.toLowerCase();
|
|
3688
3768
|
const marker = stepId === currentStepId ? "*" : "-";
|
|
3689
3769
|
return `${marker} ${stepId} [${status}]: ${title}`;
|
|
3690
3770
|
})
|
|
@@ -3695,8 +3775,7 @@ function summarizeRunStepsForPrompt(run, currentStepId, limit = 12) {
|
|
|
3695
3775
|
function stepPromptText(run, step, stepIndex, totalSteps) {
|
|
3696
3776
|
const stepId = String(step?.step_id || "").trim() || `step-${stepIndex + 1}`;
|
|
3697
3777
|
const stepTitle = String(step?.title || stepId).trim();
|
|
3698
|
-
const stepDetails =
|
|
3699
|
-
step && typeof step === "object" ? JSON.stringify(step, null, 2).trim() : "";
|
|
3778
|
+
const stepDetails = step && typeof step === "object" ? JSON.stringify(step, null, 2).trim() : "";
|
|
3700
3779
|
const stepList = summarizeRunStepsForPrompt(run, stepId);
|
|
3701
3780
|
return [
|
|
3702
3781
|
"Execute this swarm step.",
|
|
@@ -3796,7 +3875,13 @@ function buildNonWritingVerificationSummary(syncRows, messages, sessionId, optio
|
|
|
3796
3875
|
};
|
|
3797
3876
|
}
|
|
3798
3877
|
|
|
3799
|
-
async function runExecutionPromptWithVerification(
|
|
3878
|
+
async function runExecutionPromptWithVerification(
|
|
3879
|
+
session,
|
|
3880
|
+
run,
|
|
3881
|
+
prompt,
|
|
3882
|
+
sessionId = "",
|
|
3883
|
+
options = {}
|
|
3884
|
+
) {
|
|
3800
3885
|
const activeSessionId =
|
|
3801
3886
|
String(sessionId || "").trim() || (await createExecutionSession(session, run));
|
|
3802
3887
|
if (!activeSessionId) throw new Error("Failed to create execution session.");
|
|
@@ -3886,7 +3971,9 @@ async function runExecutionPromptWithVerification(session, run, prompt, sessionI
|
|
|
3886
3971
|
`/session/${encodeURIComponent(activeSessionId)}`
|
|
3887
3972
|
).catch(() => null);
|
|
3888
3973
|
lastSessionSnapshot = sessionSnapshot;
|
|
3889
|
-
const sessionMessages = Array.isArray(sessionSnapshot?.messages)
|
|
3974
|
+
const sessionMessages = Array.isArray(sessionSnapshot?.messages)
|
|
3975
|
+
? sessionSnapshot.messages
|
|
3976
|
+
: [];
|
|
3890
3977
|
messages = rowsSinceAttemptStart(sessionMessages, previousMessageCount);
|
|
3891
3978
|
previousMessageCount = sessionMessages.length;
|
|
3892
3979
|
hasAssistant = syncRows.some((row) => roleOfMessage(row) === "assistant");
|
|
@@ -3951,8 +4038,7 @@ async function runExecutionPromptWithVerification(session, run, prompt, sessionI
|
|
|
3951
4038
|
normalizeSessionModelRef(lastSessionSnapshot?.model) ||
|
|
3952
4039
|
normalizeSessionModelRef(resolvedModel?.model),
|
|
3953
4040
|
source: String(
|
|
3954
|
-
lastSessionSnapshot?.provider &&
|
|
3955
|
-
normalizeSessionModelRef(lastSessionSnapshot?.model)
|
|
4041
|
+
lastSessionSnapshot?.provider && normalizeSessionModelRef(lastSessionSnapshot?.model)
|
|
3956
4042
|
? "session_snapshot"
|
|
3957
4043
|
: resolvedModel?.source || ""
|
|
3958
4044
|
).trim(),
|
|
@@ -4048,7 +4134,9 @@ function taskTitleFromRecord(task) {
|
|
|
4048
4134
|
|
|
4049
4135
|
function taskKindFromRecord(task) {
|
|
4050
4136
|
const payload = task?.payload && typeof task.payload === "object" ? task.payload : {};
|
|
4051
|
-
const raw = String(payload?.task_kind || task?.task_type || "inspection")
|
|
4137
|
+
const raw = String(payload?.task_kind || task?.task_type || "inspection")
|
|
4138
|
+
.trim()
|
|
4139
|
+
.toLowerCase();
|
|
4052
4140
|
return ["implementation", "inspection", "research", "validation"].includes(raw)
|
|
4053
4141
|
? raw
|
|
4054
4142
|
: "inspection";
|
|
@@ -4065,7 +4153,9 @@ function summarizeBlackboardTasksForPrompt(tasks, currentTaskId, limit = 16) {
|
|
|
4065
4153
|
.map((task, index) => {
|
|
4066
4154
|
const taskId = String(task?.id || `task-${index + 1}`).trim();
|
|
4067
4155
|
const title = taskTitleFromRecord(task);
|
|
4068
|
-
const status = String(task?.status || "unknown")
|
|
4156
|
+
const status = String(task?.status || "unknown")
|
|
4157
|
+
.trim()
|
|
4158
|
+
.toLowerCase();
|
|
4069
4159
|
const marker = taskId === currentTaskId ? "*" : "-";
|
|
4070
4160
|
return `${marker} ${taskId} [${status}]: ${title}`;
|
|
4071
4161
|
})
|
|
@@ -4077,8 +4167,7 @@ function taskPromptText(run, task, workerId, workflowId) {
|
|
|
4077
4167
|
const taskId = String(task?.id || "").trim();
|
|
4078
4168
|
const taskTitle = taskTitleFromRecord(task);
|
|
4079
4169
|
const taskKind = taskKindFromRecord(task);
|
|
4080
|
-
const taskDetails =
|
|
4081
|
-
task && typeof task === "object" ? JSON.stringify(task, null, 2).trim() : "";
|
|
4170
|
+
const taskDetails = task && typeof task === "object" ? JSON.stringify(task, null, 2).trim() : "";
|
|
4082
4171
|
const taskList = summarizeBlackboardTasksForPrompt(run?.tasks, taskId);
|
|
4083
4172
|
const outputTarget =
|
|
4084
4173
|
task?.payload?.output_target && typeof task.payload.output_target === "object"
|
|
@@ -4220,10 +4309,14 @@ async function seedBlackboardTasks(session, runId, objective, taskRows, workflow
|
|
|
4220
4309
|
if (!prepared.length) {
|
|
4221
4310
|
throw new Error("No valid tasks to seed.");
|
|
4222
4311
|
}
|
|
4223
|
-
const created = await engineRequestJson(
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4312
|
+
const created = await engineRequestJson(
|
|
4313
|
+
session,
|
|
4314
|
+
`/context/runs/${encodeURIComponent(runId)}/tasks`,
|
|
4315
|
+
{
|
|
4316
|
+
method: "POST",
|
|
4317
|
+
body: { tasks: prepared },
|
|
4318
|
+
}
|
|
4319
|
+
);
|
|
4227
4320
|
if (created && created.ok === false) {
|
|
4228
4321
|
throw new Error(String(created.error || created.code || "Task seeding failed."));
|
|
4229
4322
|
}
|
|
@@ -4525,13 +4618,19 @@ async function driveBlackboardRunExecution(session, runId, options = {}) {
|
|
|
4525
4618
|
if (swarmExecutors.has(runId)) return false;
|
|
4526
4619
|
const controller = getSwarmRunController(runId);
|
|
4527
4620
|
const workflowId = String(
|
|
4528
|
-
options.workflowId ||
|
|
4621
|
+
options.workflowId ||
|
|
4622
|
+
controller?.workflowId ||
|
|
4623
|
+
swarmState.workflowId ||
|
|
4624
|
+
"swarm.blackboard.default"
|
|
4529
4625
|
).trim();
|
|
4530
4626
|
const maxAgents = Math.max(
|
|
4531
4627
|
1,
|
|
4532
4628
|
Math.min(
|
|
4533
4629
|
16,
|
|
4534
|
-
Number.parseInt(
|
|
4630
|
+
Number.parseInt(
|
|
4631
|
+
String(options.maxAgents || controller?.maxAgents || swarmState.maxAgents || 3),
|
|
4632
|
+
10
|
|
4633
|
+
) || 3
|
|
4535
4634
|
)
|
|
4536
4635
|
);
|
|
4537
4636
|
upsertSwarmRunController(runId, {
|
|
@@ -4856,7 +4955,10 @@ async function startSwarm(session, config = {}) {
|
|
|
4856
4955
|
await synced;
|
|
4857
4956
|
let plannerTasks = [];
|
|
4858
4957
|
let planSeedMode = "fallback_local";
|
|
4859
|
-
const enforceStrictTaskOutputs =
|
|
4958
|
+
const enforceStrictTaskOutputs =
|
|
4959
|
+
String(verificationMode || "strict")
|
|
4960
|
+
.trim()
|
|
4961
|
+
.toLowerCase() === "strict";
|
|
4860
4962
|
try {
|
|
4861
4963
|
const llmPlan = await generatePlanTodosWithLLM(session, run, maxTasks);
|
|
4862
4964
|
let plannerSource = "llm_objective_planner";
|
|
@@ -4912,12 +5014,18 @@ async function startSwarm(session, config = {}) {
|
|
|
4912
5014
|
if (enforceStrictTaskOutputs) {
|
|
4913
5015
|
const strictCheck = validateStrictPlannerTasks(plannerTasks);
|
|
4914
5016
|
if (!strictCheck.ok) {
|
|
4915
|
-
await appendContextRunEvent(
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
5017
|
+
await appendContextRunEvent(
|
|
5018
|
+
session,
|
|
5019
|
+
runId,
|
|
5020
|
+
"plan_failed_output_target_missing",
|
|
5021
|
+
"planning",
|
|
5022
|
+
{
|
|
5023
|
+
reason: plannerFailureReason,
|
|
5024
|
+
missing_tasks: strictCheck.missing,
|
|
5025
|
+
invalid_task_kind_tasks: strictCheck.invalidTaskKinds,
|
|
5026
|
+
recovery_source: recovered.source,
|
|
5027
|
+
}
|
|
5028
|
+
).catch(() => null);
|
|
4921
5029
|
throw new Error(
|
|
4922
5030
|
`Strict orchestration requires valid task kinds and output targets where needed: missing_output_target=${strictCheck.missing.join(", ")} invalid_task_kind=${strictCheck.invalidTaskKinds.join(", ")}`
|
|
4923
5031
|
);
|
|
@@ -5251,7 +5359,10 @@ async function handleApi(req, res) {
|
|
|
5251
5359
|
return handleControlPanelPreferences(req, res, session);
|
|
5252
5360
|
}
|
|
5253
5361
|
|
|
5254
|
-
if (
|
|
5362
|
+
if (
|
|
5363
|
+
pathname === "/api/control-panel/config" &&
|
|
5364
|
+
(req.method === "GET" || req.method === "PATCH")
|
|
5365
|
+
) {
|
|
5255
5366
|
const session = requireSession(req, res);
|
|
5256
5367
|
if (!session) return true;
|
|
5257
5368
|
return handleControlPanelConfig(req, res);
|