lastlight-workflow-engine 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Last Light Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,37 @@
1
+ import type { PhaseDefinition } from "./schema.js";
2
+ export type NodeStatus = "pending" | "running" | "succeeded" | "failed" | "skipped";
3
+ export type TriggerRule = "all_success" | "one_success" | "none_failed_min_one_success" | "all_done";
4
+ export interface DagNode {
5
+ name: string;
6
+ depends_on: string[];
7
+ trigger_rule: TriggerRule;
8
+ status: NodeStatus;
9
+ output?: string;
10
+ }
11
+ export interface BuildDagOptions {
12
+ /**
13
+ * When set and **no** phase declares `depends_on`, synthesize a chain:
14
+ * each phase depends on the one declared before it (trigger rule
15
+ * `all_success`). This reproduces the old linear runner's semantics —
16
+ * sequential execution with a failure cascade — as a degenerate DAG, so
17
+ * the unified scheduler can treat every workflow uniformly. When any phase
18
+ * declares an explicit edge, the declared graph is used verbatim (no
19
+ * synthesis).
20
+ */
21
+ chainIfNoDeps?: boolean;
22
+ }
23
+ /** Build a DAG from phase definitions. Validates edges and detects cycles. */
24
+ export declare function buildDag(phases: PhaseDefinition[], opts?: BuildDagOptions): DagNode[];
25
+ /** Evaluate a trigger rule against upstream dependency statuses. */
26
+ export declare function evaluateTriggerRule(rule: TriggerRule, depStatuses: NodeStatus[]): boolean;
27
+ /** Return nodes that are ready to run: pending, all deps terminal, trigger rule satisfied. */
28
+ export declare function getReadyNodes(dag: DagNode[]): DagNode[];
29
+ /** Return nodes that should be skipped: pending, all deps terminal, trigger rule NOT satisfied. */
30
+ export declare function getNodesToSkip(dag: DagNode[]): DagNode[];
31
+ /** Check if all nodes are in a terminal state. */
32
+ export declare function isComplete(dag: DagNode[]): boolean;
33
+ /**
34
+ * Return layers of parallelizable phase names (topological sort).
35
+ * Each layer contains phases that can run concurrently.
36
+ */
37
+ export declare function topoSort(dag: DagNode[]): string[][];
@@ -0,0 +1,118 @@
1
+ const TERMINAL = ["succeeded", "failed", "skipped"];
2
+ /** Build a DAG from phase definitions. Validates edges and detects cycles. */
3
+ export function buildDag(phases, opts = {}) {
4
+ const names = new Set(phases.map((p) => p.name));
5
+ for (const phase of phases) {
6
+ if (!phase.depends_on?.length)
7
+ continue;
8
+ for (const dep of phase.depends_on) {
9
+ if (dep === phase.name)
10
+ throw new Error(`Phase "${phase.name}" depends on itself`);
11
+ if (!names.has(dep))
12
+ throw new Error(`Phase "${phase.name}" depends on unknown phase "${dep}"`);
13
+ }
14
+ }
15
+ const anyDeclared = phases.some((p) => p.depends_on && p.depends_on.length > 0);
16
+ const synthesizeChain = opts.chainIfNoDeps === true && !anyDeclared;
17
+ const nodes = phases.map((p, i) => ({
18
+ name: p.name,
19
+ depends_on: synthesizeChain
20
+ ? (i > 0 ? [phases[i - 1].name] : [])
21
+ : (p.depends_on ?? []),
22
+ trigger_rule: p.trigger_rule ?? "all_success",
23
+ status: "pending",
24
+ }));
25
+ // Detect cycles via DFS
26
+ const nodeMap = new Map(nodes.map((n) => [n.name, n]));
27
+ const WHITE = 0, GRAY = 1, BLACK = 2;
28
+ const color = new Map(nodes.map((n) => [n.name, WHITE]));
29
+ // NOTE: DFS follows depends_on edges (child → parent), the reverse of conventional
30
+ // execution-order direction. Cycle detection still works: a GRAY ancestor reachable
31
+ // from a descendant via depends_on edges indicates a cycle in this representation.
32
+ function dfs(name, path) {
33
+ color.set(name, GRAY);
34
+ for (const dep of nodeMap.get(name).depends_on) {
35
+ if (color.get(dep) === GRAY) {
36
+ throw new Error(`Cycle detected: ${[...path, name, dep].join(" -> ")}`);
37
+ }
38
+ if (color.get(dep) === WHITE) {
39
+ dfs(dep, [...path, name]);
40
+ }
41
+ }
42
+ color.set(name, BLACK);
43
+ }
44
+ for (const node of nodes) {
45
+ if (color.get(node.name) === WHITE)
46
+ dfs(node.name, []);
47
+ }
48
+ return nodes;
49
+ }
50
+ /** Evaluate a trigger rule against upstream dependency statuses. */
51
+ export function evaluateTriggerRule(rule, depStatuses) {
52
+ if (depStatuses.length === 0)
53
+ return true;
54
+ switch (rule) {
55
+ case "all_success":
56
+ return depStatuses.every((s) => s === "succeeded");
57
+ case "one_success":
58
+ return depStatuses.some((s) => s === "succeeded");
59
+ case "none_failed_min_one_success":
60
+ return !depStatuses.some((s) => s === "failed") && depStatuses.some((s) => s === "succeeded");
61
+ case "all_done":
62
+ return depStatuses.every((s) => TERMINAL.includes(s));
63
+ }
64
+ }
65
+ /** Return nodes that are ready to run: pending, all deps terminal, trigger rule satisfied. */
66
+ export function getReadyNodes(dag) {
67
+ const nodeMap = new Map(dag.map((n) => [n.name, n]));
68
+ return dag.filter((node) => {
69
+ if (node.status !== "pending")
70
+ return false;
71
+ const deps = node.depends_on.map((d) => nodeMap.get(d));
72
+ if (!deps.every((d) => TERMINAL.includes(d.status)))
73
+ return false;
74
+ return evaluateTriggerRule(node.trigger_rule, deps.map((d) => d.status));
75
+ });
76
+ }
77
+ /** Return nodes that should be skipped: pending, all deps terminal, trigger rule NOT satisfied. */
78
+ export function getNodesToSkip(dag) {
79
+ const nodeMap = new Map(dag.map((n) => [n.name, n]));
80
+ return dag.filter((node) => {
81
+ if (node.status !== "pending")
82
+ return false;
83
+ const deps = node.depends_on.map((d) => nodeMap.get(d));
84
+ if (!deps.every((d) => TERMINAL.includes(d.status)))
85
+ return false;
86
+ return !evaluateTriggerRule(node.trigger_rule, deps.map((d) => d.status));
87
+ });
88
+ }
89
+ /** Check if all nodes are in a terminal state. */
90
+ export function isComplete(dag) {
91
+ return dag.every((n) => TERMINAL.includes(n.status));
92
+ }
93
+ /**
94
+ * Return layers of parallelizable phase names (topological sort).
95
+ * Each layer contains phases that can run concurrently.
96
+ */
97
+ export function topoSort(dag) {
98
+ const inDegree = new Map(dag.map((n) => [n.name, n.depends_on.length]));
99
+ const layers = [];
100
+ let current = dag.filter((n) => (inDegree.get(n.name) ?? 0) === 0).map((n) => n.name);
101
+ while (current.length > 0) {
102
+ layers.push([...current]);
103
+ const next = [];
104
+ for (const name of current) {
105
+ for (const node of dag) {
106
+ if (node.depends_on.includes(name)) {
107
+ const deg = (inDegree.get(node.name) ?? 0) - 1;
108
+ inDegree.set(node.name, deg);
109
+ if (deg === 0)
110
+ next.push(node.name);
111
+ }
112
+ }
113
+ }
114
+ current = next;
115
+ }
116
+ return layers;
117
+ }
118
+ //# sourceMappingURL=dag.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dag.js","sourceRoot":"","sources":["../../src/core/dag.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Minimal expression evaluator for generic loop `until` conditions.
3
+ *
4
+ * Supported forms:
5
+ * output.contains('text') — true if the output string contains 'text'
6
+ * variable == 'value' — equality check against the context map
7
+ * variable != 'value' — inequality check against the context map
8
+ *
9
+ * Deliberately limited to avoid eval() and expression-injection risk.
10
+ * Complex conditions should use until_bash instead.
11
+ */
12
+ export interface LoopEvalContext {
13
+ output: string;
14
+ /**
15
+ * Flattened key/value store. Values are serialized to strings for the
16
+ * quoted-literal comparison; the `true`/`false` bare-literal path
17
+ * special-cases boolean-ish values ("true", "false", "1", "0").
18
+ * Dotted keys (e.g. `scratch.socratic.ready`) are resolved by reading
19
+ * the first segment from top level and walking the rest through nested
20
+ * objects.
21
+ */
22
+ [key: string]: unknown;
23
+ }
24
+ /**
25
+ * Evaluate a single until expression against the given context.
26
+ * Returns false (safe default) for any unrecognised expression.
27
+ */
28
+ export declare function evalUntilExpression(expr: string, ctx: LoopEvalContext): boolean;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Minimal expression evaluator for generic loop `until` conditions.
3
+ *
4
+ * Supported forms:
5
+ * output.contains('text') — true if the output string contains 'text'
6
+ * variable == 'value' — equality check against the context map
7
+ * variable != 'value' — inequality check against the context map
8
+ *
9
+ * Deliberately limited to avoid eval() and expression-injection risk.
10
+ * Complex conditions should use until_bash instead.
11
+ */
12
+ /** Walk a dotted path through the eval context. */
13
+ function readPath(ctx, path) {
14
+ const parts = path.split(".");
15
+ let cur = ctx[parts[0]];
16
+ for (let i = 1; i < parts.length; i++) {
17
+ if (cur === null || cur === undefined || typeof cur !== "object")
18
+ return undefined;
19
+ if (!Object.prototype.hasOwnProperty.call(cur, parts[i]))
20
+ return undefined;
21
+ cur = cur[parts[i]];
22
+ }
23
+ return cur;
24
+ }
25
+ function coerceBool(v) {
26
+ if (typeof v === "boolean")
27
+ return v;
28
+ if (typeof v === "number")
29
+ return v !== 0;
30
+ if (typeof v === "string") {
31
+ const lower = v.toLowerCase();
32
+ return lower === "true" || lower === "1" || lower === "yes";
33
+ }
34
+ return false;
35
+ }
36
+ /**
37
+ * Evaluate a single until expression against the given context.
38
+ * Returns false (safe default) for any unrecognised expression.
39
+ */
40
+ export function evalUntilExpression(expr, ctx) {
41
+ const trimmed = expr.trim();
42
+ // output.contains('text') or output.contains("text")
43
+ const containsMatch = trimmed.match(/^output\.contains\(['"](.+)['"]\)$/);
44
+ if (containsMatch) {
45
+ return String(ctx.output ?? "").includes(containsMatch[1]);
46
+ }
47
+ // dotted.path == true / == false / != true / != false (bare boolean literal)
48
+ const boolMatch = trimmed.match(/^([\w.]+)\s*(==|!=)\s*(true|false)$/);
49
+ if (boolMatch) {
50
+ const [, key, op, lit] = boolMatch;
51
+ const actual = coerceBool(readPath(ctx, key));
52
+ const expected = lit === "true";
53
+ return op === "==" ? actual === expected : actual !== expected;
54
+ }
55
+ // dotted.path == 'value' or dotted.path == "value"
56
+ const eqMatch = trimmed.match(/^([\w.]+)\s*==\s*['"](.+)['"]$/);
57
+ if (eqMatch) {
58
+ const [, key, value] = eqMatch;
59
+ const v = readPath(ctx, key);
60
+ return String(v ?? "") === value;
61
+ }
62
+ // dotted.path != 'value' or dotted.path != "value"
63
+ const neqMatch = trimmed.match(/^([\w.]+)\s*!=\s*['"](.+)['"]$/);
64
+ if (neqMatch) {
65
+ const [, key, value] = neqMatch;
66
+ const v = readPath(ctx, key);
67
+ if (v === undefined)
68
+ return false; // absent variable — safe default
69
+ return String(v) !== value;
70
+ }
71
+ // Unrecognised — safe default
72
+ return false;
73
+ }
74
+ //# sourceMappingURL=loop-eval.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loop-eval.js","sourceRoot":"","sources":["../../src/core/loop-eval.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,152 @@
1
+ import type { ExecutorConfig, ExecutionResult, GitSandboxAccess, CommandSpec } from "./types.js";
2
+ import type { AgentWorkflowDefinition, PhaseDefinition } from "./schema.js";
3
+ import { type TemplateContext } from "./templates.js";
4
+ import type { DagNode } from "./dag.js";
5
+ import type { AgentPort, AssetLoader, EnginePorts, LivenessPort, ObservabilityPort, PhaseOutcome, PhaseReporter, PhaseResolver, WorkflowStateStore } from "../ports/ports.js";
6
+ export type { PhaseReporter, PhaseResolver, PhaseOutcome, PhaseResult, ReportStepOpts, } from "../ports/ports.js";
7
+ /**
8
+ * The single per-phase executor. Constructed once per workflow run, it owns
9
+ * every per-phase body — context / standard agent / reviewer-loop /
10
+ * generic-loop — behind one `execute(node, outputs)` entry point. The
11
+ * scheduler (`core/scheduler.ts`) owns the DAG, the `phases[]`/`outputs{}`
12
+ * accumulation, and the in-memory node status; `execute` reads the current
13
+ * outputs and returns its delta.
14
+ *
15
+ * Dependencies are grouped into cohesive collaborators:
16
+ * - {@link PhaseRunContext} run-scoped immutable data
17
+ * - {@link PhaseReporter} progress / notification surface
18
+ * - {@link PhaseResolver} model / variant / prompt / gate resolution
19
+ * - {@link EnginePorts} injected agent / asset / liveness / telemetry
20
+ * seams + app-registered phase-type handlers
21
+ *
22
+ * This makes the executor unit-testable with fakes and keeps the engine
23
+ * domain-agnostic (no imports of `../engine`, `../state`, `../config`, …).
24
+ */
25
+ /** Run-scoped immutable data shared by every phase execution. */
26
+ export interface PhaseRunContext {
27
+ definition: AgentWorkflowDefinition;
28
+ ctx: TemplateContext;
29
+ config: ExecutorConfig;
30
+ /** Single workspace shared by every phase + loop iteration of the run. */
31
+ taskId: string;
32
+ triggerId: string;
33
+ githubAccess: GitSandboxAccess;
34
+ /** Mutable scratch bag (generic-loop resume state). */
35
+ scratch: Record<string, unknown>;
36
+ /** Runs + dedup-ledger store (omitted ⇒ inert gates + no ledger resume). */
37
+ store?: WorkflowStateStore;
38
+ workflowId?: string;
39
+ /** Bot identity (was `getBotName()`), used in the generic-loop gate prompt. */
40
+ botName: string;
41
+ }
42
+ /**
43
+ * Build the agent prompt for a phase, handling both `prompt:` (template file)
44
+ * and `skill:`/`skills:` (skill references) phase definitions.
45
+ */
46
+ export declare function buildPhasePrompt(phase: PhaseDefinition, ctx: TemplateContext, assets: AssetLoader, extraCtx?: Partial<TemplateContext>): string;
47
+ /**
48
+ * Overlay per-phase executor config fields (`unrestricted_egress`,
49
+ * `web_search`, `sandbox_image`, resolved skill paths) onto the run-level
50
+ * config.
51
+ */
52
+ export declare function phaseConfigFor(config: ExecutorConfig, phase: PhaseDefinition, assets: AssetLoader): ExecutorConfig;
53
+ /** Check if an error was caused by manual termination (OOM/cancel/kill). */
54
+ export declare function isTerminated(error?: string): boolean;
55
+ /**
56
+ * Classify an execution outcome as *soft* — a clean exit that produced no
57
+ * usable output — versus *hard* (a real crash). Generic and phase-agnostic:
58
+ * it reads only the result fields, with no notion of reviewer/socratic. Both
59
+ * the reviewer loop and the generic loop consume it so their recovery logic
60
+ * stays in lockstep. A soft outcome is recoverable (retry / trust an on-disk
61
+ * artifact / advance); a hard one — terminated, fatal, tool error, non-zero
62
+ * exit — is not. See `mapStopReason` in the executors for the stop-reason
63
+ * vocabulary this keys off (`unknown` / `error_truncated` are the soft ones).
64
+ */
65
+ export declare function isSoftOutcome(r: Pick<ExecutionResult, "success" | "error" | "stopReason">): boolean;
66
+ /**
67
+ * The seams the dedup ledger needs: the runs+executions store, the agent
68
+ * runtime, container liveness, and telemetry. Threaded from the
69
+ * {@link PhaseExecutor}'s injected {@link EnginePorts}.
70
+ */
71
+ interface LedgerDeps {
72
+ store?: WorkflowStateStore;
73
+ agent: AgentPort;
74
+ liveness: LivenessPort;
75
+ observability: ObservabilityPort;
76
+ }
77
+ type RunPhaseResult = {
78
+ result: ExecutionResult;
79
+ executionId: string;
80
+ skipped: false;
81
+ } | {
82
+ result: ExecutionResult;
83
+ skipped: false;
84
+ } | {
85
+ skipped: true;
86
+ reason: "running" | "done";
87
+ };
88
+ /**
89
+ * Run a single agent phase with DB-tracked deduplication. The dedup ledger
90
+ * (`shouldRunPhase`) is the single source of truth for resume, so re-running
91
+ * from the top skips completed phases here.
92
+ */
93
+ export declare function runPhase(workflowName: string, phaseName: string, taskId: string, triggerId: string, prompt: string, config: ExecutorConfig, deps: LedgerDeps, modelOverride?: string, workflowRunId?: string, githubAccess?: GitSandboxAccess, variantOverride?: string): Promise<RunPhaseResult>;
94
+ /**
95
+ * Run a single deterministic `bash`/`script` phase, sharing the agent phase's
96
+ * dedup ledger + executions row so it appears (and dedups on resume) exactly
97
+ * like an agent phase — just with `turns: 0` and no model cost.
98
+ */
99
+ export declare function runCommandPhase(workflowName: string, phaseName: string, taskId: string, triggerId: string, spec: CommandSpec, config: ExecutorConfig, deps: LedgerDeps, workflowRunId?: string, githubAccess?: GitSandboxAccess, timeoutSeconds?: number, sandboxEnv?: Record<string, string>): Promise<RunPhaseResult>;
100
+ export declare class PhaseExecutor {
101
+ private readonly run;
102
+ private readonly reporter;
103
+ private readonly resolver;
104
+ private readonly ports;
105
+ constructor(run: PhaseRunContext, reporter: PhaseReporter, resolver: PhaseResolver, ports: EnginePorts);
106
+ /** The dedup-ledger seams, bundled from the injected ports + run store. */
107
+ private get ledgerDeps();
108
+ /**
109
+ * Execute one DAG node and return its delta. The scheduler accumulates
110
+ * `results` into `phases[]`, merges `outputVars` into the shared outputs
111
+ * map, and maps `status`/`paused`/`aborted` onto node state + run control.
112
+ */
113
+ execute(node: DagNode, outputs: Readonly<Record<string, unknown>>): Promise<PhaseOutcome>;
114
+ private runContext;
115
+ /** Resolve model + variant for a phase/task, honouring YAML templates first. */
116
+ private resolveModelVariant;
117
+ private runPhaseCall;
118
+ private runStandard;
119
+ /**
120
+ * Body for `type: bash` / `type: script` phases. Runs a deterministic command
121
+ * in the sandbox (no LLM), then mirrors the agent-phase lifecycle: report
122
+ * progress, expose stdout downstream via `output_var` →
123
+ * `{{phaseOutputs.<name>.output}}`, fail the workflow on a non-zero exit, and
124
+ * honour an optional `approval_gate`.
125
+ */
126
+ private runCommandBody;
127
+ /** Render a command/script phase's template fields into a {@link CommandSpec}. */
128
+ private buildCommandSpec;
129
+ private runCommandPhaseCall;
130
+ /**
131
+ * Evaluate a `generic_loop.until_bash` condition INSIDE the sandbox (against
132
+ * the persisted workspace), replacing the old harness-host `execSync`. Exit 0
133
+ * ⇒ loop complete. The check inherits the phase's egress; no session log is
134
+ * written (it's an internal loop condition, not a user-facing phase).
135
+ */
136
+ private runUntilBash;
137
+ /**
138
+ * Evaluate the `contains_BLOCKED` rule. Returns "fail" when the workflow
139
+ * should fail, or "continue" when the marker is absent or bypassed.
140
+ */
141
+ private evaluateBlocked;
142
+ /**
143
+ * Persist + pause for an approval/reply gate, atomically. The approval
144
+ * insert, the `waiting_approval` phase marker, the optional scratch persist
145
+ * (loops record their iteration/cycle state here), and the run pause all
146
+ * happen in one transaction via `db.runs.pauseForApproval` — a partial
147
+ * failure can't leave the run paused without its gate, or vice versa.
148
+ */
149
+ private pauseForApproval;
150
+ private runReviewerLoop;
151
+ private runGenericLoop;
152
+ }