bosun 0.42.1 → 0.42.2
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/agent/primary-agent.mjs +5 -125
- package/infra/session-tracker.mjs +112 -0
- package/lib/repo-map.mjs +411 -0
- package/package.json +2 -1
- package/server/ui-server.mjs +299 -1
- package/task/task-cli.mjs +93 -19
- package/task/task-executor.mjs +396 -7
- package/task/task-store.mjs +194 -1
- package/ui/demo-defaults.js +203 -45
- package/ui/demo.html +26 -0
- package/workflow/workflow-engine.mjs +83 -1
- package/workflow/workflow-nodes.mjs +125 -0
- package/workflow-templates/sub-workflows.mjs +10 -0
- package/workflow-templates/task-execution.mjs +30 -12
- package/workflow-templates/task-lifecycle.mjs +8 -1
package/agent/primary-agent.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { loadConfig } from "../config/config.mjs";
|
|
|
9
9
|
import { ensureCodexConfig, printConfigSummary } from "../shell/codex-config.mjs";
|
|
10
10
|
import { ensureRepoConfigs, printRepoConfigSummary } from "../config/repo-config.mjs";
|
|
11
11
|
import { resolveRepoRoot } from "../config/repo-root.mjs";
|
|
12
|
+
import { buildArchitectEditorFrame } from "../lib/repo-map.mjs";
|
|
12
13
|
import { getAgentToolConfig, getEffectiveTools } from "./agent-tool-config.mjs";
|
|
13
14
|
import { getSessionTracker } from "../infra/session-tracker.mjs";
|
|
14
15
|
import { getEntry, getEntryContent, resolveAgentProfileLibraryMetadata } from "../infra/library-manager.mjs";
|
|
@@ -191,133 +192,9 @@ function appendAttachmentsToPrompt(message, attachments) {
|
|
|
191
192
|
return { message: `${message}${lines.join("\n")}`, appended: true };
|
|
192
193
|
}
|
|
193
194
|
|
|
194
|
-
function normalizeRepoMap(repoMap) {
|
|
195
|
-
if (!repoMap || typeof repoMap !== "object") return null;
|
|
196
|
-
const root = String(repoMap.root || repoMap.repoRoot || "").trim();
|
|
197
|
-
const files = Array.isArray(repoMap.files)
|
|
198
|
-
? repoMap.files
|
|
199
|
-
.filter((entry) => entry && typeof entry === "object")
|
|
200
|
-
.map((entry) => ({
|
|
201
|
-
path: String(entry.path || entry.file || "").trim(),
|
|
202
|
-
summary: String(entry.summary || entry.description || "").trim(),
|
|
203
|
-
symbols: Array.isArray(entry.symbols)
|
|
204
|
-
? entry.symbols.map((symbol) => String(symbol || "").trim()).filter(Boolean)
|
|
205
|
-
: [],
|
|
206
|
-
}))
|
|
207
|
-
.filter((entry) => entry.path)
|
|
208
|
-
: [];
|
|
209
|
-
if (!root && files.length === 0) return null;
|
|
210
|
-
return { root, files };
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function formatRepoMap(repoMap) {
|
|
214
|
-
const normalized = normalizeRepoMap(repoMap);
|
|
215
|
-
if (!normalized) return "";
|
|
216
|
-
const lines = ["## Repo Map"];
|
|
217
|
-
if (normalized.root) lines.push(`- Root: ${normalized.root}`);
|
|
218
|
-
for (const file of normalized.files) {
|
|
219
|
-
const parts = [file.path];
|
|
220
|
-
if (file.symbols.length) parts.push(`symbols: ${file.symbols.join(", ")}`);
|
|
221
|
-
if (file.summary) parts.push(file.summary);
|
|
222
|
-
lines.push(`- ${parts.join(" — ")}`);
|
|
223
|
-
}
|
|
224
|
-
return lines.join("\n");
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function summarizePathSegment(segment) {
|
|
228
|
-
return String(segment || "")
|
|
229
|
-
.replace(/[-_]+/g, " ")
|
|
230
|
-
.replace(/\.m?js$/i, "")
|
|
231
|
-
.replace(/\s+/g, " ")
|
|
232
|
-
.trim();
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function inferRepoMapEntry(pathValue) {
|
|
236
|
-
const path = String(pathValue || "").trim().replace(/\\/g, "/");
|
|
237
|
-
if (!path) return null;
|
|
238
|
-
const name = path.split("/").pop() || path;
|
|
239
|
-
const stem = summarizePathSegment(name);
|
|
240
|
-
const dir = path.includes("/") ? path.split("/").slice(0, -1).join("/") : "";
|
|
241
|
-
const dirHint = dir ? summarizePathSegment(dir.split("/").pop()) : "";
|
|
242
|
-
const symbols = [];
|
|
243
|
-
const lowerStem = stem.toLowerCase();
|
|
244
|
-
if (lowerStem) {
|
|
245
|
-
const compact = lowerStem
|
|
246
|
-
.split(" ")
|
|
247
|
-
.filter(Boolean)
|
|
248
|
-
.map((part, index) => (index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)))
|
|
249
|
-
.join("");
|
|
250
|
-
if (compact) {
|
|
251
|
-
symbols.push(compact);
|
|
252
|
-
if (!compact.startsWith("test")) symbols.push(`test${compact.charAt(0).toUpperCase()}${compact.slice(1)}`);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
const summaryParts = [];
|
|
256
|
-
if (dirHint) summaryParts.push(`${dirHint} module`);
|
|
257
|
-
if (stem) summaryParts.push(stem);
|
|
258
|
-
return {
|
|
259
|
-
path,
|
|
260
|
-
summary: summaryParts.join(" — "),
|
|
261
|
-
symbols: [...new Set(symbols)].slice(0, 3),
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
195
|
|
|
265
|
-
function deriveRepoMap(options = {}) {
|
|
266
|
-
const explicit = normalizeRepoMap(options.repoMap);
|
|
267
|
-
if (explicit) return explicit;
|
|
268
|
-
const changedFiles = Array.isArray(options.changedFiles)
|
|
269
|
-
? options.changedFiles.map((value) => String(value || "").trim()).filter(Boolean)
|
|
270
|
-
: [];
|
|
271
|
-
if (!changedFiles.length) return null;
|
|
272
|
-
const root = String(options.repoRoot || options.cwd || resolveRepoRoot() || "").trim();
|
|
273
|
-
const files = changedFiles
|
|
274
|
-
.map((pathValue) => inferRepoMapEntry(pathValue))
|
|
275
|
-
.filter(Boolean)
|
|
276
|
-
.slice(0, Number(options.repoMapFileLimit) > 0 ? Number(options.repoMapFileLimit) : 12);
|
|
277
|
-
if (!root && files.length === 0) return null;
|
|
278
|
-
return { root, files };
|
|
279
|
-
}
|
|
280
196
|
|
|
281
|
-
function inferExecutionRole(options = {}, effectiveMode = "agent") {
|
|
282
|
-
const explicitRole = String(options.executionRole || "").trim().toLowerCase();
|
|
283
|
-
if (explicitRole) return explicitRole;
|
|
284
|
-
if (effectiveMode === "plan") return "architect";
|
|
285
|
-
const architectPlan = String(options.architectPlan || options.planSummary || "").trim();
|
|
286
|
-
if (architectPlan) return "editor";
|
|
287
|
-
return "";
|
|
288
|
-
}
|
|
289
|
-
function buildArchitectEditorFrame(options = {}, effectiveMode = "agent") {
|
|
290
|
-
const executionRole = inferExecutionRole(options, effectiveMode);
|
|
291
|
-
const repoMapBlock = formatRepoMap(deriveRepoMap(options));
|
|
292
|
-
const architectPlan = String(options.architectPlan || options.planSummary || "").trim();
|
|
293
|
-
const lines = ["## Architect/Editor Execution"];
|
|
294
|
-
|
|
295
|
-
if (executionRole === "architect") {
|
|
296
|
-
lines.push(
|
|
297
|
-
"You are the architect phase.",
|
|
298
|
-
"Do not implement code changes in this phase.",
|
|
299
|
-
"Use the repo map to produce a compact structural plan that an editor can execute and validate.",
|
|
300
|
-
"Editor handoff: include ordered implementation steps, touched files, risks, and validation guidance.",
|
|
301
|
-
);
|
|
302
|
-
} else if (executionRole === "editor") {
|
|
303
|
-
lines.push(
|
|
304
|
-
"You are the editor phase.",
|
|
305
|
-
"Implement the approved plan with focused edits and verification.",
|
|
306
|
-
"Prefer the supplied repo map over broad rediscovery unless validation reveals drift.",
|
|
307
|
-
);
|
|
308
|
-
if (architectPlan) {
|
|
309
|
-
lines.push("", "## Architect Plan", architectPlan);
|
|
310
|
-
}
|
|
311
|
-
} else {
|
|
312
|
-
return repoMapBlock;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (repoMapBlock) {
|
|
316
|
-
lines.push("", repoMapBlock);
|
|
317
|
-
}
|
|
318
197
|
|
|
319
|
-
return lines.join("\n");
|
|
320
|
-
}
|
|
321
198
|
|
|
322
199
|
function summarizeContextCompressionItems(items) {
|
|
323
200
|
if (!Array.isArray(items) || items.length === 0) return null;
|
|
@@ -1190,8 +1067,9 @@ export async function execPrimaryPrompt(userMessage, options = {}) {
|
|
|
1190
1067
|
const messageWithAttachments = attachments.length && !attachmentsAppended
|
|
1191
1068
|
? appendAttachmentsToPrompt(userMessage, attachments).message
|
|
1192
1069
|
: userMessage;
|
|
1070
|
+
const architectEditorFrame = buildArchitectEditorFrame(options, effectiveMode);
|
|
1193
1071
|
const toolContract = buildPrimaryToolCapabilityContract(options);
|
|
1194
|
-
const messageWithToolContract = [selectedProfile.block, toolContract, messageWithAttachments]
|
|
1072
|
+
const messageWithToolContract = [selectedProfile.block, architectEditorFrame, toolContract, messageWithAttachments]
|
|
1195
1073
|
.filter(Boolean)
|
|
1196
1074
|
.join("\n\n");
|
|
1197
1075
|
const framedMessage = modePrefix ? modePrefix + messageWithToolContract : messageWithToolContract;
|
|
@@ -1680,3 +1558,5 @@ export async function execSdkCommand(command, args = "", adapterName, options =
|
|
|
1680
1558
|
}
|
|
1681
1559
|
|
|
1682
1560
|
|
|
1561
|
+
|
|
1562
|
+
|
|
@@ -264,6 +264,8 @@ export class SessionTracker {
|
|
|
264
264
|
lastActivityAt: Date.now(),
|
|
265
265
|
metadata: {},
|
|
266
266
|
insights: buildSessionInsights({ messages: [] }),
|
|
267
|
+
trajectory: { version: 1, replayable: true, steps: [] },
|
|
268
|
+
summary: null,
|
|
267
269
|
});
|
|
268
270
|
const session = this.#sessions.get(taskId);
|
|
269
271
|
this.#markDirty(taskId);
|
|
@@ -319,6 +321,7 @@ export class SessionTracker {
|
|
|
319
321
|
if (Number.isFinite(maxMessages) && maxMessages > 0) {
|
|
320
322
|
while (session.messages.length > maxMessages) session.messages.shift();
|
|
321
323
|
}
|
|
324
|
+
this.#appendTrajectoryStep(session, event);
|
|
322
325
|
this.#refreshDerivedState(session);
|
|
323
326
|
this.#markDirty(taskId);
|
|
324
327
|
emitSessionEvent(session, msg);
|
|
@@ -352,6 +355,7 @@ export class SessionTracker {
|
|
|
352
355
|
if (Number.isFinite(maxMessages) && maxMessages > 0) {
|
|
353
356
|
while (session.messages.length > maxMessages) session.messages.shift();
|
|
354
357
|
}
|
|
358
|
+
this.#appendTrajectoryStep(session, event);
|
|
355
359
|
this.#refreshDerivedState(session);
|
|
356
360
|
this.#markDirty(taskId);
|
|
357
361
|
emitSessionEvent(session, msg);
|
|
@@ -369,6 +373,7 @@ export class SessionTracker {
|
|
|
369
373
|
if (Number.isFinite(maxMessages) && maxMessages > 0) {
|
|
370
374
|
while (session.messages.length > maxMessages) session.messages.shift();
|
|
371
375
|
}
|
|
376
|
+
this.#appendTrajectoryStep(session, event);
|
|
372
377
|
this.#refreshDerivedState(session);
|
|
373
378
|
this.#markDirty(taskId);
|
|
374
379
|
emitSessionEvent(session, msg);
|
|
@@ -619,6 +624,8 @@ export class SessionTracker {
|
|
|
619
624
|
metadata,
|
|
620
625
|
maxMessages: resolvedMax,
|
|
621
626
|
insights: buildSessionInsights({ messages: [] }),
|
|
627
|
+
trajectory: { version: 1, replayable: true, steps: [] },
|
|
628
|
+
summary: null,
|
|
622
629
|
};
|
|
623
630
|
this.#sessions.set(id, session);
|
|
624
631
|
this.#markDirty(id);
|
|
@@ -797,6 +804,13 @@ export class SessionTracker {
|
|
|
797
804
|
this.#flushDirty();
|
|
798
805
|
}
|
|
799
806
|
|
|
807
|
+
/**
|
|
808
|
+
* Flush all dirty sessions to disk immediately (alias for flush).
|
|
809
|
+
*/
|
|
810
|
+
flushNow() {
|
|
811
|
+
this.#flushDirty();
|
|
812
|
+
}
|
|
813
|
+
|
|
800
814
|
/**
|
|
801
815
|
* Stop all timers and flush pending writes (for cleanup).
|
|
802
816
|
*/
|
|
@@ -1003,6 +1017,84 @@ export class SessionTracker {
|
|
|
1003
1017
|
return true;
|
|
1004
1018
|
}
|
|
1005
1019
|
|
|
1020
|
+
#appendTrajectoryStep(session, event) {
|
|
1021
|
+
if (!session) return;
|
|
1022
|
+
if (!session.trajectory) {
|
|
1023
|
+
session.trajectory = { version: 1, replayable: true, steps: [] };
|
|
1024
|
+
}
|
|
1025
|
+
const step = this.#extractTrajectoryStep(event, session);
|
|
1026
|
+
if (step) {
|
|
1027
|
+
session.trajectory.steps.push(step);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
#extractTrajectoryStep(event, session) {
|
|
1032
|
+
const ts = new Date().toISOString();
|
|
1033
|
+
const id = `step-${Date.now()}-${randomToken(6)}`;
|
|
1034
|
+
|
|
1035
|
+
// String event
|
|
1036
|
+
if (typeof event === "string") {
|
|
1037
|
+
return { id, kind: "system", summary: event.trim().slice(0, 200), timestamp: ts };
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// Direct message format (role/content)
|
|
1041
|
+
if (event?.role && event?.content !== undefined) {
|
|
1042
|
+
const content = String(event.content).slice(0, 200);
|
|
1043
|
+
const timestamp = event.timestamp || ts;
|
|
1044
|
+
if (event.role === "user") return { id, kind: "user_message", summary: content, timestamp };
|
|
1045
|
+
if (event.role === "assistant") return { id, kind: "assistant", summary: content, timestamp };
|
|
1046
|
+
return { id, kind: event.role, summary: content, timestamp };
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// SDK item.started events
|
|
1050
|
+
if (event?.type === "item.started" && event?.item) {
|
|
1051
|
+
const item = event.item;
|
|
1052
|
+
if (item.type === "command_execution") {
|
|
1053
|
+
return { id, kind: "tool_call", summary: `Ran ${item.command || "unknown"}`, timestamp: ts };
|
|
1054
|
+
}
|
|
1055
|
+
if (item.type === "reasoning") {
|
|
1056
|
+
return { id, kind: "reasoning", summary: item.text || "", timestamp: ts };
|
|
1057
|
+
}
|
|
1058
|
+
if (item.type === "function_call" || item.type === "mcp_tool_call") {
|
|
1059
|
+
return { id, kind: "tool_call", summary: `${item.name || "call"} ${item.arguments || ""}`.trim(), timestamp: ts };
|
|
1060
|
+
}
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// SDK item.completed events
|
|
1065
|
+
if (event?.type === "item.completed" && event?.item) {
|
|
1066
|
+
const item = event.item;
|
|
1067
|
+
if (item.type === "reasoning") {
|
|
1068
|
+
return { id, kind: "reasoning", summary: item.text || "", timestamp: ts };
|
|
1069
|
+
}
|
|
1070
|
+
if (item.type === "function_call" || item.type === "mcp_tool_call") {
|
|
1071
|
+
return { id, kind: "tool_call", summary: `${item.name || "call"} ${item.arguments || ""}`.trim(), timestamp: ts };
|
|
1072
|
+
}
|
|
1073
|
+
if (item.type === "command_execution") {
|
|
1074
|
+
const cmd = item.command || "";
|
|
1075
|
+
const hasPriorStart = (session?.trajectory?.steps || []).some(
|
|
1076
|
+
(s) => s.kind === "tool_call" && s.summary === `Ran ${cmd}`,
|
|
1077
|
+
);
|
|
1078
|
+
if (hasPriorStart) {
|
|
1079
|
+
return { id, kind: "tool_result", summary: `${cmd} (exit ${item.exit_code ?? "?"})`, timestamp: ts };
|
|
1080
|
+
}
|
|
1081
|
+
return { id, kind: "command", summary: cmd, timestamp: ts };
|
|
1082
|
+
}
|
|
1083
|
+
if (item.type === "agent_message") {
|
|
1084
|
+
return { id, kind: "assistant", summary: item.text || "", timestamp: ts };
|
|
1085
|
+
}
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Assistant message events
|
|
1090
|
+
if (event?.type === "assistant.message") {
|
|
1091
|
+
const content = event?.data?.content || event?.content || "";
|
|
1092
|
+
return { id, kind: "agent_message", summary: content.slice(0, 200), timestamp: ts };
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1006
1098
|
#refreshDerivedState(session) {
|
|
1007
1099
|
if (!session) return;
|
|
1008
1100
|
try {
|
|
@@ -1013,6 +1105,22 @@ export class SessionTracker {
|
|
|
1013
1105
|
} catch {
|
|
1014
1106
|
// Inspector insights are best-effort only.
|
|
1015
1107
|
}
|
|
1108
|
+
try {
|
|
1109
|
+
const steps = session.trajectory?.steps || [];
|
|
1110
|
+
const totalSteps = steps.length;
|
|
1111
|
+
const isFailed = session.status === "failed";
|
|
1112
|
+
const isLong = totalSteps > 12;
|
|
1113
|
+
const failedOrLongRun = isFailed || isLong;
|
|
1114
|
+
const resumable = failedOrLongRun;
|
|
1115
|
+
const shortSteps = steps.slice(-12).map((s) => ({ kind: s.kind, summary: s.summary }));
|
|
1116
|
+
const latestStep =
|
|
1117
|
+
steps.length > 0
|
|
1118
|
+
? { kind: steps[steps.length - 1].kind, summary: steps[steps.length - 1].summary }
|
|
1119
|
+
: null;
|
|
1120
|
+
session.summary = { failedOrLongRun, resumable, totalSteps, shortSteps, latestStep };
|
|
1121
|
+
} catch {
|
|
1122
|
+
// Summary computation is best-effort only.
|
|
1123
|
+
}
|
|
1016
1124
|
}
|
|
1017
1125
|
|
|
1018
1126
|
#ensureDir() {
|
|
@@ -1052,6 +1160,8 @@ export class SessionTracker {
|
|
|
1052
1160
|
messages: session.messages || [],
|
|
1053
1161
|
metadata: session.metadata || {},
|
|
1054
1162
|
insights: session.insights || null,
|
|
1163
|
+
trajectory: session.trajectory || null,
|
|
1164
|
+
summary: session.summary || null,
|
|
1055
1165
|
};
|
|
1056
1166
|
writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
1057
1167
|
} catch (err) {
|
|
@@ -1131,6 +1241,8 @@ export class SessionTracker {
|
|
|
1131
1241
|
lastActivityAt: lastActive || Date.now(),
|
|
1132
1242
|
metadata: data.metadata || {},
|
|
1133
1243
|
insights: data.insights || buildSessionInsights({ messages: data.messages || [] }),
|
|
1244
|
+
trajectory: data.trajectory || { version: 1, replayable: true, steps: [] },
|
|
1245
|
+
summary: data.summary || null,
|
|
1134
1246
|
});
|
|
1135
1247
|
const restored = this.#sessions.get(id);
|
|
1136
1248
|
if (restored && isTerminalSessionStatus(restored.status) && !restored.accumulatedAt) {
|