@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.
Files changed (266) hide show
  1. package/LICENSE +153 -0
  2. package/README.md +278 -0
  3. package/bin/jarvis.ts +413 -0
  4. package/package.json +74 -0
  5. package/scripts/ensure-bun.cjs +8 -0
  6. package/src/actions/README.md +421 -0
  7. package/src/actions/app-control/desktop-controller.test.ts +26 -0
  8. package/src/actions/app-control/desktop-controller.ts +438 -0
  9. package/src/actions/app-control/interface.ts +64 -0
  10. package/src/actions/app-control/linux.ts +273 -0
  11. package/src/actions/app-control/macos.ts +54 -0
  12. package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
  13. package/src/actions/app-control/sidecar-launcher.ts +286 -0
  14. package/src/actions/app-control/windows.ts +44 -0
  15. package/src/actions/browser/cdp.ts +138 -0
  16. package/src/actions/browser/chrome-launcher.ts +252 -0
  17. package/src/actions/browser/session.ts +437 -0
  18. package/src/actions/browser/stealth.ts +49 -0
  19. package/src/actions/index.ts +20 -0
  20. package/src/actions/terminal/executor.ts +157 -0
  21. package/src/actions/terminal/wsl-bridge.ts +126 -0
  22. package/src/actions/test.ts +93 -0
  23. package/src/actions/tools/agents.ts +321 -0
  24. package/src/actions/tools/builtin.ts +846 -0
  25. package/src/actions/tools/commitments.ts +192 -0
  26. package/src/actions/tools/content.ts +217 -0
  27. package/src/actions/tools/delegate.ts +147 -0
  28. package/src/actions/tools/desktop.test.ts +55 -0
  29. package/src/actions/tools/desktop.ts +305 -0
  30. package/src/actions/tools/goals.ts +376 -0
  31. package/src/actions/tools/local-tools-guard.ts +20 -0
  32. package/src/actions/tools/registry.ts +171 -0
  33. package/src/actions/tools/research.ts +111 -0
  34. package/src/actions/tools/sidecar-list.ts +57 -0
  35. package/src/actions/tools/sidecar-route.ts +105 -0
  36. package/src/actions/tools/workflows.ts +216 -0
  37. package/src/agents/agent.ts +132 -0
  38. package/src/agents/delegation.ts +107 -0
  39. package/src/agents/hierarchy.ts +113 -0
  40. package/src/agents/index.ts +19 -0
  41. package/src/agents/messaging.ts +125 -0
  42. package/src/agents/orchestrator.ts +576 -0
  43. package/src/agents/role-discovery.ts +61 -0
  44. package/src/agents/sub-agent-runner.ts +307 -0
  45. package/src/agents/task-manager.ts +151 -0
  46. package/src/authority/approval-delivery.ts +59 -0
  47. package/src/authority/approval.ts +196 -0
  48. package/src/authority/audit.ts +158 -0
  49. package/src/authority/authority.test.ts +519 -0
  50. package/src/authority/deferred-executor.ts +103 -0
  51. package/src/authority/emergency.ts +66 -0
  52. package/src/authority/engine.ts +297 -0
  53. package/src/authority/index.ts +12 -0
  54. package/src/authority/learning.ts +111 -0
  55. package/src/authority/tool-action-map.ts +74 -0
  56. package/src/awareness/analytics.ts +466 -0
  57. package/src/awareness/awareness.test.ts +332 -0
  58. package/src/awareness/capture-engine.ts +305 -0
  59. package/src/awareness/context-graph.ts +130 -0
  60. package/src/awareness/context-tracker.ts +349 -0
  61. package/src/awareness/index.ts +25 -0
  62. package/src/awareness/intelligence.ts +321 -0
  63. package/src/awareness/ocr-engine.ts +88 -0
  64. package/src/awareness/service.ts +528 -0
  65. package/src/awareness/struggle-detector.ts +342 -0
  66. package/src/awareness/suggestion-engine.ts +476 -0
  67. package/src/awareness/types.ts +201 -0
  68. package/src/cli/autostart.ts +241 -0
  69. package/src/cli/deps.ts +449 -0
  70. package/src/cli/doctor.ts +230 -0
  71. package/src/cli/helpers.ts +401 -0
  72. package/src/cli/onboard.ts +580 -0
  73. package/src/comms/README.md +329 -0
  74. package/src/comms/auth-error.html +48 -0
  75. package/src/comms/channels/discord.ts +228 -0
  76. package/src/comms/channels/signal.ts +56 -0
  77. package/src/comms/channels/telegram.ts +316 -0
  78. package/src/comms/channels/whatsapp.ts +60 -0
  79. package/src/comms/channels.test.ts +173 -0
  80. package/src/comms/desktop-notify.ts +114 -0
  81. package/src/comms/example.ts +129 -0
  82. package/src/comms/index.ts +129 -0
  83. package/src/comms/streaming.ts +142 -0
  84. package/src/comms/voice.test.ts +152 -0
  85. package/src/comms/voice.ts +291 -0
  86. package/src/comms/websocket.test.ts +409 -0
  87. package/src/comms/websocket.ts +473 -0
  88. package/src/config/README.md +387 -0
  89. package/src/config/index.ts +6 -0
  90. package/src/config/loader.test.ts +137 -0
  91. package/src/config/loader.ts +142 -0
  92. package/src/config/types.ts +260 -0
  93. package/src/daemon/README.md +232 -0
  94. package/src/daemon/agent-service-interface.ts +9 -0
  95. package/src/daemon/agent-service.ts +600 -0
  96. package/src/daemon/api-routes.ts +2119 -0
  97. package/src/daemon/background-agent-service.ts +396 -0
  98. package/src/daemon/background-agent.test.ts +78 -0
  99. package/src/daemon/channel-service.ts +201 -0
  100. package/src/daemon/commitment-executor.ts +297 -0
  101. package/src/daemon/event-classifier.ts +239 -0
  102. package/src/daemon/event-coalescer.ts +123 -0
  103. package/src/daemon/event-reactor.ts +214 -0
  104. package/src/daemon/health.ts +220 -0
  105. package/src/daemon/index.ts +1004 -0
  106. package/src/daemon/llm-settings.ts +316 -0
  107. package/src/daemon/observer-service.ts +150 -0
  108. package/src/daemon/pid.ts +98 -0
  109. package/src/daemon/research-queue.ts +155 -0
  110. package/src/daemon/services.ts +175 -0
  111. package/src/daemon/ws-service.ts +788 -0
  112. package/src/goals/accountability.ts +240 -0
  113. package/src/goals/awareness-bridge.ts +185 -0
  114. package/src/goals/estimator.ts +185 -0
  115. package/src/goals/events.ts +28 -0
  116. package/src/goals/goals.test.ts +400 -0
  117. package/src/goals/integration.test.ts +329 -0
  118. package/src/goals/nl-builder.test.ts +220 -0
  119. package/src/goals/nl-builder.ts +256 -0
  120. package/src/goals/rhythm.test.ts +177 -0
  121. package/src/goals/rhythm.ts +275 -0
  122. package/src/goals/service.test.ts +135 -0
  123. package/src/goals/service.ts +348 -0
  124. package/src/goals/types.ts +106 -0
  125. package/src/goals/workflow-bridge.ts +96 -0
  126. package/src/integrations/google-api.ts +134 -0
  127. package/src/integrations/google-auth.ts +175 -0
  128. package/src/llm/README.md +291 -0
  129. package/src/llm/anthropic.ts +386 -0
  130. package/src/llm/gemini.ts +371 -0
  131. package/src/llm/index.ts +19 -0
  132. package/src/llm/manager.ts +153 -0
  133. package/src/llm/ollama.ts +307 -0
  134. package/src/llm/openai.ts +350 -0
  135. package/src/llm/provider.test.ts +231 -0
  136. package/src/llm/provider.ts +60 -0
  137. package/src/llm/test.ts +87 -0
  138. package/src/observers/README.md +278 -0
  139. package/src/observers/calendar.ts +113 -0
  140. package/src/observers/clipboard.ts +136 -0
  141. package/src/observers/email.ts +109 -0
  142. package/src/observers/example.ts +58 -0
  143. package/src/observers/file-watcher.ts +124 -0
  144. package/src/observers/index.ts +159 -0
  145. package/src/observers/notifications.ts +197 -0
  146. package/src/observers/observers.test.ts +203 -0
  147. package/src/observers/processes.ts +225 -0
  148. package/src/personality/README.md +61 -0
  149. package/src/personality/adapter.ts +196 -0
  150. package/src/personality/index.ts +20 -0
  151. package/src/personality/learner.ts +209 -0
  152. package/src/personality/model.ts +132 -0
  153. package/src/personality/personality.test.ts +236 -0
  154. package/src/roles/README.md +252 -0
  155. package/src/roles/authority.ts +119 -0
  156. package/src/roles/example-usage.ts +198 -0
  157. package/src/roles/index.ts +42 -0
  158. package/src/roles/loader.ts +143 -0
  159. package/src/roles/prompt-builder.ts +194 -0
  160. package/src/roles/test-multi.ts +102 -0
  161. package/src/roles/test-role.yaml +77 -0
  162. package/src/roles/test-utils.ts +93 -0
  163. package/src/roles/test.ts +106 -0
  164. package/src/roles/tool-guide.ts +190 -0
  165. package/src/roles/types.ts +36 -0
  166. package/src/roles/utils.ts +200 -0
  167. package/src/scripts/google-setup.ts +168 -0
  168. package/src/sidecar/connection.ts +179 -0
  169. package/src/sidecar/index.ts +6 -0
  170. package/src/sidecar/manager.ts +542 -0
  171. package/src/sidecar/protocol.ts +85 -0
  172. package/src/sidecar/rpc.ts +161 -0
  173. package/src/sidecar/scheduler.ts +136 -0
  174. package/src/sidecar/types.ts +112 -0
  175. package/src/sidecar/validator.ts +144 -0
  176. package/src/vault/README.md +110 -0
  177. package/src/vault/awareness.ts +341 -0
  178. package/src/vault/commitments.ts +299 -0
  179. package/src/vault/content-pipeline.ts +260 -0
  180. package/src/vault/conversations.ts +173 -0
  181. package/src/vault/entities.ts +180 -0
  182. package/src/vault/extractor.test.ts +356 -0
  183. package/src/vault/extractor.ts +345 -0
  184. package/src/vault/facts.ts +190 -0
  185. package/src/vault/goals.ts +477 -0
  186. package/src/vault/index.ts +87 -0
  187. package/src/vault/keychain.ts +99 -0
  188. package/src/vault/observations.ts +115 -0
  189. package/src/vault/relationships.ts +178 -0
  190. package/src/vault/retrieval.test.ts +126 -0
  191. package/src/vault/retrieval.ts +227 -0
  192. package/src/vault/schema.ts +658 -0
  193. package/src/vault/settings.ts +38 -0
  194. package/src/vault/vectors.ts +92 -0
  195. package/src/vault/workflows.ts +403 -0
  196. package/src/workflows/auto-suggest.ts +290 -0
  197. package/src/workflows/engine.ts +366 -0
  198. package/src/workflows/events.ts +24 -0
  199. package/src/workflows/executor.ts +207 -0
  200. package/src/workflows/nl-builder.ts +198 -0
  201. package/src/workflows/nodes/actions/agent-task.ts +73 -0
  202. package/src/workflows/nodes/actions/calendar-action.ts +85 -0
  203. package/src/workflows/nodes/actions/code-execution.ts +73 -0
  204. package/src/workflows/nodes/actions/discord.ts +77 -0
  205. package/src/workflows/nodes/actions/file-write.ts +73 -0
  206. package/src/workflows/nodes/actions/gmail.ts +69 -0
  207. package/src/workflows/nodes/actions/http-request.ts +117 -0
  208. package/src/workflows/nodes/actions/notification.ts +85 -0
  209. package/src/workflows/nodes/actions/run-tool.ts +55 -0
  210. package/src/workflows/nodes/actions/send-message.ts +82 -0
  211. package/src/workflows/nodes/actions/shell-command.ts +76 -0
  212. package/src/workflows/nodes/actions/telegram.ts +60 -0
  213. package/src/workflows/nodes/builtin.ts +119 -0
  214. package/src/workflows/nodes/error/error-handler.ts +37 -0
  215. package/src/workflows/nodes/error/fallback.ts +47 -0
  216. package/src/workflows/nodes/error/retry.ts +82 -0
  217. package/src/workflows/nodes/logic/delay.ts +42 -0
  218. package/src/workflows/nodes/logic/if-else.ts +41 -0
  219. package/src/workflows/nodes/logic/loop.ts +90 -0
  220. package/src/workflows/nodes/logic/merge.ts +38 -0
  221. package/src/workflows/nodes/logic/race.ts +40 -0
  222. package/src/workflows/nodes/logic/switch.ts +59 -0
  223. package/src/workflows/nodes/logic/template-render.ts +53 -0
  224. package/src/workflows/nodes/logic/variable-get.ts +37 -0
  225. package/src/workflows/nodes/logic/variable-set.ts +59 -0
  226. package/src/workflows/nodes/registry.ts +99 -0
  227. package/src/workflows/nodes/transform/aggregate.ts +99 -0
  228. package/src/workflows/nodes/transform/csv-parse.ts +70 -0
  229. package/src/workflows/nodes/transform/json-parse.ts +63 -0
  230. package/src/workflows/nodes/transform/map-filter.ts +84 -0
  231. package/src/workflows/nodes/transform/regex-match.ts +89 -0
  232. package/src/workflows/nodes/triggers/calendar.ts +33 -0
  233. package/src/workflows/nodes/triggers/clipboard.ts +32 -0
  234. package/src/workflows/nodes/triggers/cron.ts +40 -0
  235. package/src/workflows/nodes/triggers/email.ts +40 -0
  236. package/src/workflows/nodes/triggers/file-change.ts +45 -0
  237. package/src/workflows/nodes/triggers/git.ts +46 -0
  238. package/src/workflows/nodes/triggers/manual.ts +23 -0
  239. package/src/workflows/nodes/triggers/poll.ts +81 -0
  240. package/src/workflows/nodes/triggers/process.ts +44 -0
  241. package/src/workflows/nodes/triggers/screen-event.ts +37 -0
  242. package/src/workflows/nodes/triggers/webhook.ts +39 -0
  243. package/src/workflows/safe-eval.ts +139 -0
  244. package/src/workflows/template.ts +118 -0
  245. package/src/workflows/triggers/cron.ts +311 -0
  246. package/src/workflows/triggers/manager.ts +285 -0
  247. package/src/workflows/triggers/observer-bridge.ts +172 -0
  248. package/src/workflows/triggers/poller.ts +201 -0
  249. package/src/workflows/triggers/screen-condition.ts +218 -0
  250. package/src/workflows/triggers/triggers.test.ts +740 -0
  251. package/src/workflows/triggers/webhook.ts +191 -0
  252. package/src/workflows/types.ts +133 -0
  253. package/src/workflows/variables.ts +72 -0
  254. package/src/workflows/workflows.test.ts +383 -0
  255. package/src/workflows/yaml.ts +104 -0
  256. package/ui/dist/index-j75njzc1.css +1199 -0
  257. package/ui/dist/index-p2zh407q.js +80603 -0
  258. package/ui/dist/index.html +13 -0
  259. package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
  260. package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
  261. package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
  262. package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
  263. package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
  264. package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
  265. package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
  266. 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
+ };