pi-workflow-engine 0.10.3 → 0.12.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/.pi/extensions/pi-workflow-engine/index.ts +418 -213
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +11 -4
- package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +147 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-limits.ts +35 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +399 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +116 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +91 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +154 -530
- package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +256 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +368 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +87 -39
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +89 -0
- package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +75 -0
- package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +492 -0
- package/.pi/extensions/pi-workflow-engine/src/budget.ts +10 -3
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +55 -3
- package/.pi/extensions/pi-workflow-engine/src/command-completions.ts +36 -0
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +230 -61
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +38 -55
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +70 -17
- package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +59 -13
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +123 -63
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +357 -75
- package/.pi/extensions/pi-workflow-engine/src/filesystem-error.ts +4 -0
- package/.pi/extensions/pi-workflow-engine/src/finalizers.ts +66 -0
- package/.pi/extensions/pi-workflow-engine/src/identity-canonicalization.ts +138 -0
- package/.pi/extensions/pi-workflow-engine/src/identity-fingerprint.ts +15 -0
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +10 -5
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +156 -66
- package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +167 -0
- package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +310 -0
- package/.pi/extensions/pi-workflow-engine/src/options.ts +96 -6
- package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +32 -0
- package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +203 -39
- package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +74 -122
- package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +189 -0
- package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +48 -0
- package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +89 -0
- package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +289 -0
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +701 -0
- package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +49 -0
- package/.pi/extensions/pi-workflow-engine/src/review/github-pr-comments.ts +75 -16
- package/.pi/extensions/pi-workflow-engine/src/review/review-actions.ts +83 -17
- package/.pi/extensions/pi-workflow-engine/src/review/review-budget.ts +45 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +117 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +14 -66
- package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +38 -12
- package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +42 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +18 -22
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +128 -56
- package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +153 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +294 -0
- package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +130 -0
- package/.pi/extensions/pi-workflow-engine/src/session-identity.ts +6 -0
- package/.pi/extensions/pi-workflow-engine/src/structured-output.ts +32 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +44 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +150 -0
- package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +401 -0
- package/.pi/extensions/pi-workflow-engine/src/types.ts +87 -25
- package/.pi/extensions/pi-workflow-engine/src/ui/dynamax-editor-decoration.ts +220 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +26 -6
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +88 -46
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +7 -70
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-viewer-layout.ts +51 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +12 -67
- package/.pi/extensions/pi-workflow-engine/src/unknown-error.ts +8 -0
- package/.pi/extensions/pi-workflow-engine/src/usage.ts +124 -16
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +23 -3
- package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +121 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +19 -1
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +72 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +354 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +162 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +632 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +172 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +80 -0
- package/.pi/extensions/pi-workflow-engine/src/workflows.ts +30 -3
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +321 -43
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +62 -64
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +17 -17
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +15 -16
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +15 -15
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +170 -0
- package/README.md +82 -207
- package/USAGE.md +295 -21
- package/package.json +9 -9
|
@@ -1,19 +1,41 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import type { Static, TSchema } from "typebox";
|
|
3
|
+
import { bindParallel, bindPipeline, Semaphore } from "./concurrency.ts";
|
|
4
|
+
import { WorkflowAgentLimiter } from "./agent-limits.ts";
|
|
5
|
+
import { defaultAgentRetryScheduler, type AgentRetryScheduler } from "./agent-retry.ts";
|
|
6
|
+
import { resolveWorkflowModelProfiles, type ResolvedWorkflowModelProfiles } from "./model-profiles.ts";
|
|
7
|
+
import { abortReason, isWorkflowPauseError, linkAbortSignal, throwIfAborted } from "./cancellation.ts";
|
|
4
8
|
import { createBudget } from "./budget.ts";
|
|
5
|
-
import { runAgent, type RunContext } from "./agent-runner.ts";
|
|
9
|
+
import { runAgent, type AgentExecutionOptions, type RunContext } from "./agent-runner.ts";
|
|
6
10
|
import { ProgressTracker } from "./progress.ts";
|
|
7
|
-
import { createPerfRecorder, type PerfSnapshot } from "./perf.ts";
|
|
8
|
-
import { createWorkflowUsageRecorder } from "./usage.ts";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
+
import { createPerfRecorder, type PerfSink, type PerfSnapshot } from "./perf.ts";
|
|
12
|
+
import { createWorkflowUsageRecorder, type WorkflowUsageSink } from "./usage.ts";
|
|
13
|
+
import {
|
|
14
|
+
resolveWorkflowRunOptions,
|
|
15
|
+
type ResolvedWorkflowRunOptions,
|
|
16
|
+
} from "./options.ts";
|
|
17
|
+
import type { AgentOptions, IsolatedAgentResult, LoadedWorkflow, WorkflowApi, WorkflowProgressEvent, WorkflowRef, WorkflowRunOptions } from "./types.ts";
|
|
11
18
|
import { WorkflowInspector } from "./ui/workflow-inspector.ts";
|
|
19
|
+
import { WORKFLOW_VIEWER_OVERLAY_OPTIONS } from "./ui/workflow-viewer-layout.ts";
|
|
12
20
|
import { createWorkflowJournal, createWorkflowRunId, pruneWorkflowJournals, workflowJournalPath } from "./journal.ts";
|
|
13
21
|
import { WorktreeRegistry } from "./worktree.ts";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
import { runFinalizers } from "./finalizers.ts";
|
|
23
|
+
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
24
|
+
import {
|
|
25
|
+
captureWorkflowResumeContext,
|
|
26
|
+
type AgentResumeBaseContext,
|
|
27
|
+
} from "./resume-context.ts";
|
|
28
|
+
import {
|
|
29
|
+
DurableWorkflowRun,
|
|
30
|
+
ProjectWorkflowRunStore,
|
|
31
|
+
workflowRunRecordPath,
|
|
32
|
+
type WorkflowRunStore,
|
|
33
|
+
} from "./workflow-run-store.ts";
|
|
34
|
+
import { createWorkflowRunRecord } from "./workflow-run-record.ts";
|
|
35
|
+
import {
|
|
36
|
+
createProviderUsageLimitPauseRecord,
|
|
37
|
+
WorkflowProviderUsageLimitError,
|
|
38
|
+
} from "./provider-usage-limit.ts";
|
|
17
39
|
|
|
18
40
|
/** The workflow-facing slice of the progress tracker (satisfied by `ProgressTracker`). */
|
|
19
41
|
export interface WorkflowProgress {
|
|
@@ -22,6 +44,9 @@ export interface WorkflowProgress {
|
|
|
22
44
|
event(event: WorkflowProgressEvent): void;
|
|
23
45
|
}
|
|
24
46
|
|
|
47
|
+
/** Engine state shared by the workflow and its individual agent calls. */
|
|
48
|
+
export type WorkflowRunContext = RunContext;
|
|
49
|
+
|
|
25
50
|
/** Per-context knobs threaded through `runWorkflowWithContext` (and re-derived for each sub-workflow). */
|
|
26
51
|
export interface WorkflowContextOptions {
|
|
27
52
|
/** Controller whose `.signal` equals `rc.signal`; aborts in-flight `parallel` siblings on failure. */
|
|
@@ -29,7 +54,7 @@ export interface WorkflowContextOptions {
|
|
|
29
54
|
/** Eager-submission cap for `parallel`. */
|
|
30
55
|
submissionLimit: number;
|
|
31
56
|
/** Resolve a sub-workflow reference for `api.workflow()`. Omit to disable composition. */
|
|
32
|
-
resolveWorkflow?: (ref: WorkflowRef) => Promise<
|
|
57
|
+
resolveWorkflow?: (ref: WorkflowRef) => Promise<LoadedWorkflow>;
|
|
33
58
|
/** Nesting depth. Sub-workflows run at depth >= 1, where `api.workflow()` rejects. */
|
|
34
59
|
depth?: number;
|
|
35
60
|
/** Prefix applied to phase titles so sub-workflow phases nest as "<name> ▸ <title>". */
|
|
@@ -38,59 +63,126 @@ export interface WorkflowContextOptions {
|
|
|
38
63
|
progressNamespace?: string;
|
|
39
64
|
}
|
|
40
65
|
|
|
66
|
+
type Outcome<T> =
|
|
67
|
+
| { readonly ok: true; readonly value: T }
|
|
68
|
+
| { readonly ok: false; readonly error: unknown };
|
|
69
|
+
|
|
70
|
+
export interface WorkflowEngineDependencies {
|
|
71
|
+
readonly worktrees?: WorktreeRegistry;
|
|
72
|
+
readonly retryScheduler?: AgentRetryScheduler;
|
|
73
|
+
readonly modelProfiles?: ResolvedWorkflowModelProfiles;
|
|
74
|
+
}
|
|
75
|
+
|
|
41
76
|
/**
|
|
42
77
|
* Run a workflow module: build the per-run primitives (binding agent/parallel/pipeline
|
|
43
78
|
* to a shared semaphore + progress tracker), invoke the workflow, return its result.
|
|
44
79
|
*/
|
|
45
80
|
export async function runWorkflow(
|
|
46
81
|
ctx: ExtensionContext,
|
|
47
|
-
mod:
|
|
82
|
+
mod: LoadedWorkflow,
|
|
48
83
|
args: string,
|
|
49
84
|
options: WorkflowRunOptions = {},
|
|
85
|
+
dependencies: WorkflowEngineDependencies = {},
|
|
50
86
|
): Promise<unknown> {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const progressSource = { snapshot: () => progress.snapshot() };
|
|
54
|
-
resolvedOptions.onProgressSource?.(progressSource);
|
|
55
|
-
if (resolvedOptions.inspect && ctx.hasUI) {
|
|
56
|
-
void ctx.ui
|
|
57
|
-
.custom<void>(
|
|
58
|
-
(tui, theme, _keybindings, done) => new WorkflowInspector(() => progress.snapshot(), tui, theme, () => done(undefined)),
|
|
59
|
-
{ overlay: true, overlayOptions: { anchor: "right-center", width: "60%", maxHeight: "80%", margin: 1 } },
|
|
60
|
-
)
|
|
61
|
-
.catch((error: unknown) => progress.log(`inspector failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
62
|
-
}
|
|
87
|
+
return await runResolvedWorkflow(ctx, mod, args, resolveWorkflowRunOptions(options), dependencies);
|
|
88
|
+
}
|
|
63
89
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
90
|
+
/** Execute an options-normalized workflow without re-reading environment defaults. */
|
|
91
|
+
export async function runResolvedWorkflow(
|
|
92
|
+
ctx: ExtensionContext,
|
|
93
|
+
mod: LoadedWorkflow,
|
|
94
|
+
args: string,
|
|
95
|
+
resolvedOptions: ResolvedWorkflowRunOptions,
|
|
96
|
+
dependencies: WorkflowEngineDependencies = {},
|
|
97
|
+
): Promise<unknown> {
|
|
67
98
|
const runId = resolvedOptions.runId ?? createWorkflowRunId();
|
|
99
|
+
let durableProgress: DurableWorkflowRun | undefined;
|
|
100
|
+
const progress = new ProgressTracker(ctx, mod.meta.name, runId, (snapshot) => durableProgress?.updateProgress(snapshot));
|
|
101
|
+
const progressSource = { snapshot: () => progress.snapshot() };
|
|
102
|
+
const perf = resolvedOptions.perfRecorder ?? createPerfRecorder(resolvedOptions.perf);
|
|
103
|
+
const usage = createWorkflowUsageRecorder((snapshot) => progress.updateUsage(snapshot));
|
|
104
|
+
const budget = createBudget(resolvedOptions.budget, usage);
|
|
68
105
|
const journalPath = workflowJournalPath(ctx.cwd, runId);
|
|
69
106
|
const resumePath = resolvedOptions.resumeFromRunId ? workflowJournalPath(ctx.cwd, resolvedOptions.resumeFromRunId) : undefined;
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
107
|
+
const runStore = new ProjectWorkflowRunStore(ctx.cwd);
|
|
108
|
+
let persistenceFailureReported = false;
|
|
109
|
+
const durableRun = new DurableWorkflowRun(
|
|
110
|
+
runStore,
|
|
111
|
+
createWorkflowRunRecord({
|
|
112
|
+
runId,
|
|
113
|
+
workflow: mod,
|
|
114
|
+
options: resolvedOptions,
|
|
115
|
+
progress: progress.snapshot(),
|
|
116
|
+
argumentsPresent: args.length > 0,
|
|
117
|
+
}),
|
|
118
|
+
(error) => {
|
|
119
|
+
if (persistenceFailureReported) return;
|
|
120
|
+
persistenceFailureReported = true;
|
|
121
|
+
process.stderr.write(`[${mod.meta.name}] run persistence failed: ${unknownErrorMessage(error)}\n`);
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
durableProgress = durableRun;
|
|
125
|
+
const worktrees = dependencies.worktrees ?? new WorktreeRegistry(ctx.cwd);
|
|
74
126
|
const runAbortController = new AbortController();
|
|
75
127
|
const unlinkContextAbortSignal = linkAbortSignal(ctx.signal, runAbortController);
|
|
76
128
|
const unlinkOptionAbortSignal = linkAbortSignal(resolvedOptions.signal, runAbortController);
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
129
|
+
const workflowOutcome = await captureOutcome(async () => {
|
|
130
|
+
await notifyLifecycleObserver(progress, "progress source callback", () => resolvedOptions.onProgressSource?.(progressSource));
|
|
131
|
+
if (resolvedOptions.inspect && ctx.hasUI && ctx.mode === "tui") {
|
|
132
|
+
void ctx.ui
|
|
133
|
+
.custom<void>(
|
|
134
|
+
(tui, theme, _keybindings, done) => new WorkflowInspector(() => progress.snapshot(), tui, theme, () => done(undefined)),
|
|
135
|
+
WORKFLOW_VIEWER_OVERLAY_OPTIONS,
|
|
136
|
+
)
|
|
137
|
+
.catch((error: unknown) => {
|
|
138
|
+
try {
|
|
139
|
+
progress.log(`inspector failed: ${unknownErrorMessage(error)}`);
|
|
140
|
+
} catch {
|
|
141
|
+
// Inspector reporting is detached and must never become another rejection.
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const journal = await createWorkflowJournal({ resumePath, writePath: journalPath });
|
|
147
|
+
durableRun.transition({ state: "running", progress: progress.snapshot() });
|
|
148
|
+
await durableRun.flush().catch(() => undefined);
|
|
149
|
+
await notifyLifecycleObserver(progress, "run metadata callback", () =>
|
|
150
|
+
resolvedOptions.onRunMetadata?.({
|
|
151
|
+
runId,
|
|
152
|
+
resumedFromRunId: resolvedOptions.resumeFromRunId,
|
|
153
|
+
journalPath,
|
|
154
|
+
recordPath: workflowRunRecordPath(ctx.cwd, runId),
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
progress.log(resolvedOptions.resumeFromRunId ? `run id: ${runId} (resuming from ${resolvedOptions.resumeFromRunId})` : `run id: ${runId}`);
|
|
158
|
+
const modelProfiles = dependencies.modelProfiles ?? resolveWorkflowModelProfiles({
|
|
159
|
+
cwd: ctx.cwd,
|
|
160
|
+
modelRegistry: ctx.modelRegistry,
|
|
161
|
+
hostModel: ctx.model,
|
|
162
|
+
});
|
|
163
|
+
const rc: WorkflowRunContext = {
|
|
164
|
+
cwd: ctx.cwd,
|
|
165
|
+
hostModel: ctx.model,
|
|
166
|
+
modelRegistry: ctx.modelRegistry,
|
|
167
|
+
semaphore: new Semaphore(resolvedOptions.concurrency),
|
|
168
|
+
agentLimiter: new WorkflowAgentLimiter(resolvedOptions.maxAgents),
|
|
169
|
+
agentTimeoutMs: resolvedOptions.agentTimeoutMs,
|
|
170
|
+
agentRetries: resolvedOptions.agentRetries,
|
|
171
|
+
pauseOnProviderUsageLimit: resolvedOptions.background !== undefined,
|
|
172
|
+
resumeEditedWorkflow: resolvedOptions.resumeEditedWorkflow,
|
|
173
|
+
retryScheduler: dependencies.retryScheduler ?? defaultAgentRetryScheduler,
|
|
174
|
+
modelProfiles,
|
|
175
|
+
progress,
|
|
176
|
+
signal: runAbortController.signal,
|
|
177
|
+
perf,
|
|
178
|
+
usage,
|
|
179
|
+
budget,
|
|
180
|
+
journal,
|
|
181
|
+
worktrees,
|
|
182
|
+
};
|
|
90
183
|
|
|
91
|
-
try {
|
|
92
184
|
// perf.total_ms wraps the whole tree: sub-workflows run inside this span via api.workflow().
|
|
93
|
-
return
|
|
185
|
+
return perf.time("workflow.total_ms", () =>
|
|
94
186
|
runWorkflowWithContext(rc, progress, mod, args, {
|
|
95
187
|
abortController: runAbortController,
|
|
96
188
|
submissionLimit: resolvedOptions.parallelSubmissionLimit ?? resolvedOptions.concurrency * 2,
|
|
@@ -99,29 +191,198 @@ export async function runWorkflow(
|
|
|
99
191
|
progressPrefix: "",
|
|
100
192
|
}),
|
|
101
193
|
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
progress
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const finalizationOutcome = await captureOutcome(() =>
|
|
197
|
+
finalizeWorkflowRun({
|
|
198
|
+
cwd: ctx.cwd,
|
|
199
|
+
options: resolvedOptions,
|
|
200
|
+
perf,
|
|
201
|
+
usage,
|
|
202
|
+
progress,
|
|
203
|
+
runStore,
|
|
204
|
+
worktrees,
|
|
205
|
+
unlinkSignals: [unlinkContextAbortSignal, unlinkOptionAbortSignal],
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
if (workflowOutcome.ok && finalizationOutcome.ok) {
|
|
210
|
+
durableRun.transition({
|
|
211
|
+
state: "completed",
|
|
212
|
+
progress: progress.snapshot(),
|
|
213
|
+
usage: usage.snapshot(),
|
|
214
|
+
result: workflowOutcome.value,
|
|
215
|
+
});
|
|
216
|
+
} else if (!workflowOutcome.ok) {
|
|
217
|
+
const error = finalizationOutcome.ok
|
|
218
|
+
? workflowOutcome.error
|
|
219
|
+
: combinedWorkflowError(workflowOutcome.error, finalizationOutcome.error);
|
|
220
|
+
persistTerminalWorkflowError(error);
|
|
221
|
+
} else if (!finalizationOutcome.ok) {
|
|
222
|
+
persistTerminalWorkflowError(finalizationOutcome.error);
|
|
223
|
+
}
|
|
224
|
+
await durableRun.flush().catch(() => undefined);
|
|
225
|
+
|
|
226
|
+
function persistTerminalWorkflowError(error: unknown): void {
|
|
227
|
+
const pauseError = backgroundPauseError(error, ctx.signal, resolvedOptions.signal);
|
|
228
|
+
if (pauseError) {
|
|
229
|
+
if (pauseError instanceof WorkflowProviderUsageLimitError) {
|
|
230
|
+
if (resolvedOptions.background === undefined) {
|
|
231
|
+
durableRun.transition({
|
|
232
|
+
state: "failed",
|
|
233
|
+
progress: progress.snapshot(),
|
|
234
|
+
usage: usage.snapshot(),
|
|
235
|
+
error: pauseError,
|
|
236
|
+
});
|
|
237
|
+
return;
|
|
120
238
|
}
|
|
239
|
+
const providerPause = createProviderUsageLimitPauseRecord(
|
|
240
|
+
pauseError,
|
|
241
|
+
resolvedOptions,
|
|
242
|
+
mod.source.kind === "file" && args.length === 0,
|
|
243
|
+
);
|
|
244
|
+
durableRun.transition({
|
|
245
|
+
state: "paused",
|
|
246
|
+
progress: progress.snapshot(),
|
|
247
|
+
...providerPause,
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
121
250
|
}
|
|
122
|
-
|
|
251
|
+
durableRun.transition({
|
|
252
|
+
state: "paused",
|
|
253
|
+
progress: progress.snapshot(),
|
|
254
|
+
message: unknownErrorMessage(pauseError),
|
|
255
|
+
});
|
|
256
|
+
return;
|
|
123
257
|
}
|
|
258
|
+
durableRun.transition({
|
|
259
|
+
state: ctx.signal?.aborted || resolvedOptions.signal?.aborted ? "stopped" : "failed",
|
|
260
|
+
progress: progress.snapshot(),
|
|
261
|
+
usage: usage.snapshot(),
|
|
262
|
+
error,
|
|
263
|
+
});
|
|
124
264
|
}
|
|
265
|
+
|
|
266
|
+
if (workflowOutcome.ok) {
|
|
267
|
+
if (finalizationOutcome.ok) return workflowOutcome.value;
|
|
268
|
+
throw finalizationOutcome.error;
|
|
269
|
+
}
|
|
270
|
+
if (finalizationOutcome.ok) throw workflowOutcome.error;
|
|
271
|
+
throw combinedWorkflowError(workflowOutcome.error, finalizationOutcome.error);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function backgroundPauseError(error: unknown, ...signals: Array<AbortSignal | undefined>): unknown {
|
|
275
|
+
if (isWorkflowPauseError(error)) return error;
|
|
276
|
+
for (const signal of signals) {
|
|
277
|
+
if (!signal?.aborted) continue;
|
|
278
|
+
const reason = abortReason(signal);
|
|
279
|
+
if (isWorkflowPauseError(reason)) return reason;
|
|
280
|
+
}
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function captureOutcome<T>(operation: () => Promise<T>): Promise<Outcome<T>> {
|
|
285
|
+
try {
|
|
286
|
+
return { ok: true, value: await operation() };
|
|
287
|
+
} catch (error) {
|
|
288
|
+
return { ok: false, error };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
interface WorkflowFinalizationInput {
|
|
293
|
+
readonly cwd: string;
|
|
294
|
+
readonly options: ResolvedWorkflowRunOptions;
|
|
295
|
+
readonly perf: PerfSink;
|
|
296
|
+
readonly usage: WorkflowUsageSink;
|
|
297
|
+
readonly progress: ProgressTracker;
|
|
298
|
+
readonly runStore: WorkflowRunStore;
|
|
299
|
+
readonly worktrees: WorktreeRegistry;
|
|
300
|
+
readonly unlinkSignals: readonly (() => void)[];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function finalizeWorkflowRun(input: WorkflowFinalizationInput): Promise<void> {
|
|
304
|
+
await runFinalizers(
|
|
305
|
+
[
|
|
306
|
+
{
|
|
307
|
+
name: "usage snapshot callback",
|
|
308
|
+
criticality: "best-effort",
|
|
309
|
+
run: () => input.options.onUsageSnapshot?.(input.usage.snapshot()),
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
name: "performance snapshot callback",
|
|
313
|
+
criticality: "best-effort",
|
|
314
|
+
run: async () => {
|
|
315
|
+
if (!input.options.perf) return;
|
|
316
|
+
const snapshot = input.perf.snapshot();
|
|
317
|
+
await input.options.onPerfSnapshot?.(snapshot);
|
|
318
|
+
input.progress.log(formatPerfSummary(snapshot));
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
...input.unlinkSignals.map((run, index) => ({
|
|
322
|
+
name: `abort signal unlink ${index + 1}`,
|
|
323
|
+
criticality: "required" as const,
|
|
324
|
+
run,
|
|
325
|
+
})),
|
|
326
|
+
{
|
|
327
|
+
name: "worktree cleanup",
|
|
328
|
+
criticality: "required",
|
|
329
|
+
run: () => input.worktrees.removeAll(),
|
|
330
|
+
},
|
|
331
|
+
{ name: "journal pruning", criticality: "best-effort", run: () => pruneWorkflowJournals(input.cwd) },
|
|
332
|
+
{ name: "run record pruning", criticality: "best-effort", run: () => input.runStore.prune() },
|
|
333
|
+
{ name: "progress completion", criticality: "best-effort", run: () => input.progress.done() },
|
|
334
|
+
{
|
|
335
|
+
name: "progress snapshot callback",
|
|
336
|
+
criticality: "best-effort",
|
|
337
|
+
run: () => input.options.onProgressSnapshot?.(input.progress.snapshot()),
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
name: "progress source clear",
|
|
341
|
+
criticality: "best-effort",
|
|
342
|
+
run: () => input.options.onProgressSource?.(undefined),
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
{
|
|
346
|
+
onBestEffortFailure: (failure) => {
|
|
347
|
+
try {
|
|
348
|
+
input.progress.log(`${failure.name} failed: ${unknownErrorMessage(failure.error)}`);
|
|
349
|
+
} catch {
|
|
350
|
+
// Observer failures must never replace the workflow outcome.
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function combinedWorkflowError(workflowError: unknown, finalizationError: unknown): AggregateError {
|
|
358
|
+
return new AggregateError(
|
|
359
|
+
[workflowError, finalizationError],
|
|
360
|
+
`Workflow failed: ${unknownErrorMessage(workflowError)}; finalization also failed: ${unknownErrorMessage(finalizationError)}`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function createWorkflowAgent(
|
|
365
|
+
rc: WorkflowRunContext,
|
|
366
|
+
scope: WorkflowScope,
|
|
367
|
+
mod: LoadedWorkflow,
|
|
368
|
+
resumeContext: AgentResumeBaseContext,
|
|
369
|
+
): WorkflowApi["agent"] {
|
|
370
|
+
function agent<S extends TSchema>(
|
|
371
|
+
prompt: string,
|
|
372
|
+
opts: AgentOptions<S> & { schema: S; isolation: "worktree" },
|
|
373
|
+
): Promise<IsolatedAgentResult<Static<S>>>;
|
|
374
|
+
function agent(prompt: string, opts: AgentOptions & { isolation: "worktree" }): Promise<IsolatedAgentResult<string>>;
|
|
375
|
+
function agent<S extends TSchema>(prompt: string, opts: AgentOptions<S> & { schema: S }): Promise<Static<S>>;
|
|
376
|
+
function agent(prompt: string, opts?: AgentOptions): Promise<string>;
|
|
377
|
+
function agent(prompt: string, agentOpts?: AgentOptions): Promise<unknown> {
|
|
378
|
+
const scopedOptions = scope.agentOptions(agentOpts);
|
|
379
|
+
const executionOptions: AgentExecutionOptions =
|
|
380
|
+
scopedOptions.isolation === "worktree" && mod.isolatedWorktreeBaseline !== undefined
|
|
381
|
+
? { ...scopedOptions, worktreeBaseline: mod.isolatedWorktreeBaseline }
|
|
382
|
+
: scopedOptions;
|
|
383
|
+
return runAgent(rc, prompt, executionOptions, resumeContext);
|
|
384
|
+
}
|
|
385
|
+
return agent;
|
|
125
386
|
}
|
|
126
387
|
|
|
127
388
|
/**
|
|
@@ -130,16 +391,19 @@ export async function runWorkflow(
|
|
|
130
391
|
* abort signal, and perf sink. No setup/teardown of its own — that belongs to `runWorkflow`.
|
|
131
392
|
*/
|
|
132
393
|
export async function runWorkflowWithContext(
|
|
133
|
-
rc:
|
|
394
|
+
rc: WorkflowRunContext,
|
|
134
395
|
progress: WorkflowProgress,
|
|
135
|
-
mod:
|
|
396
|
+
mod: LoadedWorkflow,
|
|
136
397
|
args: string,
|
|
137
398
|
opts: WorkflowContextOptions,
|
|
138
399
|
): Promise<unknown> {
|
|
139
400
|
const depth = opts.depth ?? 0;
|
|
140
401
|
const scope = createWorkflowScope(progress, opts.progressPrefix ?? "", opts.progressNamespace);
|
|
402
|
+
const resumeContext: AgentResumeBaseContext = {
|
|
403
|
+
workflow: await captureWorkflowResumeContext(mod, rc.signal),
|
|
404
|
+
};
|
|
141
405
|
|
|
142
|
-
const agent = (
|
|
406
|
+
const agent = createWorkflowAgent(rc, scope, mod, resumeContext);
|
|
143
407
|
|
|
144
408
|
const workflow: WorkflowApi["workflow"] =
|
|
145
409
|
depth >= 1
|
|
@@ -171,12 +435,11 @@ export async function runWorkflowWithContext(
|
|
|
171
435
|
const api: WorkflowApi = {
|
|
172
436
|
agent,
|
|
173
437
|
workflow,
|
|
174
|
-
parallel: (
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}),
|
|
438
|
+
parallel: bindParallel({
|
|
439
|
+
signal: rc.signal,
|
|
440
|
+
abortController: opts.abortController,
|
|
441
|
+
limit: opts.submissionLimit,
|
|
442
|
+
}),
|
|
180
443
|
pipeline: bindPipeline({ signal: rc.signal, abortController: opts.abortController }),
|
|
181
444
|
phase: scope.phase,
|
|
182
445
|
log: scope.log,
|
|
@@ -187,7 +450,10 @@ export async function runWorkflowWithContext(
|
|
|
187
450
|
signal: rc.signal,
|
|
188
451
|
};
|
|
189
452
|
|
|
190
|
-
|
|
453
|
+
throwIfAborted(rc.signal);
|
|
454
|
+
const result = await mod.default(api);
|
|
455
|
+
throwIfAborted(rc.signal);
|
|
456
|
+
return result;
|
|
191
457
|
}
|
|
192
458
|
|
|
193
459
|
interface WorkflowScope {
|
|
@@ -242,3 +508,19 @@ function formatPerfSummary(snapshot: PerfSnapshot): string {
|
|
|
242
508
|
.map((aggregate) => `${aggregate.name} ${Math.round(aggregate.total)}ms`);
|
|
243
509
|
return parts.length > 0 ? `perf: ${parts.join(", ")}` : "perf: no samples";
|
|
244
510
|
}
|
|
511
|
+
|
|
512
|
+
async function notifyLifecycleObserver(
|
|
513
|
+
progress: ProgressTracker,
|
|
514
|
+
name: string,
|
|
515
|
+
notify: () => void | Promise<void>,
|
|
516
|
+
): Promise<void> {
|
|
517
|
+
try {
|
|
518
|
+
await notify();
|
|
519
|
+
} catch (error) {
|
|
520
|
+
try {
|
|
521
|
+
progress.log(`${name} failed: ${unknownErrorMessage(error)}`);
|
|
522
|
+
} catch {
|
|
523
|
+
// Lifecycle observers are advisory and must not replace the workflow outcome.
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
2
|
+
|
|
3
|
+
export type FinalizerCriticality = "required" | "best-effort";
|
|
4
|
+
|
|
5
|
+
export interface Finalizer {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly criticality: FinalizerCriticality;
|
|
8
|
+
readonly run: () => void | Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface FinalizerFailure {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly criticality: FinalizerCriticality;
|
|
14
|
+
readonly error: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RunFinalizersOptions {
|
|
18
|
+
readonly onBestEffortFailure?: (failure: FinalizerFailure) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class RequiredFinalizerError extends AggregateError {
|
|
22
|
+
readonly failures: readonly FinalizerFailure[];
|
|
23
|
+
|
|
24
|
+
constructor(failures: readonly FinalizerFailure[]) {
|
|
25
|
+
super(
|
|
26
|
+
failures.map((failure) => failure.error),
|
|
27
|
+
`Required workflow finalization failed: ${failures.map((failure) => `${failure.name} (${unknownErrorMessage(failure.error)})`).join(", ")}`,
|
|
28
|
+
);
|
|
29
|
+
this.name = "RequiredFinalizerError";
|
|
30
|
+
this.failures = failures;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Execute every finalizer once, reporting advisory failures and aggregating only required failures. */
|
|
35
|
+
export async function runFinalizers(
|
|
36
|
+
finalizers: readonly Finalizer[],
|
|
37
|
+
options: RunFinalizersOptions = {},
|
|
38
|
+
): Promise<readonly FinalizerFailure[]> {
|
|
39
|
+
const requiredFailures: FinalizerFailure[] = [];
|
|
40
|
+
const bestEffortFailures: FinalizerFailure[] = [];
|
|
41
|
+
|
|
42
|
+
for (const finalizer of finalizers) {
|
|
43
|
+
try {
|
|
44
|
+
await finalizer.run();
|
|
45
|
+
} catch (error) {
|
|
46
|
+
const failure: FinalizerFailure = {
|
|
47
|
+
name: finalizer.name,
|
|
48
|
+
criticality: finalizer.criticality,
|
|
49
|
+
error,
|
|
50
|
+
};
|
|
51
|
+
if (finalizer.criticality === "required") {
|
|
52
|
+
requiredFailures.push(failure);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
bestEffortFailures.push(failure);
|
|
56
|
+
try {
|
|
57
|
+
options.onBestEffortFailure?.(failure);
|
|
58
|
+
} catch {
|
|
59
|
+
// Failure observers are advisory and must not change finalization semantics.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (requiredFailures.length > 0) throw new RequiredFinalizerError(requiredFailures);
|
|
65
|
+
return bestEffortFailures;
|
|
66
|
+
}
|