@acpus/runtime 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-telemetry.d.ts.map +1 -1
- package/dist/agent-telemetry.js.map +1 -1
- package/dist/artifacts.d.ts.map +1 -1
- package/dist/attempt-artifacts.d.ts.map +1 -1
- package/dist/attempt-artifacts.js.map +1 -1
- package/dist/client.d.ts +1 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +2 -2
- package/dist/client.js.map +1 -1
- package/dist/evaluator.d.ts.map +1 -1
- package/dist/executors/agent.d.ts.map +1 -1
- package/dist/executors/agent.js.map +1 -1
- package/dist/executors/mock-program.d.ts.map +1 -1
- package/dist/executors/program.d.ts +1 -1
- package/dist/executors/program.d.ts.map +1 -1
- package/dist/executors/program.js +10 -3
- package/dist/executors/program.js.map +1 -1
- package/dist/executors/types.d.ts +2 -0
- package/dist/executors/types.d.ts.map +1 -1
- package/dist/fork.d.ts +4 -0
- package/dist/fork.d.ts.map +1 -1
- package/dist/fork.js +9 -1
- package/dist/fork.js.map +1 -1
- package/dist/hooks/journal.d.ts +20 -0
- package/dist/hooks/journal.d.ts.map +1 -0
- package/dist/hooks/journal.js +49 -0
- package/dist/hooks/journal.js.map +1 -0
- package/dist/hooks/loader.d.ts +41 -0
- package/dist/hooks/loader.d.ts.map +1 -0
- package/dist/hooks/loader.js +168 -0
- package/dist/hooks/loader.js.map +1 -0
- package/dist/hooks/payload.d.ts +17 -0
- package/dist/hooks/payload.d.ts.map +1 -0
- package/dist/hooks/payload.js +45 -0
- package/dist/hooks/payload.js.map +1 -0
- package/dist/hooks/runner.d.ts +42 -0
- package/dist/hooks/runner.d.ts.map +1 -0
- package/dist/hooks/runner.js +161 -0
- package/dist/hooks/runner.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/interpreter.d.ts +28 -5
- package/dist/interpreter.d.ts.map +1 -1
- package/dist/interpreter.js +382 -2
- package/dist/interpreter.js.map +1 -1
- package/dist/keys.js.map +1 -1
- package/dist/run-control.d.ts.map +1 -1
- package/dist/run-control.js.map +1 -1
- package/dist/store.d.ts +9 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +21 -1
- package/dist/store.js.map +1 -1
- package/dist/supervisor-app.d.ts.map +1 -1
- package/dist/supervisor-app.js +33 -3
- package/dist/supervisor-app.js.map +1 -1
- package/dist/supervisor-discovery.js.map +1 -1
- package/dist/supervisor-runner.js.map +1 -1
- package/dist/types.d.ts +15 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-input.d.ts.map +1 -1
- package/dist/validate-input.js.map +1 -1
- package/dist/validate-signal.d.ts.map +1 -1
- package/dist/workflow-outputs.js.map +1 -1
- package/package.json +5 -4
package/dist/interpreter.d.ts
CHANGED
|
@@ -17,6 +17,17 @@ export declare class WorkflowInterpreter {
|
|
|
17
17
|
private readonly runControl;
|
|
18
18
|
private readonly maxConcurrency;
|
|
19
19
|
private readonly sleep;
|
|
20
|
+
/** Frozen hook runner for this Run; undefined disables all hook machinery. */
|
|
21
|
+
private readonly hookRunner?;
|
|
22
|
+
/** Lazily-created per-Run injector journal (only when injectors fire). */
|
|
23
|
+
private hookJournal?;
|
|
24
|
+
/**
|
|
25
|
+
* Per-node leaf execution metadata for hook onNodeComplete/Error payloads,
|
|
26
|
+
* keyed by node key. Populated by executeProgram/executeAgent and deleted by
|
|
27
|
+
* executeNode once the node's terminal lifecycle events have fired, so the map
|
|
28
|
+
* never accumulates across a long-lived Run.
|
|
29
|
+
*/
|
|
30
|
+
private readonly leafMeta;
|
|
20
31
|
/** Pending external-decision resolvers for Signal Nodes awaiting a payload,
|
|
21
32
|
* keyed by "runId:nodeKey". An entry exists only while a node is `awaiting`.
|
|
22
33
|
* The resolver validates the payload against the node's output schema and
|
|
@@ -108,11 +119,6 @@ export declare class WorkflowInterpreter {
|
|
|
108
119
|
private syncAgentTelemetryFromDisk;
|
|
109
120
|
private executeParallel;
|
|
110
121
|
private executeFanout;
|
|
111
|
-
/**
|
|
112
|
-
* Resolve fanout lanes per the wait strategy. Lanes never reject on normal
|
|
113
|
-
* failure (captured as LaneResult); only NodeAbortedError rejects, which we
|
|
114
|
-
* let propagate. Losing/excess lanes are silently consumed.
|
|
115
|
-
*/
|
|
116
122
|
private waitForFanout;
|
|
117
123
|
private materializePendingNode;
|
|
118
124
|
private executeSwitch;
|
|
@@ -141,6 +147,23 @@ export declare class WorkflowInterpreter {
|
|
|
141
147
|
private extractNodeIdFromKey;
|
|
142
148
|
private findNodeById;
|
|
143
149
|
private extractItemId;
|
|
150
|
+
/** Run scope for the active Run, derived from the frozen IR + metadata. */
|
|
151
|
+
private resolveRunScope;
|
|
152
|
+
/** Lazily create the per-Run injector journal. */
|
|
153
|
+
private journalFor;
|
|
154
|
+
private runInjector;
|
|
155
|
+
private emitNodeLifecycle;
|
|
156
|
+
private emitRunEvent;
|
|
157
|
+
/** Recover the dynamic key dimensions from an expression context. */
|
|
158
|
+
private dynamicFromCtx;
|
|
159
|
+
/**
|
|
160
|
+
* Populate parent_node_key/parent_node_kind from the Run's frozen IR. The
|
|
161
|
+
* parent's resolved key shares this node's dynamic prefix, so we map the
|
|
162
|
+
* static parent path onto the dynamic portion of the child key.
|
|
163
|
+
*/
|
|
164
|
+
private fillParentFields;
|
|
165
|
+
/** Drop per-node leaf metadata once its terminal events have fired. */
|
|
166
|
+
private clearLeafMeta;
|
|
144
167
|
}
|
|
145
168
|
/** Generate a locally sortable run ID: yyyyMMddHHmmss + 20 uppercase hex chars. */
|
|
146
169
|
export declare function generateRunId(now?: Date): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interpreter.d.ts","sourceRoot":"","sources":["../src/interpreter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAA2B,MAAM,aAAa,CAAC;AAIpE,OAAO,KAAK,EAAwE,kBAAkB,EAAkB,UAAU,EAAE,MAAM,YAAY,CAAC;AACvJ,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAMtC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAE5D,OAAO,KAAK,EAAiC,YAAY,EAAkB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"interpreter.d.ts","sourceRoot":"","sources":["../src/interpreter.ts"],"names":[],"mappings":"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"}
|
package/dist/interpreter.js
CHANGED
|
@@ -15,6 +15,11 @@ import { randomBytes } from "node:crypto";
|
|
|
15
15
|
import pLimit from "p-limit";
|
|
16
16
|
import { AgentTelemetryAccumulator, upsertAgentAttemptTelemetry } from "./agent-telemetry.js";
|
|
17
17
|
import { buildWorkflowExpressionContext } from "./workflow-context.js";
|
|
18
|
+
import { HookFailureError } from "./hooks/runner.js";
|
|
19
|
+
import { HookJournal } from "./hooks/journal.js";
|
|
20
|
+
import { basePayload, runScope, withNodeFields } from "./hooks/payload.js";
|
|
21
|
+
import { isRunTerminal } from "./types.js";
|
|
22
|
+
import { join } from "node:path";
|
|
18
23
|
/**
|
|
19
24
|
* The core IR interpreter that drives state transitions, orchestrates
|
|
20
25
|
* execution, and persists state.
|
|
@@ -29,6 +34,17 @@ export class WorkflowInterpreter {
|
|
|
29
34
|
runControl;
|
|
30
35
|
maxConcurrency;
|
|
31
36
|
sleep;
|
|
37
|
+
/** Frozen hook runner for this Run; undefined disables all hook machinery. */
|
|
38
|
+
hookRunner;
|
|
39
|
+
/** Lazily-created per-Run injector journal (only when injectors fire). */
|
|
40
|
+
hookJournal;
|
|
41
|
+
/**
|
|
42
|
+
* Per-node leaf execution metadata for hook onNodeComplete/Error payloads,
|
|
43
|
+
* keyed by node key. Populated by executeProgram/executeAgent and deleted by
|
|
44
|
+
* executeNode once the node's terminal lifecycle events have fired, so the map
|
|
45
|
+
* never accumulates across a long-lived Run.
|
|
46
|
+
*/
|
|
47
|
+
leafMeta = new Map();
|
|
32
48
|
/** Pending external-decision resolvers for Signal Nodes awaiting a payload,
|
|
33
49
|
* keyed by "runId:nodeKey". An entry exists only while a node is `awaiting`.
|
|
34
50
|
* The resolver validates the payload against the node's output schema and
|
|
@@ -46,6 +62,7 @@ export class WorkflowInterpreter {
|
|
|
46
62
|
this.runControl = new RunControl(store);
|
|
47
63
|
this.maxConcurrency = options?.maxConcurrency ?? 10;
|
|
48
64
|
this.sleep = options?.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
65
|
+
this.hookRunner = options?.hookRunner;
|
|
49
66
|
}
|
|
50
67
|
/**
|
|
51
68
|
* Start a new workflow run to completion (init + execute). Convenience for
|
|
@@ -66,7 +83,8 @@ export class WorkflowInterpreter {
|
|
|
66
83
|
workflowRef: opts.workflowRef,
|
|
67
84
|
workflowSourcePath: opts.workflowSourcePath,
|
|
68
85
|
agentOverrides: opts.agentOverrides,
|
|
69
|
-
submissionWarnings: opts.submissionWarnings
|
|
86
|
+
submissionWarnings: opts.submissionWarnings,
|
|
87
|
+
skipHooks: opts.skipHooks
|
|
70
88
|
});
|
|
71
89
|
}
|
|
72
90
|
/**
|
|
@@ -74,6 +92,18 @@ export class WorkflowInterpreter {
|
|
|
74
92
|
*/
|
|
75
93
|
async runToCompletion(ir, opts, runId) {
|
|
76
94
|
const meta = this.store.readRunMeta(runId);
|
|
95
|
+
const scope = this.hookRunner ? runScope(ir, meta) : undefined;
|
|
96
|
+
// `beforeRun` fires only at the first execution entry (no node states yet),
|
|
97
|
+
// never on retry/resume where the Run is already underway.
|
|
98
|
+
const isFirstExecution = this.store.listNodeStates(runId).length === 0;
|
|
99
|
+
if (scope && isFirstExecution) {
|
|
100
|
+
await this.emitRunEvent("beforeRun", scope, (p) => {
|
|
101
|
+
p.input = opts.input;
|
|
102
|
+
p.run_attempt = meta.runAttempt;
|
|
103
|
+
p.ir_digest = meta.irDigest;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const runStartedAt = Date.now();
|
|
77
107
|
try {
|
|
78
108
|
const ctx = this.buildContext(ir, opts.input, runId);
|
|
79
109
|
await this.executeNode(ir.root, ctx, runId, {});
|
|
@@ -108,6 +138,19 @@ export class WorkflowInterpreter {
|
|
|
108
138
|
}
|
|
109
139
|
meta.updatedAt = new Date().toISOString();
|
|
110
140
|
this.store.writeRunMeta(runId, meta);
|
|
141
|
+
// `afterRun` fires once the Run reaches a terminal status.
|
|
142
|
+
if (scope && isRunTerminal(meta.status)) {
|
|
143
|
+
await this.emitRunEvent("afterRun", scope, (p) => {
|
|
144
|
+
p.run_status = meta.status;
|
|
145
|
+
p.run_attempt = meta.runAttempt;
|
|
146
|
+
p.ir_digest = meta.irDigest;
|
|
147
|
+
p.duration_ms = Date.now() - runStartedAt;
|
|
148
|
+
if (meta.output !== undefined)
|
|
149
|
+
p.output = meta.output;
|
|
150
|
+
if (meta.error !== undefined)
|
|
151
|
+
p.error = meta.error;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
111
154
|
return meta;
|
|
112
155
|
}
|
|
113
156
|
/**
|
|
@@ -364,6 +407,7 @@ export class WorkflowInterpreter {
|
|
|
364
407
|
const controller = new AbortController();
|
|
365
408
|
this.runControl.registerAbortController(runId, nodeKey, controller);
|
|
366
409
|
// Transition to running
|
|
410
|
+
const fromState = state.state;
|
|
367
411
|
if (canTransition(state.state, "running")) {
|
|
368
412
|
state.state = transition(state.state, "running");
|
|
369
413
|
}
|
|
@@ -379,6 +423,9 @@ export class WorkflowInterpreter {
|
|
|
379
423
|
state.dynamicContext = snapshot;
|
|
380
424
|
}
|
|
381
425
|
this.store.writeNodeState(runId, state);
|
|
426
|
+
if (fromState !== "running") {
|
|
427
|
+
await this.emitNodeLifecycle(runId, "onNodeStart", node, nodeKey, state, dynamic, ctx, fromState);
|
|
428
|
+
}
|
|
382
429
|
try {
|
|
383
430
|
let output;
|
|
384
431
|
let completeScope = false;
|
|
@@ -448,7 +495,10 @@ export class WorkflowInterpreter {
|
|
|
448
495
|
if (artifactRefs)
|
|
449
496
|
state.artifactRefs = artifactRefs;
|
|
450
497
|
state.completedAt = new Date().toISOString();
|
|
498
|
+
// A Signal Node completes from `awaiting`, every other leaf from `running`.
|
|
499
|
+
const completeFrom = node.kind === "run.signal" ? "awaiting" : "running";
|
|
451
500
|
this.store.writeNodeState(runId, state);
|
|
501
|
+
await this.emitNodeLifecycle(runId, "onNodeComplete", node, nodeKey, state, dynamic, ctx, completeFrom);
|
|
452
502
|
// Add output to step context
|
|
453
503
|
ctx.steps[node.id] = output;
|
|
454
504
|
if (completeScope) {
|
|
@@ -475,6 +525,7 @@ export class WorkflowInterpreter {
|
|
|
475
525
|
state.artifactRefs = error.artifactRefs;
|
|
476
526
|
this.syncAgentTelemetryFromDisk(runId, nodeKey, state);
|
|
477
527
|
this.store.writeNodeState(runId, state);
|
|
528
|
+
await this.emitNodeLifecycle(runId, error.state === "paused" ? "onNodePaused" : "onNodeCancelled", node, nodeKey, state, dynamic, ctx, "running");
|
|
478
529
|
throw error;
|
|
479
530
|
}
|
|
480
531
|
// Don't clobber an external pause/cancel that landed while this node's
|
|
@@ -502,10 +553,13 @@ export class WorkflowInterpreter {
|
|
|
502
553
|
this.syncAgentTelemetryFromDisk(runId, nodeKey, state);
|
|
503
554
|
state.completedAt = new Date().toISOString();
|
|
504
555
|
this.store.writeNodeState(runId, state);
|
|
556
|
+
await this.emitNodeLifecycle(runId, "onNodeError", node, nodeKey, state, dynamic, ctx, "running", error);
|
|
505
557
|
throw error;
|
|
506
558
|
}
|
|
507
559
|
finally {
|
|
508
560
|
this.runControl.clearInFlightNode(runId, nodeKey);
|
|
561
|
+
// Drop per-node leaf hook metadata so the map never grows across a Run.
|
|
562
|
+
this.clearLeafMeta(nodeKey);
|
|
509
563
|
}
|
|
510
564
|
}
|
|
511
565
|
// ─── Kind-specific execution ───────────────────────────────────
|
|
@@ -530,6 +584,13 @@ export class WorkflowInterpreter {
|
|
|
530
584
|
const maxRetries = typeof retry?.max === "number" ? retry.max : hasOutputSchema ? 2 : 0;
|
|
531
585
|
const backoffMs = retry?.backoff ? parseDurationMs(retry.backoff) : 5e3;
|
|
532
586
|
const allArtifactRefs = [...(this.store.readNodeState(runId, nodeKey)?.artifactRefs ?? [])];
|
|
587
|
+
// Run `beforeAgentExec` once per Agent Step execution (per persisted attempt),
|
|
588
|
+
// not per internal parse/schema auto-retry iteration. The returned prompt
|
|
589
|
+
// prefix is prepended to every rendered prompt below.
|
|
590
|
+
const injectedPrompt = await this.runInjector("beforeAgentExec", node, ctx, runId, nodeKey, (p) => {
|
|
591
|
+
p.agent_use = node.metadata.agent?.use;
|
|
592
|
+
p.is_continuation = Boolean(continuation);
|
|
593
|
+
});
|
|
533
594
|
// `attempt` is local to this executor call and controls parse/schema
|
|
534
595
|
// auto-retry. The persisted `state.attempt` is the durable attempt sequence
|
|
535
596
|
// used for node state and artifact filenames across pause/resume/manual retry.
|
|
@@ -545,6 +606,9 @@ export class WorkflowInterpreter {
|
|
|
545
606
|
const use = node.metadata.agent?.use ?? "?";
|
|
546
607
|
throw new LeafExecutionError(`Agent step '${node.id}' (use: ${use}) failed (config): Failed to evaluate agent configuration template: ${error instanceof Error ? error.message : String(error)}`);
|
|
547
608
|
}
|
|
609
|
+
if (injectedPrompt?.prependPrompt) {
|
|
610
|
+
preparedPrompt = `${injectedPrompt.prependPrompt}\n\n${preparedPrompt}`;
|
|
611
|
+
}
|
|
548
612
|
const rawAcpDebug = process.env.ACPUS_AGENT_RAW_ACP_DEBUG === "1";
|
|
549
613
|
const liveArtifacts = this.attemptArtifacts.startAgentAttempt(runId, nodeKey, attemptNo, preparedPrompt, { rawAcpDebug });
|
|
550
614
|
this.attemptArtifacts.mergeAttemptRefs(allArtifactRefs, liveArtifacts.artifactRefs);
|
|
@@ -647,6 +711,19 @@ export class WorkflowInterpreter {
|
|
|
647
711
|
await this.sleep(backoffMs);
|
|
648
712
|
continue;
|
|
649
713
|
}
|
|
714
|
+
// Stash agent detail for hook onNodeComplete/Error payloads (terminal attempt).
|
|
715
|
+
if (this.hookRunner) {
|
|
716
|
+
const agent = node.metadata.agent;
|
|
717
|
+
this.leafMeta.set(nodeKey, {
|
|
718
|
+
failureKind: result.failureKind,
|
|
719
|
+
agentModel: agent?.model,
|
|
720
|
+
agentType: agent?.type,
|
|
721
|
+
agentPolicy: node.metadata.policy ?? agent?.policy,
|
|
722
|
+
sessionKey: renderedSessionKey,
|
|
723
|
+
agentExitCode: result.exitCode,
|
|
724
|
+
agentResponseText: result.responseText
|
|
725
|
+
});
|
|
726
|
+
}
|
|
650
727
|
if (result.failureKind || (result.error && !result.partial)) {
|
|
651
728
|
const use = node.metadata.agent?.use ?? "?";
|
|
652
729
|
throw new LeafExecutionError(`Agent step '${node.id}' (use: ${use}) failed${result.failureKind ? ` (${result.failureKind})` : ""}: ${result.error ?? "unknown"}`, allArtifactRefs.length > 0 ? allArtifactRefs : artifactRefs, result.output);
|
|
@@ -659,7 +736,29 @@ export class WorkflowInterpreter {
|
|
|
659
736
|
}
|
|
660
737
|
}
|
|
661
738
|
async executeProgram(node, ctx, runId, signal, nodeKey) {
|
|
662
|
-
|
|
739
|
+
// Run `beforeProgramExec` before the executor call. cmd/env render inside
|
|
740
|
+
// the executor, so injected env is passed through ExecutionRequest and the
|
|
741
|
+
// executor merges it into the subprocess environment.
|
|
742
|
+
const injected = await this.runInjector("beforeProgramExec", node, ctx, runId, nodeKey);
|
|
743
|
+
const result = await this.programExecutor.execute({
|
|
744
|
+
node,
|
|
745
|
+
context: ctx,
|
|
746
|
+
signal,
|
|
747
|
+
nodeKey,
|
|
748
|
+
injectedEnv: injected?.env
|
|
749
|
+
});
|
|
750
|
+
// Stash rendered command/env/output for hook onNodeComplete/Error payloads.
|
|
751
|
+
if (this.hookRunner) {
|
|
752
|
+
this.leafMeta.set(nodeKey, {
|
|
753
|
+
failureKind: result.failureKind,
|
|
754
|
+
command: result.command,
|
|
755
|
+
shell: result.shell,
|
|
756
|
+
subprocessEnv: result.subprocessEnv,
|
|
757
|
+
exitCode: result.exitCode,
|
|
758
|
+
stdout: result.stdout,
|
|
759
|
+
stderr: result.stderr
|
|
760
|
+
});
|
|
761
|
+
}
|
|
663
762
|
// Operator abort → paused/cancelled (carry output on the abort error).
|
|
664
763
|
if (result.partial) {
|
|
665
764
|
throw new NodeAbortedError(nodeKey, this.runControl.abortIntent(runId, nodeKey), undefined, result.output);
|
|
@@ -1024,8 +1123,11 @@ export class WorkflowInterpreter {
|
|
|
1024
1123
|
// machine); a signal payload resolves it, a cancel aborts it.
|
|
1025
1124
|
const enterState = this.store.readNodeState(runId, nodeKey);
|
|
1026
1125
|
if (enterState && canTransition(enterState.state, "awaiting")) {
|
|
1126
|
+
const from = enterState.state;
|
|
1027
1127
|
enterState.state = transition(enterState.state, "awaiting");
|
|
1028
1128
|
this.store.writeNodeState(runId, enterState);
|
|
1129
|
+
// Surface the running → awaiting transition to onStateChange observers.
|
|
1130
|
+
await this.emitNodeLifecycle(runId, "onStateChange", node, nodeKey, enterState, this.dynamicFromCtx(ctx), ctx, from);
|
|
1029
1131
|
}
|
|
1030
1132
|
return new Promise((resolve, reject) => {
|
|
1031
1133
|
const onAbort = () => {
|
|
@@ -1224,6 +1326,230 @@ export class WorkflowInterpreter {
|
|
|
1224
1326
|
}
|
|
1225
1327
|
return String(index ?? 0);
|
|
1226
1328
|
}
|
|
1329
|
+
// ─── Hook integration ───────────────────────────────────────────
|
|
1330
|
+
/** Run scope for the active Run, derived from the frozen IR + metadata. */
|
|
1331
|
+
resolveRunScope(runId) {
|
|
1332
|
+
if (!this.hookRunner)
|
|
1333
|
+
return undefined;
|
|
1334
|
+
const ir = this.store.readIr(runId);
|
|
1335
|
+
const meta = this.store.readRunMeta(runId);
|
|
1336
|
+
if (!ir || !meta)
|
|
1337
|
+
return undefined;
|
|
1338
|
+
return runScope(ir, meta);
|
|
1339
|
+
}
|
|
1340
|
+
/** Lazily create the per-Run injector journal. */
|
|
1341
|
+
journalFor(runId) {
|
|
1342
|
+
if (!this.hookJournal) {
|
|
1343
|
+
this.hookJournal = new HookJournal(join(this.store.getBaseDir(), runId));
|
|
1344
|
+
}
|
|
1345
|
+
return this.hookJournal;
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Run an injector for a node, journaling each handler invocation. Returns the
|
|
1349
|
+
* merged InjectorResult, or undefined when no hook runner / no handlers.
|
|
1350
|
+
* An injector failure under `fail` policy propagates as HookFailureError,
|
|
1351
|
+
* which executeNode maps to a node failure with failureKind hook_failure.
|
|
1352
|
+
*/
|
|
1353
|
+
async runInjector(name, node, ctx, runId, nodeKey, enrich) {
|
|
1354
|
+
if (!this.hookRunner || !this.hookRunner.hasInjector(name))
|
|
1355
|
+
return undefined;
|
|
1356
|
+
const scope = this.resolveRunScope(runId);
|
|
1357
|
+
if (!scope)
|
|
1358
|
+
return undefined;
|
|
1359
|
+
const state = this.store.readNodeState(runId, nodeKey);
|
|
1360
|
+
const nodeAttempt = state?.attempt ?? 1;
|
|
1361
|
+
const isRetry = nodeAttempt > 1;
|
|
1362
|
+
const payload = withNodeFields(basePayload(scope, name), node, nodeKey, state, this.dynamicFromCtx(ctx), ctx);
|
|
1363
|
+
payload.is_retry = isRetry;
|
|
1364
|
+
// Note: `beforeAgentExec` runs before the prompt is rendered, so we do NOT
|
|
1365
|
+
// surface state.renderedPrompt here — it would be absent on first execution
|
|
1366
|
+
// and stale on retry. The rendered prompt is carried by lifecycle events.
|
|
1367
|
+
enrich?.(payload);
|
|
1368
|
+
const journal = this.journalFor(runId);
|
|
1369
|
+
return this.hookRunner.runInjector(name, payload, (handlerIndex, result, durationMs) => {
|
|
1370
|
+
journal.append({
|
|
1371
|
+
node_key: nodeKey,
|
|
1372
|
+
injector: name,
|
|
1373
|
+
handler_index: handlerIndex,
|
|
1374
|
+
node_attempt: nodeAttempt,
|
|
1375
|
+
is_retry: isRetry,
|
|
1376
|
+
prepend_prompt: name === "beforeAgentExec" ? (result.prependPrompt ?? null) : null,
|
|
1377
|
+
env: result.env ?? null,
|
|
1378
|
+
timestamp: new Date().toISOString(),
|
|
1379
|
+
duration_ms: durationMs
|
|
1380
|
+
});
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
/** Fire a node lifecycle event plus onStateChange for an actual transition. */
|
|
1384
|
+
async emitNodeLifecycle(runId, name, node, nodeKey, state, dynamic, ctx, fromState, error) {
|
|
1385
|
+
if (!this.hookRunner)
|
|
1386
|
+
return;
|
|
1387
|
+
const firesSpecific = this.hookRunner.hasEvent(name);
|
|
1388
|
+
const firesStateChange = this.hookRunner.hasEvent("onStateChange");
|
|
1389
|
+
if (!firesSpecific && !firesStateChange)
|
|
1390
|
+
return;
|
|
1391
|
+
const scope = this.resolveRunScope(runId);
|
|
1392
|
+
if (!scope)
|
|
1393
|
+
return;
|
|
1394
|
+
const hookFailure = error instanceof HookFailureError;
|
|
1395
|
+
const build = (eventName) => {
|
|
1396
|
+
const p = withNodeFields(basePayload(scope, eventName), node, nodeKey, state, dynamic, ctx);
|
|
1397
|
+
if (state.output !== undefined)
|
|
1398
|
+
p.output = state.output;
|
|
1399
|
+
if (state.startedAt && state.completedAt) {
|
|
1400
|
+
p.duration_ms = Date.parse(state.completedAt) - Date.parse(state.startedAt);
|
|
1401
|
+
}
|
|
1402
|
+
// beforeAgentExec aside, lifecycle events carry the rendered prompt.
|
|
1403
|
+
if (state.renderedPrompt)
|
|
1404
|
+
p.prompt = state.renderedPrompt;
|
|
1405
|
+
if (state.renderedSessionKey)
|
|
1406
|
+
p.session_key = state.renderedSessionKey;
|
|
1407
|
+
if (node.kind === "run.agent") {
|
|
1408
|
+
const telemetry = hookAgentTelemetry(state);
|
|
1409
|
+
if (telemetry)
|
|
1410
|
+
p.agent_telemetry = telemetry;
|
|
1411
|
+
}
|
|
1412
|
+
if (error !== undefined)
|
|
1413
|
+
p.error = error instanceof Error ? error.message : String(error);
|
|
1414
|
+
this.fillParentFields(runId, node, nodeKey, p);
|
|
1415
|
+
fillCompositeFields(node, p);
|
|
1416
|
+
const leaf = this.leafMeta.get(nodeKey);
|
|
1417
|
+
if (leaf) {
|
|
1418
|
+
if (leaf.failureKind !== undefined)
|
|
1419
|
+
p.failure_kind = leaf.failureKind;
|
|
1420
|
+
// Program detail.
|
|
1421
|
+
if (leaf.command !== undefined)
|
|
1422
|
+
p.command = leaf.command;
|
|
1423
|
+
if (leaf.shell !== undefined)
|
|
1424
|
+
p.shell = leaf.shell;
|
|
1425
|
+
if (leaf.subprocessEnv !== undefined)
|
|
1426
|
+
p.subprocess_env = leaf.subprocessEnv;
|
|
1427
|
+
if (leaf.exitCode !== undefined)
|
|
1428
|
+
p.exit_code = leaf.exitCode;
|
|
1429
|
+
if (leaf.stdout !== undefined)
|
|
1430
|
+
p.stdout = leaf.stdout;
|
|
1431
|
+
if (leaf.stderr !== undefined)
|
|
1432
|
+
p.stderr = leaf.stderr;
|
|
1433
|
+
// Agent detail.
|
|
1434
|
+
if (leaf.agentModel !== undefined)
|
|
1435
|
+
p.agent_model = leaf.agentModel;
|
|
1436
|
+
if (leaf.agentType !== undefined)
|
|
1437
|
+
p.agent_type = leaf.agentType;
|
|
1438
|
+
if (leaf.agentPolicy !== undefined)
|
|
1439
|
+
p.agent_policy = leaf.agentPolicy;
|
|
1440
|
+
if (leaf.sessionKey !== undefined)
|
|
1441
|
+
p.session_key = leaf.sessionKey;
|
|
1442
|
+
if (leaf.agentExitCode !== undefined)
|
|
1443
|
+
p.agent_exit_code = leaf.agentExitCode;
|
|
1444
|
+
if (leaf.agentResponseText !== undefined)
|
|
1445
|
+
p.agent_response_text = leaf.agentResponseText;
|
|
1446
|
+
}
|
|
1447
|
+
else if (hookFailure) {
|
|
1448
|
+
// A node that failed because an injector failed under `fail` policy.
|
|
1449
|
+
p.failure_kind = "hook_failure";
|
|
1450
|
+
}
|
|
1451
|
+
return p;
|
|
1452
|
+
};
|
|
1453
|
+
// The specific lifecycle event (onNodeStart/Complete/... ); onStateChange is
|
|
1454
|
+
// never emitted via this branch — it always carries from/to below.
|
|
1455
|
+
if (firesSpecific && name !== "onStateChange")
|
|
1456
|
+
await this.hookRunner.emitEvent(name, build(name));
|
|
1457
|
+
// onStateChange only fires when the state field actually changed.
|
|
1458
|
+
if (firesStateChange && fromState !== state.state) {
|
|
1459
|
+
const p = build("onStateChange");
|
|
1460
|
+
p.from_state = fromState;
|
|
1461
|
+
p.to_state = state.state;
|
|
1462
|
+
await this.hookRunner.emitEvent("onStateChange", p);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
/** Fire a run-level event (beforeRun / afterRun). */
|
|
1466
|
+
async emitRunEvent(name, scope, enrich) {
|
|
1467
|
+
if (!this.hookRunner || !this.hookRunner.hasEvent(name))
|
|
1468
|
+
return;
|
|
1469
|
+
const payload = basePayload(scope, name);
|
|
1470
|
+
enrich(payload);
|
|
1471
|
+
await this.hookRunner.emitEvent(name, payload);
|
|
1472
|
+
}
|
|
1473
|
+
/** Recover the dynamic key dimensions from an expression context. */
|
|
1474
|
+
dynamicFromCtx(ctx) {
|
|
1475
|
+
const dynamic = {};
|
|
1476
|
+
if (ctx.loop?.iter !== undefined)
|
|
1477
|
+
dynamic.loopRound = ctx.loop.iter;
|
|
1478
|
+
if (ctx.item_id !== undefined)
|
|
1479
|
+
dynamic.fanoutItemId = ctx.item_id;
|
|
1480
|
+
return dynamic;
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Populate parent_node_key/parent_node_kind from the Run's frozen IR. The
|
|
1484
|
+
* parent's resolved key shares this node's dynamic prefix, so we map the
|
|
1485
|
+
* static parent path onto the dynamic portion of the child key.
|
|
1486
|
+
*/
|
|
1487
|
+
fillParentFields(runId, node, nodeKey, payload) {
|
|
1488
|
+
const ir = this.store.readIr(runId);
|
|
1489
|
+
if (!ir)
|
|
1490
|
+
return;
|
|
1491
|
+
const parent = findParentNode(ir.root, node.id);
|
|
1492
|
+
if (!parent || parent.id === ir.root.id && parent.kind === "pipeline") {
|
|
1493
|
+
// The workflow root is an implicit container; only surface real parents.
|
|
1494
|
+
if (!parent || parent.nodePath.length === 0)
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
if (!parent)
|
|
1498
|
+
return;
|
|
1499
|
+
payload.parent_node_kind = parent.kind;
|
|
1500
|
+
// The child static path is parent path + child id; the parent's resolved key
|
|
1501
|
+
// is the child key with the trailing static `/childId` segment removed.
|
|
1502
|
+
const childStatic = staticNodePathFromKey(nodeKey);
|
|
1503
|
+
const childIdSeg = `/${node.id}`;
|
|
1504
|
+
if (childStatic.endsWith(childIdSeg)) {
|
|
1505
|
+
const idx = nodeKey.lastIndexOf(childIdSeg);
|
|
1506
|
+
if (idx > 0)
|
|
1507
|
+
payload.parent_node_key = nodeKey.slice(0, idx);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
/** Drop per-node leaf metadata once its terminal events have fired. */
|
|
1511
|
+
clearLeafMeta(nodeKey) {
|
|
1512
|
+
this.leafMeta.delete(nodeKey);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
function hookAgentTelemetry(state) {
|
|
1516
|
+
const telemetry = state.agentTelemetry;
|
|
1517
|
+
const attempt = telemetry?.attempts.find((item) => item.attempt === telemetry.currentAttempt)
|
|
1518
|
+
?? telemetry?.attempts[telemetry.attempts.length - 1];
|
|
1519
|
+
if (!attempt)
|
|
1520
|
+
return undefined;
|
|
1521
|
+
const result = {
|
|
1522
|
+
attempt: attempt.attempt,
|
|
1523
|
+
state: attempt.state,
|
|
1524
|
+
updated_at: attempt.updatedAt
|
|
1525
|
+
};
|
|
1526
|
+
if (attempt.completedAt)
|
|
1527
|
+
result.completed_at = attempt.completedAt;
|
|
1528
|
+
if (attempt.context) {
|
|
1529
|
+
result.context = {
|
|
1530
|
+
used: attempt.context.used,
|
|
1531
|
+
size: attempt.context.size,
|
|
1532
|
+
updated_at: attempt.context.updatedAt
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
if (attempt.tokenUsage) {
|
|
1536
|
+
result.token_usage = {
|
|
1537
|
+
source: attempt.tokenUsage.source
|
|
1538
|
+
};
|
|
1539
|
+
if (attempt.tokenUsage.inputTokens !== undefined)
|
|
1540
|
+
result.token_usage.input_tokens = attempt.tokenUsage.inputTokens;
|
|
1541
|
+
if (attempt.tokenUsage.outputTokens !== undefined)
|
|
1542
|
+
result.token_usage.output_tokens = attempt.tokenUsage.outputTokens;
|
|
1543
|
+
if (attempt.tokenUsage.cachedReadTokens !== undefined)
|
|
1544
|
+
result.token_usage.cached_read_tokens = attempt.tokenUsage.cachedReadTokens;
|
|
1545
|
+
if (attempt.tokenUsage.cachedWriteTokens !== undefined)
|
|
1546
|
+
result.token_usage.cached_write_tokens = attempt.tokenUsage.cachedWriteTokens;
|
|
1547
|
+
if (attempt.tokenUsage.thoughtTokens !== undefined)
|
|
1548
|
+
result.token_usage.thought_tokens = attempt.tokenUsage.thoughtTokens;
|
|
1549
|
+
if (attempt.tokenUsage.totalTokens !== undefined)
|
|
1550
|
+
result.token_usage.total_tokens = attempt.tokenUsage.totalTokens;
|
|
1551
|
+
}
|
|
1552
|
+
return result;
|
|
1227
1553
|
}
|
|
1228
1554
|
/** Generate a locally sortable run ID: yyyyMMddHHmmss + 20 uppercase hex chars. */
|
|
1229
1555
|
export function generateRunId(now = new Date()) {
|
|
@@ -1245,6 +1571,60 @@ function nestedParallelBranchDynamic(dynamic, branchIndex) {
|
|
|
1245
1571
|
parallelBranchId: dynamic.parallelBranchId === undefined ? branchId : `${dynamic.parallelBranchId}.${branchId}`
|
|
1246
1572
|
};
|
|
1247
1573
|
}
|
|
1574
|
+
/** Find the immediate parent IR node of `childId`, or undefined for the root. */
|
|
1575
|
+
function findParentNode(node, childId) {
|
|
1576
|
+
for (const child of node.children ?? []) {
|
|
1577
|
+
if (child.id === childId)
|
|
1578
|
+
return node;
|
|
1579
|
+
const found = findParentNode(child, childId);
|
|
1580
|
+
if (found)
|
|
1581
|
+
return found;
|
|
1582
|
+
}
|
|
1583
|
+
for (const branch of node.branches ?? []) {
|
|
1584
|
+
for (const child of branch.children) {
|
|
1585
|
+
if (child.id === childId)
|
|
1586
|
+
return node;
|
|
1587
|
+
const found = findParentNode(child, childId);
|
|
1588
|
+
if (found)
|
|
1589
|
+
return found;
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
return undefined;
|
|
1593
|
+
}
|
|
1594
|
+
/** Populate composite-container-specific payload fields from node metadata. */
|
|
1595
|
+
function fillCompositeFields(node, p) {
|
|
1596
|
+
const m = node.metadata;
|
|
1597
|
+
switch (node.kind) {
|
|
1598
|
+
case "parallel":
|
|
1599
|
+
if (typeof m.join === "string")
|
|
1600
|
+
p.join_strategy = m.join;
|
|
1601
|
+
if (typeof m.max_concurrency === "number")
|
|
1602
|
+
p.max_concurrency = m.max_concurrency;
|
|
1603
|
+
break;
|
|
1604
|
+
case "fanout":
|
|
1605
|
+
if (typeof m.join === "string")
|
|
1606
|
+
p.join_strategy = m.join;
|
|
1607
|
+
if (typeof m.max_concurrency === "number")
|
|
1608
|
+
p.max_concurrency = m.max_concurrency;
|
|
1609
|
+
break;
|
|
1610
|
+
case "loop":
|
|
1611
|
+
if (typeof m.max_iterations === "number")
|
|
1612
|
+
p.max_iterations = m.max_iterations;
|
|
1613
|
+
break;
|
|
1614
|
+
case "subworkflow":
|
|
1615
|
+
if (typeof m.subworkflow === "string")
|
|
1616
|
+
p.subworkflow_spec_path = m.subworkflow;
|
|
1617
|
+
break;
|
|
1618
|
+
case "run.signal":
|
|
1619
|
+
if (typeof m.timeout === "string")
|
|
1620
|
+
p.signal_timeout = m.timeout;
|
|
1621
|
+
if (typeof m.on_timeout === "string")
|
|
1622
|
+
p.signal_on_timeout = m.on_timeout;
|
|
1623
|
+
break;
|
|
1624
|
+
default:
|
|
1625
|
+
break;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1248
1628
|
class NodeAbortedError extends Error {
|
|
1249
1629
|
nodeKey;
|
|
1250
1630
|
state;
|