@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,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Auto-Suggest — Analyzes M13 awareness patterns to suggest workflows
|
|
3
|
+
*
|
|
4
|
+
* Detects:
|
|
5
|
+
* - Repeated app switches (e.g., copy from browser → paste in editor)
|
|
6
|
+
* - Recurring errors in the same app
|
|
7
|
+
* - Regular-interval manual tasks (e.g., checking email every 30 min)
|
|
8
|
+
* - Repeated tool usage patterns
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { NodeRegistry } from './nodes/registry.ts';
|
|
12
|
+
import type { WorkflowDefinition } from './types.ts';
|
|
13
|
+
|
|
14
|
+
export type WorkflowSuggestion = {
|
|
15
|
+
id: string;
|
|
16
|
+
title: string;
|
|
17
|
+
description: string;
|
|
18
|
+
confidence: number; // 0-1
|
|
19
|
+
category: 'repetitive_action' | 'error_response' | 'scheduled_task' | 'app_pattern';
|
|
20
|
+
previewDefinition: WorkflowDefinition;
|
|
21
|
+
patternEvidence: string[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type AwarenessPattern = {
|
|
25
|
+
type: string;
|
|
26
|
+
data: Record<string, unknown>;
|
|
27
|
+
timestamp: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export class WorkflowAutoSuggest {
|
|
31
|
+
private nodeRegistry: NodeRegistry;
|
|
32
|
+
private llmManager: any;
|
|
33
|
+
private patterns: AwarenessPattern[] = [];
|
|
34
|
+
private suggestions: WorkflowSuggestion[] = [];
|
|
35
|
+
private lastAnalysis = 0;
|
|
36
|
+
private analysisCooldownMs = 300_000; // 5 min
|
|
37
|
+
|
|
38
|
+
constructor(nodeRegistry: NodeRegistry, llmManager: unknown) {
|
|
39
|
+
this.nodeRegistry = nodeRegistry;
|
|
40
|
+
this.llmManager = llmManager;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Feed awareness events into the pattern buffer
|
|
45
|
+
*/
|
|
46
|
+
addPattern(event: AwarenessPattern): void {
|
|
47
|
+
this.patterns.push(event);
|
|
48
|
+
// Keep last 500 events
|
|
49
|
+
if (this.patterns.length > 500) {
|
|
50
|
+
this.patterns = this.patterns.slice(-500);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Generate workflow suggestions from accumulated patterns
|
|
56
|
+
*/
|
|
57
|
+
async generateSuggestions(): Promise<WorkflowSuggestion[]> {
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
// Don't analyze too frequently
|
|
60
|
+
if (now - this.lastAnalysis < this.analysisCooldownMs && this.suggestions.length > 0) {
|
|
61
|
+
return this.suggestions;
|
|
62
|
+
}
|
|
63
|
+
this.lastAnalysis = now;
|
|
64
|
+
|
|
65
|
+
if (this.patterns.length < 10) {
|
|
66
|
+
return this.suggestions;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const detectedPatterns: WorkflowSuggestion[] = [];
|
|
70
|
+
|
|
71
|
+
// Detect repeated app switches
|
|
72
|
+
const appSwitches = this.detectAppSwitchPatterns();
|
|
73
|
+
detectedPatterns.push(...appSwitches);
|
|
74
|
+
|
|
75
|
+
// Detect recurring errors
|
|
76
|
+
const errorPatterns = this.detectErrorPatterns();
|
|
77
|
+
detectedPatterns.push(...errorPatterns);
|
|
78
|
+
|
|
79
|
+
// Detect scheduled-like behavior
|
|
80
|
+
const scheduledPatterns = this.detectScheduledPatterns();
|
|
81
|
+
detectedPatterns.push(...scheduledPatterns);
|
|
82
|
+
|
|
83
|
+
// Use LLM for more complex pattern detection if we have enough data
|
|
84
|
+
if (this.patterns.length > 50) {
|
|
85
|
+
try {
|
|
86
|
+
const llmSuggestions = await this.llmAnalyzePatterns();
|
|
87
|
+
detectedPatterns.push(...llmSuggestions);
|
|
88
|
+
} catch {
|
|
89
|
+
// LLM analysis is best-effort
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Deduplicate by title
|
|
94
|
+
const seen = new Set<string>();
|
|
95
|
+
this.suggestions = detectedPatterns.filter(s => {
|
|
96
|
+
if (seen.has(s.title)) return false;
|
|
97
|
+
seen.add(s.title);
|
|
98
|
+
return true;
|
|
99
|
+
}).slice(0, 10);
|
|
100
|
+
|
|
101
|
+
return this.suggestions;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Dismiss a suggestion
|
|
106
|
+
*/
|
|
107
|
+
dismiss(id: string): void {
|
|
108
|
+
this.suggestions = this.suggestions.filter(s => s.id !== id);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Pattern detection ──
|
|
112
|
+
|
|
113
|
+
private detectAppSwitchPatterns(): WorkflowSuggestion[] {
|
|
114
|
+
const suggestions: WorkflowSuggestion[] = [];
|
|
115
|
+
const appSwitchEvents = this.patterns.filter(p => p.type === 'context_change');
|
|
116
|
+
|
|
117
|
+
// Look for repeated A→B→A switches (copy-paste patterns)
|
|
118
|
+
const switchPairs: Map<string, number> = new Map();
|
|
119
|
+
for (let i = 1; i < appSwitchEvents.length; i++) {
|
|
120
|
+
const from = String(appSwitchEvents[i - 1]!.data.appName ?? '');
|
|
121
|
+
const to = String(appSwitchEvents[i]!.data.appName ?? '');
|
|
122
|
+
if (from && to && from !== to) {
|
|
123
|
+
const key = `${from}→${to}`;
|
|
124
|
+
switchPairs.set(key, (switchPairs.get(key) ?? 0) + 1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const [pair, count] of switchPairs) {
|
|
129
|
+
if (count >= 5) {
|
|
130
|
+
const [from, to] = pair.split('→');
|
|
131
|
+
suggestions.push({
|
|
132
|
+
id: `suggest-switch-${Date.now()}-${from}-${to}`,
|
|
133
|
+
title: `Automate ${from} → ${to} workflow`,
|
|
134
|
+
description: `You frequently switch between ${from} and ${to} (${count} times recently). Consider automating this.`,
|
|
135
|
+
confidence: Math.min(count / 20, 0.9),
|
|
136
|
+
category: 'app_pattern',
|
|
137
|
+
patternEvidence: [`${count} switches between ${from} and ${to}`],
|
|
138
|
+
previewDefinition: {
|
|
139
|
+
nodes: [
|
|
140
|
+
{ id: 'trigger-1', type: 'trigger.screen', label: `Watch ${from}`, position: { x: 100, y: 200 }, config: { condition_type: 'app_active', app_name: from } },
|
|
141
|
+
{ id: 'action-1', type: 'action.notification', label: `Notify about ${to}`, position: { x: 400, y: 200 }, config: { message: `Ready to switch to ${to}?` } },
|
|
142
|
+
],
|
|
143
|
+
edges: [
|
|
144
|
+
{ id: 'e-1', source: 'trigger-1', target: 'action-1' },
|
|
145
|
+
],
|
|
146
|
+
settings: { maxRetries: 1, retryDelayMs: 5000, timeoutMs: 60000, parallelism: 'sequential', onError: 'stop' },
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return suggestions;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private detectErrorPatterns(): WorkflowSuggestion[] {
|
|
156
|
+
const suggestions: WorkflowSuggestion[] = [];
|
|
157
|
+
const errorEvents = this.patterns.filter(p => p.type === 'error_detected');
|
|
158
|
+
|
|
159
|
+
// Group errors by app
|
|
160
|
+
const errorsByApp: Map<string, number> = new Map();
|
|
161
|
+
for (const evt of errorEvents) {
|
|
162
|
+
const app = String(evt.data.appName ?? 'unknown');
|
|
163
|
+
errorsByApp.set(app, (errorsByApp.get(app) ?? 0) + 1);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const [app, count] of errorsByApp) {
|
|
167
|
+
if (count >= 3) {
|
|
168
|
+
suggestions.push({
|
|
169
|
+
id: `suggest-error-${Date.now()}-${app}`,
|
|
170
|
+
title: `Auto-respond to ${app} errors`,
|
|
171
|
+
description: `${app} has had ${count} errors recently. Set up automatic error handling.`,
|
|
172
|
+
confidence: Math.min(count / 10, 0.85),
|
|
173
|
+
category: 'error_response',
|
|
174
|
+
patternEvidence: [`${count} errors in ${app}`],
|
|
175
|
+
previewDefinition: {
|
|
176
|
+
nodes: [
|
|
177
|
+
{ id: 'trigger-1', type: 'trigger.screen', label: `Detect ${app} error`, position: { x: 100, y: 200 }, config: { condition_type: 'text_present', text: 'error' } },
|
|
178
|
+
{ id: 'action-1', type: 'action.agent_task', label: 'Research fix', position: { x: 400, y: 200 }, config: { task: `Research and suggest a fix for the error in ${app}` } },
|
|
179
|
+
{ id: 'action-2', type: 'action.notification', label: 'Notify solution', position: { x: 700, y: 200 }, config: { message: '{{$node["action-1"].data.response}}' } },
|
|
180
|
+
],
|
|
181
|
+
edges: [
|
|
182
|
+
{ id: 'e-1', source: 'trigger-1', target: 'action-1' },
|
|
183
|
+
{ id: 'e-2', source: 'action-1', target: 'action-2' },
|
|
184
|
+
],
|
|
185
|
+
settings: { maxRetries: 2, retryDelayMs: 5000, timeoutMs: 120000, parallelism: 'sequential', onError: 'continue' },
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return suggestions;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private detectScheduledPatterns(): WorkflowSuggestion[] {
|
|
195
|
+
const suggestions: WorkflowSuggestion[] = [];
|
|
196
|
+
|
|
197
|
+
// Look for events that happen at roughly the same time daily
|
|
198
|
+
const eventsByHour: Map<number, AwarenessPattern[]> = new Map();
|
|
199
|
+
for (const p of this.patterns) {
|
|
200
|
+
const hour = new Date(p.timestamp).getHours();
|
|
201
|
+
const existing = eventsByHour.get(hour) ?? [];
|
|
202
|
+
existing.push(p);
|
|
203
|
+
eventsByHour.set(hour, existing);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
for (const [hour, events] of eventsByHour) {
|
|
207
|
+
// Group by type within the hour
|
|
208
|
+
const typeCount: Map<string, number> = new Map();
|
|
209
|
+
for (const e of events) {
|
|
210
|
+
typeCount.set(e.type, (typeCount.get(e.type) ?? 0) + 1);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (const [type, count] of typeCount) {
|
|
214
|
+
if (count >= 3 && type === 'context_change') {
|
|
215
|
+
const apps = [...new Set(events.filter(e => e.type === type).map(e => String(e.data.appName ?? '')))].filter(Boolean);
|
|
216
|
+
if (apps.length > 0) {
|
|
217
|
+
suggestions.push({
|
|
218
|
+
id: `suggest-scheduled-${Date.now()}-${hour}`,
|
|
219
|
+
title: `Schedule ${hour}:00 routine`,
|
|
220
|
+
description: `You regularly use ${apps.slice(0, 3).join(', ')} around ${hour}:00. Automate this routine.`,
|
|
221
|
+
confidence: Math.min(count / 10, 0.7),
|
|
222
|
+
category: 'scheduled_task',
|
|
223
|
+
patternEvidence: [`${count} occurrences around ${hour}:00`],
|
|
224
|
+
previewDefinition: {
|
|
225
|
+
nodes: [
|
|
226
|
+
{ id: 'trigger-1', type: 'trigger.cron', label: `Daily at ${hour}:00`, position: { x: 100, y: 200 }, config: { expression: `0 ${hour} * * *` } },
|
|
227
|
+
{ id: 'action-1', type: 'action.notification', label: 'Start routine', position: { x: 400, y: 200 }, config: { message: `Time for your ${hour}:00 routine with ${apps[0]}` } },
|
|
228
|
+
],
|
|
229
|
+
edges: [
|
|
230
|
+
{ id: 'e-1', source: 'trigger-1', target: 'action-1' },
|
|
231
|
+
],
|
|
232
|
+
settings: { maxRetries: 1, retryDelayMs: 5000, timeoutMs: 60000, parallelism: 'sequential', onError: 'stop' },
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return suggestions;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private async llmAnalyzePatterns(): Promise<WorkflowSuggestion[]> {
|
|
244
|
+
// Summarize recent patterns for LLM analysis
|
|
245
|
+
const recentPatterns = this.patterns.slice(-100).map(p => ({
|
|
246
|
+
type: p.type,
|
|
247
|
+
app: p.data.appName ?? p.data.app ?? undefined,
|
|
248
|
+
time: new Date(p.timestamp).toLocaleTimeString(),
|
|
249
|
+
}));
|
|
250
|
+
|
|
251
|
+
const prompt = [
|
|
252
|
+
{
|
|
253
|
+
role: 'system' as const,
|
|
254
|
+
content: `You analyze user behavior patterns and suggest automation workflows. Respond with a JSON array of suggestions. Each suggestion should have: title (string), description (string), confidence (0-1), category (repetitive_action|error_response|scheduled_task|app_pattern). Only suggest if you see clear patterns. Return [] if no strong patterns.`,
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
role: 'user' as const,
|
|
258
|
+
content: `Here are recent user activity patterns:\n${JSON.stringify(recentPatterns, null, 2)}\n\nWhat workflow automations would you suggest?`,
|
|
259
|
+
},
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
const response = await this.llmManager.chat(prompt, { temperature: 0.3, max_tokens: 2000 });
|
|
263
|
+
const text = response.content;
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
const jsonMatch = text.match(/\[[\s\S]*\]/);
|
|
267
|
+
if (!jsonMatch) return [];
|
|
268
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
269
|
+
if (!Array.isArray(parsed)) return [];
|
|
270
|
+
|
|
271
|
+
return parsed.slice(0, 3).map((s: any, i: number) => ({
|
|
272
|
+
id: `suggest-llm-${Date.now()}-${i}`,
|
|
273
|
+
title: s.title ?? 'Suggested workflow',
|
|
274
|
+
description: s.description ?? '',
|
|
275
|
+
confidence: Math.min(s.confidence ?? 0.5, 0.9),
|
|
276
|
+
category: s.category ?? 'repetitive_action',
|
|
277
|
+
patternEvidence: ['Detected by AI analysis'],
|
|
278
|
+
previewDefinition: {
|
|
279
|
+
nodes: [
|
|
280
|
+
{ id: 'trigger-1', type: 'trigger.manual', label: 'Manual Trigger', position: { x: 100, y: 200 }, config: {} },
|
|
281
|
+
],
|
|
282
|
+
edges: [],
|
|
283
|
+
settings: { maxRetries: 3, retryDelayMs: 5000, timeoutMs: 300000, parallelism: 'parallel', onError: 'stop' },
|
|
284
|
+
},
|
|
285
|
+
}));
|
|
286
|
+
} catch {
|
|
287
|
+
return [];
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Engine — Service that orchestrates workflow execution
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Service, ServiceStatus } from '../daemon/services.ts';
|
|
6
|
+
import type { NodeRegistry, ExecutionContext, StepLogger, NodeInput } from './nodes/registry.ts';
|
|
7
|
+
import type { ToolRegistry } from '../actions/tools/registry.ts';
|
|
8
|
+
import type { WorkflowDefinition, WorkflowExecution, ExecutionStatus } from './types.ts';
|
|
9
|
+
import type { WorkflowEvent } from './events.ts';
|
|
10
|
+
import { VariableScope } from './variables.ts';
|
|
11
|
+
import { topologicalSort, getOutgoingEdges, executeNode } from './executor.ts';
|
|
12
|
+
import type { TemplateContext } from './template.ts';
|
|
13
|
+
import * as vault from '../vault/workflows.ts';
|
|
14
|
+
|
|
15
|
+
export class WorkflowEngine implements Service {
|
|
16
|
+
name = 'workflow-engine';
|
|
17
|
+
private _status: ServiceStatus = 'stopped';
|
|
18
|
+
private nodeRegistry: NodeRegistry;
|
|
19
|
+
private toolRegistry: ToolRegistry;
|
|
20
|
+
private llmManager: unknown;
|
|
21
|
+
private activeExecutions: Map<string, AbortController> = new Map();
|
|
22
|
+
private eventCallback: ((event: WorkflowEvent) => void) | null = null;
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
nodeRegistry: NodeRegistry,
|
|
26
|
+
toolRegistry: ToolRegistry,
|
|
27
|
+
llmManager: unknown,
|
|
28
|
+
) {
|
|
29
|
+
this.nodeRegistry = nodeRegistry;
|
|
30
|
+
this.toolRegistry = toolRegistry;
|
|
31
|
+
this.llmManager = llmManager;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async start(): Promise<void> {
|
|
35
|
+
this._status = 'running';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async stop(): Promise<void> {
|
|
39
|
+
// Cancel all active executions
|
|
40
|
+
for (const [id, controller] of this.activeExecutions) {
|
|
41
|
+
controller.abort();
|
|
42
|
+
}
|
|
43
|
+
this.activeExecutions.clear();
|
|
44
|
+
this._status = 'stopped';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
status(): ServiceStatus {
|
|
48
|
+
return this._status;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setEventCallback(cb: (event: WorkflowEvent) => void): void {
|
|
52
|
+
this.eventCallback = cb;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getActiveCount(): number {
|
|
56
|
+
return this.activeExecutions.size;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Execute a workflow by ID.
|
|
61
|
+
*/
|
|
62
|
+
async execute(
|
|
63
|
+
workflowId: string,
|
|
64
|
+
triggerType: string,
|
|
65
|
+
triggerData?: Record<string, unknown>,
|
|
66
|
+
): Promise<WorkflowExecution> {
|
|
67
|
+
const workflow = vault.getWorkflow(workflowId);
|
|
68
|
+
if (!workflow) throw new Error(`Workflow '${workflowId}' not found`);
|
|
69
|
+
if (!workflow.enabled) throw new Error(`Workflow '${workflow.name}' is disabled`);
|
|
70
|
+
|
|
71
|
+
const version = vault.getLatestVersion(workflowId);
|
|
72
|
+
if (!version) throw new Error(`No versions found for workflow '${workflow.name}'`);
|
|
73
|
+
|
|
74
|
+
const definition = version.definition;
|
|
75
|
+
|
|
76
|
+
// Create execution record
|
|
77
|
+
const execution = vault.createExecution(workflowId, version.version, triggerType, triggerData);
|
|
78
|
+
|
|
79
|
+
this.emit({
|
|
80
|
+
type: 'execution_started',
|
|
81
|
+
workflowId,
|
|
82
|
+
executionId: execution.id,
|
|
83
|
+
data: { triggerType, triggerData: triggerData ?? {}, workflowName: workflow.name },
|
|
84
|
+
timestamp: Date.now(),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Run in background (don't block the caller)
|
|
88
|
+
this.runExecution(execution.id, workflowId, definition, triggerType, triggerData ?? {}).catch(err => {
|
|
89
|
+
console.error(`[WorkflowEngine] Execution ${execution.id} crashed: ${err.message}`);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return execution;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Cancel a running execution.
|
|
97
|
+
*/
|
|
98
|
+
async cancel(executionId: string): Promise<void> {
|
|
99
|
+
const controller = this.activeExecutions.get(executionId);
|
|
100
|
+
if (controller) {
|
|
101
|
+
controller.abort();
|
|
102
|
+
vault.updateExecution(executionId, { status: 'cancelled', completed_at: Date.now() });
|
|
103
|
+
this.activeExecutions.delete(executionId);
|
|
104
|
+
|
|
105
|
+
const exec = vault.getExecution(executionId);
|
|
106
|
+
this.emit({
|
|
107
|
+
type: 'execution_cancelled',
|
|
108
|
+
workflowId: exec?.workflow_id ?? '',
|
|
109
|
+
executionId,
|
|
110
|
+
data: {},
|
|
111
|
+
timestamp: Date.now(),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Internal ──
|
|
117
|
+
|
|
118
|
+
private async runExecution(
|
|
119
|
+
executionId: string,
|
|
120
|
+
workflowId: string,
|
|
121
|
+
definition: WorkflowDefinition,
|
|
122
|
+
triggerType: string,
|
|
123
|
+
triggerData: Record<string, unknown>,
|
|
124
|
+
): Promise<void> {
|
|
125
|
+
const abortController = new AbortController();
|
|
126
|
+
this.activeExecutions.set(executionId, abortController);
|
|
127
|
+
|
|
128
|
+
const startedAt = Date.now();
|
|
129
|
+
const variables = new VariableScope(workflowId);
|
|
130
|
+
const nodeMap = new Map(definition.nodes.map(n => [n.id, n]));
|
|
131
|
+
const nodeOutputs = new Map<string, Record<string, unknown>>();
|
|
132
|
+
|
|
133
|
+
// Build template context
|
|
134
|
+
const templateCtx: TemplateContext = {
|
|
135
|
+
variables: variables.toObject(),
|
|
136
|
+
nodeOutputs,
|
|
137
|
+
triggerData,
|
|
138
|
+
env: process.env as Record<string, string>,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
// Run graph execution using BFS with routing
|
|
143
|
+
const levels = topologicalSort(definition);
|
|
144
|
+
|
|
145
|
+
// Find trigger node(s) — they already fired, seed their output
|
|
146
|
+
const triggerNodes = definition.nodes.filter(n => n.type.startsWith('trigger.'));
|
|
147
|
+
for (const tn of triggerNodes) {
|
|
148
|
+
nodeOutputs.set(tn.id, { ...triggerData, timestamp: Date.now() });
|
|
149
|
+
// Also store by label for $node["label"] references
|
|
150
|
+
nodeOutputs.set(tn.label, { ...triggerData, timestamp: Date.now() });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Track which nodes should be skipped (not on active route)
|
|
154
|
+
const activeNodes = new Set<string>(triggerNodes.map(n => n.id));
|
|
155
|
+
|
|
156
|
+
// Seed: all nodes directly connected from trigger nodes are active
|
|
157
|
+
for (const tn of triggerNodes) {
|
|
158
|
+
for (const targetId of getOutgoingEdges(definition, tn.id)) {
|
|
159
|
+
activeNodes.add(targetId);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
for (const level of levels) {
|
|
164
|
+
if (abortController.signal.aborted) break;
|
|
165
|
+
|
|
166
|
+
// Filter to only active nodes at this level
|
|
167
|
+
const toExecute = level.filter(id => activeNodes.has(id) && !triggerNodes.some(t => t.id === id));
|
|
168
|
+
if (toExecute.length === 0) continue;
|
|
169
|
+
|
|
170
|
+
const settings = definition.settings;
|
|
171
|
+
const isParallel = settings.parallelism === 'parallel';
|
|
172
|
+
|
|
173
|
+
if (isParallel) {
|
|
174
|
+
await Promise.all(toExecute.map(nodeId => this.executeStep(
|
|
175
|
+
executionId, workflowId, nodeId, nodeMap, nodeOutputs, variables,
|
|
176
|
+
templateCtx, definition, abortController, activeNodes,
|
|
177
|
+
)));
|
|
178
|
+
} else {
|
|
179
|
+
for (const nodeId of toExecute) {
|
|
180
|
+
if (abortController.signal.aborted) break;
|
|
181
|
+
await this.executeStep(
|
|
182
|
+
executionId, workflowId, nodeId, nodeMap, nodeOutputs, variables,
|
|
183
|
+
templateCtx, definition, abortController, activeNodes,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Complete
|
|
190
|
+
const duration = Date.now() - startedAt;
|
|
191
|
+
vault.updateExecution(executionId, {
|
|
192
|
+
status: 'completed',
|
|
193
|
+
completed_at: Date.now(),
|
|
194
|
+
duration_ms: duration,
|
|
195
|
+
variables: variables.toObject(),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
this.emit({
|
|
199
|
+
type: 'execution_completed',
|
|
200
|
+
workflowId,
|
|
201
|
+
executionId,
|
|
202
|
+
data: { duration_ms: duration },
|
|
203
|
+
timestamp: Date.now(),
|
|
204
|
+
});
|
|
205
|
+
} catch (err) {
|
|
206
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
207
|
+
const duration = Date.now() - startedAt;
|
|
208
|
+
|
|
209
|
+
vault.updateExecution(executionId, {
|
|
210
|
+
status: 'failed',
|
|
211
|
+
error_message: errMsg,
|
|
212
|
+
completed_at: Date.now(),
|
|
213
|
+
duration_ms: duration,
|
|
214
|
+
variables: variables.toObject(),
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
this.emit({
|
|
218
|
+
type: 'execution_failed',
|
|
219
|
+
workflowId,
|
|
220
|
+
executionId,
|
|
221
|
+
data: { error: errMsg, duration_ms: duration },
|
|
222
|
+
timestamp: Date.now(),
|
|
223
|
+
});
|
|
224
|
+
} finally {
|
|
225
|
+
this.activeExecutions.delete(executionId);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private async executeStep(
|
|
230
|
+
executionId: string,
|
|
231
|
+
workflowId: string,
|
|
232
|
+
nodeId: string,
|
|
233
|
+
nodeMap: Map<string, import('./types.ts').WorkflowNode>,
|
|
234
|
+
nodeOutputs: Map<string, Record<string, unknown>>,
|
|
235
|
+
variables: VariableScope,
|
|
236
|
+
templateCtx: TemplateContext,
|
|
237
|
+
definition: WorkflowDefinition,
|
|
238
|
+
abortController: AbortController,
|
|
239
|
+
activeNodes: Set<string>,
|
|
240
|
+
): Promise<void> {
|
|
241
|
+
const node = nodeMap.get(nodeId);
|
|
242
|
+
if (!node) return;
|
|
243
|
+
|
|
244
|
+
const stepResult = vault.createStepResult(executionId, nodeId, node.type);
|
|
245
|
+
vault.updateStepResult(stepResult.id, { status: 'running', started_at: Date.now() });
|
|
246
|
+
|
|
247
|
+
this.emit({
|
|
248
|
+
type: 'step_started',
|
|
249
|
+
workflowId,
|
|
250
|
+
executionId,
|
|
251
|
+
nodeId,
|
|
252
|
+
data: { nodeType: node.type, label: node.label },
|
|
253
|
+
timestamp: Date.now(),
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Collect input from incoming edges
|
|
257
|
+
const incomingEdges = definition.edges.filter(e => e.target === nodeId);
|
|
258
|
+
const inputData: Record<string, unknown> = {};
|
|
259
|
+
for (const edge of incomingEdges) {
|
|
260
|
+
const sourceOutput = nodeOutputs.get(edge.source);
|
|
261
|
+
if (sourceOutput) Object.assign(inputData, sourceOutput);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const input: NodeInput = {
|
|
265
|
+
data: inputData,
|
|
266
|
+
variables: variables.toObject(),
|
|
267
|
+
executionId,
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const logger: StepLogger = {
|
|
271
|
+
info: (msg) => console.log(`[Workflow:${nodeId}] ${msg}`),
|
|
272
|
+
warn: (msg) => console.warn(`[Workflow:${nodeId}] ${msg}`),
|
|
273
|
+
error: (msg) => console.error(`[Workflow:${nodeId}] ${msg}`),
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const ctx: ExecutionContext = {
|
|
277
|
+
executionId,
|
|
278
|
+
workflowId,
|
|
279
|
+
toolRegistry: this.toolRegistry,
|
|
280
|
+
llmManager: this.llmManager,
|
|
281
|
+
variables,
|
|
282
|
+
logger,
|
|
283
|
+
abortSignal: abortController.signal,
|
|
284
|
+
nodeRegistry: this.nodeRegistry,
|
|
285
|
+
broadcast: (type, data) => this.emit({
|
|
286
|
+
type: type as any,
|
|
287
|
+
workflowId,
|
|
288
|
+
executionId,
|
|
289
|
+
data,
|
|
290
|
+
timestamp: Date.now(),
|
|
291
|
+
}),
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// Refresh template context
|
|
295
|
+
templateCtx.variables = variables.toObject();
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
const startTime = Date.now();
|
|
299
|
+
const output = await executeNode(node, input, this.nodeRegistry, ctx, templateCtx, definition.settings);
|
|
300
|
+
const duration = Date.now() - startTime;
|
|
301
|
+
|
|
302
|
+
// Store output
|
|
303
|
+
nodeOutputs.set(nodeId, output.data);
|
|
304
|
+
nodeOutputs.set(node.label, output.data); // For $node["label"] references
|
|
305
|
+
|
|
306
|
+
vault.updateStepResult(stepResult.id, {
|
|
307
|
+
status: 'completed',
|
|
308
|
+
input_data: inputData,
|
|
309
|
+
output_data: output.data,
|
|
310
|
+
completed_at: Date.now(),
|
|
311
|
+
duration_ms: duration,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
this.emit({
|
|
315
|
+
type: 'step_completed',
|
|
316
|
+
workflowId,
|
|
317
|
+
executionId,
|
|
318
|
+
nodeId,
|
|
319
|
+
data: { duration_ms: duration, output: output.data },
|
|
320
|
+
timestamp: Date.now(),
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Activate downstream nodes based on route
|
|
324
|
+
const targets = getOutgoingEdges(definition, nodeId, output.route);
|
|
325
|
+
for (const t of targets) {
|
|
326
|
+
activeNodes.add(t);
|
|
327
|
+
}
|
|
328
|
+
} catch (err) {
|
|
329
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
330
|
+
|
|
331
|
+
vault.updateStepResult(stepResult.id, {
|
|
332
|
+
status: 'failed',
|
|
333
|
+
input_data: inputData,
|
|
334
|
+
error_message: errMsg,
|
|
335
|
+
completed_at: Date.now(),
|
|
336
|
+
duration_ms: Date.now() - (stepResult.started_at ?? Date.now()),
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
this.emit({
|
|
340
|
+
type: 'step_failed',
|
|
341
|
+
workflowId,
|
|
342
|
+
executionId,
|
|
343
|
+
nodeId,
|
|
344
|
+
data: { error: errMsg, nodeType: node.type },
|
|
345
|
+
timestamp: Date.now(),
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Check fallback
|
|
349
|
+
if (node.fallbackNodeId && nodeMap.has(node.fallbackNodeId)) {
|
|
350
|
+
logger.info(`Falling back to node: ${node.fallbackNodeId}`);
|
|
351
|
+
activeNodes.add(node.fallbackNodeId);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// If onError is 'stop', throw to halt execution
|
|
356
|
+
if (definition.settings.onError === 'stop') {
|
|
357
|
+
throw err;
|
|
358
|
+
}
|
|
359
|
+
// 'continue' — log and move on
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private emit(event: WorkflowEvent): void {
|
|
364
|
+
this.eventCallback?.(event);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Event Types — for real-time WebSocket broadcasting
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type WorkflowEventType =
|
|
6
|
+
| 'execution_started'
|
|
7
|
+
| 'execution_completed'
|
|
8
|
+
| 'execution_failed'
|
|
9
|
+
| 'execution_cancelled'
|
|
10
|
+
| 'step_started'
|
|
11
|
+
| 'step_completed'
|
|
12
|
+
| 'step_failed'
|
|
13
|
+
| 'variable_changed'
|
|
14
|
+
| 'workflow_enabled'
|
|
15
|
+
| 'workflow_disabled';
|
|
16
|
+
|
|
17
|
+
export type WorkflowEvent = {
|
|
18
|
+
type: WorkflowEventType;
|
|
19
|
+
workflowId: string;
|
|
20
|
+
executionId?: string;
|
|
21
|
+
nodeId?: string;
|
|
22
|
+
data: Record<string, unknown>;
|
|
23
|
+
timestamp: number;
|
|
24
|
+
};
|