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 +21 -0
- package/dist/core/dag.d.ts +37 -0
- package/dist/core/dag.js +118 -0
- package/dist/core/dag.js.map +1 -0
- package/dist/core/loop-eval.d.ts +28 -0
- package/dist/core/loop-eval.js +74 -0
- package/dist/core/loop-eval.js.map +1 -0
- package/dist/core/phase-executor.d.ts +152 -0
- package/dist/core/phase-executor.js +922 -0
- package/dist/core/phase-executor.js.map +1 -0
- package/dist/core/phase-ref.d.ts +50 -0
- package/dist/core/phase-ref.js +91 -0
- package/dist/core/phase-ref.js.map +1 -0
- package/dist/core/scheduler.d.ts +45 -0
- package/dist/core/scheduler.js +196 -0
- package/dist/core/scheduler.js.map +1 -0
- package/dist/core/schema.d.ts +344 -0
- package/dist/core/schema.js +460 -0
- package/dist/core/schema.js.map +1 -0
- package/dist/core/templates.d.ts +88 -0
- package/dist/core/templates.js +127 -0
- package/dist/core/templates.js.map +1 -0
- package/dist/core/types.d.ts +261 -0
- package/dist/core/types.js +15 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/verdict.d.ts +19 -0
- package/dist/core/verdict.js +30 -0
- package/dist/core/verdict.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/ports/ports.d.ts +233 -0
- package/dist/ports/ports.js +11 -0
- package/dist/ports/ports.js.map +1 -0
- package/dist/test-support/fakes.d.ts +86 -0
- package/dist/test-support/fakes.js +0 -0
- package/dist/test-support/fakes.js.map +1 -0
- package/package.json +39 -0
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[][];
|
package/dist/core/dag.js
ADDED
|
@@ -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
|
+
}
|