@usejarvis/brain 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +153 -0
- package/README.md +278 -0
- package/bin/jarvis.ts +413 -0
- package/package.json +74 -0
- package/scripts/ensure-bun.cjs +8 -0
- package/src/actions/README.md +421 -0
- package/src/actions/app-control/desktop-controller.test.ts +26 -0
- package/src/actions/app-control/desktop-controller.ts +438 -0
- package/src/actions/app-control/interface.ts +64 -0
- package/src/actions/app-control/linux.ts +273 -0
- package/src/actions/app-control/macos.ts +54 -0
- package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
- package/src/actions/app-control/sidecar-launcher.ts +286 -0
- package/src/actions/app-control/windows.ts +44 -0
- package/src/actions/browser/cdp.ts +138 -0
- package/src/actions/browser/chrome-launcher.ts +252 -0
- package/src/actions/browser/session.ts +437 -0
- package/src/actions/browser/stealth.ts +49 -0
- package/src/actions/index.ts +20 -0
- package/src/actions/terminal/executor.ts +157 -0
- package/src/actions/terminal/wsl-bridge.ts +126 -0
- package/src/actions/test.ts +93 -0
- package/src/actions/tools/agents.ts +321 -0
- package/src/actions/tools/builtin.ts +846 -0
- package/src/actions/tools/commitments.ts +192 -0
- package/src/actions/tools/content.ts +217 -0
- package/src/actions/tools/delegate.ts +147 -0
- package/src/actions/tools/desktop.test.ts +55 -0
- package/src/actions/tools/desktop.ts +305 -0
- package/src/actions/tools/goals.ts +376 -0
- package/src/actions/tools/local-tools-guard.ts +20 -0
- package/src/actions/tools/registry.ts +171 -0
- package/src/actions/tools/research.ts +111 -0
- package/src/actions/tools/sidecar-list.ts +57 -0
- package/src/actions/tools/sidecar-route.ts +105 -0
- package/src/actions/tools/workflows.ts +216 -0
- package/src/agents/agent.ts +132 -0
- package/src/agents/delegation.ts +107 -0
- package/src/agents/hierarchy.ts +113 -0
- package/src/agents/index.ts +19 -0
- package/src/agents/messaging.ts +125 -0
- package/src/agents/orchestrator.ts +576 -0
- package/src/agents/role-discovery.ts +61 -0
- package/src/agents/sub-agent-runner.ts +307 -0
- package/src/agents/task-manager.ts +151 -0
- package/src/authority/approval-delivery.ts +59 -0
- package/src/authority/approval.ts +196 -0
- package/src/authority/audit.ts +158 -0
- package/src/authority/authority.test.ts +519 -0
- package/src/authority/deferred-executor.ts +103 -0
- package/src/authority/emergency.ts +66 -0
- package/src/authority/engine.ts +297 -0
- package/src/authority/index.ts +12 -0
- package/src/authority/learning.ts +111 -0
- package/src/authority/tool-action-map.ts +74 -0
- package/src/awareness/analytics.ts +466 -0
- package/src/awareness/awareness.test.ts +332 -0
- package/src/awareness/capture-engine.ts +305 -0
- package/src/awareness/context-graph.ts +130 -0
- package/src/awareness/context-tracker.ts +349 -0
- package/src/awareness/index.ts +25 -0
- package/src/awareness/intelligence.ts +321 -0
- package/src/awareness/ocr-engine.ts +88 -0
- package/src/awareness/service.ts +528 -0
- package/src/awareness/struggle-detector.ts +342 -0
- package/src/awareness/suggestion-engine.ts +476 -0
- package/src/awareness/types.ts +201 -0
- package/src/cli/autostart.ts +241 -0
- package/src/cli/deps.ts +449 -0
- package/src/cli/doctor.ts +230 -0
- package/src/cli/helpers.ts +401 -0
- package/src/cli/onboard.ts +580 -0
- package/src/comms/README.md +329 -0
- package/src/comms/auth-error.html +48 -0
- package/src/comms/channels/discord.ts +228 -0
- package/src/comms/channels/signal.ts +56 -0
- package/src/comms/channels/telegram.ts +316 -0
- package/src/comms/channels/whatsapp.ts +60 -0
- package/src/comms/channels.test.ts +173 -0
- package/src/comms/desktop-notify.ts +114 -0
- package/src/comms/example.ts +129 -0
- package/src/comms/index.ts +129 -0
- package/src/comms/streaming.ts +142 -0
- package/src/comms/voice.test.ts +152 -0
- package/src/comms/voice.ts +291 -0
- package/src/comms/websocket.test.ts +409 -0
- package/src/comms/websocket.ts +473 -0
- package/src/config/README.md +387 -0
- package/src/config/index.ts +6 -0
- package/src/config/loader.test.ts +137 -0
- package/src/config/loader.ts +142 -0
- package/src/config/types.ts +260 -0
- package/src/daemon/README.md +232 -0
- package/src/daemon/agent-service-interface.ts +9 -0
- package/src/daemon/agent-service.ts +600 -0
- package/src/daemon/api-routes.ts +2119 -0
- package/src/daemon/background-agent-service.ts +396 -0
- package/src/daemon/background-agent.test.ts +78 -0
- package/src/daemon/channel-service.ts +201 -0
- package/src/daemon/commitment-executor.ts +297 -0
- package/src/daemon/event-classifier.ts +239 -0
- package/src/daemon/event-coalescer.ts +123 -0
- package/src/daemon/event-reactor.ts +214 -0
- package/src/daemon/health.ts +220 -0
- package/src/daemon/index.ts +1004 -0
- package/src/daemon/llm-settings.ts +316 -0
- package/src/daemon/observer-service.ts +150 -0
- package/src/daemon/pid.ts +98 -0
- package/src/daemon/research-queue.ts +155 -0
- package/src/daemon/services.ts +175 -0
- package/src/daemon/ws-service.ts +788 -0
- package/src/goals/accountability.ts +240 -0
- package/src/goals/awareness-bridge.ts +185 -0
- package/src/goals/estimator.ts +185 -0
- package/src/goals/events.ts +28 -0
- package/src/goals/goals.test.ts +400 -0
- package/src/goals/integration.test.ts +329 -0
- package/src/goals/nl-builder.test.ts +220 -0
- package/src/goals/nl-builder.ts +256 -0
- package/src/goals/rhythm.test.ts +177 -0
- package/src/goals/rhythm.ts +275 -0
- package/src/goals/service.test.ts +135 -0
- package/src/goals/service.ts +348 -0
- package/src/goals/types.ts +106 -0
- package/src/goals/workflow-bridge.ts +96 -0
- package/src/integrations/google-api.ts +134 -0
- package/src/integrations/google-auth.ts +175 -0
- package/src/llm/README.md +291 -0
- package/src/llm/anthropic.ts +386 -0
- package/src/llm/gemini.ts +371 -0
- package/src/llm/index.ts +19 -0
- package/src/llm/manager.ts +153 -0
- package/src/llm/ollama.ts +307 -0
- package/src/llm/openai.ts +350 -0
- package/src/llm/provider.test.ts +231 -0
- package/src/llm/provider.ts +60 -0
- package/src/llm/test.ts +87 -0
- package/src/observers/README.md +278 -0
- package/src/observers/calendar.ts +113 -0
- package/src/observers/clipboard.ts +136 -0
- package/src/observers/email.ts +109 -0
- package/src/observers/example.ts +58 -0
- package/src/observers/file-watcher.ts +124 -0
- package/src/observers/index.ts +159 -0
- package/src/observers/notifications.ts +197 -0
- package/src/observers/observers.test.ts +203 -0
- package/src/observers/processes.ts +225 -0
- package/src/personality/README.md +61 -0
- package/src/personality/adapter.ts +196 -0
- package/src/personality/index.ts +20 -0
- package/src/personality/learner.ts +209 -0
- package/src/personality/model.ts +132 -0
- package/src/personality/personality.test.ts +236 -0
- package/src/roles/README.md +252 -0
- package/src/roles/authority.ts +119 -0
- package/src/roles/example-usage.ts +198 -0
- package/src/roles/index.ts +42 -0
- package/src/roles/loader.ts +143 -0
- package/src/roles/prompt-builder.ts +194 -0
- package/src/roles/test-multi.ts +102 -0
- package/src/roles/test-role.yaml +77 -0
- package/src/roles/test-utils.ts +93 -0
- package/src/roles/test.ts +106 -0
- package/src/roles/tool-guide.ts +190 -0
- package/src/roles/types.ts +36 -0
- package/src/roles/utils.ts +200 -0
- package/src/scripts/google-setup.ts +168 -0
- package/src/sidecar/connection.ts +179 -0
- package/src/sidecar/index.ts +6 -0
- package/src/sidecar/manager.ts +542 -0
- package/src/sidecar/protocol.ts +85 -0
- package/src/sidecar/rpc.ts +161 -0
- package/src/sidecar/scheduler.ts +136 -0
- package/src/sidecar/types.ts +112 -0
- package/src/sidecar/validator.ts +144 -0
- package/src/vault/README.md +110 -0
- package/src/vault/awareness.ts +341 -0
- package/src/vault/commitments.ts +299 -0
- package/src/vault/content-pipeline.ts +260 -0
- package/src/vault/conversations.ts +173 -0
- package/src/vault/entities.ts +180 -0
- package/src/vault/extractor.test.ts +356 -0
- package/src/vault/extractor.ts +345 -0
- package/src/vault/facts.ts +190 -0
- package/src/vault/goals.ts +477 -0
- package/src/vault/index.ts +87 -0
- package/src/vault/keychain.ts +99 -0
- package/src/vault/observations.ts +115 -0
- package/src/vault/relationships.ts +178 -0
- package/src/vault/retrieval.test.ts +126 -0
- package/src/vault/retrieval.ts +227 -0
- package/src/vault/schema.ts +658 -0
- package/src/vault/settings.ts +38 -0
- package/src/vault/vectors.ts +92 -0
- package/src/vault/workflows.ts +403 -0
- package/src/workflows/auto-suggest.ts +290 -0
- package/src/workflows/engine.ts +366 -0
- package/src/workflows/events.ts +24 -0
- package/src/workflows/executor.ts +207 -0
- package/src/workflows/nl-builder.ts +198 -0
- package/src/workflows/nodes/actions/agent-task.ts +73 -0
- package/src/workflows/nodes/actions/calendar-action.ts +85 -0
- package/src/workflows/nodes/actions/code-execution.ts +73 -0
- package/src/workflows/nodes/actions/discord.ts +77 -0
- package/src/workflows/nodes/actions/file-write.ts +73 -0
- package/src/workflows/nodes/actions/gmail.ts +69 -0
- package/src/workflows/nodes/actions/http-request.ts +117 -0
- package/src/workflows/nodes/actions/notification.ts +85 -0
- package/src/workflows/nodes/actions/run-tool.ts +55 -0
- package/src/workflows/nodes/actions/send-message.ts +82 -0
- package/src/workflows/nodes/actions/shell-command.ts +76 -0
- package/src/workflows/nodes/actions/telegram.ts +60 -0
- package/src/workflows/nodes/builtin.ts +119 -0
- package/src/workflows/nodes/error/error-handler.ts +37 -0
- package/src/workflows/nodes/error/fallback.ts +47 -0
- package/src/workflows/nodes/error/retry.ts +82 -0
- package/src/workflows/nodes/logic/delay.ts +42 -0
- package/src/workflows/nodes/logic/if-else.ts +41 -0
- package/src/workflows/nodes/logic/loop.ts +90 -0
- package/src/workflows/nodes/logic/merge.ts +38 -0
- package/src/workflows/nodes/logic/race.ts +40 -0
- package/src/workflows/nodes/logic/switch.ts +59 -0
- package/src/workflows/nodes/logic/template-render.ts +53 -0
- package/src/workflows/nodes/logic/variable-get.ts +37 -0
- package/src/workflows/nodes/logic/variable-set.ts +59 -0
- package/src/workflows/nodes/registry.ts +99 -0
- package/src/workflows/nodes/transform/aggregate.ts +99 -0
- package/src/workflows/nodes/transform/csv-parse.ts +70 -0
- package/src/workflows/nodes/transform/json-parse.ts +63 -0
- package/src/workflows/nodes/transform/map-filter.ts +84 -0
- package/src/workflows/nodes/transform/regex-match.ts +89 -0
- package/src/workflows/nodes/triggers/calendar.ts +33 -0
- package/src/workflows/nodes/triggers/clipboard.ts +32 -0
- package/src/workflows/nodes/triggers/cron.ts +40 -0
- package/src/workflows/nodes/triggers/email.ts +40 -0
- package/src/workflows/nodes/triggers/file-change.ts +45 -0
- package/src/workflows/nodes/triggers/git.ts +46 -0
- package/src/workflows/nodes/triggers/manual.ts +23 -0
- package/src/workflows/nodes/triggers/poll.ts +81 -0
- package/src/workflows/nodes/triggers/process.ts +44 -0
- package/src/workflows/nodes/triggers/screen-event.ts +37 -0
- package/src/workflows/nodes/triggers/webhook.ts +39 -0
- package/src/workflows/safe-eval.ts +139 -0
- package/src/workflows/template.ts +118 -0
- package/src/workflows/triggers/cron.ts +311 -0
- package/src/workflows/triggers/manager.ts +285 -0
- package/src/workflows/triggers/observer-bridge.ts +172 -0
- package/src/workflows/triggers/poller.ts +201 -0
- package/src/workflows/triggers/screen-condition.ts +218 -0
- package/src/workflows/triggers/triggers.test.ts +740 -0
- package/src/workflows/triggers/webhook.ts +191 -0
- package/src/workflows/types.ts +133 -0
- package/src/workflows/variables.ts +72 -0
- package/src/workflows/workflows.test.ts +383 -0
- package/src/workflows/yaml.ts +104 -0
- package/ui/dist/index-j75njzc1.css +1199 -0
- package/ui/dist/index-p2zh407q.js +80603 -0
- package/ui/dist/index.html +13 -0
- package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
- package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
- package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
- package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
- package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph Executor — topological sort, parallel branches, retry, fallback, self-heal
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { WorkflowDefinition, WorkflowNode, WorkflowSettings, RetryPolicy } from './types.ts';
|
|
6
|
+
import type { NodeRegistry, NodeInput, NodeOutput, ExecutionContext } from './nodes/registry.ts';
|
|
7
|
+
import { resolveAllTemplates, type TemplateContext } from './template.ts';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Topological sort of workflow nodes, grouped by execution level.
|
|
11
|
+
* Nodes at the same level can be executed in parallel.
|
|
12
|
+
* Returns string[][] where each inner array is a set of node IDs to execute concurrently.
|
|
13
|
+
*/
|
|
14
|
+
export function topologicalSort(definition: WorkflowDefinition): string[][] {
|
|
15
|
+
const { nodes, edges } = definition;
|
|
16
|
+
const nodeIds = new Set(nodes.map(n => n.id));
|
|
17
|
+
|
|
18
|
+
// Build adjacency list and in-degree map
|
|
19
|
+
const inDegree = new Map<string, number>();
|
|
20
|
+
const adjacency = new Map<string, string[]>();
|
|
21
|
+
|
|
22
|
+
for (const id of nodeIds) {
|
|
23
|
+
inDegree.set(id, 0);
|
|
24
|
+
adjacency.set(id, []);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const edge of edges) {
|
|
28
|
+
if (nodeIds.has(edge.source) && nodeIds.has(edge.target)) {
|
|
29
|
+
adjacency.get(edge.source)!.push(edge.target);
|
|
30
|
+
inDegree.set(edge.target, (inDegree.get(edge.target) ?? 0) + 1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Kahn's algorithm, grouping by level
|
|
35
|
+
const levels: string[][] = [];
|
|
36
|
+
let queue = [...nodeIds].filter(id => inDegree.get(id) === 0);
|
|
37
|
+
|
|
38
|
+
while (queue.length > 0) {
|
|
39
|
+
levels.push([...queue]);
|
|
40
|
+
const nextQueue: string[] = [];
|
|
41
|
+
|
|
42
|
+
for (const nodeId of queue) {
|
|
43
|
+
for (const neighbor of adjacency.get(nodeId) ?? []) {
|
|
44
|
+
const newDegree = (inDegree.get(neighbor) ?? 1) - 1;
|
|
45
|
+
inDegree.set(neighbor, newDegree);
|
|
46
|
+
if (newDegree === 0) {
|
|
47
|
+
nextQueue.push(neighbor);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
queue = nextQueue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return levels;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get the outgoing edges from a node, optionally filtered by source handle.
|
|
60
|
+
*/
|
|
61
|
+
export function getOutgoingEdges(
|
|
62
|
+
definition: WorkflowDefinition,
|
|
63
|
+
nodeId: string,
|
|
64
|
+
route?: string,
|
|
65
|
+
): string[] {
|
|
66
|
+
return definition.edges
|
|
67
|
+
.filter(e => {
|
|
68
|
+
if (e.source !== nodeId) return false;
|
|
69
|
+
if (route !== undefined && e.sourceHandle && e.sourceHandle !== route) return false;
|
|
70
|
+
return true;
|
|
71
|
+
})
|
|
72
|
+
.map(e => e.target);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Execute a single node with retry and fallback.
|
|
77
|
+
*/
|
|
78
|
+
export async function executeNode(
|
|
79
|
+
node: WorkflowNode,
|
|
80
|
+
input: NodeInput,
|
|
81
|
+
nodeRegistry: NodeRegistry,
|
|
82
|
+
ctx: ExecutionContext,
|
|
83
|
+
templateCtx: TemplateContext,
|
|
84
|
+
settings: WorkflowSettings,
|
|
85
|
+
): Promise<NodeOutput> {
|
|
86
|
+
const nodeDef = nodeRegistry.get(node.type);
|
|
87
|
+
if (!nodeDef) {
|
|
88
|
+
throw new Error(`Unknown node type: ${node.type}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Resolve template expressions in node config
|
|
92
|
+
const resolvedConfig = resolveAllTemplates(node.config, templateCtx);
|
|
93
|
+
|
|
94
|
+
const retryPolicy: RetryPolicy = node.retryPolicy ?? {
|
|
95
|
+
maxRetries: settings.maxRetries,
|
|
96
|
+
delayMs: settings.retryDelayMs,
|
|
97
|
+
backoff: 'fixed',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
let lastError: Error | null = null;
|
|
101
|
+
|
|
102
|
+
for (let attempt = 0; attempt <= retryPolicy.maxRetries; attempt++) {
|
|
103
|
+
if (ctx.abortSignal.aborted) {
|
|
104
|
+
throw new Error('Execution cancelled');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
// Enforce node-level timeout from workflow settings
|
|
109
|
+
const nodeTimeout = settings.timeoutMs ?? 300_000;
|
|
110
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
111
|
+
const output = await Promise.race([
|
|
112
|
+
nodeDef.execute(input, resolvedConfig, ctx),
|
|
113
|
+
new Promise<never>((_, reject) => {
|
|
114
|
+
timer = setTimeout(
|
|
115
|
+
() => reject(new Error(`Node '${node.label}' timed out after ${nodeTimeout}ms`)),
|
|
116
|
+
nodeTimeout,
|
|
117
|
+
);
|
|
118
|
+
}),
|
|
119
|
+
]);
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
return output;
|
|
122
|
+
} catch (err) {
|
|
123
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
124
|
+
ctx.logger.warn(`Node ${node.label} attempt ${attempt + 1} failed: ${lastError.message}`);
|
|
125
|
+
|
|
126
|
+
if (attempt < retryPolicy.maxRetries) {
|
|
127
|
+
const delay = retryPolicy.backoff === 'exponential'
|
|
128
|
+
? retryPolicy.delayMs * Math.pow(2, attempt)
|
|
129
|
+
: retryPolicy.delayMs;
|
|
130
|
+
await sleep(delay, ctx.abortSignal);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Self-heal: if onError is self_heal and we have an LLM manager, ask AI for fix
|
|
136
|
+
if (settings.onError === 'self_heal' && ctx.llmManager && lastError) {
|
|
137
|
+
try {
|
|
138
|
+
ctx.logger.info(`Node ${node.label}: attempting self-heal...`);
|
|
139
|
+
const healed = await selfHeal(node, lastError, resolvedConfig, input, ctx);
|
|
140
|
+
if (healed) return healed;
|
|
141
|
+
} catch (healErr) {
|
|
142
|
+
ctx.logger.warn(`Self-heal failed for ${node.label}: ${healErr instanceof Error ? healErr.message : healErr}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
throw lastError ?? new Error(`Node ${node.label} failed after ${retryPolicy.maxRetries + 1} attempts`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Self-heal: ask the LLM to diagnose the failure and provide corrected config.
|
|
151
|
+
* If the LLM provides a corrected config, retry the node once with the new config.
|
|
152
|
+
*/
|
|
153
|
+
async function selfHeal(
|
|
154
|
+
node: WorkflowNode,
|
|
155
|
+
error: Error,
|
|
156
|
+
config: Record<string, unknown>,
|
|
157
|
+
input: NodeInput,
|
|
158
|
+
ctx: ExecutionContext,
|
|
159
|
+
): Promise<NodeOutput | null> {
|
|
160
|
+
const llm = ctx.llmManager as any;
|
|
161
|
+
if (!llm?.chat) return null;
|
|
162
|
+
|
|
163
|
+
const diagnosticPrompt = [
|
|
164
|
+
{
|
|
165
|
+
role: 'system' as const,
|
|
166
|
+
content: `You are a workflow debugger. A workflow node failed. Analyze the error and provide a corrected config that might fix it. Respond with ONLY a JSON object: { "fix": "explanation", "correctedConfig": { ... } }. If you cannot fix it, respond with { "fix": null }.`,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
role: 'user' as const,
|
|
170
|
+
content: `Node: ${node.type} (${node.label})\nConfig: ${JSON.stringify(config)}\nInput data: ${JSON.stringify(input.data).slice(0, 1000)}\nError: ${error.message}\n\nWhat config change would fix this?`,
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
const response = await llm.chat(diagnosticPrompt, { temperature: 0.1, max_tokens: 1000 });
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const text = response.content;
|
|
178
|
+
const jsonStart = text.indexOf('{');
|
|
179
|
+
const jsonEnd = text.lastIndexOf('}');
|
|
180
|
+
if (jsonStart < 0 || jsonEnd <= jsonStart) return null;
|
|
181
|
+
|
|
182
|
+
const parsed = JSON.parse(text.slice(jsonStart, jsonEnd + 1));
|
|
183
|
+
if (!parsed.fix || !parsed.correctedConfig) return null;
|
|
184
|
+
|
|
185
|
+
ctx.logger.info(`Self-heal applying fix: ${parsed.fix}`);
|
|
186
|
+
|
|
187
|
+
// Re-execute with corrected config
|
|
188
|
+
const nodeDef = ctx.nodeRegistry?.get(node.type);
|
|
189
|
+
if (!nodeDef) return null;
|
|
190
|
+
|
|
191
|
+
return await nodeDef.execute(input, parsed.correctedConfig, ctx);
|
|
192
|
+
} catch (parseErr) {
|
|
193
|
+
ctx.logger.warn(`Self-heal parse/execution failed: ${parseErr instanceof Error ? parseErr.message : parseErr}`);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Sleep with abort support.
|
|
200
|
+
*/
|
|
201
|
+
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
202
|
+
return new Promise((resolve, reject) => {
|
|
203
|
+
if (signal?.aborted) { reject(new Error('Aborted')); return; }
|
|
204
|
+
const timer = setTimeout(resolve, ms);
|
|
205
|
+
signal?.addEventListener('abort', () => { clearTimeout(timer); reject(new Error('Aborted')); }, { once: true });
|
|
206
|
+
});
|
|
207
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NL Workflow Builder — Conversational workflow construction via LLM
|
|
3
|
+
*
|
|
4
|
+
* Parses natural language descriptions into WorkflowDefinition objects,
|
|
5
|
+
* modifies existing workflows via NL instructions, and supports
|
|
6
|
+
* conversational building with context.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { WorkflowDefinition, WorkflowNode, WorkflowEdge } from './types.ts';
|
|
10
|
+
import type { NodeRegistry } from './nodes/registry.ts';
|
|
11
|
+
import * as vault from '../vault/workflows.ts';
|
|
12
|
+
|
|
13
|
+
type ChatMessage = { role: 'user' | 'assistant'; content: string };
|
|
14
|
+
|
|
15
|
+
export class NLWorkflowBuilder {
|
|
16
|
+
private nodeRegistry: NodeRegistry;
|
|
17
|
+
private llmManager: any; // LLMManager
|
|
18
|
+
|
|
19
|
+
constructor(nodeRegistry: NodeRegistry, llmManager: unknown) {
|
|
20
|
+
this.nodeRegistry = nodeRegistry;
|
|
21
|
+
this.llmManager = llmManager;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parse a natural language description into a full WorkflowDefinition.
|
|
26
|
+
*/
|
|
27
|
+
async parseDescription(text: string): Promise<WorkflowDefinition> {
|
|
28
|
+
const catalog = this.buildCatalogPrompt();
|
|
29
|
+
const prompt = [
|
|
30
|
+
{ role: 'system' as const, content: this.buildSystemPrompt(catalog) },
|
|
31
|
+
{ role: 'user' as const, content: `Create a workflow from this description:\n\n${text}\n\nRespond with ONLY valid JSON matching the WorkflowDefinition schema. No explanation.` },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const response = await this.llmManager.chat(prompt, {
|
|
35
|
+
temperature: 0.2,
|
|
36
|
+
max_tokens: 4000,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return this.parseDefinitionResponse(response.content);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Modify an existing workflow definition via NL instruction.
|
|
44
|
+
*/
|
|
45
|
+
async modifyWorkflow(
|
|
46
|
+
definition: WorkflowDefinition,
|
|
47
|
+
instruction: string,
|
|
48
|
+
): Promise<{ definition: WorkflowDefinition; changes: string[] }> {
|
|
49
|
+
const catalog = this.buildCatalogPrompt();
|
|
50
|
+
const prompt = [
|
|
51
|
+
{ role: 'system' as const, content: this.buildSystemPrompt(catalog) },
|
|
52
|
+
{
|
|
53
|
+
role: 'user' as const,
|
|
54
|
+
content: `Here is the current workflow definition:\n\`\`\`json\n${JSON.stringify(definition, null, 2)}\n\`\`\`\n\nModify it according to this instruction: ${instruction}\n\nRespond with JSON: { "definition": <updated WorkflowDefinition>, "changes": ["list of changes made"] }`,
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const response = await this.llmManager.chat(prompt, {
|
|
59
|
+
temperature: 0.2,
|
|
60
|
+
max_tokens: 6000,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const parsed = JSON.parse(this.extractJson(response.content));
|
|
64
|
+
return {
|
|
65
|
+
definition: this.validateDefinition(parsed.definition),
|
|
66
|
+
changes: parsed.changes ?? [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Conversational workflow building — chat with history.
|
|
72
|
+
*/
|
|
73
|
+
async chat(
|
|
74
|
+
workflowId: string,
|
|
75
|
+
message: string,
|
|
76
|
+
history: ChatMessage[],
|
|
77
|
+
): Promise<{ reply: string; updated: boolean }> {
|
|
78
|
+
const catalog = this.buildCatalogPrompt();
|
|
79
|
+
const latestVersion = vault.getLatestVersion(workflowId);
|
|
80
|
+
const currentDef = latestVersion?.definition;
|
|
81
|
+
|
|
82
|
+
const messages = [
|
|
83
|
+
{
|
|
84
|
+
role: 'system' as const,
|
|
85
|
+
content: this.buildSystemPrompt(catalog) +
|
|
86
|
+
(currentDef
|
|
87
|
+
? `\n\nCurrent workflow definition:\n\`\`\`json\n${JSON.stringify(currentDef, null, 2)}\n\`\`\``
|
|
88
|
+
: '\n\nNo workflow definition exists yet.') +
|
|
89
|
+
`\n\nYou are helping the user build/modify their workflow via chat. If the user asks to add, remove, or modify nodes/edges, output the FULL updated definition in a JSON code block. If they're asking a question, just answer it. Always explain what you changed.`,
|
|
90
|
+
},
|
|
91
|
+
...history.map(m => ({ role: m.role as 'user' | 'assistant', content: m.content })),
|
|
92
|
+
{ role: 'user' as const, content: message },
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const response = await this.llmManager.chat(messages, {
|
|
96
|
+
temperature: 0.3,
|
|
97
|
+
max_tokens: 4000,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const text = response.content;
|
|
101
|
+
|
|
102
|
+
// Check if response contains a definition update
|
|
103
|
+
const jsonMatch = text.match(/```json\s*([\s\S]*?)```/);
|
|
104
|
+
if (jsonMatch) {
|
|
105
|
+
try {
|
|
106
|
+
const newDef = this.validateDefinition(JSON.parse(jsonMatch[1]));
|
|
107
|
+
// Save as new version
|
|
108
|
+
vault.createVersion(workflowId, newDef, 'NL chat update');
|
|
109
|
+
return { reply: text.replace(/```json[\s\S]*?```/, '*(definition updated)*'), updated: true };
|
|
110
|
+
} catch {
|
|
111
|
+
// JSON parse failed — treat as conversational
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { reply: text, updated: false };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Private helpers ──
|
|
119
|
+
|
|
120
|
+
private buildSystemPrompt(catalog: string): string {
|
|
121
|
+
return `You are a workflow builder AI. You create and modify workflow definitions for an automation system.
|
|
122
|
+
|
|
123
|
+
Available node types:
|
|
124
|
+
${catalog}
|
|
125
|
+
|
|
126
|
+
A WorkflowDefinition has this structure:
|
|
127
|
+
{
|
|
128
|
+
"nodes": [{ "id": string, "type": string, "label": string, "position": { "x": number, "y": number }, "config": {} }],
|
|
129
|
+
"edges": [{ "id": string, "source": string, "target": string, "sourceHandle?": string, "label?": string }],
|
|
130
|
+
"settings": { "maxRetries": 3, "retryDelayMs": 5000, "timeoutMs": 300000, "parallelism": "parallel"|"sequential", "onError": "stop"|"continue"|"self_heal" }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
Rules:
|
|
134
|
+
- Every workflow must have at least one trigger node
|
|
135
|
+
- Node IDs should be descriptive (e.g., "trigger-cron-1", "action-email-1")
|
|
136
|
+
- Position nodes left-to-right, top-to-bottom, with ~200px spacing
|
|
137
|
+
- Connect nodes with edges from source to target
|
|
138
|
+
- Use "sourceHandle" for branching nodes (if-else: "true"/"false", switch: "case_0"/"case_1"/etc.)
|
|
139
|
+
- Config fields must match the node's configSchema`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private buildCatalogPrompt(): string {
|
|
143
|
+
const nodes = this.nodeRegistry.list();
|
|
144
|
+
return nodes.map(n =>
|
|
145
|
+
`- ${n.type} (${n.category}): ${n.description} | config: ${Object.keys(n.configSchema).join(', ') || 'none'}`
|
|
146
|
+
).join('\n');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private extractJson(text: string): string {
|
|
150
|
+
// Try to extract from code block
|
|
151
|
+
const codeBlock = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
152
|
+
if (codeBlock) return codeBlock[1]!.trim();
|
|
153
|
+
// Try raw JSON
|
|
154
|
+
const start = text.indexOf('{');
|
|
155
|
+
const end = text.lastIndexOf('}');
|
|
156
|
+
if (start >= 0 && end > start) return text.slice(start, end + 1);
|
|
157
|
+
throw new Error('No JSON found in response');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private parseDefinitionResponse(text: string): WorkflowDefinition {
|
|
161
|
+
const json = this.extractJson(text);
|
|
162
|
+
const parsed = JSON.parse(json);
|
|
163
|
+
return this.validateDefinition(parsed);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private validateDefinition(def: any): WorkflowDefinition {
|
|
167
|
+
if (!def.nodes || !Array.isArray(def.nodes)) throw new Error('Missing nodes array');
|
|
168
|
+
if (!def.edges || !Array.isArray(def.edges)) throw new Error('Missing edges array');
|
|
169
|
+
|
|
170
|
+
const nodes: WorkflowNode[] = def.nodes.map((n: any, i: number) => ({
|
|
171
|
+
id: n.id ?? `node-${i}`,
|
|
172
|
+
type: n.type ?? 'action.send_message',
|
|
173
|
+
label: n.label ?? n.type ?? `Node ${i}`,
|
|
174
|
+
position: n.position ?? { x: 100 + i * 200, y: 200 },
|
|
175
|
+
config: n.config ?? {},
|
|
176
|
+
}));
|
|
177
|
+
|
|
178
|
+
const edges: WorkflowEdge[] = (def.edges ?? []).map((e: any, i: number) => ({
|
|
179
|
+
id: e.id ?? `edge-${i}`,
|
|
180
|
+
source: e.source,
|
|
181
|
+
target: e.target,
|
|
182
|
+
sourceHandle: e.sourceHandle,
|
|
183
|
+
label: e.label,
|
|
184
|
+
}));
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
nodes,
|
|
188
|
+
edges,
|
|
189
|
+
settings: {
|
|
190
|
+
maxRetries: def.settings?.maxRetries ?? 3,
|
|
191
|
+
retryDelayMs: def.settings?.retryDelayMs ?? 5000,
|
|
192
|
+
timeoutMs: def.settings?.timeoutMs ?? 300000,
|
|
193
|
+
parallelism: def.settings?.parallelism ?? 'parallel',
|
|
194
|
+
onError: def.settings?.onError ?? 'stop',
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { NodeDefinition } from '../registry.ts';
|
|
2
|
+
|
|
3
|
+
export const agentTaskAction: NodeDefinition = {
|
|
4
|
+
type: 'action.agent_task',
|
|
5
|
+
label: 'Agent Task',
|
|
6
|
+
description: 'Dispatch a task to a sub-agent and await its response.',
|
|
7
|
+
category: 'action',
|
|
8
|
+
icon: '🤖',
|
|
9
|
+
color: '#3b82f6',
|
|
10
|
+
configSchema: {
|
|
11
|
+
task: {
|
|
12
|
+
type: 'template',
|
|
13
|
+
label: 'Task',
|
|
14
|
+
description: 'The task description to send to the sub-agent. Supports template expressions.',
|
|
15
|
+
required: true,
|
|
16
|
+
placeholder: 'Summarize the following: {{data.content}}',
|
|
17
|
+
},
|
|
18
|
+
max_iterations: {
|
|
19
|
+
type: 'number',
|
|
20
|
+
label: 'Max Iterations',
|
|
21
|
+
description: 'Maximum tool-loop iterations the sub-agent may run.',
|
|
22
|
+
required: false,
|
|
23
|
+
default: 100,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
inputs: ['default'],
|
|
27
|
+
outputs: ['default'],
|
|
28
|
+
execute: async (input, config, ctx) => {
|
|
29
|
+
const task = String(config.task ?? '');
|
|
30
|
+
const maxIterations = typeof config.max_iterations === 'number' ? config.max_iterations : 100;
|
|
31
|
+
|
|
32
|
+
ctx.logger.info(`Dispatching agent task (max ${maxIterations} iterations): ${task.slice(0, 120)}`);
|
|
33
|
+
|
|
34
|
+
const llm = ctx.llmManager as any;
|
|
35
|
+
if (!llm?.chat) {
|
|
36
|
+
throw new Error('LLM manager not available — cannot dispatch agent task');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Build context from input data
|
|
40
|
+
const dataContext = JSON.stringify(input.data).slice(0, 2000);
|
|
41
|
+
const messages = [
|
|
42
|
+
{
|
|
43
|
+
role: 'system' as const,
|
|
44
|
+
content: `You are a workflow sub-agent. Complete the following task concisely. You have access to the following context data:\n\n${dataContext}\n\nRespond with the task result only.`,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
role: 'user' as const,
|
|
48
|
+
content: task,
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const llmResponse = await llm.chat(messages, {
|
|
53
|
+
temperature: 0.3,
|
|
54
|
+
max_tokens: 2000,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const response = typeof llmResponse.content === 'string'
|
|
58
|
+
? llmResponse.content
|
|
59
|
+
: JSON.stringify(llmResponse.content);
|
|
60
|
+
|
|
61
|
+
ctx.logger.info(`Agent task completed (${response.length} chars)`);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
data: {
|
|
65
|
+
...input.data,
|
|
66
|
+
task,
|
|
67
|
+
response,
|
|
68
|
+
success: true,
|
|
69
|
+
max_iterations: maxIterations,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { NodeDefinition } from '../registry.ts';
|
|
2
|
+
|
|
3
|
+
export const calendarActionNode: NodeDefinition = {
|
|
4
|
+
type: 'action.calendar_action',
|
|
5
|
+
label: 'Calendar Action',
|
|
6
|
+
description: 'Create, update, or delete a Google Calendar event.',
|
|
7
|
+
category: 'action',
|
|
8
|
+
icon: '📆',
|
|
9
|
+
color: '#3b82f6',
|
|
10
|
+
configSchema: {
|
|
11
|
+
action: {
|
|
12
|
+
type: 'select',
|
|
13
|
+
label: 'Action',
|
|
14
|
+
description: 'Operation to perform on the calendar.',
|
|
15
|
+
required: true,
|
|
16
|
+
default: 'create',
|
|
17
|
+
options: [
|
|
18
|
+
{ label: 'Create', value: 'create' },
|
|
19
|
+
{ label: 'Update', value: 'update' },
|
|
20
|
+
{ label: 'Delete', value: 'delete' },
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
title: {
|
|
24
|
+
type: 'template',
|
|
25
|
+
label: 'Title',
|
|
26
|
+
description: 'Event title. Supports template expressions.',
|
|
27
|
+
required: true,
|
|
28
|
+
placeholder: 'Team standup',
|
|
29
|
+
},
|
|
30
|
+
start: {
|
|
31
|
+
type: 'template',
|
|
32
|
+
label: 'Start Time',
|
|
33
|
+
description: 'ISO 8601 start datetime. Supports template expressions.',
|
|
34
|
+
required: true,
|
|
35
|
+
placeholder: '2026-03-02T09:00:00Z',
|
|
36
|
+
},
|
|
37
|
+
end: {
|
|
38
|
+
type: 'template',
|
|
39
|
+
label: 'End Time',
|
|
40
|
+
description: 'ISO 8601 end datetime. Supports template expressions.',
|
|
41
|
+
required: true,
|
|
42
|
+
placeholder: '2026-03-02T09:30:00Z',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
inputs: ['default'],
|
|
46
|
+
outputs: ['default'],
|
|
47
|
+
execute: async (input, config, ctx) => {
|
|
48
|
+
const action = String(config.action ?? 'create');
|
|
49
|
+
const title = String(config.title ?? '');
|
|
50
|
+
const start = String(config.start ?? '');
|
|
51
|
+
const end = String(config.end ?? '');
|
|
52
|
+
|
|
53
|
+
ctx.logger.info(`Calendar action: ${action} event "${title}" from ${start} to ${end}`);
|
|
54
|
+
|
|
55
|
+
let success = false;
|
|
56
|
+
let note = '';
|
|
57
|
+
|
|
58
|
+
// Try the google_calendar tool if registered
|
|
59
|
+
const toolName = 'google_calendar';
|
|
60
|
+
if (ctx.toolRegistry.has(toolName)) {
|
|
61
|
+
try {
|
|
62
|
+
await ctx.toolRegistry.execute(toolName, { action, title, start, end });
|
|
63
|
+
success = true;
|
|
64
|
+
} catch (err) {
|
|
65
|
+
throw new Error(`Calendar ${action} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
note = 'Google Calendar tool not configured — set up Google API integration to enable calendar actions';
|
|
69
|
+
ctx.logger.warn(note);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
data: {
|
|
74
|
+
...input.data,
|
|
75
|
+
calendar_action: action,
|
|
76
|
+
title,
|
|
77
|
+
start,
|
|
78
|
+
end,
|
|
79
|
+
success,
|
|
80
|
+
note: note || undefined,
|
|
81
|
+
executedAt: Date.now(),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { NodeDefinition } from '../registry.ts';
|
|
2
|
+
import { safeExecuteCode } from '../../safe-eval.ts';
|
|
3
|
+
|
|
4
|
+
export const codeExecutionAction: NodeDefinition = {
|
|
5
|
+
type: 'action.code_execution',
|
|
6
|
+
label: 'Code Execution',
|
|
7
|
+
description: 'Execute a JavaScript or TypeScript snippet inline.',
|
|
8
|
+
category: 'action',
|
|
9
|
+
icon: '⚡',
|
|
10
|
+
color: '#3b82f6',
|
|
11
|
+
configSchema: {
|
|
12
|
+
code: {
|
|
13
|
+
type: 'code',
|
|
14
|
+
label: 'Code',
|
|
15
|
+
description: 'JavaScript/TypeScript code to execute. Has access to `input` (NodeInput data) and `ctx` (minimal context).',
|
|
16
|
+
required: true,
|
|
17
|
+
placeholder: '// Return a value from this function\nreturn { result: input.data.value * 2 };',
|
|
18
|
+
},
|
|
19
|
+
language: {
|
|
20
|
+
type: 'select',
|
|
21
|
+
label: 'Language',
|
|
22
|
+
description: 'Code language. TypeScript is transpiled to JS before execution.',
|
|
23
|
+
required: true,
|
|
24
|
+
default: 'javascript',
|
|
25
|
+
options: [
|
|
26
|
+
{ label: 'JavaScript', value: 'javascript' },
|
|
27
|
+
{ label: 'TypeScript', value: 'typescript' },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
timeout_ms: {
|
|
31
|
+
type: 'number',
|
|
32
|
+
label: 'Timeout (ms)',
|
|
33
|
+
description: 'Maximum execution time before the code is killed.',
|
|
34
|
+
required: false,
|
|
35
|
+
default: 10000,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
inputs: ['default'],
|
|
39
|
+
outputs: ['default'],
|
|
40
|
+
execute: async (input, config, ctx) => {
|
|
41
|
+
const code = String(config.code ?? '');
|
|
42
|
+
if (!code) throw new Error('code is required');
|
|
43
|
+
const language = String(config.language ?? 'javascript');
|
|
44
|
+
const timeoutMs = typeof config.timeout_ms === 'number' ? config.timeout_ms : 10_000;
|
|
45
|
+
|
|
46
|
+
ctx.logger.info(`Executing ${language} snippet (${code.length} chars)`);
|
|
47
|
+
|
|
48
|
+
// Minimal safe context exposed to user code
|
|
49
|
+
const safeCtx = {
|
|
50
|
+
log: (msg: string) => ctx.logger.info(`[code] ${msg}`),
|
|
51
|
+
warn: (msg: string) => ctx.logger.warn(`[code] ${msg}`),
|
|
52
|
+
variables: input.variables,
|
|
53
|
+
executionId: input.executionId,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
let result: unknown;
|
|
57
|
+
try {
|
|
58
|
+
result = await safeExecuteCode(code, input, safeCtx, timeoutMs);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
61
|
+
ctx.logger.error(`Code execution failed: ${message}`);
|
|
62
|
+
throw new Error(`Code execution failed: ${message}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
data: {
|
|
67
|
+
...input.data,
|
|
68
|
+
code_result: result,
|
|
69
|
+
language,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
};
|