aimeat 1.23.2 → 1.24.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/dist/locales/en.json +580 -21
- package/dist/locales/fi.json +592 -33
- package/dist/public/components/ContactCard.js +28 -0
- package/dist/public/css/views/admin.css +12 -0
- package/dist/public/css/views/agents-detail.css +62 -10
- package/dist/public/css/views/landing.css +87 -0
- package/dist/public/css/views/offers.css +96 -0
- package/dist/public/css/views/portfolio.css +18 -0
- package/dist/public/css/views/profile.css +153 -8
- package/dist/public/css/views/start.css +96 -0
- package/dist/public/css/views/workflows.css +111 -0
- package/dist/public/img/business-hero.png +0 -0
- package/dist/public/img/toimitus.png +0 -0
- package/dist/public/js/components/tag-cloud.js +46 -31
- package/dist/public/js/components/tag-editor.js +2 -2
- package/dist/public/js/recents.js +40 -0
- package/dist/public/js/services/admin.js +6 -0
- package/dist/public/js/services/agents.js +34 -0
- package/dist/public/js/services/offers.js +152 -0
- package/dist/public/js/services/packages.js +1 -0
- package/dist/public/js/services/workflows.js +57 -0
- package/dist/public/llms-template.txt +7 -0
- package/dist/public/spa.html +35 -3
- package/dist/public/views/admin/subdomains-tab.js +182 -0
- package/dist/public/views/admin.js +2 -0
- package/dist/public/views/business.js +119 -0
- package/dist/public/views/how-it-works.js +159 -0
- package/dist/public/views/landing.js +235 -0
- package/dist/public/views/portfolio.js +120 -23
- package/dist/public/views/pricing.js +75 -0
- package/dist/public/views/profile/access-tab.js +124 -51
- package/dist/public/views/profile/agents/agent-card.js +68 -37
- package/dist/public/views/profile/agents/shared-board.js +56 -22
- package/dist/public/views/profile/agents/tab-activity.js +24 -12
- package/dist/public/views/profile/agents/tab-agent-config.js +31 -1
- package/dist/public/views/profile/agents/tab-data-access.js +9 -13
- package/dist/public/views/profile/agents/tab-integration.js +27 -1
- package/dist/public/views/profile/agents/tab-messages.js +6 -11
- package/dist/public/views/profile/agents/tab-quality.js +45 -5
- package/dist/public/views/profile/agents-tab.js +41 -6
- package/dist/public/views/profile/apps-tab.js +7 -1
- package/dist/public/views/profile/knowledge-tab.js +4 -1
- package/dist/public/views/profile/landing-page.js +428 -123
- package/dist/public/views/profile/memory-tab.js +409 -211
- package/dist/public/views/profile/nodes-tab.js +18 -3
- package/dist/public/views/profile/offers-tab.js +310 -0
- package/dist/public/views/profile/organisms-tab.js +289 -28
- package/dist/public/views/profile/packages-tab.js +152 -16
- package/dist/public/views/profile/portfolio-tab.js +98 -31
- package/dist/public/views/profile/workflows-form.js +220 -0
- package/dist/public/views/profile/workflows-tab.js +244 -0
- package/dist/public/views/profile.js +4 -0
- package/dist/public/views/start-flows.js +281 -0
- package/dist/public/views/start.js +258 -0
- package/dist/src/cli/connect/mcp/tools/index.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/index.js +2 -0
- package/dist/src/cli/connect/mcp/tools/index.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/workflows.d.ts +12 -0
- package/dist/src/cli/connect/mcp/tools/workflows.d.ts.map +1 -0
- package/dist/src/cli/connect/mcp/tools/workflows.js +37 -0
- package/dist/src/cli/connect/mcp/tools/workflows.js.map +1 -0
- package/dist/src/cli/connect/tool-call.d.ts.map +1 -1
- package/dist/src/cli/connect/tool-call.js +37 -0
- package/dist/src/cli/connect/tool-call.js.map +1 -1
- package/dist/src/generated/api-types.d.ts +1474 -66
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/generated/prisma-postgres/edge.js +15 -3
- package/dist/src/generated/prisma-postgres/index-browser.js +12 -0
- package/dist/src/generated/prisma-postgres/index.d.ts +1476 -160
- package/dist/src/generated/prisma-postgres/index.js +15 -3
- package/dist/src/generated/prisma-postgres/package.json +1 -1
- package/dist/src/generated/prisma-postgres/schema.prisma +11 -0
- package/dist/src/generated/prisma-postgres/wasm.js +15 -3
- package/dist/src/mcp/annotations.d.ts.map +1 -1
- package/dist/src/mcp/annotations.js +3 -0
- package/dist/src/mcp/annotations.js.map +1 -1
- package/dist/src/mcp/catalog/definitions.d.ts.map +1 -1
- package/dist/src/mcp/catalog/definitions.js +27 -0
- package/dist/src/mcp/catalog/definitions.js.map +1 -1
- package/dist/src/mcp/catalog/scopes.d.ts.map +1 -1
- package/dist/src/mcp/catalog/scopes.js +5 -1
- package/dist/src/mcp/catalog/scopes.js.map +1 -1
- package/dist/src/mcp/catalog/surfaces.d.ts.map +1 -1
- package/dist/src/mcp/catalog/surfaces.js +1 -0
- package/dist/src/mcp/catalog/surfaces.js.map +1 -1
- package/dist/src/mcp/index.d.ts.map +1 -1
- package/dist/src/mcp/index.js +2 -0
- package/dist/src/mcp/index.js.map +1 -1
- package/dist/src/mcp/workflows.d.ts +18 -0
- package/dist/src/mcp/workflows.d.ts.map +1 -0
- package/dist/src/mcp/workflows.js +68 -0
- package/dist/src/mcp/workflows.js.map +1 -0
- package/dist/src/middleware/subdomain.d.ts +28 -0
- package/dist/src/middleware/subdomain.d.ts.map +1 -0
- package/dist/src/middleware/subdomain.js +32 -0
- package/dist/src/middleware/subdomain.js.map +1 -0
- package/dist/src/models/offer-schemas.d.ts +236 -0
- package/dist/src/models/offer-schemas.d.ts.map +1 -0
- package/dist/src/models/offer-schemas.js +112 -0
- package/dist/src/models/offer-schemas.js.map +1 -0
- package/dist/src/models/workflow-schemas.d.ts +220 -0
- package/dist/src/models/workflow-schemas.d.ts.map +1 -0
- package/dist/src/models/workflow-schemas.js +102 -0
- package/dist/src/models/workflow-schemas.js.map +1 -0
- package/dist/src/routes/agent-tasks.d.ts.map +1 -1
- package/dist/src/routes/agent-tasks.js +49 -0
- package/dist/src/routes/agent-tasks.js.map +1 -1
- package/dist/src/routes/agents.d.ts.map +1 -1
- package/dist/src/routes/agents.js +146 -1
- package/dist/src/routes/agents.js.map +1 -1
- package/dist/src/routes/apps-backup.d.ts +20 -0
- package/dist/src/routes/apps-backup.d.ts.map +1 -0
- package/dist/src/routes/apps-backup.js +165 -0
- package/dist/src/routes/apps-backup.js.map +1 -0
- package/dist/src/routes/ghii.d.ts.map +1 -1
- package/dist/src/routes/ghii.js +4 -0
- package/dist/src/routes/ghii.js.map +1 -1
- package/dist/src/routes/instances.d.ts +2 -1
- package/dist/src/routes/instances.d.ts.map +1 -1
- package/dist/src/routes/instances.js +59 -1
- package/dist/src/routes/instances.js.map +1 -1
- package/dist/src/routes/memory.d.ts.map +1 -1
- package/dist/src/routes/memory.js +5 -0
- package/dist/src/routes/memory.js.map +1 -1
- package/dist/src/routes/packages.d.ts.map +1 -1
- package/dist/src/routes/packages.js +32 -0
- package/dist/src/routes/packages.js.map +1 -1
- package/dist/src/routes/permissions.d.ts +12 -0
- package/dist/src/routes/permissions.d.ts.map +1 -1
- package/dist/src/routes/permissions.js +35 -3
- package/dist/src/routes/permissions.js.map +1 -1
- package/dist/src/routes/portal.d.ts +3 -0
- package/dist/src/routes/portal.d.ts.map +1 -1
- package/dist/src/routes/portal.js +12 -0
- package/dist/src/routes/portal.js.map +1 -1
- package/dist/src/routes/public-stats.d.ts +15 -0
- package/dist/src/routes/public-stats.d.ts.map +1 -0
- package/dist/src/routes/public-stats.js +90 -0
- package/dist/src/routes/public-stats.js.map +1 -0
- package/dist/src/routes/sse.d.ts +7 -2
- package/dist/src/routes/sse.d.ts.map +1 -1
- package/dist/src/routes/sse.js +35 -5
- package/dist/src/routes/sse.js.map +1 -1
- package/dist/src/routes/stats.d.ts.map +1 -1
- package/dist/src/routes/stats.js +13 -0
- package/dist/src/routes/stats.js.map +1 -1
- package/dist/src/routes/subdomains.d.ts +33 -0
- package/dist/src/routes/subdomains.d.ts.map +1 -0
- package/dist/src/routes/subdomains.js +208 -0
- package/dist/src/routes/subdomains.js.map +1 -0
- package/dist/src/routes/workflows.d.ts +30 -0
- package/dist/src/routes/workflows.d.ts.map +1 -0
- package/dist/src/routes/workflows.js +178 -0
- package/dist/src/routes/workflows.js.map +1 -0
- package/dist/src/server-bootstrap/routes-loader.d.ts +2 -0
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +19 -2
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/server-bootstrap/service-init.d.ts +2 -0
- package/dist/src/server-bootstrap/service-init.d.ts.map +1 -1
- package/dist/src/server-bootstrap/service-init.js +6 -0
- package/dist/src/server-bootstrap/service-init.js.map +1 -1
- package/dist/src/server.d.ts.map +1 -1
- package/dist/src/server.js +5 -0
- package/dist/src/server.js.map +1 -1
- package/dist/src/services/apps-backup-export.d.ts +22 -0
- package/dist/src/services/apps-backup-export.d.ts.map +1 -0
- package/dist/src/services/apps-backup-export.js +136 -0
- package/dist/src/services/apps-backup-export.js.map +1 -0
- package/dist/src/services/apps-backup-import.d.ts +120 -0
- package/dist/src/services/apps-backup-import.d.ts.map +1 -0
- package/dist/src/services/apps-backup-import.js +259 -0
- package/dist/src/services/apps-backup-import.js.map +1 -0
- package/dist/src/services/component-registrar.d.ts +17 -0
- package/dist/src/services/component-registrar.d.ts.map +1 -1
- package/dist/src/services/component-registrar.js +63 -5
- package/dist/src/services/component-registrar.js.map +1 -1
- package/dist/src/services/handbooks/agent.d.ts +1 -1
- package/dist/src/services/handbooks/agent.d.ts.map +1 -1
- package/dist/src/services/handbooks/agent.js +10 -0
- package/dist/src/services/handbooks/agent.js.map +1 -1
- package/dist/src/services/handbooks/appdev.d.ts +1 -1
- package/dist/src/services/handbooks/appdev.d.ts.map +1 -1
- package/dist/src/services/handbooks/appdev.js +5 -2
- package/dist/src/services/handbooks/appdev.js.map +1 -1
- package/dist/src/services/scheduler.d.ts +6 -0
- package/dist/src/services/scheduler.d.ts.map +1 -1
- package/dist/src/services/scheduler.js +22 -0
- package/dist/src/services/scheduler.js.map +1 -1
- package/dist/src/services/schema-validator.d.ts +9 -0
- package/dist/src/services/schema-validator.d.ts.map +1 -1
- package/dist/src/services/schema-validator.js +16 -0
- package/dist/src/services/schema-validator.js.map +1 -1
- package/dist/src/services/workflow/engine.d.ts +84 -0
- package/dist/src/services/workflow/engine.d.ts.map +1 -0
- package/dist/src/services/workflow/engine.js +545 -0
- package/dist/src/services/workflow/engine.js.map +1 -0
- package/dist/src/services/workflow/eval-context.d.ts +23 -0
- package/dist/src/services/workflow/eval-context.d.ts.map +1 -0
- package/dist/src/services/workflow/eval-context.js +52 -0
- package/dist/src/services/workflow/eval-context.js.map +1 -0
- package/dist/src/services/workflow/lifecycle.d.ts +38 -0
- package/dist/src/services/workflow/lifecycle.d.ts.map +1 -0
- package/dist/src/services/workflow/lifecycle.js +99 -0
- package/dist/src/services/workflow/lifecycle.js.map +1 -0
- package/dist/src/services/workflow/signal-eval.d.ts +70 -0
- package/dist/src/services/workflow/signal-eval.d.ts.map +1 -0
- package/dist/src/services/workflow/signal-eval.js +174 -0
- package/dist/src/services/workflow/signal-eval.js.map +1 -0
- package/dist/src/services/workflow/store.d.ts +89 -0
- package/dist/src/services/workflow/store.d.ts.map +1 -0
- package/dist/src/services/workflow/store.js +284 -0
- package/dist/src/services/workflow/store.js.map +1 -0
- package/dist/src/storage/interface.d.ts +13 -3
- package/dist/src/storage/interface.d.ts.map +1 -1
- package/dist/src/storage/interface.js.map +1 -1
- package/dist/src/storage/providers/mongodb/index.d.ts +10 -1
- package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.js +76 -0
- package/dist/src/storage/providers/mongodb/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/index.d.ts +8 -1
- package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/index.js +61 -0
- package/dist/src/storage/providers/sqlite/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.js +11 -0
- package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
- package/dist/src/storage/repositories/index.d.ts +1 -0
- package/dist/src/storage/repositories/index.d.ts.map +1 -1
- package/dist/src/storage/repositories/package.repository.d.ts +2 -0
- package/dist/src/storage/repositories/package.repository.d.ts.map +1 -1
- package/dist/src/storage/repositories/subdomain-site.repository.d.ts +19 -0
- package/dist/src/storage/repositories/subdomain-site.repository.d.ts.map +1 -0
- package/dist/src/storage/repositories/subdomain-site.repository.js +2 -0
- package/dist/src/storage/repositories/subdomain-site.repository.js.map +1 -0
- package/dist/static/app-catalog.html +1139 -28
- package/package.json +1 -1
- package/prisma/schema.postgres.prisma +11 -0
- package/prisma/schema.prisma +11 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { AimeatConfig } from '../../config.js';
|
|
2
|
+
import type { Storage, AgentTaskRecord } from '../../storage/interface.js';
|
|
3
|
+
import type { createWebhookDispatcher } from '../webhook-dispatcher.js';
|
|
4
|
+
import type { PushService } from '../push.js';
|
|
5
|
+
import type { WorkflowDef, WorkflowRunStep, WorkflowStep } from '../../models/workflow-schemas.js';
|
|
6
|
+
type WebhookDispatcher = ReturnType<typeof createWebhookDispatcher>;
|
|
7
|
+
export declare function setActiveWorkflowEngine(e: WorkflowEngine): void;
|
|
8
|
+
export declare function getActiveWorkflowEngine(): WorkflowEngine | null;
|
|
9
|
+
/**
|
|
10
|
+
* The steps that may start now: pending, all `after` deps green, and past any retry backoff.
|
|
11
|
+
* Pure — operates on the def + the current run-step map.
|
|
12
|
+
*/
|
|
13
|
+
export declare function computeReadySteps(def: WorkflowDef, steps: Record<string, WorkflowRunStep>, now: string): WorkflowStep[];
|
|
14
|
+
/** The terminal run status given the step states: done if all green, else partial; running otherwise. */
|
|
15
|
+
export declare function runOutcome(steps: Record<string, WorkflowRunStep>): 'done' | 'partial' | 'running';
|
|
16
|
+
export interface StartRunOpts {
|
|
17
|
+
mode: 'signals-only' | 'full-live' | 'full-sandbox';
|
|
18
|
+
vars?: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
export declare class WorkflowEngine {
|
|
21
|
+
private config;
|
|
22
|
+
private storage;
|
|
23
|
+
private webhookDispatcher?;
|
|
24
|
+
private pushService?;
|
|
25
|
+
/** Per-run serialization: advancing the same run from two task completions is a RMW race. */
|
|
26
|
+
private locks;
|
|
27
|
+
private sweepTimer?;
|
|
28
|
+
constructor(config: AimeatConfig, storage: Storage);
|
|
29
|
+
setWebhookDispatcher(d: WebhookDispatcher): void;
|
|
30
|
+
setPushService(p: PushService): void;
|
|
31
|
+
/** Start the watchdog (timeouts + due retries). Idempotent; the timer is unref'd. */
|
|
32
|
+
start(): Promise<void>;
|
|
33
|
+
stop(): void;
|
|
34
|
+
/** Run a single run's mutation under its lock (serializes concurrent advances). */
|
|
35
|
+
private withLock;
|
|
36
|
+
startRun(ownerGhii: string, ownerName: string, workflowId: string, opts: StartRunOpts): Promise<{
|
|
37
|
+
runId: string;
|
|
38
|
+
} | {
|
|
39
|
+
error: string[];
|
|
40
|
+
}>;
|
|
41
|
+
/** The pinned per-step resolved signals (from start time) as a lookup. No mid-run re-resolution. */
|
|
42
|
+
private resolvedMap;
|
|
43
|
+
/** signals-only: evaluate every step's input + output against existing memory; no dispatch. */
|
|
44
|
+
private runSignalsOnly;
|
|
45
|
+
private tick;
|
|
46
|
+
onTaskTerminal(task: AgentTaskRecord, outcome: 'done' | 'failed'): Promise<void>;
|
|
47
|
+
sweep(): Promise<void>;
|
|
48
|
+
private sweepRun;
|
|
49
|
+
onMemoryWrite(ownerGhii: string, key: string): Promise<void>;
|
|
50
|
+
onOfferOrdered(ownerGhii: string, offerId: string): Promise<void>;
|
|
51
|
+
private fireEventTriggers;
|
|
52
|
+
/**
|
|
53
|
+
* On boot, re-sync in-flight runs: a step's task may have reached done/failed during the restart
|
|
54
|
+
* gap (after the HTTP response, before the fire-and-forget onTaskTerminal ran), leaving the step
|
|
55
|
+
* stuck `dispatched`. Re-check each dispatched step's tasks and advance any that already finished —
|
|
56
|
+
* otherwise the watchdog would wrongly TIME OUT a step whose task actually succeeded. Remaining
|
|
57
|
+
* in-flight runs are then carried by live task events + the watchdog.
|
|
58
|
+
*/
|
|
59
|
+
resumeInflight(): Promise<void>;
|
|
60
|
+
private resolveVars;
|
|
61
|
+
private template;
|
|
62
|
+
/** Evaluate a signal (or 'none'/undefined → pass), accumulating the keys it reads. */
|
|
63
|
+
private evalSignal;
|
|
64
|
+
/** Mark every step that (transitively) depends on a failed step as skipped (partial-fail policy). */
|
|
65
|
+
private skipSubtree;
|
|
66
|
+
/** Dispatch a step's agent task(s); tag with the workflow-run scope for onTaskTerminal. */
|
|
67
|
+
private dispatchStep;
|
|
68
|
+
/**
|
|
69
|
+
* On a RED step: GUARANTEE the owner sees it (push — node-owned, never silent), then best-effort
|
|
70
|
+
* dispatch the crew `workflow-inspector` agent for diagnosis/repair. The push is the contract; the
|
|
71
|
+
* inspector is enrichment, so a missing/offline inspector never hides the failure.
|
|
72
|
+
*/
|
|
73
|
+
private onStepFail;
|
|
74
|
+
/**
|
|
75
|
+
* Queue a task to the owner's `workflow-inspector` agent (crew-owned) with full run context: the
|
|
76
|
+
* run record (defSnapshot + every step's state + expected-vs-observed) is at a known memory key.
|
|
77
|
+
* Tagged `workflow-inspect` (NOT `workflow-run`) so completing it never advances the run. Returns
|
|
78
|
+
* the task id, or null when no inspector agent is installed.
|
|
79
|
+
*/
|
|
80
|
+
private dispatchInspector;
|
|
81
|
+
private persist;
|
|
82
|
+
}
|
|
83
|
+
export {};
|
|
84
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../../src/services/workflow/engine.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file engine.ts
|
|
3
|
+
* @description The Agent Workflows engine — the deterministic, node-owned run loop. A workflow run
|
|
4
|
+
* is an ASYNC persisted state machine (NOT a synchronous loop): the node dispatches an agent_task
|
|
5
|
+
* and parks; the run advances when that task reaches a terminal state (the agent-tasks route calls
|
|
6
|
+
* onTaskTerminal) or when a step times out (the watchdog sweep). State + recovery live in the run
|
|
7
|
+
* record in owner memory, so a restart resumes in-flight runs. Per-step input/output signals are
|
|
8
|
+
* evaluated deterministically (signal-eval.ts); the `llm` leaf uses the node OpenRouter only when
|
|
9
|
+
* the workflow's owner has approved it AND a key is configured. Run-fail policy = PARTIAL: a RED
|
|
10
|
+
* step fails its dependent subtree, independent branches finish, the run ends `partial`. See
|
|
11
|
+
* docs/plans/2026-06-13-agent-workflows-node-plan.md §4.
|
|
12
|
+
* @structure
|
|
13
|
+
* - WorkflowEngine — startRun / onTaskTerminal / sweep / resumeInflight / start / stop
|
|
14
|
+
* - setActiveWorkflowEngine / getActiveWorkflowEngine — process-wide handle (so the task route +
|
|
15
|
+
* scheduler reach the live engine without threading it through every signature)
|
|
16
|
+
* - computeReadySteps / runOutcome — pure decision helpers (exported for tests)
|
|
17
|
+
* @usage
|
|
18
|
+
* const engine = new WorkflowEngine(config, storage);
|
|
19
|
+
* setActiveWorkflowEngine(engine); engine.setWebhookDispatcher(d); await engine.start();
|
|
20
|
+
* @version-history
|
|
21
|
+
* v1.0.0 — 2026-06-13 — Phase 4: state machine (signals-only + full-live), dispatch, task-terminal
|
|
22
|
+
* advance, notBefore-backoff retry + timeout via the watchdog sweep, restart recovery.
|
|
23
|
+
*/
|
|
24
|
+
import { randomUUID } from 'node:crypto';
|
|
25
|
+
import { buildGAII } from '../../utils/gaii.js';
|
|
26
|
+
import { emitChange } from '../event-bus.js';
|
|
27
|
+
import { logger } from '../../utils/logger.js';
|
|
28
|
+
import { evaluateSignal, globToRegExp } from './signal-eval.js';
|
|
29
|
+
import { buildEvalCtx } from './eval-context.js';
|
|
30
|
+
import { getWorkflow, validateWorkflow, runKey } from './store.js';
|
|
31
|
+
import { readEventTriggers, readActiveRuns, reconcileActiveRun } from './lifecycle.js';
|
|
32
|
+
let _active = null;
|
|
33
|
+
export function setActiveWorkflowEngine(e) { _active = e; }
|
|
34
|
+
export function getActiveWorkflowEngine() { return _active; }
|
|
35
|
+
/** Pick a display string from a localized value (prefers en_US, else fi_FI, else first). */
|
|
36
|
+
function loc(s) {
|
|
37
|
+
if (!s)
|
|
38
|
+
return '';
|
|
39
|
+
if (typeof s === 'string')
|
|
40
|
+
return s;
|
|
41
|
+
return s.en_US ?? s.fi_FI ?? Object.values(s)[0] ?? '';
|
|
42
|
+
}
|
|
43
|
+
const TERMINAL_STEP_STATES = new Set(['green', 'input-red', 'output-red', 'timed-out', 'skipped']);
|
|
44
|
+
/**
|
|
45
|
+
* The steps that may start now: pending, all `after` deps green, and past any retry backoff.
|
|
46
|
+
* Pure — operates on the def + the current run-step map.
|
|
47
|
+
*/
|
|
48
|
+
export function computeReadySteps(def, steps, now) {
|
|
49
|
+
return def.steps.filter(s => {
|
|
50
|
+
const rs = steps[s.id];
|
|
51
|
+
if (!rs || rs.state !== 'pending')
|
|
52
|
+
return false;
|
|
53
|
+
if (rs.notBefore && rs.notBefore > now)
|
|
54
|
+
return false;
|
|
55
|
+
return (s.after ?? []).every(dep => steps[dep]?.state === 'green');
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/** The terminal run status given the step states: done if all green, else partial; running otherwise. */
|
|
59
|
+
export function runOutcome(steps) {
|
|
60
|
+
const all = Object.values(steps);
|
|
61
|
+
if (all.some(s => !TERMINAL_STEP_STATES.has(s.state)))
|
|
62
|
+
return 'running';
|
|
63
|
+
return all.every(s => s.state === 'green') ? 'done' : 'partial';
|
|
64
|
+
}
|
|
65
|
+
export class WorkflowEngine {
|
|
66
|
+
config;
|
|
67
|
+
storage;
|
|
68
|
+
webhookDispatcher;
|
|
69
|
+
pushService;
|
|
70
|
+
/** Per-run serialization: advancing the same run from two task completions is a RMW race. */
|
|
71
|
+
locks = new Map();
|
|
72
|
+
sweepTimer;
|
|
73
|
+
constructor(config, storage) {
|
|
74
|
+
this.config = config;
|
|
75
|
+
this.storage = storage;
|
|
76
|
+
}
|
|
77
|
+
setWebhookDispatcher(d) { this.webhookDispatcher = d; }
|
|
78
|
+
setPushService(p) { this.pushService = p; }
|
|
79
|
+
/** Start the watchdog (timeouts + due retries). Idempotent; the timer is unref'd. */
|
|
80
|
+
async start() {
|
|
81
|
+
await this.resumeInflight();
|
|
82
|
+
if (!this.sweepTimer) {
|
|
83
|
+
this.sweepTimer = setInterval(() => { this.sweep().catch(err => logger.error('workflow sweep failed', { error: String(err) })); }, 60_000);
|
|
84
|
+
this.sweepTimer.unref?.();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
stop() { if (this.sweepTimer) {
|
|
88
|
+
clearInterval(this.sweepTimer);
|
|
89
|
+
this.sweepTimer = undefined;
|
|
90
|
+
} }
|
|
91
|
+
/** Run a single run's mutation under its lock (serializes concurrent advances). */
|
|
92
|
+
async withLock(runId, fn) {
|
|
93
|
+
const prev = this.locks.get(runId) ?? Promise.resolve();
|
|
94
|
+
let release;
|
|
95
|
+
const gate = new Promise(r => { release = r; });
|
|
96
|
+
this.locks.set(runId, prev.then(() => gate));
|
|
97
|
+
await prev;
|
|
98
|
+
try {
|
|
99
|
+
return await fn();
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
release();
|
|
103
|
+
if (this.locks.get(runId) === prev.then(() => gate))
|
|
104
|
+
this.locks.delete(runId);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// ── start a run ──────────────────────────────────────────────────────────────
|
|
108
|
+
async startRun(ownerGhii, ownerName, workflowId, opts) {
|
|
109
|
+
const def = await getWorkflow(this.storage, ownerGhii, workflowId);
|
|
110
|
+
if (!def)
|
|
111
|
+
return { error: [`workflow "${workflowId}" not found`] };
|
|
112
|
+
const v = await validateWorkflow(this.storage, this.config, ownerName, def);
|
|
113
|
+
if (!v.ok || !v.resolved)
|
|
114
|
+
return { error: v.errors };
|
|
115
|
+
// Overlap guard: a full run that dispatches agents must not run twice at once (the steps share
|
|
116
|
+
// templated keys). If one is already in flight for this workflow, return it instead of starting a
|
|
117
|
+
// second. signals-only is read-only, so it always runs fresh.
|
|
118
|
+
if (opts.mode === 'full-live') {
|
|
119
|
+
const active = await readActiveRuns(this.storage, this.config.nodeId);
|
|
120
|
+
const inFlight = active.find(a => a.workflowId === workflowId && a.ownerGhii === ownerGhii);
|
|
121
|
+
if (inFlight) {
|
|
122
|
+
logger.info(`workflow "${workflowId}": a run is already in flight (${inFlight.runId}); skipping this start`);
|
|
123
|
+
return { runId: inFlight.runId };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const runId = randomUUID();
|
|
127
|
+
const vars = this.resolveVars(def, opts.vars);
|
|
128
|
+
const now = new Date().toISOString();
|
|
129
|
+
const steps = {};
|
|
130
|
+
for (const s of def.steps)
|
|
131
|
+
steps[s.id] = { state: 'pending', attempt: 0, reads: [], writes: [] };
|
|
132
|
+
const run = {
|
|
133
|
+
runId, workflowId, defSnapshot: def, resolved: v.resolved, vars,
|
|
134
|
+
mode: opts.mode, keyPrefix: opts.mode === 'full-sandbox' ? `wf-test.${runId}.` : '',
|
|
135
|
+
status: 'running', steps, startedAt: now,
|
|
136
|
+
};
|
|
137
|
+
if (opts.mode === 'signals-only') {
|
|
138
|
+
await this.runSignalsOnly(ownerGhii, run);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
await this.persist(ownerGhii, run);
|
|
142
|
+
await this.tick(ownerGhii, run);
|
|
143
|
+
}
|
|
144
|
+
return { runId };
|
|
145
|
+
}
|
|
146
|
+
/** The pinned per-step resolved signals (from start time) as a lookup. No mid-run re-resolution. */
|
|
147
|
+
resolvedMap(run) {
|
|
148
|
+
return new Map((run.resolved ?? []).map(r => [r.stepId, r]));
|
|
149
|
+
}
|
|
150
|
+
/** signals-only: evaluate every step's input + output against existing memory; no dispatch. */
|
|
151
|
+
async runSignalsOnly(ownerGhii, run) {
|
|
152
|
+
const resolved = this.resolvedMap(run);
|
|
153
|
+
const ctx = buildEvalCtx(this.storage, this.config, ownerGhii, run);
|
|
154
|
+
for (const step of run.defSnapshot.steps) {
|
|
155
|
+
const r = resolved.get(step.id);
|
|
156
|
+
const rs = run.steps[step.id];
|
|
157
|
+
const reads = new Set();
|
|
158
|
+
// input
|
|
159
|
+
const input = await this.evalSignal(r?.required_to_function, ctx, reads);
|
|
160
|
+
if (!input.ok) {
|
|
161
|
+
rs.state = 'input-red';
|
|
162
|
+
rs.inputObserved = input.observed;
|
|
163
|
+
rs.reads = [...reads];
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
// output
|
|
167
|
+
const output = await this.evalSignal(r?.success_signal, ctx, reads);
|
|
168
|
+
rs.outputObserved = output.observed;
|
|
169
|
+
rs.reads = [...reads];
|
|
170
|
+
rs.state = output.ok ? 'green' : 'output-red';
|
|
171
|
+
}
|
|
172
|
+
run.status = runOutcome(run.steps);
|
|
173
|
+
run.endedAt = new Date().toISOString();
|
|
174
|
+
await this.persist(ownerGhii, run);
|
|
175
|
+
emitChange('workflows');
|
|
176
|
+
}
|
|
177
|
+
// ── the advance loop (full-live) ──────────────────────────────────────────────
|
|
178
|
+
async tick(ownerGhii, run) {
|
|
179
|
+
const resolved = this.resolvedMap(run);
|
|
180
|
+
const now = new Date().toISOString();
|
|
181
|
+
const ctx = buildEvalCtx(this.storage, this.config, ownerGhii, run);
|
|
182
|
+
let dispatchedAny = false;
|
|
183
|
+
for (const step of computeReadySteps(run.defSnapshot, run.steps, now)) {
|
|
184
|
+
const r = resolved.get(step.id);
|
|
185
|
+
const rs = run.steps[step.id];
|
|
186
|
+
const reads = new Set();
|
|
187
|
+
const input = await this.evalSignal(r?.required_to_function, ctx, reads);
|
|
188
|
+
rs.reads = [...reads];
|
|
189
|
+
if (!input.ok) {
|
|
190
|
+
rs.state = 'input-red';
|
|
191
|
+
rs.inputObserved = input.observed;
|
|
192
|
+
rs.endedAt = now;
|
|
193
|
+
this.skipSubtree(run, step.id);
|
|
194
|
+
await this.onStepFail(ownerGhii, run, step.id, 'input-red');
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
// dispatch
|
|
198
|
+
const taskIds = await this.dispatchStep(ownerGhii, run, step, r);
|
|
199
|
+
rs.state = 'dispatched';
|
|
200
|
+
rs.taskIds = taskIds;
|
|
201
|
+
rs.startedAt = now;
|
|
202
|
+
rs.notBefore = undefined;
|
|
203
|
+
dispatchedAny = true;
|
|
204
|
+
}
|
|
205
|
+
const outcome = runOutcome(run.steps);
|
|
206
|
+
if (outcome !== 'running') {
|
|
207
|
+
run.status = outcome;
|
|
208
|
+
run.endedAt = new Date().toISOString();
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
run.status = Object.values(run.steps).some(s => s.state === 'dispatched') ? 'waiting-step' : 'running';
|
|
212
|
+
}
|
|
213
|
+
await this.persist(ownerGhii, run);
|
|
214
|
+
if (dispatchedAny || outcome !== 'running')
|
|
215
|
+
emitChange('workflows');
|
|
216
|
+
}
|
|
217
|
+
// ── task terminal → advance ────────────────────────────────────────────────────
|
|
218
|
+
async onTaskTerminal(task, outcome) {
|
|
219
|
+
const scope = task.scope?.find(s => s.name === 'workflow-run');
|
|
220
|
+
if (!scope?.value)
|
|
221
|
+
return;
|
|
222
|
+
const [workflowId, runId] = scope.value.split('/');
|
|
223
|
+
const stepId = scope.description;
|
|
224
|
+
if (!workflowId || !runId || !stepId)
|
|
225
|
+
return;
|
|
226
|
+
const ownerGhii = task.ownerGaii;
|
|
227
|
+
await this.withLock(runId, async () => {
|
|
228
|
+
const rec = await this.storage.getMemory(ownerGhii, runKey(workflowId, runId));
|
|
229
|
+
if (!rec)
|
|
230
|
+
return;
|
|
231
|
+
const run = rec.value;
|
|
232
|
+
const rs = run.steps[stepId];
|
|
233
|
+
if (!rs || rs.state !== 'dispatched')
|
|
234
|
+
return; // already resolved / not awaiting
|
|
235
|
+
// Wait until every task of this step is terminal (a step may fan out to several agents).
|
|
236
|
+
for (const tid of rs.taskIds ?? []) {
|
|
237
|
+
const t = await this.storage.getAgentTask(tid);
|
|
238
|
+
if (t && t.status !== 'done' && t.status !== 'failed')
|
|
239
|
+
return; // still in flight
|
|
240
|
+
}
|
|
241
|
+
// Use the signals PINNED at start time (not current offers) — a mid-run offer edit/delete must
|
|
242
|
+
// not silently turn this output check into a false pass.
|
|
243
|
+
const r = this.resolvedMap(run).get(stepId);
|
|
244
|
+
const ctx = buildEvalCtx(this.storage, this.config, ownerGhii, run);
|
|
245
|
+
const reads = new Set(rs.reads);
|
|
246
|
+
const output = await this.evalSignal(r?.success_signal, ctx, reads);
|
|
247
|
+
const now = new Date().toISOString();
|
|
248
|
+
rs.outputObserved = output.observed;
|
|
249
|
+
rs.reads = [...reads];
|
|
250
|
+
if (r?.deliverableKey)
|
|
251
|
+
rs.writes = [...new Set([...rs.writes, this.template(r.deliverableKey, run.vars)])];
|
|
252
|
+
const stepDef = run.defSnapshot.steps.find(s => s.id === stepId);
|
|
253
|
+
if (output.ok && outcome === 'done') {
|
|
254
|
+
rs.state = 'green';
|
|
255
|
+
rs.endedAt = now;
|
|
256
|
+
}
|
|
257
|
+
else if (stepDef.retry && rs.attempt < stepDef.retry.max) {
|
|
258
|
+
// Deterministic retry with backoff: back to pending, gated by notBefore; the sweep re-ticks.
|
|
259
|
+
rs.attempt += 1;
|
|
260
|
+
rs.state = 'pending';
|
|
261
|
+
rs.taskIds = undefined;
|
|
262
|
+
rs.notBefore = new Date(Date.now() + stepDef.retry.backoff_min * 60_000).toISOString();
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
rs.state = 'output-red';
|
|
266
|
+
rs.endedAt = now;
|
|
267
|
+
this.skipSubtree(run, stepId);
|
|
268
|
+
await this.onStepFail(ownerGhii, run, stepId, 'output-red');
|
|
269
|
+
}
|
|
270
|
+
await this.tick(ownerGhii, run);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
// ── watchdog: timeouts + due retries ─────────────────────────────────────────────
|
|
274
|
+
async sweep() {
|
|
275
|
+
for (const e of await readActiveRuns(this.storage, this.config.nodeId)) {
|
|
276
|
+
const rec = await this.storage.getMemory(e.ownerGhii, runKey(e.workflowId, e.runId));
|
|
277
|
+
if (!rec) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
await this.sweepRun(e.ownerGhii, rec.value);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async sweepRun(ownerGhii, run) {
|
|
284
|
+
await this.withLock(run.runId, async () => {
|
|
285
|
+
const fresh = await this.storage.getMemory(ownerGhii, runKey(run.workflowId, run.runId));
|
|
286
|
+
if (!fresh)
|
|
287
|
+
return;
|
|
288
|
+
const r = fresh.value;
|
|
289
|
+
if (r.status !== 'running' && r.status !== 'waiting-step')
|
|
290
|
+
return;
|
|
291
|
+
const now = Date.now();
|
|
292
|
+
let changed = false;
|
|
293
|
+
// timeouts on dispatched steps
|
|
294
|
+
for (const step of r.defSnapshot.steps) {
|
|
295
|
+
const rs = r.steps[step.id];
|
|
296
|
+
if (rs.state === 'dispatched' && rs.startedAt) {
|
|
297
|
+
const deadline = new Date(rs.startedAt).getTime() + (step.timeout_min ?? 60) * 60_000;
|
|
298
|
+
if (now >= deadline) {
|
|
299
|
+
if (step.retry && rs.attempt < step.retry.max) {
|
|
300
|
+
rs.attempt += 1;
|
|
301
|
+
rs.state = 'pending';
|
|
302
|
+
rs.taskIds = undefined;
|
|
303
|
+
rs.notBefore = new Date(now + step.retry.backoff_min * 60_000).toISOString();
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
rs.state = 'timed-out';
|
|
307
|
+
rs.endedAt = new Date().toISOString();
|
|
308
|
+
this.skipSubtree(r, step.id);
|
|
309
|
+
await this.onStepFail(ownerGhii, r, step.id, 'timed-out');
|
|
310
|
+
}
|
|
311
|
+
changed = true;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// re-tick (fires any now-due pending retries), using the run's pinned resolved signals.
|
|
316
|
+
if (changed)
|
|
317
|
+
await this.persist(ownerGhii, r);
|
|
318
|
+
await this.tick(ownerGhii, r);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
// ── event triggers (Phase 8) ──────────────────────────────────────────────────
|
|
322
|
+
// The descriptor's `trigger.kind:'event'` is registered in a system-namespace index
|
|
323
|
+
// (lifecycle.ts). These hooks are called from the write/order sites; a match starts a run.
|
|
324
|
+
// Loop guard: skip if the workflow already has an in-flight run (so a workflow that produces a
|
|
325
|
+
// key it also listens on doesn't re-trigger itself).
|
|
326
|
+
async onMemoryWrite(ownerGhii, key) {
|
|
327
|
+
await this.fireEventTriggers('memory.write', ownerGhii, t => {
|
|
328
|
+
const pat = t.match.key;
|
|
329
|
+
return !!pat && globToRegExp(pat).test(key);
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
async onOfferOrdered(ownerGhii, offerId) {
|
|
333
|
+
await this.fireEventTriggers('offer.ordered', ownerGhii, t => {
|
|
334
|
+
const pat = t.match.offer;
|
|
335
|
+
return !!pat && globToRegExp(pat).test(offerId);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
async fireEventTriggers(on, ownerGhii, matches) {
|
|
339
|
+
let triggers;
|
|
340
|
+
try {
|
|
341
|
+
triggers = await readEventTriggers(this.storage, this.config.nodeId);
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
logger.error('readEventTriggers failed', { on, error: String(err) });
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const hits = triggers.filter(t => t.on === on && t.ownerGhii === ownerGhii && matches(t));
|
|
348
|
+
if (hits.length === 0)
|
|
349
|
+
return;
|
|
350
|
+
const active = await readActiveRuns(this.storage, this.config.nodeId);
|
|
351
|
+
for (const t of hits) {
|
|
352
|
+
if (active.some(a => a.workflowId === t.workflowId && a.ownerGhii === t.ownerGhii))
|
|
353
|
+
continue; // loop/overlap guard (owner-scoped)
|
|
354
|
+
this.startRun(ownerGhii, ownerGhii.split('@')[0], t.workflowId, { mode: 'full-live' })
|
|
355
|
+
.catch(err => logger.error('event-triggered workflow run failed', { workflowId: t.workflowId, error: String(err) }));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* On boot, re-sync in-flight runs: a step's task may have reached done/failed during the restart
|
|
360
|
+
* gap (after the HTTP response, before the fire-and-forget onTaskTerminal ran), leaving the step
|
|
361
|
+
* stuck `dispatched`. Re-check each dispatched step's tasks and advance any that already finished —
|
|
362
|
+
* otherwise the watchdog would wrongly TIME OUT a step whose task actually succeeded. Remaining
|
|
363
|
+
* in-flight runs are then carried by live task events + the watchdog.
|
|
364
|
+
*/
|
|
365
|
+
async resumeInflight() {
|
|
366
|
+
const active = await readActiveRuns(this.storage, this.config.nodeId);
|
|
367
|
+
if (!active.length)
|
|
368
|
+
return;
|
|
369
|
+
logger.info(`WorkflowEngine: re-syncing ${active.length} in-flight run(s) after restart`);
|
|
370
|
+
for (const a of active) {
|
|
371
|
+
try {
|
|
372
|
+
const rec = await this.storage.getMemory(a.ownerGhii, runKey(a.workflowId, a.runId));
|
|
373
|
+
if (!rec)
|
|
374
|
+
continue;
|
|
375
|
+
const run = rec.value;
|
|
376
|
+
for (const [, rs] of Object.entries(run.steps)) {
|
|
377
|
+
if (rs.state !== 'dispatched')
|
|
378
|
+
continue;
|
|
379
|
+
for (const tid of rs.taskIds ?? []) {
|
|
380
|
+
const task = await this.storage.getAgentTask(tid);
|
|
381
|
+
if (task && (task.status === 'done' || task.status === 'failed')) {
|
|
382
|
+
await this.onTaskTerminal(task, task.status); // advances the run (re-checks all step tasks)
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
catch (err) {
|
|
389
|
+
logger.error('resumeInflight re-sync failed', { runId: a.runId, error: String(err) });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// ── helpers ──────────────────────────────────────────────────────────────────────
|
|
394
|
+
resolveVars(def, overrides) {
|
|
395
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
396
|
+
const out = {};
|
|
397
|
+
for (const v of def.vars) {
|
|
398
|
+
const override = overrides?.[v.name];
|
|
399
|
+
const def0 = v.default === '<run-date>' ? today : v.default;
|
|
400
|
+
out[v.name] = override ?? def0 ?? '';
|
|
401
|
+
}
|
|
402
|
+
return out;
|
|
403
|
+
}
|
|
404
|
+
template(tmpl, vars) {
|
|
405
|
+
return tmpl.replace(/\{([a-zA-Z0-9_]+)\}/g, (_m, n) => vars[n] ?? `{${n}}`);
|
|
406
|
+
}
|
|
407
|
+
/** Evaluate a signal (or 'none'/undefined → pass), accumulating the keys it reads. */
|
|
408
|
+
async evalSignal(signal, ctx, reads) {
|
|
409
|
+
if (!signal || signal === 'none')
|
|
410
|
+
return { ok: true, observed: { skipped: 'none' } };
|
|
411
|
+
// wrap ctx.read/listGlob to record reads
|
|
412
|
+
const tracking = {
|
|
413
|
+
...ctx,
|
|
414
|
+
read: async (k) => { reads.add(k); return ctx.read(k); },
|
|
415
|
+
listGlob: async (g) => { reads.add(g); return ctx.listGlob(g); },
|
|
416
|
+
};
|
|
417
|
+
return evaluateSignal(signal, tracking);
|
|
418
|
+
}
|
|
419
|
+
/** Mark every step that (transitively) depends on a failed step as skipped (partial-fail policy). */
|
|
420
|
+
skipSubtree(run, failedId) {
|
|
421
|
+
const dependents = (id) => run.defSnapshot.steps.filter(s => (s.after ?? []).includes(id)).map(s => s.id);
|
|
422
|
+
const queue = [...dependents(failedId)];
|
|
423
|
+
while (queue.length) {
|
|
424
|
+
const id = queue.shift();
|
|
425
|
+
const rs = run.steps[id];
|
|
426
|
+
if (rs && rs.state === 'pending') {
|
|
427
|
+
rs.state = 'skipped';
|
|
428
|
+
queue.push(...dependents(id));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
/** Dispatch a step's agent task(s); tag with the workflow-run scope for onTaskTerminal. */
|
|
433
|
+
async dispatchStep(ownerGhii, run, step, resolved) {
|
|
434
|
+
const ownerName = ownerGhii.split('@')[0];
|
|
435
|
+
const agents = Array.isArray(step.agent) ? step.agent : [step.agent];
|
|
436
|
+
const now = new Date().toISOString();
|
|
437
|
+
const ids = [];
|
|
438
|
+
for (const agentName of agents) {
|
|
439
|
+
const agentGaii = buildGAII(agentName, ownerName, this.config.nodeId);
|
|
440
|
+
const scope = [
|
|
441
|
+
{ name: 'workflow-run', value: `${run.workflowId}/${run.runId}`, type: 'text', description: step.id },
|
|
442
|
+
{ name: 'offer', value: step.offer, type: 'text', description: loc(step.description) },
|
|
443
|
+
];
|
|
444
|
+
// Sandbox run: tell the agent the key prefix to write under (signals read under it too), so a
|
|
445
|
+
// test run doesn't clobber production keys. A cooperating agent honors it; the node can't force it.
|
|
446
|
+
if (run.keyPrefix)
|
|
447
|
+
scope.push({ name: 'wf-key-prefix', value: run.keyPrefix, type: 'text', description: 'prefix all deliverable keys with this' });
|
|
448
|
+
const record = {
|
|
449
|
+
id: randomUUID(), agentGaii, ownerGaii: ownerGhii,
|
|
450
|
+
title: loc(step.description) || `${run.workflowId} · ${step.id}`,
|
|
451
|
+
description: loc(run.defSnapshot.description),
|
|
452
|
+
scope, rules: [], verification: { userExpects: '', technicalChecks: [] },
|
|
453
|
+
todos: [], status: 'active', createdAt: now, updatedAt: now, lastEventAt: now,
|
|
454
|
+
};
|
|
455
|
+
await this.storage.createAgentTask(record);
|
|
456
|
+
await this.storage.appendTaskEvent({ id: randomUUID(), taskId: record.id, type: 'started', message: `Dispatched by workflow "${run.workflowId}" step "${step.id}"`, timestamp: now });
|
|
457
|
+
this.webhookDispatcher?.dispatchWebhookEvent(agentGaii, 'task.approved', {
|
|
458
|
+
task_id: record.id, title: record.title, description: record.description ?? '',
|
|
459
|
+
has_todos: false, todo_count: 0, scope_summary: scope.map(s => `${s.type}:${s.value}`),
|
|
460
|
+
created_at: now, auto_activated: true, workflow_id: run.workflowId,
|
|
461
|
+
});
|
|
462
|
+
ids.push(record.id);
|
|
463
|
+
}
|
|
464
|
+
void resolved;
|
|
465
|
+
return ids;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* On a RED step: GUARANTEE the owner sees it (push — node-owned, never silent), then best-effort
|
|
469
|
+
* dispatch the crew `workflow-inspector` agent for diagnosis/repair. The push is the contract; the
|
|
470
|
+
* inspector is enrichment, so a missing/offline inspector never hides the failure.
|
|
471
|
+
*/
|
|
472
|
+
async onStepFail(ownerGhii, run, stepId, reason) {
|
|
473
|
+
const ownerName = ownerGhii.split('@')[0];
|
|
474
|
+
logger.warn(`workflow ${run.workflowId} run ${run.runId}: step "${stepId}" ${reason}`);
|
|
475
|
+
// 1. Guaranteed owner alert (deterministic, node-owned).
|
|
476
|
+
if (this.pushService?.enabled) {
|
|
477
|
+
this.pushService.sendNotification(ownerName, {
|
|
478
|
+
title: 'Workflow step failed',
|
|
479
|
+
body: `${loc(run.defSnapshot.title) || run.workflowId}: step "${stepId}" → ${reason}`,
|
|
480
|
+
url: '/v1/profile?tab=workflows',
|
|
481
|
+
tag: `workflow:${run.workflowId}`,
|
|
482
|
+
}).catch(() => { });
|
|
483
|
+
}
|
|
484
|
+
// 2. Best-effort inspector dispatch (crew-owned; absent ⇒ skip silently, the push already fired).
|
|
485
|
+
const taskId = await this.dispatchInspector(ownerGhii, ownerName, run, stepId, reason);
|
|
486
|
+
if (taskId) {
|
|
487
|
+
run.inspections = [...(run.inspections ?? []), { stepId, taskId, reason, at: new Date().toISOString() }];
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Queue a task to the owner's `workflow-inspector` agent (crew-owned) with full run context: the
|
|
492
|
+
* run record (defSnapshot + every step's state + expected-vs-observed) is at a known memory key.
|
|
493
|
+
* Tagged `workflow-inspect` (NOT `workflow-run`) so completing it never advances the run. Returns
|
|
494
|
+
* the task id, or null when no inspector agent is installed.
|
|
495
|
+
*/
|
|
496
|
+
async dispatchInspector(ownerGhii, ownerName, run, stepId, reason) {
|
|
497
|
+
const inspectorGaii = buildGAII('workflow-inspector', ownerName, this.config.nodeId);
|
|
498
|
+
const inspector = await this.storage.getAgent(inspectorGaii);
|
|
499
|
+
if (!inspector)
|
|
500
|
+
return null;
|
|
501
|
+
const rk = runKey(run.workflowId, run.runId);
|
|
502
|
+
const failing = run.steps[stepId];
|
|
503
|
+
const failingAgents = (run.defSnapshot.steps.find(s => s.id === stepId)?.agent) ?? '';
|
|
504
|
+
const now = new Date().toISOString();
|
|
505
|
+
const scope = [
|
|
506
|
+
{ name: 'workflow-inspect', value: `${run.workflowId}/${run.runId}`, type: 'text', description: stepId },
|
|
507
|
+
];
|
|
508
|
+
const record = {
|
|
509
|
+
id: randomUUID(), agentGaii: inspectorGaii, ownerGaii: ownerGhii,
|
|
510
|
+
title: `Inspect workflow "${run.workflowId}" — step "${stepId}" ${reason}`,
|
|
511
|
+
description: [
|
|
512
|
+
`A workflow step failed (${reason}).`,
|
|
513
|
+
`Read the full run record at owner memory key "${rk}" — it carries defSnapshot, every step's`,
|
|
514
|
+
`state (green / input-red / output-red / timed-out / skipped), and per-leaf expected-vs-observed.`,
|
|
515
|
+
`Failing step: "${stepId}" (agent: ${Array.isArray(failingAgents) ? failingAgents.join(', ') : failingAgents}).`,
|
|
516
|
+
`Observed: ${JSON.stringify(failing?.outputObserved ?? failing?.inputObserved ?? {}).slice(0, 1000)}.`,
|
|
517
|
+
`Diagnose, auto-run any safe deterministic repairs, and report recommendations.`,
|
|
518
|
+
].join(' '),
|
|
519
|
+
scope, rules: [], verification: { userExpects: '', technicalChecks: [] },
|
|
520
|
+
resources: { memoryKeys: [rk] },
|
|
521
|
+
todos: [], status: 'active', createdAt: now, updatedAt: now, lastEventAt: now,
|
|
522
|
+
};
|
|
523
|
+
await this.storage.createAgentTask(record);
|
|
524
|
+
await this.storage.appendTaskEvent({ id: randomUUID(), taskId: record.id, type: 'started', message: `Workflow inspection requested for "${run.workflowId}" step "${stepId}" (${reason})`, timestamp: now });
|
|
525
|
+
this.webhookDispatcher?.dispatchWebhookEvent(inspectorGaii, 'task.approved', {
|
|
526
|
+
task_id: record.id, title: record.title, description: record.description ?? '',
|
|
527
|
+
has_todos: false, todo_count: 0, scope_summary: scope.map(s => `${s.name}:${s.value}`),
|
|
528
|
+
created_at: now, auto_activated: true, workflow_id: run.workflowId,
|
|
529
|
+
});
|
|
530
|
+
return record.id;
|
|
531
|
+
}
|
|
532
|
+
async persist(ownerGhii, run) {
|
|
533
|
+
const key = runKey(run.workflowId, run.runId);
|
|
534
|
+
const existing = await this.storage.getMemory(ownerGhii, key);
|
|
535
|
+
const now = new Date().toISOString();
|
|
536
|
+
await this.storage.setMemory({
|
|
537
|
+
key, ownerGaii: ownerGhii, value: run,
|
|
538
|
+
visibility: 'private', tags: ['workflow-run'], ttlHours: null,
|
|
539
|
+
version: existing ? existing.version + 1 : 1,
|
|
540
|
+
createdAt: existing?.createdAt ?? now, updatedAt: now,
|
|
541
|
+
});
|
|
542
|
+
await reconcileActiveRun(this.storage, this.config.nodeId, ownerGhii, run);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
//# sourceMappingURL=engine.js.map
|