bosun 0.41.0 → 0.41.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.
Files changed (64) hide show
  1. package/.env.example +8 -0
  2. package/README.md +20 -0
  3. package/agent/agent-event-bus.mjs +248 -6
  4. package/agent/agent-pool.mjs +125 -28
  5. package/agent/agent-work-analyzer.mjs +8 -16
  6. package/agent/retry-queue.mjs +164 -0
  7. package/bosun.config.example.json +25 -0
  8. package/bosun.schema.json +825 -183
  9. package/cli.mjs +59 -5
  10. package/config/config.mjs +130 -3
  11. package/infra/monitor.mjs +693 -67
  12. package/infra/runtime-accumulator.mjs +376 -84
  13. package/infra/session-tracker.mjs +82 -25
  14. package/lib/codebase-audit.mjs +133 -18
  15. package/package.json +23 -4
  16. package/server/setup-web-server.mjs +25 -0
  17. package/server/ui-server.mjs +248 -29
  18. package/setup.mjs +27 -24
  19. package/shell/codex-shell.mjs +34 -3
  20. package/shell/copilot-shell.mjs +50 -8
  21. package/task/msg-hub.mjs +193 -0
  22. package/task/pipeline.mjs +544 -0
  23. package/task/task-cli.mjs +38 -2
  24. package/task/task-executor-pipeline.mjs +143 -0
  25. package/task/task-executor.mjs +36 -27
  26. package/telegram/get-telegram-chat-id.mjs +57 -47
  27. package/ui/components/workspace-switcher.js +7 -7
  28. package/ui/demo-defaults.js +15694 -10573
  29. package/ui/modules/settings-schema.js +2 -0
  30. package/ui/modules/state.js +54 -57
  31. package/ui/modules/voice-client-sdk.js +375 -36
  32. package/ui/modules/voice-client.js +140 -31
  33. package/ui/setup.html +68 -2
  34. package/ui/styles/components.css +57 -0
  35. package/ui/styles.css +201 -1
  36. package/ui/tabs/dashboard.js +74 -0
  37. package/ui/tabs/logs.js +10 -0
  38. package/ui/tabs/settings.js +178 -99
  39. package/ui/tabs/tasks.js +31 -1
  40. package/ui/tabs/telemetry.js +34 -0
  41. package/ui/tabs/workflow-canvas-utils.mjs +8 -1
  42. package/ui/tabs/workflows.js +532 -275
  43. package/voice/voice-agents-sdk.mjs +1 -1
  44. package/voice/voice-relay.mjs +6 -6
  45. package/workflow/declarative-workflows.mjs +145 -0
  46. package/workflow/msg-hub.mjs +237 -0
  47. package/workflow/pipeline-workflows.mjs +287 -0
  48. package/workflow/pipeline.mjs +828 -315
  49. package/workflow/workflow-cli.mjs +128 -0
  50. package/workflow/workflow-engine.mjs +329 -17
  51. package/workflow/workflow-nodes/custom-loader.mjs +250 -0
  52. package/workflow/workflow-nodes.mjs +1955 -223
  53. package/workflow/workflow-templates.mjs +26 -8
  54. package/workflow-templates/agents.mjs +0 -1
  55. package/workflow-templates/bosun-native.mjs +212 -2
  56. package/workflow-templates/continuation-loop.mjs +339 -0
  57. package/workflow-templates/github.mjs +516 -40
  58. package/workflow-templates/planning.mjs +446 -17
  59. package/workflow-templates/reliability.mjs +65 -12
  60. package/workflow-templates/task-batch.mjs +24 -8
  61. package/workflow-templates/task-lifecycle.mjs +83 -6
  62. package/workspace/context-cache.mjs +66 -18
  63. package/workspace/workspace-manager.mjs +2 -1
  64. package/workflow-templates/issue-continuation.mjs +0 -243
@@ -291,7 +291,7 @@ export async function createRealtimeSession(agent, provider, config = {}, option
291
291
  turnDetection: {
292
292
  type: turnDetection,
293
293
  ...(turnDetection === "server_vad"
294
- ? { threshold: 0.5, prefix_padding_ms: 300, silence_duration_ms: 500 }
294
+ ? { threshold: 0.75, prefix_padding_ms: 500, silence_duration_ms: 1200 }
295
295
  : {}),
296
296
  ...(turnDetection === "semantic_vad"
297
297
  ? { eagerness: "low" }
@@ -1236,16 +1236,16 @@ async function createOpenAIEphemeralToken(cfg, toolDefinitions = [], callContext
1236
1236
  turn_detection: {
1237
1237
  type: cfg.turnDetection,
1238
1238
  ...(cfg.turnDetection === "server_vad" ? {
1239
- threshold: 0.55,
1239
+ threshold: 0.75,
1240
1240
  prefix_padding_ms: 500,
1241
1241
  silence_duration_ms: 1800,
1242
1242
  create_response: true,
1243
- interrupt_response: true,
1243
+ interrupt_response: false,
1244
1244
  } : {}),
1245
1245
  ...(cfg.turnDetection === "semantic_vad" ? {
1246
1246
  eagerness: "low",
1247
1247
  create_response: true,
1248
- interrupt_response: true,
1248
+ interrupt_response: false,
1249
1249
  } : {}),
1250
1250
  },
1251
1251
  ...(transcriptionEnabled ? { input_audio_transcription: { model: transcriptionModel } } : {}),
@@ -1335,16 +1335,16 @@ async function createAzureEphemeralToken(cfg, toolDefinitions = [], callContext
1335
1335
  turn_detection: {
1336
1336
  type: cfg.turnDetection,
1337
1337
  ...(cfg.turnDetection === "server_vad" ? {
1338
- threshold: 0.55,
1338
+ threshold: 0.75,
1339
1339
  prefix_padding_ms: 500,
1340
1340
  silence_duration_ms: 1800,
1341
1341
  create_response: true,
1342
- interrupt_response: true,
1342
+ interrupt_response: false,
1343
1343
  } : {}),
1344
1344
  ...(cfg.turnDetection === "semantic_vad" ? {
1345
1345
  eagerness: "low",
1346
1346
  create_response: true,
1347
- interrupt_response: true,
1347
+ interrupt_response: false,
1348
1348
  } : {}),
1349
1349
  },
1350
1350
  ...(transcriptionEnabled ? { input_audio_transcription: { model: transcriptionModel } } : {}),
@@ -0,0 +1,145 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { execWithRetry, getPoolSdkName } from "../agent/agent-pool.mjs";
4
+ import { loadConfig } from "../config/config.mjs";
5
+ import {
6
+ BUILTIN_WORKFLOWS,
7
+ createConfiguredPipeline,
8
+ listWorkflowDefinitions,
9
+ resolveWorkflowDefinition,
10
+ runConfiguredWorkflow as runPipelineConfiguredWorkflow,
11
+ } from "./pipeline.mjs";
12
+
13
+ function normalizeWorkflowDefinition(name, definition = {}, config = {}) {
14
+ const resolved = resolveWorkflowDefinition(name, {
15
+ ...(config.workflows || {}),
16
+ [name]: definition,
17
+ });
18
+ return resolved?.definition || null;
19
+ }
20
+
21
+ function listConfiguredWorkflows(config = {}) {
22
+ return listWorkflowDefinitions(config.workflows || {}).map((entry) => {
23
+ const definition = entry?.definition || {};
24
+ const rawAgents = Array.isArray(definition.stages) && definition.stages.length > 0
25
+ ? definition.stages
26
+ : Array.isArray(definition.agents)
27
+ ? definition.agents
28
+ : [];
29
+ return {
30
+ id: definition.id || entry?.name,
31
+ name: definition.name || entry?.name,
32
+ type: definition.type || "sequential",
33
+ description: definition.description || "",
34
+ source: entry?.source || "unknown",
35
+ agents: rawAgents.map((agent, index) => {
36
+ if (typeof agent === "string") {
37
+ return { id: agent, name: agent, role: agent, sdk: null };
38
+ }
39
+ return {
40
+ id: agent?.id || agent?.name || `agent-${index + 1}`,
41
+ name: agent?.name || agent?.id || `agent-${index + 1}`,
42
+ role: agent?.stage || agent?.role || null,
43
+ sdk: agent?.sdk || agent?.executor || null,
44
+ };
45
+ }),
46
+ };
47
+ });
48
+ }
49
+
50
+ function buildConsensus(result) {
51
+ const votes = new Map();
52
+ for (const output of result.outputs || []) {
53
+ const text = String(output.summary || output.output || "").trim();
54
+ if (!text) continue;
55
+ votes.set(text, (votes.get(text) || 0) + 1);
56
+ }
57
+ let winner = null;
58
+ for (const [text, count] of votes.entries()) {
59
+ if (!winner || count > winner.count) winner = { text, count };
60
+ }
61
+ return winner;
62
+ }
63
+
64
+ function createWorkflowAgentPool(options = {}) {
65
+ if (options.dryRun === true) {
66
+ return {
67
+ getPoolSdkName,
68
+ async execWithRetry(prompt, execOptions = {}) {
69
+ const sdk = execOptions.sdk || getPoolSdkName() || "codex";
70
+ return {
71
+ success: true,
72
+ output: `[${sdk}] ${String(prompt).split("\n")[0]}`,
73
+ sdk,
74
+ attempts: 1,
75
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
76
+ };
77
+ },
78
+ };
79
+ }
80
+ return { execWithRetry, getPoolSdkName };
81
+ }
82
+
83
+ async function runConfiguredWorkflow(name, input, options = {}) {
84
+ const config = options.config || loadConfig();
85
+ const result = await runPipelineConfiguredWorkflow(name, input, {
86
+ workflows: config.workflows || {},
87
+ services: {
88
+ agentPool: options.services?.agentPool || createWorkflowAgentPool(options),
89
+ },
90
+ runOptions: options.runOptions || {},
91
+ });
92
+
93
+ const resolved = resolveWorkflowDefinition(name, config.workflows || {});
94
+ const workflow = resolved?.definition || { id: name, name, type: "sequential" };
95
+ const status = result.success ? "success" : "failed";
96
+ return {
97
+ ...result,
98
+ status,
99
+ workflow: {
100
+ id: workflow.id || name,
101
+ name: workflow.name || name,
102
+ type: workflow.type || "sequential",
103
+ },
104
+ consensus:
105
+ String(workflow.id || name) === "consensus-vote" ? buildConsensus(result) : undefined,
106
+ };
107
+ }
108
+
109
+ function loadWorkflowInputFromFile(pathValue) {
110
+ const fullPath = resolve(pathValue);
111
+ const raw = readFileSync(fullPath, "utf8");
112
+ try {
113
+ return JSON.parse(raw);
114
+ } catch {
115
+ return raw;
116
+ }
117
+ }
118
+
119
+ function createWorkflowInstance(definition, options = {}) {
120
+ return createConfiguredPipeline(definition, {
121
+ services: {
122
+ agentPool: options.services?.agentPool || createWorkflowAgentPool(options),
123
+ },
124
+ hub: options.hub,
125
+ createHub: options.createHub,
126
+ hubOptions: options.hubOptions,
127
+ });
128
+ }
129
+
130
+ export {
131
+ BUILTIN_WORKFLOWS,
132
+ normalizeWorkflowDefinition,
133
+ listConfiguredWorkflows,
134
+ loadWorkflowInputFromFile,
135
+ createWorkflowInstance,
136
+ runConfiguredWorkflow,
137
+ };
138
+ export default {
139
+ BUILTIN_WORKFLOWS,
140
+ normalizeWorkflowDefinition,
141
+ listConfiguredWorkflows,
142
+ loadWorkflowInputFromFile,
143
+ createWorkflowInstance,
144
+ runConfiguredWorkflow,
145
+ };
@@ -0,0 +1,237 @@
1
+ /**
2
+ * @module msg-hub
3
+ * @description Lightweight reference-passing message hub for active agents.
4
+ */
5
+
6
+ import { randomUUID } from "node:crypto";
7
+ import { EventEmitter } from "node:events";
8
+
9
+ const DEFAULT_HISTORY_LIMIT = 100;
10
+ const DEFAULT_MAX_MESSAGE_BYTES = 4096;
11
+ const DISALLOWED_KEYS = new Set([
12
+ "context",
13
+ "conversation",
14
+ "history",
15
+ "messages",
16
+ "raw",
17
+ "transcript",
18
+ ]);
19
+ const ALLOWED_KEYS = [
20
+ "kind",
21
+ "taskId",
22
+ "taskIds",
23
+ "workflowId",
24
+ "runId",
25
+ "branch",
26
+ "filePaths",
27
+ "paths",
28
+ "summary",
29
+ "metadata",
30
+ "source",
31
+ ];
32
+
33
+ function truncateText(value, limit = 320) {
34
+ const text = String(value ?? "").trim();
35
+ if (!text) return "";
36
+ if (text.length <= limit) return text;
37
+ return `${text.slice(0, Math.max(0, limit - 1)).trimEnd()}…`;
38
+ }
39
+
40
+ function normalizeParticipant(participant) {
41
+ if (typeof participant === "string") {
42
+ return {
43
+ id: participant,
44
+ name: participant,
45
+ onMessage: null,
46
+ };
47
+ }
48
+ if (!participant || typeof participant !== "object") {
49
+ throw new TypeError("MsgHub participant must be a string or object.");
50
+ }
51
+ const id = String(participant.id || participant.name || randomUUID()).trim();
52
+ if (!id) throw new Error("MsgHub participant must have an id or name.");
53
+ return {
54
+ id,
55
+ name: String(participant.name || id),
56
+ onMessage:
57
+ typeof participant.onMessage === "function"
58
+ ? participant.onMessage.bind(participant)
59
+ : null,
60
+ };
61
+ }
62
+
63
+ function sanitizeReference(message, maxBytes) {
64
+ const raw =
65
+ message && typeof message === "object" && !Array.isArray(message)
66
+ ? message
67
+ : { summary: String(message ?? "") };
68
+ const ref = {};
69
+
70
+ for (const key of ALLOWED_KEYS) {
71
+ if (raw[key] == null) continue;
72
+ if (key === "summary") {
73
+ ref.summary = truncateText(raw.summary);
74
+ continue;
75
+ }
76
+ if (key === "filePaths" || key === "paths") {
77
+ const values = Array.isArray(raw[key])
78
+ ? raw[key].map((entry) => String(entry || "").trim()).filter(Boolean)
79
+ : [];
80
+ if (values.length > 0) ref[key] = [...new Set(values)];
81
+ continue;
82
+ }
83
+ ref[key] = raw[key];
84
+ }
85
+
86
+ for (const [key, value] of Object.entries(raw)) {
87
+ if (key.startsWith("_")) continue;
88
+ if (DISALLOWED_KEYS.has(key) || ALLOWED_KEYS.includes(key)) continue;
89
+ if (value == null) continue;
90
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
91
+ if (!ref.metadata || typeof ref.metadata !== "object") ref.metadata = {};
92
+ ref.metadata[key] = typeof value === "string" ? truncateText(value, 120) : value;
93
+ }
94
+ }
95
+
96
+ let serialized = JSON.stringify(ref);
97
+ if (Buffer.byteLength(serialized, "utf8") <= maxBytes) {
98
+ return ref;
99
+ }
100
+
101
+ if (ref.metadata && typeof ref.metadata === "object") {
102
+ ref.metadata = { note: "trimmed" };
103
+ }
104
+ if (ref.summary) {
105
+ ref.summary = truncateText(ref.summary, 160);
106
+ }
107
+ serialized = JSON.stringify(ref);
108
+ if (Buffer.byteLength(serialized, "utf8") <= maxBytes) {
109
+ return ref;
110
+ }
111
+
112
+ return {
113
+ kind: ref.kind || "reference",
114
+ taskId: ref.taskId || null,
115
+ branch: ref.branch || null,
116
+ summary: truncateText(ref.summary || "reference trimmed", 96),
117
+ };
118
+ }
119
+
120
+ export class MsgHub extends EventEmitter {
121
+ static async create(participants = [], options = {}) {
122
+ return new MsgHub(participants, options);
123
+ }
124
+
125
+ constructor(participants = [], options = {}) {
126
+ super();
127
+ this.options = {
128
+ autoBroadcast: options.autoBroadcast !== false,
129
+ historyLimit: Number(options.historyLimit || DEFAULT_HISTORY_LIMIT),
130
+ maxMessageBytes: Number(options.maxMessageBytes || DEFAULT_MAX_MESSAGE_BYTES),
131
+ };
132
+ this._participants = new Map();
133
+ this._history = [];
134
+ this._closed = false;
135
+ for (const participant of participants) {
136
+ this.add(participant);
137
+ }
138
+ }
139
+
140
+ add(participant) {
141
+ if (this._closed) throw new Error("MsgHub is closed.");
142
+ const normalized = normalizeParticipant(participant);
143
+ this._participants.set(normalized.id, normalized);
144
+ this.emit("participant:added", {
145
+ participantId: normalized.id,
146
+ participantName: normalized.name,
147
+ size: this._participants.size,
148
+ });
149
+ return normalized;
150
+ }
151
+
152
+ remove(participantOrId) {
153
+ const id = typeof participantOrId === "string"
154
+ ? participantOrId
155
+ : participantOrId?.id || participantOrId?.name || "";
156
+ const removed = this._participants.delete(String(id || "").trim());
157
+ if (removed) {
158
+ this.emit("participant:removed", {
159
+ participantId: String(id || "").trim(),
160
+ size: this._participants.size,
161
+ });
162
+ }
163
+ return removed;
164
+ }
165
+
166
+ listParticipants() {
167
+ return [...this._participants.values()].map((participant) => ({
168
+ id: participant.id,
169
+ name: participant.name,
170
+ }));
171
+ }
172
+
173
+ get history() {
174
+ return this._history.map((entry) => ({ ...entry }));
175
+ }
176
+
177
+ subscribe(listener) {
178
+ this.on("message", listener);
179
+ return () => this.off("message", listener);
180
+ }
181
+
182
+ async publish(from, message, options = {}) {
183
+ if (this._closed) throw new Error("MsgHub is closed.");
184
+ const senderId = typeof from === "string"
185
+ ? String(from).trim()
186
+ : String(from?.id || from?.name || "").trim();
187
+ if (senderId && !this._participants.has(senderId)) {
188
+ this.add(senderId);
189
+ }
190
+
191
+ const to = Array.isArray(options.to)
192
+ ? options.to.map((entry) => String(entry || "").trim()).filter(Boolean)
193
+ : null;
194
+ const recipients = to || [...this._participants.keys()].filter((id) => id !== senderId);
195
+ const envelope = {
196
+ id: randomUUID(),
197
+ from: senderId || null,
198
+ to: recipients,
199
+ message: sanitizeReference(message, this.options.maxMessageBytes),
200
+ timestamp: new Date().toISOString(),
201
+ timestampMs: Date.now(),
202
+ };
203
+
204
+ this._history.push(envelope);
205
+ if (this._history.length > this.options.historyLimit) {
206
+ this._history.splice(0, this._history.length - this.options.historyLimit);
207
+ }
208
+
209
+ this.emit("message", envelope);
210
+ if (this.options.autoBroadcast) {
211
+ for (const recipientId of recipients) {
212
+ const participant = this._participants.get(recipientId);
213
+ if (!participant?.onMessage) continue;
214
+ try {
215
+ await participant.onMessage(envelope, this);
216
+ } catch (error) {
217
+ this.emit("delivery:error", {
218
+ participantId: recipientId,
219
+ error: String(error?.message || error),
220
+ envelope,
221
+ });
222
+ }
223
+ }
224
+ }
225
+ return envelope;
226
+ }
227
+
228
+ async close() {
229
+ if (this._closed) return;
230
+ this._closed = true;
231
+ this._participants.clear();
232
+ this.emit("closed", { closedAt: new Date().toISOString() });
233
+ this.removeAllListeners();
234
+ }
235
+ }
236
+
237
+ export default MsgHub;