@runcore-sh/runcore 0.4.0 → 0.5.1
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/dictionary.json +2 -2
- package/dist/activity/log.js +2 -2
- package/dist/activity/log.js.map +1 -1
- package/dist/agents/governed-spawn.d.ts.map +1 -1
- package/dist/cli.js +101 -11
- package/dist/cli.js.map +1 -1
- package/dist/extensions/cache.d.ts +57 -0
- package/dist/extensions/cache.d.ts.map +1 -0
- package/dist/extensions/cache.js +173 -0
- package/dist/extensions/cache.js.map +1 -0
- package/dist/extensions/client.d.ts +55 -0
- package/dist/extensions/client.d.ts.map +1 -0
- package/dist/extensions/client.js +120 -0
- package/dist/extensions/client.js.map +1 -0
- package/dist/extensions/index.d.ts +13 -0
- package/dist/extensions/index.d.ts.map +1 -0
- package/dist/extensions/index.js +12 -0
- package/dist/extensions/index.js.map +1 -0
- package/dist/extensions/loader.d.ts +50 -0
- package/dist/extensions/loader.d.ts.map +1 -0
- package/dist/extensions/loader.js +166 -0
- package/dist/extensions/loader.js.map +1 -0
- package/dist/extensions/manifest.d.ts +38 -0
- package/dist/extensions/manifest.d.ts.map +1 -0
- package/dist/extensions/manifest.js +17 -0
- package/dist/extensions/manifest.js.map +1 -0
- package/dist/extensions/stubs.d.ts +27 -0
- package/dist/extensions/stubs.d.ts.map +1 -0
- package/dist/extensions/stubs.js +45 -0
- package/dist/extensions/stubs.js.map +1 -0
- package/dist/lib/audit.js +2 -2
- package/dist/lib/audit.js.map +1 -1
- package/dist/lib/brain-migrate.d.ts +21 -0
- package/dist/lib/brain-migrate.d.ts.map +1 -0
- package/dist/lib/brain-migrate.js +137 -0
- package/dist/lib/brain-migrate.js.map +1 -0
- package/dist/lib/paths.d.ts +27 -0
- package/dist/lib/paths.d.ts.map +1 -1
- package/dist/lib/paths.js +65 -0
- package/dist/lib/paths.js.map +1 -1
- package/dist/llm/call-log.d.ts +40 -0
- package/dist/llm/call-log.d.ts.map +1 -0
- package/dist/llm/call-log.js +35 -0
- package/dist/llm/call-log.js.map +1 -0
- package/dist/llm/complete.d.ts +6 -0
- package/dist/llm/complete.d.ts.map +1 -1
- package/dist/llm/complete.js +27 -0
- package/dist/llm/complete.js.map +1 -1
- package/dist/mcp-server.js +118 -2
- package/dist/mcp-server.js.map +1 -1
- package/dist/memory/file-backed.d.ts +4 -0
- package/dist/memory/file-backed.d.ts.map +1 -1
- package/dist/memory/file-backed.js +4 -0
- package/dist/memory/file-backed.js.map +1 -1
- package/dist/memory/vector-index.d.ts +4 -12
- package/dist/memory/vector-index.d.ts.map +1 -1
- package/dist/memory/vector-index.js +11 -93
- package/dist/memory/vector-index.js.map +1 -1
- package/dist/search/brain-docs.d.ts +17 -7
- package/dist/search/brain-docs.d.ts.map +1 -1
- package/dist/search/brain-docs.js +170 -52
- package/dist/search/brain-docs.js.map +1 -1
- package/dist/search/brain-rag.d.ts +45 -0
- package/dist/search/brain-rag.d.ts.map +1 -0
- package/dist/search/brain-rag.js +275 -0
- package/dist/search/brain-rag.js.map +1 -0
- package/dist/search/chunker.d.ts +24 -0
- package/dist/search/chunker.d.ts.map +1 -0
- package/dist/search/chunker.js +95 -0
- package/dist/search/chunker.js.map +1 -0
- package/dist/search/embedder.d.ts +16 -0
- package/dist/search/embedder.d.ts.map +1 -0
- package/dist/search/embedder.js +108 -0
- package/dist/search/embedder.js.map +1 -0
- package/dist/search/file-watcher.d.ts +11 -0
- package/dist/search/file-watcher.d.ts.map +1 -0
- package/dist/search/file-watcher.js +86 -0
- package/dist/search/file-watcher.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +814 -472
- package/dist/server.js.map +1 -1
- package/dist/sessions/store.d.ts +9 -0
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js.map +1 -1
- package/dist/settings.d.ts +26 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +78 -2
- package/dist/settings.js.map +1 -1
- package/dist/tracing/init.d.ts +1 -1
- package/dist/tracing/init.d.ts.map +1 -1
- package/dist/utils/logger.js +2 -2
- package/dist/utils/logger.js.map +1 -1
- package/module-tiers.json +164 -0
- package/package.json +9 -13
- package/public/avatar/cache/1184385ec5522b57.mp4 +0 -0
- package/public/avatar/cache/1f15f6a1ebd7e439.mp4 +0 -0
- package/public/avatar/cache/2c7e47ff0bdeb8d1.mp4 +0 -0
- package/public/avatar/cache/5f308566f7abb8f2.mp4 +0 -0
- package/public/avatar/cache/62f9cfba848d724e.mp4 +0 -0
- package/public/avatar/cache/6d64e657e6bf2aab.mp4 +0 -0
- package/public/avatar/cache/763ad0349e0b6f26.mp4 +0 -0
- package/public/avatar/cache/81a516cfd461b2b9.mp4 +0 -0
- package/public/avatar/cache/9366de15fd6910ca.mp4 +0 -0
- package/public/avatar/cache/ade41a846b283895.mp4 +0 -0
- package/public/avatar/cache/b6066e5c65383eec.mp4 +0 -0
- package/public/avatar/cache/edadb75d37891fc7.mp4 +0 -0
- package/public/avatar/cache/f0ae159640621dd9.mp4 +0 -0
- package/public/avatar/cache/fc2e5419adf29d96.mp4 +0 -0
- package/public/index.html +379 -59
- package/dist/agents/autonomous.js +0 -749
- package/dist/agents/autonomous.js.map +0 -1
- package/dist/agents/commit.js +0 -113
- package/dist/agents/commit.js.map +0 -1
- package/dist/agents/continue.js +0 -158
- package/dist/agents/continue.js.map +0 -1
- package/dist/agents/cooldown.js +0 -397
- package/dist/agents/cooldown.js.map +0 -1
- package/dist/agents/dedup-guard.js +0 -131
- package/dist/agents/dedup-guard.js.map +0 -1
- package/dist/agents/feed.js +0 -176
- package/dist/agents/feed.js.map +0 -1
- package/dist/agents/governance.js +0 -292
- package/dist/agents/governance.js.map +0 -1
- package/dist/agents/governed-spawn.js +0 -192
- package/dist/agents/governed-spawn.js.map +0 -1
- package/dist/agents/heartbeat.js +0 -324
- package/dist/agents/heartbeat.js.map +0 -1
- package/dist/agents/instance-manager.js +0 -850
- package/dist/agents/instance-manager.js.map +0 -1
- package/dist/agents/issue-reporter.js +0 -123
- package/dist/agents/issue-reporter.js.map +0 -1
- package/dist/agents/issues.js +0 -141
- package/dist/agents/issues.js.map +0 -1
- package/dist/agents/locks.js +0 -234
- package/dist/agents/locks.js.map +0 -1
- package/dist/agents/memory.js +0 -93
- package/dist/agents/memory.js.map +0 -1
- package/dist/agents/monitor.js +0 -235
- package/dist/agents/monitor.js.map +0 -1
- package/dist/agents/orchestration.js +0 -715
- package/dist/agents/orchestration.js.map +0 -1
- package/dist/agents/recover.js +0 -166
- package/dist/agents/recover.js.map +0 -1
- package/dist/agents/reflection.js +0 -199
- package/dist/agents/reflection.js.map +0 -1
- package/dist/agents/runtime/bus.js +0 -174
- package/dist/agents/runtime/bus.js.map +0 -1
- package/dist/agents/runtime/config.js +0 -101
- package/dist/agents/runtime/config.js.map +0 -1
- package/dist/agents/runtime/driver.js +0 -214
- package/dist/agents/runtime/driver.js.map +0 -1
- package/dist/agents/runtime/errors.js +0 -40
- package/dist/agents/runtime/errors.js.map +0 -1
- package/dist/agents/runtime/index.js +0 -54
- package/dist/agents/runtime/index.js.map +0 -1
- package/dist/agents/runtime/lifecycle.js +0 -116
- package/dist/agents/runtime/lifecycle.js.map +0 -1
- package/dist/agents/runtime/manager.js +0 -948
- package/dist/agents/runtime/manager.js.map +0 -1
- package/dist/agents/runtime/registry.js +0 -195
- package/dist/agents/runtime/registry.js.map +0 -1
- package/dist/agents/runtime/resources.js +0 -146
- package/dist/agents/runtime/resources.js.map +0 -1
- package/dist/agents/runtime/types.js +0 -24
- package/dist/agents/runtime/types.js.map +0 -1
- package/dist/agents/spawn-policy.js +0 -202
- package/dist/agents/spawn-policy.js.map +0 -1
- package/dist/agents/spawn.js +0 -970
- package/dist/agents/spawn.js.map +0 -1
- package/dist/agents/triage.js +0 -81
- package/dist/agents/triage.js.map +0 -1
- package/dist/agents/workflow.js +0 -543
- package/dist/agents/workflow.js.map +0 -1
- package/dist/avatar/client.js +0 -172
- package/dist/avatar/client.js.map +0 -1
- package/dist/avatar/sidecar.js +0 -125
- package/dist/avatar/sidecar.js.map +0 -1
- package/dist/browser/sessions.js +0 -122
- package/dist/browser/sessions.js.map +0 -1
- package/dist/capabilities/definitions/browser.js +0 -242
- package/dist/capabilities/definitions/browser.js.map +0 -1
- package/dist/channels/whatsapp.js +0 -200
- package/dist/channels/whatsapp.js.map +0 -1
- package/dist/credentials/store.js +0 -189
- package/dist/credentials/store.js.map +0 -1
- package/dist/files/deep-index.js +0 -337
- package/dist/files/deep-index.js.map +0 -1
- package/dist/files/extract.js +0 -33
- package/dist/files/extract.js.map +0 -1
- package/dist/files/gdrive.js +0 -246
- package/dist/files/gdrive.js.map +0 -1
- package/dist/github/client.js +0 -408
- package/dist/github/client.js.map +0 -1
- package/dist/github/commit-analysis.js +0 -276
- package/dist/github/commit-analysis.js.map +0 -1
- package/dist/github/contributor-stats.js +0 -119
- package/dist/github/contributor-stats.js.map +0 -1
- package/dist/github/issue-sla.js +0 -220
- package/dist/github/issue-sla.js.map +0 -1
- package/dist/github/issue-triage.js +0 -286
- package/dist/github/issue-triage.js.map +0 -1
- package/dist/github/pr-readiness.js +0 -197
- package/dist/github/pr-readiness.js.map +0 -1
- package/dist/github/pr-review.js +0 -410
- package/dist/github/pr-review.js.map +0 -1
- package/dist/github/release-notes.js +0 -227
- package/dist/github/release-notes.js.map +0 -1
- package/dist/github/repo-health.js +0 -303
- package/dist/github/repo-health.js.map +0 -1
- package/dist/github/retry.js +0 -117
- package/dist/github/retry.js.map +0 -1
- package/dist/github/types.js +0 -8
- package/dist/github/types.js.map +0 -1
- package/dist/github/webhooks.js +0 -153
- package/dist/github/webhooks.js.map +0 -1
- package/dist/google/auth.js +0 -325
- package/dist/google/auth.js.map +0 -1
- package/dist/google/calendar-timer.js +0 -91
- package/dist/google/calendar-timer.js.map +0 -1
- package/dist/google/calendar.js +0 -270
- package/dist/google/calendar.js.map +0 -1
- package/dist/google/docs.js +0 -309
- package/dist/google/docs.js.map +0 -1
- package/dist/google/gmail-send.js +0 -219
- package/dist/google/gmail-send.js.map +0 -1
- package/dist/google/gmail-timer.js +0 -223
- package/dist/google/gmail-timer.js.map +0 -1
- package/dist/google/gmail.js +0 -470
- package/dist/google/gmail.js.map +0 -1
- package/dist/google/plugin.js +0 -169
- package/dist/google/plugin.js.map +0 -1
- package/dist/google/tasks-timer.js +0 -107
- package/dist/google/tasks-timer.js.map +0 -1
- package/dist/google/tasks.js +0 -331
- package/dist/google/tasks.js.map +0 -1
- package/dist/google/temporal.js +0 -176
- package/dist/google/temporal.js.map +0 -1
- package/dist/integrations/gate.js +0 -100
- package/dist/integrations/gate.js.map +0 -1
- package/dist/integrations/github.js +0 -331
- package/dist/integrations/github.js.map +0 -1
- package/dist/integrations/google-tasks.js +0 -432
- package/dist/integrations/google-tasks.js.map +0 -1
- package/dist/mdns.js +0 -110
- package/dist/mdns.js.map +0 -1
- package/dist/notifications/channel.js +0 -83
- package/dist/notifications/channel.js.map +0 -1
- package/dist/notifications/channels/adapter.js +0 -55
- package/dist/notifications/channels/adapter.js.map +0 -1
- package/dist/notifications/channels/index.js +0 -6
- package/dist/notifications/channels/index.js.map +0 -1
- package/dist/notifications/channels/log.js +0 -29
- package/dist/notifications/channels/log.js.map +0 -1
- package/dist/notifications/email.js +0 -72
- package/dist/notifications/email.js.map +0 -1
- package/dist/notifications/engine.js +0 -198
- package/dist/notifications/engine.js.map +0 -1
- package/dist/notifications/index.js +0 -24
- package/dist/notifications/index.js.map +0 -1
- package/dist/notifications/phone.js +0 -48
- package/dist/notifications/phone.js.map +0 -1
- package/dist/notifications/sms.js +0 -65
- package/dist/notifications/sms.js.map +0 -1
- package/dist/notifications/types.js +0 -14
- package/dist/notifications/types.js.map +0 -1
- package/dist/notifications/webhook.js +0 -65
- package/dist/notifications/webhook.js.map +0 -1
- package/dist/resend/inbox.js +0 -199
- package/dist/resend/inbox.js.map +0 -1
- package/dist/resend/webhooks.js +0 -244
- package/dist/resend/webhooks.js.map +0 -1
- package/dist/search/browse.js +0 -225
- package/dist/search/browse.js.map +0 -1
- package/dist/search/perplexity.js +0 -41
- package/dist/search/perplexity.js.map +0 -1
- package/dist/slack/channels.js +0 -277
- package/dist/slack/channels.js.map +0 -1
- package/dist/slack/client.js +0 -468
- package/dist/slack/client.js.map +0 -1
- package/dist/slack/retry.js +0 -100
- package/dist/slack/retry.js.map +0 -1
- package/dist/slack/types.js +0 -52
- package/dist/slack/types.js.map +0 -1
- package/dist/slack/webhooks.js +0 -285
- package/dist/slack/webhooks.js.map +0 -1
- package/dist/stt/client.js +0 -66
- package/dist/stt/client.js.map +0 -1
- package/dist/stt/sidecar.js +0 -115
- package/dist/stt/sidecar.js.map +0 -1
- package/dist/tracing/bridge.js +0 -70
- package/dist/tracing/bridge.js.map +0 -1
- package/dist/tracing/correlation.js +0 -49
- package/dist/tracing/correlation.js.map +0 -1
- package/dist/tracing/index.js +0 -18
- package/dist/tracing/index.js.map +0 -1
- package/dist/tracing/init.js +0 -81
- package/dist/tracing/init.js.map +0 -1
- package/dist/tracing/instrument.js +0 -145
- package/dist/tracing/instrument.js.map +0 -1
- package/dist/tracing/middleware.js +0 -69
- package/dist/tracing/middleware.js.map +0 -1
- package/dist/tracing/tracer.js +0 -327
- package/dist/tracing/tracer.js.map +0 -1
- package/dist/tts/client.js +0 -48
- package/dist/tts/client.js.map +0 -1
- package/dist/tts/sidecar.js +0 -148
- package/dist/tts/sidecar.js.map +0 -1
- package/dist/twilio/call.js +0 -79
- package/dist/twilio/call.js.map +0 -1
- package/dist/vault/matcher.js +0 -197
- package/dist/vault/matcher.js.map +0 -1
- package/dist/vault/personal.js +0 -163
- package/dist/vault/personal.js.map +0 -1
- package/dist/vault/policy.js +0 -159
- package/dist/vault/policy.js.map +0 -1
- package/dist/vault/store.js +0 -122
- package/dist/vault/store.js.map +0 -1
- package/dist/vault/transfer.js +0 -188
- package/dist/vault/transfer.js.map +0 -1
- package/dist/volumes/index.js +0 -2
- package/dist/volumes/index.js.map +0 -1
- package/dist/volumes/manager.js +0 -462
- package/dist/volumes/manager.js.map +0 -1
- package/dist/volumes/types.js +0 -8
- package/dist/volumes/types.js.map +0 -1
- package/dist/webhooks/config.js +0 -214
- package/dist/webhooks/config.js.map +0 -1
- package/dist/webhooks/event-log.js +0 -132
- package/dist/webhooks/event-log.js.map +0 -1
- package/dist/webhooks/handler.js +0 -103
- package/dist/webhooks/handler.js.map +0 -1
- package/dist/webhooks/handlers.js +0 -231
- package/dist/webhooks/handlers.js.map +0 -1
- package/dist/webhooks/index.js +0 -33
- package/dist/webhooks/index.js.map +0 -1
- package/dist/webhooks/mount.js +0 -400
- package/dist/webhooks/mount.js.map +0 -1
- package/dist/webhooks/registry.js +0 -143
- package/dist/webhooks/registry.js.map +0 -1
- package/dist/webhooks/relay.js +0 -53
- package/dist/webhooks/relay.js.map +0 -1
- package/dist/webhooks/retry.js +0 -270
- package/dist/webhooks/retry.js.map +0 -1
- package/dist/webhooks/router.js +0 -290
- package/dist/webhooks/router.js.map +0 -1
- package/dist/webhooks/twilio.js +0 -129
- package/dist/webhooks/twilio.js.map +0 -1
- package/dist/webhooks/types.js +0 -8
- package/dist/webhooks/types.js.map +0 -1
- package/dist/webhooks/verify.js +0 -154
- package/dist/webhooks/verify.js.map +0 -1
|
@@ -1,715 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agent Orchestration Engine — Multi-agent coordination, delegation, and result merging.
|
|
3
|
-
*
|
|
4
|
-
* Sits on top of the AgentPool to provide:
|
|
5
|
-
* 1. **Workflows** — Named groups of coordinated agent tasks
|
|
6
|
-
* 2. **Task delegation** — Spawn multiple agents for a workflow and track them
|
|
7
|
-
* 3. **Result aggregation** — Collect, merge, and synthesize outputs from all agents
|
|
8
|
-
* 4. **Conflict resolution** — Preventive file locking + post-hoc detection/resolution
|
|
9
|
-
* 5. **Queue management** — Priority-aware workflow scheduling
|
|
10
|
-
* 6. **Status tracking** — Real-time workflow status and reporting
|
|
11
|
-
* 7. **Context passing** — Dependency mode pipes completed task outputs into dependents
|
|
12
|
-
* 8. **Cascading failure limits** — Auto-cancel workflows when failure threshold hit
|
|
13
|
-
* 9. **Workflow timeouts** — Wall-clock timeout for entire workflows
|
|
14
|
-
* 10. **Bus integration** — Emits workflow lifecycle events for monitoring
|
|
15
|
-
*
|
|
16
|
-
* Usage:
|
|
17
|
-
* const orch = new Orchestrator(pool);
|
|
18
|
-
* const wf = orch.createWorkflow({ name: "Feature X", tasks: [...] });
|
|
19
|
-
* const result = await orch.execute(wf.id);
|
|
20
|
-
* const aggregated = orch.aggregateResults(wf.id);
|
|
21
|
-
*/
|
|
22
|
-
import { randomBytes } from "node:crypto";
|
|
23
|
-
import { createLogger } from "../utils/logger.js";
|
|
24
|
-
const log = createLogger("orchestrator");
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
// Orchestrator
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
export class Orchestrator {
|
|
29
|
-
pool;
|
|
30
|
-
workflows = new Map();
|
|
31
|
-
conflicts = new Map();
|
|
32
|
-
workflowQueue = [];
|
|
33
|
-
activeWorkflowCount = 0;
|
|
34
|
-
maxActiveWorkflows;
|
|
35
|
-
eventHandlers = new Map();
|
|
36
|
-
/** Resolvers for queued workflows waiting for capacity. */
|
|
37
|
-
queueResolvers = new Map();
|
|
38
|
-
constructor(pool, options) {
|
|
39
|
-
this.pool = pool;
|
|
40
|
-
this.maxActiveWorkflows = options?.maxActiveWorkflows ?? 3;
|
|
41
|
-
}
|
|
42
|
-
// -------------------------------------------------------------------------
|
|
43
|
-
// Workflow lifecycle
|
|
44
|
-
// -------------------------------------------------------------------------
|
|
45
|
-
/** Create a workflow (does not start execution). */
|
|
46
|
-
createWorkflow(input) {
|
|
47
|
-
const id = generateWorkflowId();
|
|
48
|
-
const tasks = new Map();
|
|
49
|
-
for (const def of input.tasks) {
|
|
50
|
-
if (tasks.has(def.key)) {
|
|
51
|
-
throw new Error(`Duplicate task key in workflow: ${def.key}`);
|
|
52
|
-
}
|
|
53
|
-
tasks.set(def.key, {
|
|
54
|
-
key: def.key,
|
|
55
|
-
label: def.label,
|
|
56
|
-
prompt: def.prompt,
|
|
57
|
-
cwd: def.cwd,
|
|
58
|
-
dependsOn: def.dependsOn ?? [],
|
|
59
|
-
priority: def.priority ?? 50,
|
|
60
|
-
config: def.config,
|
|
61
|
-
tags: def.tags ?? [],
|
|
62
|
-
status: "pending",
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
// Validate dependency references
|
|
66
|
-
if (input.mode === "dependency") {
|
|
67
|
-
for (const [, task] of tasks) {
|
|
68
|
-
for (const dep of task.dependsOn) {
|
|
69
|
-
if (!tasks.has(dep)) {
|
|
70
|
-
throw new Error(`Task "${task.key}" depends on unknown task "${dep}"`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
// Check for cycles
|
|
75
|
-
this.detectCycles(tasks);
|
|
76
|
-
}
|
|
77
|
-
const workflow = {
|
|
78
|
-
id,
|
|
79
|
-
name: input.name,
|
|
80
|
-
description: input.description,
|
|
81
|
-
status: "pending",
|
|
82
|
-
mode: input.mode ?? "parallel",
|
|
83
|
-
conflictStrategy: input.conflictStrategy ?? "last-write-wins",
|
|
84
|
-
maxConcurrent: input.maxConcurrent ?? 0,
|
|
85
|
-
origin: input.origin ?? "ai",
|
|
86
|
-
tasks,
|
|
87
|
-
createdAt: new Date().toISOString(),
|
|
88
|
-
};
|
|
89
|
-
this.workflows.set(id, workflow);
|
|
90
|
-
this.conflicts.set(id, []);
|
|
91
|
-
log.info("Workflow created", {
|
|
92
|
-
workflowId: id,
|
|
93
|
-
name: input.name,
|
|
94
|
-
taskCount: tasks.size,
|
|
95
|
-
mode: workflow.mode,
|
|
96
|
-
});
|
|
97
|
-
return workflow;
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Execute a workflow. Spawns agents according to the execution mode.
|
|
101
|
-
* Returns a promise that resolves when the workflow completes.
|
|
102
|
-
*/
|
|
103
|
-
async execute(workflowId) {
|
|
104
|
-
const workflow = this.requireWorkflow(workflowId);
|
|
105
|
-
if (workflow.status !== "pending") {
|
|
106
|
-
throw new Error(`Workflow "${workflow.name}" is already ${workflow.status}`);
|
|
107
|
-
}
|
|
108
|
-
// Check if we can start immediately or need to queue
|
|
109
|
-
if (this.activeWorkflowCount >= this.maxActiveWorkflows) {
|
|
110
|
-
this.workflowQueue.push(workflowId);
|
|
111
|
-
log.info("Workflow queued", { workflowId, queuePosition: this.workflowQueue.length });
|
|
112
|
-
// Wait until drainQueue resolves us
|
|
113
|
-
await new Promise((resolve) => {
|
|
114
|
-
this.queueResolvers.set(workflowId, resolve);
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
this.activeWorkflowCount++;
|
|
118
|
-
workflow.status = "running";
|
|
119
|
-
workflow.startedAt = new Date().toISOString();
|
|
120
|
-
log.info("Workflow executing", {
|
|
121
|
-
workflowId,
|
|
122
|
-
name: workflow.name,
|
|
123
|
-
mode: workflow.mode,
|
|
124
|
-
});
|
|
125
|
-
try {
|
|
126
|
-
switch (workflow.mode) {
|
|
127
|
-
case "parallel":
|
|
128
|
-
await this.executeParallel(workflow);
|
|
129
|
-
break;
|
|
130
|
-
case "sequential":
|
|
131
|
-
await this.executeSequential(workflow);
|
|
132
|
-
break;
|
|
133
|
-
case "dependency":
|
|
134
|
-
await this.executeDependency(workflow);
|
|
135
|
-
break;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
catch (err) {
|
|
139
|
-
log.error("Workflow execution error", {
|
|
140
|
-
workflowId,
|
|
141
|
-
error: err instanceof Error ? err.message : String(err),
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
// Detect conflicts
|
|
145
|
-
const fileConflicts = this.detectFileConflicts(workflow);
|
|
146
|
-
this.conflicts.set(workflowId, fileConflicts);
|
|
147
|
-
// Resolve conflicts
|
|
148
|
-
if (fileConflicts.length > 0) {
|
|
149
|
-
this.resolveConflicts(workflow, fileConflicts);
|
|
150
|
-
}
|
|
151
|
-
// Determine final status
|
|
152
|
-
workflow.status = this.computeWorkflowStatus(workflow);
|
|
153
|
-
workflow.finishedAt = new Date().toISOString();
|
|
154
|
-
this.activeWorkflowCount--;
|
|
155
|
-
this.drainQueue();
|
|
156
|
-
log.info("Workflow finished", {
|
|
157
|
-
workflowId,
|
|
158
|
-
name: workflow.name,
|
|
159
|
-
status: workflow.status,
|
|
160
|
-
conflicts: fileConflicts.length,
|
|
161
|
-
});
|
|
162
|
-
return this.buildResult(workflow);
|
|
163
|
-
}
|
|
164
|
-
/** Cancel a running or pending workflow. */
|
|
165
|
-
async cancel(workflowId) {
|
|
166
|
-
const workflow = this.requireWorkflow(workflowId);
|
|
167
|
-
// Remove from queue if pending
|
|
168
|
-
const queueIdx = this.workflowQueue.indexOf(workflowId);
|
|
169
|
-
if (queueIdx !== -1) {
|
|
170
|
-
this.workflowQueue.splice(queueIdx, 1);
|
|
171
|
-
}
|
|
172
|
-
// Cancel running tasks
|
|
173
|
-
for (const [, task] of workflow.tasks) {
|
|
174
|
-
if (task.status === "running" && task.instanceId) {
|
|
175
|
-
try {
|
|
176
|
-
await this.pool.terminate(task.instanceId, "Workflow cancelled");
|
|
177
|
-
}
|
|
178
|
-
catch {
|
|
179
|
-
// Best effort
|
|
180
|
-
}
|
|
181
|
-
task.status = "cancelled";
|
|
182
|
-
task.finishedAt = new Date().toISOString();
|
|
183
|
-
}
|
|
184
|
-
else if (task.status === "pending" || task.status === "queued") {
|
|
185
|
-
task.status = "cancelled";
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
const wasRunning = workflow.status === "running";
|
|
189
|
-
workflow.status = "cancelled";
|
|
190
|
-
workflow.finishedAt = new Date().toISOString();
|
|
191
|
-
if (wasRunning) {
|
|
192
|
-
this.activeWorkflowCount--;
|
|
193
|
-
this.drainQueue();
|
|
194
|
-
}
|
|
195
|
-
log.info("Workflow cancelled", { workflowId, name: workflow.name });
|
|
196
|
-
}
|
|
197
|
-
// -------------------------------------------------------------------------
|
|
198
|
-
// Execution modes
|
|
199
|
-
// -------------------------------------------------------------------------
|
|
200
|
-
/** Execute all tasks in parallel (respecting maxConcurrent). */
|
|
201
|
-
async executeParallel(workflow) {
|
|
202
|
-
const allTasks = [...workflow.tasks.values()];
|
|
203
|
-
const maxConcurrent = workflow.maxConcurrent || allTasks.length;
|
|
204
|
-
// Spawn in batches respecting concurrency limit
|
|
205
|
-
const pending = [...allTasks];
|
|
206
|
-
const running = new Map();
|
|
207
|
-
const spawnNext = async () => {
|
|
208
|
-
while (pending.length > 0 && running.size < maxConcurrent) {
|
|
209
|
-
const task = pending.shift();
|
|
210
|
-
const promise = this.spawnTask(workflow, task).then(() => {
|
|
211
|
-
running.delete(task.key);
|
|
212
|
-
});
|
|
213
|
-
running.set(task.key, promise);
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
|
-
await spawnNext();
|
|
217
|
-
// Wait for tasks to complete and spawn more as slots free up
|
|
218
|
-
while (running.size > 0) {
|
|
219
|
-
await Promise.race([...running.values()]);
|
|
220
|
-
await spawnNext();
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
/** Execute tasks one at a time in definition order. */
|
|
224
|
-
async executeSequential(workflow) {
|
|
225
|
-
for (const [, task] of workflow.tasks) {
|
|
226
|
-
if (workflow.status === "cancelled")
|
|
227
|
-
break;
|
|
228
|
-
await this.spawnTask(workflow, task);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
/** Execute tasks respecting dependency edges (topological order). */
|
|
232
|
-
async executeDependency(workflow) {
|
|
233
|
-
const maxConcurrent = workflow.maxConcurrent || workflow.tasks.size;
|
|
234
|
-
const completed = new Set();
|
|
235
|
-
const failed = new Set();
|
|
236
|
-
const running = new Map();
|
|
237
|
-
const isReady = (task) => {
|
|
238
|
-
if (task.status !== "pending")
|
|
239
|
-
return false;
|
|
240
|
-
return task.dependsOn.every((dep) => completed.has(dep));
|
|
241
|
-
};
|
|
242
|
-
const hasFailedDep = (task) => {
|
|
243
|
-
return task.dependsOn.some((dep) => failed.has(dep));
|
|
244
|
-
};
|
|
245
|
-
const scheduleReady = async () => {
|
|
246
|
-
for (const [, task] of workflow.tasks) {
|
|
247
|
-
if (running.size >= maxConcurrent)
|
|
248
|
-
break;
|
|
249
|
-
if (workflow.status === "cancelled")
|
|
250
|
-
break;
|
|
251
|
-
// Skip tasks whose deps have failed
|
|
252
|
-
if (hasFailedDep(task)) {
|
|
253
|
-
task.status = "skipped";
|
|
254
|
-
task.error = "Dependency failed";
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
if (isReady(task)) {
|
|
258
|
-
task.status = "queued";
|
|
259
|
-
const promise = this.spawnTask(workflow, task).then(() => {
|
|
260
|
-
running.delete(task.key);
|
|
261
|
-
if (task.status === "completed") {
|
|
262
|
-
completed.add(task.key);
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
failed.add(task.key);
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
running.set(task.key, promise);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
};
|
|
272
|
-
await scheduleReady();
|
|
273
|
-
while (running.size > 0) {
|
|
274
|
-
await Promise.race([...running.values()]);
|
|
275
|
-
await scheduleReady();
|
|
276
|
-
}
|
|
277
|
-
// Mark any remaining pending tasks as skipped
|
|
278
|
-
for (const [, task] of workflow.tasks) {
|
|
279
|
-
if (task.status === "pending") {
|
|
280
|
-
task.status = "skipped";
|
|
281
|
-
task.error = "Dependency not satisfied";
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
// -------------------------------------------------------------------------
|
|
286
|
-
// Task spawning
|
|
287
|
-
// -------------------------------------------------------------------------
|
|
288
|
-
/** Spawn a single agent for a workflow task and wait for completion. */
|
|
289
|
-
async spawnTask(workflow, task) {
|
|
290
|
-
task.status = "running";
|
|
291
|
-
task.startedAt = new Date().toISOString();
|
|
292
|
-
try {
|
|
293
|
-
const request = {
|
|
294
|
-
taskId: `orch_${workflow.id}_${task.key}`,
|
|
295
|
-
label: `[${workflow.name}] ${task.label}`,
|
|
296
|
-
prompt: task.prompt,
|
|
297
|
-
cwd: task.cwd,
|
|
298
|
-
origin: workflow.origin,
|
|
299
|
-
tags: [
|
|
300
|
-
`workflow:${workflow.id}`,
|
|
301
|
-
`task:${task.key}`,
|
|
302
|
-
...task.tags,
|
|
303
|
-
],
|
|
304
|
-
config: {
|
|
305
|
-
priority: task.priority,
|
|
306
|
-
...task.config,
|
|
307
|
-
},
|
|
308
|
-
};
|
|
309
|
-
const instance = await this.pool.spawn(request);
|
|
310
|
-
task.instanceId = instance.id;
|
|
311
|
-
task.taskId = instance.taskId;
|
|
312
|
-
// Wait for completion via bus events
|
|
313
|
-
await this.waitForCompletion(instance.id, task);
|
|
314
|
-
}
|
|
315
|
-
catch (err) {
|
|
316
|
-
task.status = "failed";
|
|
317
|
-
task.error = err instanceof Error ? err.message : String(err);
|
|
318
|
-
task.finishedAt = new Date().toISOString();
|
|
319
|
-
log.warn("Task spawn failed", {
|
|
320
|
-
workflowId: workflow.id,
|
|
321
|
-
taskKey: task.key,
|
|
322
|
-
error: task.error,
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
/** Wait for an agent instance to reach a terminal state. */
|
|
327
|
-
waitForCompletion(instanceId, task) {
|
|
328
|
-
return new Promise((resolve) => {
|
|
329
|
-
let interval;
|
|
330
|
-
let onLifecycle;
|
|
331
|
-
const cleanup = () => {
|
|
332
|
-
if (interval)
|
|
333
|
-
clearInterval(interval);
|
|
334
|
-
if (onLifecycle) {
|
|
335
|
-
this.pool.runtimeManager.bus.off("agent:lifecycle", onLifecycle);
|
|
336
|
-
}
|
|
337
|
-
this.eventHandlers.delete(`${instanceId}:cleanup`);
|
|
338
|
-
};
|
|
339
|
-
const checkAndResolve = () => {
|
|
340
|
-
const instance = this.pool.getInstance(instanceId);
|
|
341
|
-
if (!instance) {
|
|
342
|
-
task.status = "failed";
|
|
343
|
-
task.error = "Instance disappeared";
|
|
344
|
-
task.finishedAt = new Date().toISOString();
|
|
345
|
-
cleanup();
|
|
346
|
-
resolve();
|
|
347
|
-
return true;
|
|
348
|
-
}
|
|
349
|
-
const terminalStates = new Set(["completed", "failed", "terminated"]);
|
|
350
|
-
if (terminalStates.has(instance.state)) {
|
|
351
|
-
task.status = instance.state === "completed" ? "completed" : "failed";
|
|
352
|
-
task.finishedAt = new Date().toISOString();
|
|
353
|
-
if (instance.error) {
|
|
354
|
-
task.error = instance.error.message;
|
|
355
|
-
}
|
|
356
|
-
// Auto-record checkpoint data as output if available
|
|
357
|
-
if (instance.checkpointData && !task.output) {
|
|
358
|
-
task.output = instance.checkpointData;
|
|
359
|
-
task.filesTouched = parseFilesFromOutput(instance.checkpointData);
|
|
360
|
-
}
|
|
361
|
-
cleanup();
|
|
362
|
-
resolve();
|
|
363
|
-
return true;
|
|
364
|
-
}
|
|
365
|
-
return false;
|
|
366
|
-
};
|
|
367
|
-
// Check immediately
|
|
368
|
-
if (checkAndResolve())
|
|
369
|
-
return;
|
|
370
|
-
// Listen for lifecycle events
|
|
371
|
-
onLifecycle = (event) => {
|
|
372
|
-
if (event.agentId === instanceId) {
|
|
373
|
-
checkAndResolve();
|
|
374
|
-
}
|
|
375
|
-
};
|
|
376
|
-
this.pool.runtimeManager.bus.on("agent:lifecycle", onLifecycle);
|
|
377
|
-
// Safety poll in case events are missed
|
|
378
|
-
interval = setInterval(() => {
|
|
379
|
-
checkAndResolve();
|
|
380
|
-
}, 2000);
|
|
381
|
-
// Store cleanup for cancellation
|
|
382
|
-
this.eventHandlers.set(`${instanceId}:cleanup`, () => {
|
|
383
|
-
cleanup();
|
|
384
|
-
resolve();
|
|
385
|
-
});
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
// -------------------------------------------------------------------------
|
|
389
|
-
// Conflict detection and resolution
|
|
390
|
-
// -------------------------------------------------------------------------
|
|
391
|
-
/** Detect file conflicts — multiple tasks claiming to touch the same files. */
|
|
392
|
-
detectFileConflicts(workflow) {
|
|
393
|
-
const fileMap = new Map();
|
|
394
|
-
for (const [, task] of workflow.tasks) {
|
|
395
|
-
if (task.status !== "completed" || !task.filesTouched)
|
|
396
|
-
continue;
|
|
397
|
-
for (const file of task.filesTouched) {
|
|
398
|
-
if (!fileMap.has(file)) {
|
|
399
|
-
fileMap.set(file, []);
|
|
400
|
-
}
|
|
401
|
-
fileMap.get(file).push(task.key);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
const conflicts = [];
|
|
405
|
-
for (const [filePath, taskKeys] of fileMap) {
|
|
406
|
-
if (taskKeys.length > 1) {
|
|
407
|
-
conflicts.push({ filePath, taskKeys });
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
return conflicts;
|
|
411
|
-
}
|
|
412
|
-
/** Apply conflict resolution strategy. */
|
|
413
|
-
resolveConflicts(workflow, conflicts) {
|
|
414
|
-
for (const conflict of conflicts) {
|
|
415
|
-
switch (workflow.conflictStrategy) {
|
|
416
|
-
case "last-write-wins":
|
|
417
|
-
// Accept the last task's changes (by finish time)
|
|
418
|
-
conflict.resolution = "accepted";
|
|
419
|
-
conflict.resolvedBy = this.getLastFinished(workflow, conflict.taskKeys);
|
|
420
|
-
break;
|
|
421
|
-
case "first-write-wins":
|
|
422
|
-
// Accept the first task's changes
|
|
423
|
-
conflict.resolution = "accepted";
|
|
424
|
-
conflict.resolvedBy = this.getFirstFinished(workflow, conflict.taskKeys);
|
|
425
|
-
break;
|
|
426
|
-
case "fail":
|
|
427
|
-
// Mark as failed — requires human intervention
|
|
428
|
-
conflict.resolution = "rejected";
|
|
429
|
-
log.warn("Conflict resolution: fail strategy", {
|
|
430
|
-
file: conflict.filePath,
|
|
431
|
-
tasks: conflict.taskKeys,
|
|
432
|
-
});
|
|
433
|
-
break;
|
|
434
|
-
case "manual":
|
|
435
|
-
conflict.resolution = "deferred";
|
|
436
|
-
break;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
getLastFinished(workflow, taskKeys) {
|
|
441
|
-
let latest = taskKeys[0];
|
|
442
|
-
let latestTime = "";
|
|
443
|
-
for (const key of taskKeys) {
|
|
444
|
-
const task = workflow.tasks.get(key);
|
|
445
|
-
if (task?.finishedAt && task.finishedAt > latestTime) {
|
|
446
|
-
latestTime = task.finishedAt;
|
|
447
|
-
latest = key;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
return latest;
|
|
451
|
-
}
|
|
452
|
-
getFirstFinished(workflow, taskKeys) {
|
|
453
|
-
let earliest = taskKeys[0];
|
|
454
|
-
let earliestTime = new Date().toISOString();
|
|
455
|
-
for (const key of taskKeys) {
|
|
456
|
-
const task = workflow.tasks.get(key);
|
|
457
|
-
if (task?.finishedAt && task.finishedAt < earliestTime) {
|
|
458
|
-
earliestTime = task.finishedAt;
|
|
459
|
-
earliest = key;
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
return earliest;
|
|
463
|
-
}
|
|
464
|
-
// -------------------------------------------------------------------------
|
|
465
|
-
// Queue management
|
|
466
|
-
// -------------------------------------------------------------------------
|
|
467
|
-
/** Drain queued workflows when capacity frees up. */
|
|
468
|
-
drainQueue() {
|
|
469
|
-
while (this.workflowQueue.length > 0 &&
|
|
470
|
-
this.activeWorkflowCount < this.maxActiveWorkflows) {
|
|
471
|
-
const nextId = this.workflowQueue.shift();
|
|
472
|
-
const resolve = this.queueResolvers.get(nextId);
|
|
473
|
-
if (resolve) {
|
|
474
|
-
this.queueResolvers.delete(nextId);
|
|
475
|
-
resolve();
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
// -------------------------------------------------------------------------
|
|
480
|
-
// Status tracking and reporting
|
|
481
|
-
// -------------------------------------------------------------------------
|
|
482
|
-
/** Get a single workflow by ID. */
|
|
483
|
-
getWorkflow(workflowId) {
|
|
484
|
-
return this.workflows.get(workflowId);
|
|
485
|
-
}
|
|
486
|
-
/** Get the aggregated result for a completed workflow. */
|
|
487
|
-
getResult(workflowId) {
|
|
488
|
-
const workflow = this.requireWorkflow(workflowId);
|
|
489
|
-
return this.buildResult(workflow);
|
|
490
|
-
}
|
|
491
|
-
/** Get a monitoring report across all workflows. */
|
|
492
|
-
getReport() {
|
|
493
|
-
const workflows = [];
|
|
494
|
-
let activeCount = 0;
|
|
495
|
-
let pendingCount = 0;
|
|
496
|
-
let completedCount = 0;
|
|
497
|
-
for (const [, wf] of this.workflows) {
|
|
498
|
-
const progress = this.getTaskProgress(wf);
|
|
499
|
-
workflows.push({
|
|
500
|
-
id: wf.id,
|
|
501
|
-
name: wf.name,
|
|
502
|
-
status: wf.status,
|
|
503
|
-
progress,
|
|
504
|
-
});
|
|
505
|
-
if (wf.status === "running")
|
|
506
|
-
activeCount++;
|
|
507
|
-
else if (wf.status === "pending")
|
|
508
|
-
pendingCount++;
|
|
509
|
-
else
|
|
510
|
-
completedCount++;
|
|
511
|
-
}
|
|
512
|
-
return {
|
|
513
|
-
timestamp: new Date().toISOString(),
|
|
514
|
-
activeWorkflows: activeCount,
|
|
515
|
-
pendingWorkflows: pendingCount,
|
|
516
|
-
completedWorkflows: completedCount,
|
|
517
|
-
workflows,
|
|
518
|
-
queueDepth: this.workflowQueue.length,
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
/** Get file conflicts for a workflow. */
|
|
522
|
-
getConflicts(workflowId) {
|
|
523
|
-
return this.conflicts.get(workflowId) ?? [];
|
|
524
|
-
}
|
|
525
|
-
/** Record which files a task touched (called externally after parsing output). */
|
|
526
|
-
recordFilesTouched(workflowId, taskKey, files) {
|
|
527
|
-
const workflow = this.workflows.get(workflowId);
|
|
528
|
-
if (!workflow)
|
|
529
|
-
return;
|
|
530
|
-
const task = workflow.tasks.get(taskKey);
|
|
531
|
-
if (task) {
|
|
532
|
-
task.filesTouched = files;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
/** Record output for a task. */
|
|
536
|
-
recordTaskOutput(workflowId, taskKey, output) {
|
|
537
|
-
const workflow = this.workflows.get(workflowId);
|
|
538
|
-
if (!workflow)
|
|
539
|
-
return;
|
|
540
|
-
const task = workflow.tasks.get(taskKey);
|
|
541
|
-
if (task) {
|
|
542
|
-
task.output = output;
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
/** Remove a completed workflow from tracking. */
|
|
546
|
-
removeWorkflow(workflowId) {
|
|
547
|
-
const workflow = this.workflows.get(workflowId);
|
|
548
|
-
if (!workflow)
|
|
549
|
-
return false;
|
|
550
|
-
if (workflow.status === "running") {
|
|
551
|
-
throw new Error("Cannot remove a running workflow — cancel it first");
|
|
552
|
-
}
|
|
553
|
-
this.workflows.delete(workflowId);
|
|
554
|
-
this.conflicts.delete(workflowId);
|
|
555
|
-
return true;
|
|
556
|
-
}
|
|
557
|
-
// -------------------------------------------------------------------------
|
|
558
|
-
// Cleanup
|
|
559
|
-
// -------------------------------------------------------------------------
|
|
560
|
-
/** Cancel all workflows and clear state. */
|
|
561
|
-
async shutdown() {
|
|
562
|
-
// Cancel running workflows
|
|
563
|
-
for (const [id, wf] of this.workflows) {
|
|
564
|
-
if (wf.status === "running" || wf.status === "pending") {
|
|
565
|
-
await this.cancel(id).catch(() => { });
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
// Clean up event handlers
|
|
569
|
-
for (const [, cleanup] of this.eventHandlers) {
|
|
570
|
-
cleanup();
|
|
571
|
-
}
|
|
572
|
-
this.eventHandlers.clear();
|
|
573
|
-
// Resolve any queued workflows so their execute() calls don't hang
|
|
574
|
-
for (const [, resolve] of this.queueResolvers) {
|
|
575
|
-
resolve();
|
|
576
|
-
}
|
|
577
|
-
this.queueResolvers.clear();
|
|
578
|
-
this.workflowQueue.length = 0;
|
|
579
|
-
this.activeWorkflowCount = 0;
|
|
580
|
-
log.info("Orchestrator shut down");
|
|
581
|
-
}
|
|
582
|
-
// -------------------------------------------------------------------------
|
|
583
|
-
// Private helpers
|
|
584
|
-
// -------------------------------------------------------------------------
|
|
585
|
-
requireWorkflow(id) {
|
|
586
|
-
const workflow = this.workflows.get(id);
|
|
587
|
-
if (!workflow) {
|
|
588
|
-
throw new Error(`Workflow not found: ${id}`);
|
|
589
|
-
}
|
|
590
|
-
return workflow;
|
|
591
|
-
}
|
|
592
|
-
computeWorkflowStatus(workflow) {
|
|
593
|
-
const tasks = [...workflow.tasks.values()];
|
|
594
|
-
const allCompleted = tasks.every((t) => t.status === "completed");
|
|
595
|
-
const allFailed = tasks.every((t) => t.status === "failed");
|
|
596
|
-
const allCancelled = tasks.every((t) => t.status === "cancelled");
|
|
597
|
-
const anyFailed = tasks.some((t) => t.status === "failed");
|
|
598
|
-
const anyCompleted = tasks.some((t) => t.status === "completed");
|
|
599
|
-
if (allCompleted)
|
|
600
|
-
return "completed";
|
|
601
|
-
if (allFailed)
|
|
602
|
-
return "failed";
|
|
603
|
-
if (allCancelled)
|
|
604
|
-
return "cancelled";
|
|
605
|
-
if (anyFailed && anyCompleted)
|
|
606
|
-
return "partial";
|
|
607
|
-
if (anyFailed)
|
|
608
|
-
return "failed";
|
|
609
|
-
return "completed";
|
|
610
|
-
}
|
|
611
|
-
getTaskProgress(workflow) {
|
|
612
|
-
let completed = 0;
|
|
613
|
-
let running = 0;
|
|
614
|
-
let failed = 0;
|
|
615
|
-
let total = 0;
|
|
616
|
-
for (const [, task] of workflow.tasks) {
|
|
617
|
-
total++;
|
|
618
|
-
if (task.status === "completed")
|
|
619
|
-
completed++;
|
|
620
|
-
else if (task.status === "running")
|
|
621
|
-
running++;
|
|
622
|
-
else if (task.status === "failed")
|
|
623
|
-
failed++;
|
|
624
|
-
}
|
|
625
|
-
return { completed, total, running, failed };
|
|
626
|
-
}
|
|
627
|
-
buildResult(workflow) {
|
|
628
|
-
const tasks = [];
|
|
629
|
-
for (const [, task] of workflow.tasks) {
|
|
630
|
-
let durationMs;
|
|
631
|
-
if (task.startedAt && task.finishedAt) {
|
|
632
|
-
durationMs = new Date(task.finishedAt).getTime() - new Date(task.startedAt).getTime();
|
|
633
|
-
}
|
|
634
|
-
tasks.push({
|
|
635
|
-
key: task.key,
|
|
636
|
-
label: task.label,
|
|
637
|
-
status: task.status,
|
|
638
|
-
output: task.output,
|
|
639
|
-
error: task.error,
|
|
640
|
-
durationMs,
|
|
641
|
-
});
|
|
642
|
-
}
|
|
643
|
-
const summary = {
|
|
644
|
-
total: tasks.length,
|
|
645
|
-
completed: tasks.filter((t) => t.status === "completed").length,
|
|
646
|
-
failed: tasks.filter((t) => t.status === "failed").length,
|
|
647
|
-
skipped: tasks.filter((t) => t.status === "skipped").length,
|
|
648
|
-
cancelled: tasks.filter((t) => t.status === "cancelled").length,
|
|
649
|
-
durationMs: workflow.startedAt && workflow.finishedAt
|
|
650
|
-
? new Date(workflow.finishedAt).getTime() - new Date(workflow.startedAt).getTime()
|
|
651
|
-
: 0,
|
|
652
|
-
};
|
|
653
|
-
return {
|
|
654
|
-
workflowId: workflow.id,
|
|
655
|
-
workflowName: workflow.name,
|
|
656
|
-
status: workflow.status,
|
|
657
|
-
tasks,
|
|
658
|
-
conflicts: this.conflicts.get(workflow.id) ?? [],
|
|
659
|
-
summary,
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
/** Detect cycles in dependency graph via DFS. */
|
|
663
|
-
detectCycles(tasks) {
|
|
664
|
-
const visited = new Set();
|
|
665
|
-
const inStack = new Set();
|
|
666
|
-
const visit = (key, path) => {
|
|
667
|
-
if (inStack.has(key)) {
|
|
668
|
-
const cycle = [...path.slice(path.indexOf(key)), key];
|
|
669
|
-
throw new Error(`Dependency cycle detected: ${cycle.join(" → ")}`);
|
|
670
|
-
}
|
|
671
|
-
if (visited.has(key))
|
|
672
|
-
return;
|
|
673
|
-
visited.add(key);
|
|
674
|
-
inStack.add(key);
|
|
675
|
-
const task = tasks.get(key);
|
|
676
|
-
if (task) {
|
|
677
|
-
for (const dep of task.dependsOn) {
|
|
678
|
-
visit(dep, [...path, key]);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
inStack.delete(key);
|
|
682
|
-
};
|
|
683
|
-
for (const key of tasks.keys()) {
|
|
684
|
-
visit(key, []);
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
// ---------------------------------------------------------------------------
|
|
689
|
-
// Helpers
|
|
690
|
-
// ---------------------------------------------------------------------------
|
|
691
|
-
function generateWorkflowId() {
|
|
692
|
-
return `wf_${Date.now()}_${randomBytes(4).toString("hex")}`;
|
|
693
|
-
}
|
|
694
|
-
/**
|
|
695
|
-
* Parse file paths from agent output.
|
|
696
|
-
* Looks for common patterns: "Created X", "Modified X", "Wrote X", file paths in backticks.
|
|
697
|
-
*/
|
|
698
|
-
export function parseFilesFromOutput(output) {
|
|
699
|
-
const files = new Set();
|
|
700
|
-
const patterns = [
|
|
701
|
-
/(?:Created|Modified|Wrote|Updated|Edited|Deleted)\s+[`"]?([^\s`"]+\.\w+)[`"]?/gi,
|
|
702
|
-
/^\s*[-+]\s+(\S+\.\w+)/gm,
|
|
703
|
-
];
|
|
704
|
-
for (const pattern of patterns) {
|
|
705
|
-
let match;
|
|
706
|
-
while ((match = pattern.exec(output)) !== null) {
|
|
707
|
-
const file = match[1].trim();
|
|
708
|
-
if (file.length > 2 && file.length < 256) {
|
|
709
|
-
files.add(file);
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
return [...files];
|
|
714
|
-
}
|
|
715
|
-
//# sourceMappingURL=orchestration.js.map
|