bosun 0.37.0 → 0.37.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/.env.example +4 -1
- package/agent-tool-config.mjs +338 -0
- package/bosun-skills.mjs +59 -4
- package/bosun.schema.json +1 -1
- package/desktop/launch.mjs +18 -0
- package/desktop/main.mjs +52 -13
- package/fleet-coordinator.mjs +34 -1
- package/kanban-adapter.mjs +30 -3
- package/library-manager.mjs +66 -0
- package/maintenance.mjs +30 -5
- package/monitor.mjs +56 -0
- package/package.json +4 -1
- package/setup-web-server.mjs +73 -12
- package/setup.mjs +3 -3
- package/ui/app.js +40 -3
- package/ui/components/session-list.js +25 -7
- package/ui/components/workspace-switcher.js +48 -1
- package/ui/demo.html +176 -0
- package/ui/modules/mic-track-registry.js +83 -0
- package/ui/modules/settings-schema.js +4 -1
- package/ui/modules/state.js +25 -0
- package/ui/modules/streaming.js +1 -1
- package/ui/modules/voice-barge-in.js +27 -0
- package/ui/modules/voice-client-sdk.js +268 -42
- package/ui/modules/voice-client.js +665 -61
- package/ui/modules/voice-overlay.js +829 -47
- package/ui/setup.html +151 -9
- package/ui/styles.css +258 -0
- package/ui/tabs/chat.js +11 -0
- package/ui/tabs/library.js +890 -15
- package/ui/tabs/settings.js +51 -11
- package/ui/tabs/telemetry.js +327 -105
- package/ui/tabs/workflows.js +86 -0
- package/ui-server.mjs +1201 -107
- package/voice-action-dispatcher.mjs +81 -0
- package/voice-agents-sdk.mjs +2 -2
- package/voice-relay.mjs +131 -14
- package/voice-tools.mjs +475 -9
- package/workflow-engine.mjs +54 -0
- package/workflow-nodes.mjs +177 -28
- package/workflow-templates/github.mjs +205 -94
- package/workflow-templates/task-batch.mjs +247 -0
- package/workflow-templates.mjs +15 -0
package/voice-tools.mjs
CHANGED
|
@@ -330,6 +330,83 @@ async function getRecentSessionContextSnippet(sessionId, limit = 8) {
|
|
|
330
330
|
}
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
+
function parseObjectArg(value, fieldName) {
|
|
334
|
+
if (value == null || value === "") return {};
|
|
335
|
+
if (typeof value === "object" && !Array.isArray(value)) return value;
|
|
336
|
+
if (typeof value === "string") {
|
|
337
|
+
try {
|
|
338
|
+
const parsed = JSON.parse(value);
|
|
339
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
340
|
+
return parsed;
|
|
341
|
+
}
|
|
342
|
+
throw new Error(`${fieldName} must decode to a JSON object`);
|
|
343
|
+
} catch (err) {
|
|
344
|
+
throw new Error(`Invalid JSON for ${fieldName}: ${err.message}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
throw new Error(`${fieldName} must be an object or JSON string`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function parseAnyJson(text) {
|
|
351
|
+
const raw = String(text || "").trim();
|
|
352
|
+
if (!raw) return null;
|
|
353
|
+
try {
|
|
354
|
+
return JSON.parse(raw);
|
|
355
|
+
} catch {
|
|
356
|
+
// fall through
|
|
357
|
+
}
|
|
358
|
+
const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
359
|
+
if (fenced?.[1]) {
|
|
360
|
+
try {
|
|
361
|
+
return JSON.parse(fenced[1].trim());
|
|
362
|
+
} catch {
|
|
363
|
+
// fall through
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const firstObj = raw.indexOf("{");
|
|
367
|
+
const lastObj = raw.lastIndexOf("}");
|
|
368
|
+
if (firstObj >= 0 && lastObj > firstObj) {
|
|
369
|
+
try {
|
|
370
|
+
return JSON.parse(raw.slice(firstObj, lastObj + 1));
|
|
371
|
+
} catch {
|
|
372
|
+
// fall through
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function normalizeWorkflowDefinition(def, args = {}) {
|
|
379
|
+
const input = def && typeof def === "object" && !Array.isArray(def) ? { ...def } : {};
|
|
380
|
+
if (typeof args.workflowId === "string" && args.workflowId.trim()) {
|
|
381
|
+
input.id = args.workflowId.trim();
|
|
382
|
+
} else if (typeof args.id === "string" && args.id.trim()) {
|
|
383
|
+
input.id = args.id.trim();
|
|
384
|
+
}
|
|
385
|
+
if (!input.name) {
|
|
386
|
+
const fallbackName = String(args.name || "").trim();
|
|
387
|
+
input.name = fallbackName || input.id || "Voice Workflow";
|
|
388
|
+
}
|
|
389
|
+
if (typeof args.description === "string" && args.description.trim()) {
|
|
390
|
+
input.description = args.description.trim();
|
|
391
|
+
}
|
|
392
|
+
if (!Array.isArray(input.nodes)) input.nodes = [];
|
|
393
|
+
if (!Array.isArray(input.edges)) input.edges = [];
|
|
394
|
+
if (!Array.isArray(input.triggers) && input.triggers != null) input.triggers = [];
|
|
395
|
+
if (!input.variables || typeof input.variables !== "object" || Array.isArray(input.variables)) {
|
|
396
|
+
input.variables = {};
|
|
397
|
+
}
|
|
398
|
+
if (!input.metadata || typeof input.metadata !== "object" || Array.isArray(input.metadata)) {
|
|
399
|
+
input.metadata = {};
|
|
400
|
+
}
|
|
401
|
+
input.metadata.updatedBy = "voice-tool";
|
|
402
|
+
if (typeof args.enabled === "boolean") {
|
|
403
|
+
input.enabled = args.enabled;
|
|
404
|
+
} else if (input.enabled == null) {
|
|
405
|
+
input.enabled = false;
|
|
406
|
+
}
|
|
407
|
+
return input;
|
|
408
|
+
}
|
|
409
|
+
|
|
333
410
|
function normalizeCandidatePath(input) {
|
|
334
411
|
if (!input) return "";
|
|
335
412
|
try {
|
|
@@ -810,6 +887,90 @@ const TOOL_DEFS = [
|
|
|
810
887
|
description: "List available workflow templates and installed workflow definitions.",
|
|
811
888
|
parameters: { type: "object", properties: {} },
|
|
812
889
|
},
|
|
890
|
+
{
|
|
891
|
+
type: "function",
|
|
892
|
+
name: "create_workflow",
|
|
893
|
+
description: "Create a new workflow from a JSON definition (or create a blank workflow by name).",
|
|
894
|
+
parameters: {
|
|
895
|
+
type: "object",
|
|
896
|
+
properties: {
|
|
897
|
+
definition: {
|
|
898
|
+
description: "Workflow definition object (or JSON string)",
|
|
899
|
+
},
|
|
900
|
+
name: { type: "string", description: "Workflow name (used for blank workflow creation)" },
|
|
901
|
+
description: { type: "string", description: "Workflow description" },
|
|
902
|
+
enabled: { type: "boolean", description: "Whether workflow should be enabled on save. Default: false" },
|
|
903
|
+
workflowId: { type: "string", description: "Optional explicit workflow id" },
|
|
904
|
+
},
|
|
905
|
+
},
|
|
906
|
+
},
|
|
907
|
+
{
|
|
908
|
+
type: "function",
|
|
909
|
+
name: "update_workflow_definition",
|
|
910
|
+
description: "Update an existing workflow definition (merge patch by default, or replace).",
|
|
911
|
+
parameters: {
|
|
912
|
+
type: "object",
|
|
913
|
+
properties: {
|
|
914
|
+
workflowId: { type: "string", description: "Workflow id to update" },
|
|
915
|
+
patch: {
|
|
916
|
+
description: "Partial workflow object patch (or JSON string)",
|
|
917
|
+
},
|
|
918
|
+
replace: {
|
|
919
|
+
type: "boolean",
|
|
920
|
+
description: "Replace the definition instead of merge-patch. Default: false",
|
|
921
|
+
},
|
|
922
|
+
},
|
|
923
|
+
required: ["workflowId"],
|
|
924
|
+
},
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
type: "function",
|
|
928
|
+
name: "delete_workflow",
|
|
929
|
+
description: "Delete a workflow definition by id.",
|
|
930
|
+
parameters: {
|
|
931
|
+
type: "object",
|
|
932
|
+
properties: {
|
|
933
|
+
workflowId: { type: "string", description: "Workflow id" },
|
|
934
|
+
},
|
|
935
|
+
required: ["workflowId"],
|
|
936
|
+
},
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
type: "function",
|
|
940
|
+
name: "create_workflow_from_template",
|
|
941
|
+
description: "Install a workflow template as a new workflow instance, with optional variable overrides.",
|
|
942
|
+
parameters: {
|
|
943
|
+
type: "object",
|
|
944
|
+
properties: {
|
|
945
|
+
templateId: { type: "string", description: "Workflow template id" },
|
|
946
|
+
overrides: {
|
|
947
|
+
description: "Variable override object (or JSON string)",
|
|
948
|
+
},
|
|
949
|
+
executeAfterCreate: { type: "boolean", description: "Run the new workflow immediately. Default: false" },
|
|
950
|
+
input: { description: "Input object (or JSON string) for executeAfterCreate" },
|
|
951
|
+
},
|
|
952
|
+
required: ["templateId"],
|
|
953
|
+
},
|
|
954
|
+
},
|
|
955
|
+
{
|
|
956
|
+
type: "function",
|
|
957
|
+
name: "generate_workflow_with_agent",
|
|
958
|
+
description: "Ask the coding agent to generate a workflow JSON, then optionally save it.",
|
|
959
|
+
parameters: {
|
|
960
|
+
type: "object",
|
|
961
|
+
properties: {
|
|
962
|
+
prompt: { type: "string", description: "What workflow to generate" },
|
|
963
|
+
save: { type: "boolean", description: "Save generated workflow automatically. Default: true" },
|
|
964
|
+
enabled: { type: "boolean", description: "When saving, set enabled state. Default: false" },
|
|
965
|
+
executor: {
|
|
966
|
+
type: "string",
|
|
967
|
+
enum: ["codex-sdk", "copilot-sdk", "claude-sdk", "gemini-sdk", "opencode-sdk"],
|
|
968
|
+
description: "Agent executor to use for generation",
|
|
969
|
+
},
|
|
970
|
+
},
|
|
971
|
+
required: ["prompt"],
|
|
972
|
+
},
|
|
973
|
+
},
|
|
813
974
|
{
|
|
814
975
|
type: "function",
|
|
815
976
|
name: "get_workflow_definition",
|
|
@@ -826,6 +987,20 @@ const TOOL_DEFS = [
|
|
|
826
987
|
required: ["workflowId"],
|
|
827
988
|
},
|
|
828
989
|
},
|
|
990
|
+
{
|
|
991
|
+
type: "function",
|
|
992
|
+
name: "execute_workflow",
|
|
993
|
+
description: "Execute a workflow now with optional input payload.",
|
|
994
|
+
parameters: {
|
|
995
|
+
type: "object",
|
|
996
|
+
properties: {
|
|
997
|
+
workflowId: { type: "string", description: "Workflow id to run" },
|
|
998
|
+
input: { description: "Input payload object (or JSON string)" },
|
|
999
|
+
force: { type: "boolean", description: "Force run even if workflow is disabled. Default: false" },
|
|
1000
|
+
},
|
|
1001
|
+
required: ["workflowId"],
|
|
1002
|
+
},
|
|
1003
|
+
},
|
|
829
1004
|
{
|
|
830
1005
|
type: "function",
|
|
831
1006
|
name: "list_workflow_runs",
|
|
@@ -867,10 +1042,22 @@ const TOOL_DEFS = [
|
|
|
867
1042
|
required: ["runId"],
|
|
868
1043
|
},
|
|
869
1044
|
},
|
|
1045
|
+
{
|
|
1046
|
+
type: "function",
|
|
1047
|
+
name: "analyze_workflow",
|
|
1048
|
+
description: "Analyze workflow health using structure + recent run history.",
|
|
1049
|
+
parameters: {
|
|
1050
|
+
type: "object",
|
|
1051
|
+
properties: {
|
|
1052
|
+
workflowId: { type: "string", description: "Workflow id. Omit to analyze multiple workflows." },
|
|
1053
|
+
limit: { type: "number", description: "Max runs used for analysis. Default: 30" },
|
|
1054
|
+
},
|
|
1055
|
+
},
|
|
1056
|
+
},
|
|
870
1057
|
{
|
|
871
1058
|
type: "function",
|
|
872
1059
|
name: "retry_workflow_run",
|
|
873
|
-
description: "Retry
|
|
1060
|
+
description: "Retry workflow run: from_failed (only failed runs) or from_scratch (any run).",
|
|
874
1061
|
parameters: {
|
|
875
1062
|
type: "object",
|
|
876
1063
|
properties: {
|
|
@@ -1837,10 +2024,12 @@ const TOOL_HANDLERS = {
|
|
|
1837
2024
|
id: w.id,
|
|
1838
2025
|
name: w.name || w.id,
|
|
1839
2026
|
enabled: w.enabled !== false,
|
|
1840
|
-
triggerCount: Array.isArray(w.triggers)
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
2027
|
+
triggerCount: Array.isArray(w.triggers)
|
|
2028
|
+
? w.triggers.length
|
|
2029
|
+
: (w.trigger ? 1 : 0),
|
|
2030
|
+
nodeCount: Number(w.nodeCount || (Array.isArray(w.nodes) ? w.nodes.length : 0)),
|
|
2031
|
+
edgeCount: Number(w.edgeCount || (Array.isArray(w.edges) ? w.edges.length : 0)),
|
|
2032
|
+
updatedAt: w?.metadata?.updatedAt || w.updatedAt || null,
|
|
1844
2033
|
})),
|
|
1845
2034
|
};
|
|
1846
2035
|
} catch {
|
|
@@ -1852,16 +2041,200 @@ const TOOL_HANDLERS = {
|
|
|
1852
2041
|
id: w.id,
|
|
1853
2042
|
name: w.name || w.id,
|
|
1854
2043
|
enabled: w.enabled !== false,
|
|
1855
|
-
triggerCount: Array.isArray(w.triggers)
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
2044
|
+
triggerCount: Array.isArray(w.triggers)
|
|
2045
|
+
? w.triggers.length
|
|
2046
|
+
: (w.trigger ? 1 : 0),
|
|
2047
|
+
nodeCount: Number(w.nodeCount || (Array.isArray(w.nodes) ? w.nodes.length : 0)),
|
|
2048
|
+
edgeCount: Number(w.edgeCount || (Array.isArray(w.edges) ? w.edges.length : 0)),
|
|
2049
|
+
updatedAt: w?.metadata?.updatedAt || w.updatedAt || null,
|
|
1859
2050
|
})),
|
|
1860
2051
|
error: "Workflow templates not available.",
|
|
1861
2052
|
};
|
|
1862
2053
|
}
|
|
1863
2054
|
},
|
|
1864
2055
|
|
|
2056
|
+
async create_workflow(args = {}) {
|
|
2057
|
+
const wfEngineMod = await getWorkflowEngineModule();
|
|
2058
|
+
const engine = typeof wfEngineMod.getWorkflowEngine === "function"
|
|
2059
|
+
? wfEngineMod.getWorkflowEngine()
|
|
2060
|
+
: null;
|
|
2061
|
+
if (!engine?.save) return { ok: false, error: "Workflow engine is unavailable." };
|
|
2062
|
+
|
|
2063
|
+
const hasDefinition = args.definition != null && args.definition !== "";
|
|
2064
|
+
const defInput = hasDefinition
|
|
2065
|
+
? parseObjectArg(args.definition, "definition")
|
|
2066
|
+
: {};
|
|
2067
|
+
const normalized = normalizeWorkflowDefinition(defInput, args);
|
|
2068
|
+
const saved = engine.save(normalized);
|
|
2069
|
+
return {
|
|
2070
|
+
ok: true,
|
|
2071
|
+
workflow: {
|
|
2072
|
+
id: saved.id,
|
|
2073
|
+
name: saved.name || saved.id,
|
|
2074
|
+
enabled: saved.enabled !== false,
|
|
2075
|
+
nodeCount: Array.isArray(saved.nodes) ? saved.nodes.length : 0,
|
|
2076
|
+
edgeCount: Array.isArray(saved.edges) ? saved.edges.length : 0,
|
|
2077
|
+
updatedAt: saved?.metadata?.updatedAt || null,
|
|
2078
|
+
},
|
|
2079
|
+
};
|
|
2080
|
+
},
|
|
2081
|
+
|
|
2082
|
+
async update_workflow_definition(args = {}) {
|
|
2083
|
+
const workflowId = String(args.workflowId || args.id || "").trim();
|
|
2084
|
+
if (!workflowId) return { ok: false, error: "workflowId is required." };
|
|
2085
|
+
const wfEngineMod = await getWorkflowEngineModule();
|
|
2086
|
+
const engine = typeof wfEngineMod.getWorkflowEngine === "function"
|
|
2087
|
+
? wfEngineMod.getWorkflowEngine()
|
|
2088
|
+
: null;
|
|
2089
|
+
if (!engine?.get || !engine?.save) return { ok: false, error: "Workflow engine is unavailable." };
|
|
2090
|
+
const existing = engine.get(workflowId);
|
|
2091
|
+
if (!existing) return { ok: false, error: `Workflow "${workflowId}" not found.` };
|
|
2092
|
+
|
|
2093
|
+
const patchObj = parseObjectArg(
|
|
2094
|
+
args.patch ?? args.definition ?? args.workflow ?? {},
|
|
2095
|
+
"patch",
|
|
2096
|
+
);
|
|
2097
|
+
const replace = args.replace === true;
|
|
2098
|
+
const merged = replace
|
|
2099
|
+
? { ...patchObj }
|
|
2100
|
+
: {
|
|
2101
|
+
...existing,
|
|
2102
|
+
...patchObj,
|
|
2103
|
+
metadata: {
|
|
2104
|
+
...(existing.metadata || {}),
|
|
2105
|
+
...(patchObj.metadata || {}),
|
|
2106
|
+
},
|
|
2107
|
+
};
|
|
2108
|
+
const normalized = normalizeWorkflowDefinition(merged, {
|
|
2109
|
+
...args,
|
|
2110
|
+
workflowId,
|
|
2111
|
+
name: merged.name || existing.name,
|
|
2112
|
+
});
|
|
2113
|
+
const saved = engine.save(normalized);
|
|
2114
|
+
return {
|
|
2115
|
+
ok: true,
|
|
2116
|
+
workflow: {
|
|
2117
|
+
id: saved.id,
|
|
2118
|
+
name: saved.name || saved.id,
|
|
2119
|
+
enabled: saved.enabled !== false,
|
|
2120
|
+
nodeCount: Array.isArray(saved.nodes) ? saved.nodes.length : 0,
|
|
2121
|
+
edgeCount: Array.isArray(saved.edges) ? saved.edges.length : 0,
|
|
2122
|
+
updatedAt: saved?.metadata?.updatedAt || null,
|
|
2123
|
+
},
|
|
2124
|
+
};
|
|
2125
|
+
},
|
|
2126
|
+
|
|
2127
|
+
async delete_workflow(args = {}) {
|
|
2128
|
+
const workflowId = String(args.workflowId || args.id || "").trim();
|
|
2129
|
+
if (!workflowId) return { ok: false, error: "workflowId is required." };
|
|
2130
|
+
const wfEngineMod = await getWorkflowEngineModule();
|
|
2131
|
+
const engine = typeof wfEngineMod.getWorkflowEngine === "function"
|
|
2132
|
+
? wfEngineMod.getWorkflowEngine()
|
|
2133
|
+
: null;
|
|
2134
|
+
if (!engine?.delete) return { ok: false, error: "Workflow engine is unavailable." };
|
|
2135
|
+
const deleted = await engine.delete(workflowId);
|
|
2136
|
+
return { ok: Boolean(deleted), workflowId, deleted: Boolean(deleted) };
|
|
2137
|
+
},
|
|
2138
|
+
|
|
2139
|
+
async create_workflow_from_template(args = {}) {
|
|
2140
|
+
const templateId = String(args.templateId || args.id || "").trim();
|
|
2141
|
+
if (!templateId) return { ok: false, error: "templateId is required." };
|
|
2142
|
+
const wfEngineMod = await getWorkflowEngineModule();
|
|
2143
|
+
const engine = typeof wfEngineMod.getWorkflowEngine === "function"
|
|
2144
|
+
? wfEngineMod.getWorkflowEngine()
|
|
2145
|
+
: null;
|
|
2146
|
+
if (!engine?.save || !engine?.execute) return { ok: false, error: "Workflow engine is unavailable." };
|
|
2147
|
+
const wfTemplates = await import("./workflow-templates.mjs");
|
|
2148
|
+
if (typeof wfTemplates.installTemplate !== "function") {
|
|
2149
|
+
return { ok: false, error: "Workflow template installer is unavailable." };
|
|
2150
|
+
}
|
|
2151
|
+
const overrides = parseObjectArg(args.overrides ?? {}, "overrides");
|
|
2152
|
+
const saved = wfTemplates.installTemplate(templateId, engine, overrides);
|
|
2153
|
+
const executeAfterCreate = args.executeAfterCreate === true;
|
|
2154
|
+
let run = null;
|
|
2155
|
+
if (executeAfterCreate) {
|
|
2156
|
+
const input = parseObjectArg(args.input ?? {}, "input");
|
|
2157
|
+
const ctx = await engine.execute(saved.id, input);
|
|
2158
|
+
run = {
|
|
2159
|
+
runId: ctx?.id || null,
|
|
2160
|
+
status: Array.isArray(ctx?.errors) && ctx.errors.length > 0 ? "failed" : "completed",
|
|
2161
|
+
errorCount: Array.isArray(ctx?.errors) ? ctx.errors.length : 0,
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
return {
|
|
2165
|
+
ok: true,
|
|
2166
|
+
workflow: {
|
|
2167
|
+
id: saved.id,
|
|
2168
|
+
name: saved.name || saved.id,
|
|
2169
|
+
enabled: saved.enabled !== false,
|
|
2170
|
+
installedFrom: saved?.metadata?.installedFrom || templateId,
|
|
2171
|
+
nodeCount: Array.isArray(saved.nodes) ? saved.nodes.length : 0,
|
|
2172
|
+
edgeCount: Array.isArray(saved.edges) ? saved.edges.length : 0,
|
|
2173
|
+
},
|
|
2174
|
+
run,
|
|
2175
|
+
};
|
|
2176
|
+
},
|
|
2177
|
+
|
|
2178
|
+
async generate_workflow_with_agent(args = {}, context = {}) {
|
|
2179
|
+
const prompt = String(args.prompt || "").trim();
|
|
2180
|
+
if (!prompt) return { ok: false, error: "prompt is required." };
|
|
2181
|
+
const cfg = loadConfig();
|
|
2182
|
+
const requestedExecutor = String(
|
|
2183
|
+
args.executor || context?.executor || cfg.voice?.delegateExecutor || cfg.primaryAgent || "codex-sdk",
|
|
2184
|
+
).trim().toLowerCase();
|
|
2185
|
+
const executor = VALID_EXECUTORS.has(requestedExecutor)
|
|
2186
|
+
? requestedExecutor
|
|
2187
|
+
: (VALID_EXECUTORS.has(cfg.primaryAgent) ? cfg.primaryAgent : "codex-sdk");
|
|
2188
|
+
|
|
2189
|
+
const generationPrompt =
|
|
2190
|
+
"Generate a Bosun workflow JSON definition only. " +
|
|
2191
|
+
"Return strict JSON with keys: name, description, enabled, variables, nodes, edges, triggers, metadata. " +
|
|
2192
|
+
"Do not include markdown fences.\n\n" +
|
|
2193
|
+
`User request:\n${prompt}`;
|
|
2194
|
+
const result = await execPooledPrompt(generationPrompt, {
|
|
2195
|
+
sdk: executor,
|
|
2196
|
+
mode: "agent",
|
|
2197
|
+
sessionId: context?.sessionId || `voice-workflow-generate-${Date.now()}`,
|
|
2198
|
+
metadata: { source: "voice-workflow-generator" },
|
|
2199
|
+
timeoutMs: 45_000,
|
|
2200
|
+
});
|
|
2201
|
+
const text = typeof result === "string"
|
|
2202
|
+
? result
|
|
2203
|
+
: result?.finalResponse || result?.text || result?.message || JSON.stringify(result);
|
|
2204
|
+
const parsed = parseAnyJson(text);
|
|
2205
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2206
|
+
return {
|
|
2207
|
+
ok: false,
|
|
2208
|
+
error: "Agent did not return valid workflow JSON.",
|
|
2209
|
+
raw: String(text || "").slice(0, 2000),
|
|
2210
|
+
};
|
|
2211
|
+
}
|
|
2212
|
+
const normalized = normalizeWorkflowDefinition(parsed, {
|
|
2213
|
+
enabled: typeof args.enabled === "boolean" ? args.enabled : false,
|
|
2214
|
+
});
|
|
2215
|
+
const shouldSave = args.save !== false;
|
|
2216
|
+
if (!shouldSave) {
|
|
2217
|
+
return { ok: true, saved: false, workflow: normalized };
|
|
2218
|
+
}
|
|
2219
|
+
const wfEngineMod = await getWorkflowEngineModule();
|
|
2220
|
+
const engine = typeof wfEngineMod.getWorkflowEngine === "function"
|
|
2221
|
+
? wfEngineMod.getWorkflowEngine()
|
|
2222
|
+
: null;
|
|
2223
|
+
if (!engine?.save) return { ok: false, error: "Workflow engine is unavailable." };
|
|
2224
|
+
const saved = engine.save(normalized);
|
|
2225
|
+
return {
|
|
2226
|
+
ok: true,
|
|
2227
|
+
saved: true,
|
|
2228
|
+
workflow: {
|
|
2229
|
+
id: saved.id,
|
|
2230
|
+
name: saved.name || saved.id,
|
|
2231
|
+
enabled: saved.enabled !== false,
|
|
2232
|
+
nodeCount: Array.isArray(saved.nodes) ? saved.nodes.length : 0,
|
|
2233
|
+
edgeCount: Array.isArray(saved.edges) ? saved.edges.length : 0,
|
|
2234
|
+
},
|
|
2235
|
+
};
|
|
2236
|
+
},
|
|
2237
|
+
|
|
1865
2238
|
async get_workflow_definition(args = {}) {
|
|
1866
2239
|
const workflowId = String(args.workflowId || args.id || "").trim();
|
|
1867
2240
|
if (!workflowId) return { ok: false, error: "workflowId is required." };
|
|
@@ -1885,6 +2258,31 @@ const TOOL_HANDLERS = {
|
|
|
1885
2258
|
};
|
|
1886
2259
|
},
|
|
1887
2260
|
|
|
2261
|
+
async execute_workflow(args = {}) {
|
|
2262
|
+
const workflowId = String(args.workflowId || args.id || "").trim();
|
|
2263
|
+
if (!workflowId) return { ok: false, error: "workflowId is required." };
|
|
2264
|
+
const wfEngineMod = await getWorkflowEngineModule();
|
|
2265
|
+
const engine = typeof wfEngineMod.getWorkflowEngine === "function"
|
|
2266
|
+
? wfEngineMod.getWorkflowEngine()
|
|
2267
|
+
: null;
|
|
2268
|
+
if (!engine?.execute) return { ok: false, error: "Workflow engine is unavailable." };
|
|
2269
|
+
const input = parseObjectArg(args.input ?? {}, "input");
|
|
2270
|
+
const force = args.force === true;
|
|
2271
|
+
const ctx = await engine.execute(workflowId, input, { force });
|
|
2272
|
+
return {
|
|
2273
|
+
ok: true,
|
|
2274
|
+
run: {
|
|
2275
|
+
runId: ctx?.id || null,
|
|
2276
|
+
workflowId,
|
|
2277
|
+
status: Array.isArray(ctx?.errors) && ctx.errors.length > 0 ? "failed" : "completed",
|
|
2278
|
+
startedAt: Number(ctx?.startedAt) || null,
|
|
2279
|
+
endedAt: Number(ctx?.endedAt) || Date.now(),
|
|
2280
|
+
duration: Number(ctx?.duration) || null,
|
|
2281
|
+
errorCount: Array.isArray(ctx?.errors) ? ctx.errors.length : 0,
|
|
2282
|
+
},
|
|
2283
|
+
};
|
|
2284
|
+
},
|
|
2285
|
+
|
|
1888
2286
|
async list_workflow_runs(args = {}) {
|
|
1889
2287
|
const workflowId = String(args.workflowId || args.id || "").trim();
|
|
1890
2288
|
const statusFilter = String(args.status || "").trim().toLowerCase();
|
|
@@ -1991,6 +2389,15 @@ const TOOL_HANDLERS = {
|
|
|
1991
2389
|
if (!engine?.retryRun) {
|
|
1992
2390
|
return { ok: false, error: "Workflow retry is unavailable." };
|
|
1993
2391
|
}
|
|
2392
|
+
const currentRun = engine?.getRunDetail ? engine.getRunDetail(runId) : null;
|
|
2393
|
+
if (!currentRun) return { ok: false, error: `Workflow run "${runId}" not found.` };
|
|
2394
|
+
const currentStatus = String(currentRun?.status || "").trim().toLowerCase();
|
|
2395
|
+
if (mode === "from_failed" && currentStatus !== "failed") {
|
|
2396
|
+
return {
|
|
2397
|
+
ok: false,
|
|
2398
|
+
error: `retry mode "from_failed" requires a failed run. Current status is "${currentRun?.status || "unknown"}".`,
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
1994
2401
|
const result = await engine.retryRun(runId, { mode });
|
|
1995
2402
|
return {
|
|
1996
2403
|
ok: true,
|
|
@@ -2001,6 +2408,65 @@ const TOOL_HANDLERS = {
|
|
|
2001
2408
|
};
|
|
2002
2409
|
},
|
|
2003
2410
|
|
|
2411
|
+
async analyze_workflow(args = {}) {
|
|
2412
|
+
const wfEngineMod = await getWorkflowEngineModule();
|
|
2413
|
+
const engine = typeof wfEngineMod.getWorkflowEngine === "function"
|
|
2414
|
+
? wfEngineMod.getWorkflowEngine()
|
|
2415
|
+
: null;
|
|
2416
|
+
if (!engine?.list || !engine?.getRunHistory) {
|
|
2417
|
+
return { ok: false, error: "Workflow engine is unavailable." };
|
|
2418
|
+
}
|
|
2419
|
+
const workflowId = String(args.workflowId || args.id || "").trim();
|
|
2420
|
+
const rawLimit = Number(args.limit);
|
|
2421
|
+
const limit = Number.isFinite(rawLimit) && rawLimit > 0
|
|
2422
|
+
? Math.min(200, Math.floor(rawLimit))
|
|
2423
|
+
: 30;
|
|
2424
|
+
const targets = workflowId
|
|
2425
|
+
? [engine.get(workflowId)].filter(Boolean)
|
|
2426
|
+
: engine.list().map((w) => engine.get(w.id)).filter(Boolean);
|
|
2427
|
+
if (workflowId && !targets.length) {
|
|
2428
|
+
return { ok: false, error: `Workflow "${workflowId}" not found.` };
|
|
2429
|
+
}
|
|
2430
|
+
const analyses = targets.map((wf) => {
|
|
2431
|
+
const runs = engine.getRunHistory(wf.id, limit) || [];
|
|
2432
|
+
const byStatus = {};
|
|
2433
|
+
for (const run of runs) {
|
|
2434
|
+
const status = String(run?.status || "unknown").trim().toLowerCase();
|
|
2435
|
+
byStatus[status] = (byStatus[status] || 0) + 1;
|
|
2436
|
+
}
|
|
2437
|
+
const failedCount = Number(byStatus.failed || 0);
|
|
2438
|
+
const completedCount = Number(byStatus.completed || 0);
|
|
2439
|
+
const evaluated = failedCount + completedCount;
|
|
2440
|
+
const failureRate = evaluated > 0 ? failedCount / evaluated : null;
|
|
2441
|
+
const latest = runs[0] || null;
|
|
2442
|
+
return {
|
|
2443
|
+
workflowId: wf.id,
|
|
2444
|
+
name: wf.name || wf.id,
|
|
2445
|
+
enabled: wf.enabled !== false,
|
|
2446
|
+
nodeCount: Array.isArray(wf.nodes) ? wf.nodes.length : Number(wf.nodeCount || 0),
|
|
2447
|
+
edgeCount: Array.isArray(wf.edges) ? wf.edges.length : Number(wf.edgeCount || 0),
|
|
2448
|
+
runCount: runs.length,
|
|
2449
|
+
byStatus,
|
|
2450
|
+
failureRate,
|
|
2451
|
+
latestRun: latest
|
|
2452
|
+
? {
|
|
2453
|
+
runId: latest.runId || null,
|
|
2454
|
+
status: latest.status || "unknown",
|
|
2455
|
+
startedAt: latest.startedAt || null,
|
|
2456
|
+
duration: latest.duration ?? null,
|
|
2457
|
+
errorCount: latest.errorCount ?? 0,
|
|
2458
|
+
isStuck: latest.isStuck === true,
|
|
2459
|
+
}
|
|
2460
|
+
: null,
|
|
2461
|
+
};
|
|
2462
|
+
});
|
|
2463
|
+
return {
|
|
2464
|
+
ok: true,
|
|
2465
|
+
count: analyses.length,
|
|
2466
|
+
analyses,
|
|
2467
|
+
};
|
|
2468
|
+
},
|
|
2469
|
+
|
|
2004
2470
|
async list_skills() {
|
|
2005
2471
|
try {
|
|
2006
2472
|
const skills = await import("./bosun-skills.mjs");
|
package/workflow-engine.mjs
CHANGED
|
@@ -899,6 +899,60 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
899
899
|
return triggered;
|
|
900
900
|
}
|
|
901
901
|
|
|
902
|
+
// ── Schedule trigger evaluation ──────────────────────────────────────────
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Evaluate all workflows that use `trigger.schedule` or `trigger.scheduled_once`.
|
|
906
|
+
* Unlike evaluateTriggers() (event-driven), this is polling-based and should
|
|
907
|
+
* be called periodically (e.g. every 60s) by the monitor.
|
|
908
|
+
*
|
|
909
|
+
* Returns an array of { workflowId, triggeredBy } for workflows whose
|
|
910
|
+
* schedule interval has elapsed since their last completed run.
|
|
911
|
+
*/
|
|
912
|
+
evaluateScheduleTriggers() {
|
|
913
|
+
if (!this._loaded) this.load();
|
|
914
|
+
|
|
915
|
+
const triggered = [];
|
|
916
|
+
const runIndex = this._readRunIndex();
|
|
917
|
+
|
|
918
|
+
for (const [id, def] of this._workflows) {
|
|
919
|
+
if (def.enabled === false) continue;
|
|
920
|
+
|
|
921
|
+
// Skip workflows that are already running
|
|
922
|
+
const alreadyRunning = Array.from(this._activeRuns.values()).some(
|
|
923
|
+
(info) => info?.workflowId === id,
|
|
924
|
+
);
|
|
925
|
+
if (alreadyRunning) continue;
|
|
926
|
+
|
|
927
|
+
const triggerNodes = (def.nodes || []).filter(
|
|
928
|
+
(n) => n.type === "trigger.schedule" || n.type === "trigger.scheduled_once",
|
|
929
|
+
);
|
|
930
|
+
|
|
931
|
+
for (const tNode of triggerNodes) {
|
|
932
|
+
const intervalMs = Number(tNode.config?.intervalMs) || 3600000;
|
|
933
|
+
|
|
934
|
+
// Find the most recent completed run for this workflow
|
|
935
|
+
let lastRunAt = 0;
|
|
936
|
+
for (const entry of runIndex) {
|
|
937
|
+
if (entry?.workflowId !== id) continue;
|
|
938
|
+
const ts = Number(entry?.startedAt || entry?.completedAt || 0);
|
|
939
|
+
if (ts > lastRunAt) lastRunAt = ts;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const elapsed = Date.now() - lastRunAt;
|
|
943
|
+
if (elapsed >= intervalMs) {
|
|
944
|
+
triggered.push({ workflowId: id, triggeredBy: tNode.id });
|
|
945
|
+
|
|
946
|
+
// For scheduled_once, only fire if never run before
|
|
947
|
+
if (tNode.type === "trigger.scheduled_once" && lastRunAt > 0) {
|
|
948
|
+
triggered.pop(); // undo — already ran once
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return triggered;
|
|
954
|
+
}
|
|
955
|
+
|
|
902
956
|
/** Get status of active runs */
|
|
903
957
|
getActiveRuns() {
|
|
904
958
|
return Array.from(this._activeRuns.entries())
|