@jameslovespancakes/pi-plus 1.0.11 → 1.0.13
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/README.md +67 -22
- package/config/skills/workflow-code-review-actions/SKILL.md +44 -0
- package/package.json +9 -4
- package/src/core/accounts/oauth-pool.ts +137 -0
- package/src/core/accounts/registry.ts +12 -25
- package/src/core/accounts/routing.ts +144 -0
- package/src/core/anthropic/client-identity.ts +9 -64
- package/src/core/anthropic/identity.ts +54 -0
- package/src/core/anthropic/oauth.ts +3 -0
- package/src/core/anthropic/quota.ts +232 -78
- package/src/core/anthropic/routing.ts +40 -115
- package/src/core/anthropic/store.ts +11 -10
- package/src/core/catalog/quality.ts +33 -15
- package/src/core/codex/quota.ts +46 -8
- package/src/core/codex/store.ts +29 -16
- package/src/core/config.ts +16 -17
- package/src/core/store.ts +13 -12
- package/src/domains/agents/format.ts +97 -0
- package/src/domains/agents/index.ts +58 -47
- package/src/domains/compact/archive.ts +140 -0
- package/src/domains/compact/chunking.ts +166 -0
- package/src/domains/compact/index.ts +452 -0
- package/src/domains/compact/jev.ts +242 -0
- package/src/domains/compact/policy.ts +255 -0
- package/src/domains/compact/types.ts +79 -0
- package/src/domains/models/provider-picker.ts +1 -1
- package/src/domains/setup/index.ts +23 -23
- package/src/domains/subscriptions/accounts-picker.ts +61 -40
- package/src/domains/subscriptions/accounts.ts +53 -36
- package/src/domains/subscriptions/footer.ts +48 -29
- package/src/domains/subscriptions/index.ts +11 -27
- package/src/domains/subscriptions/provider.ts +79 -103
- package/src/domains/subscriptions/providers/anthropic.ts +42 -22
- package/src/domains/subscriptions/providers/codex.ts +146 -113
- package/src/domains/subscriptions/providers/hosted.ts +18 -0
- package/src/domains/subscriptions/providers/oauth-pool.ts +325 -0
- package/src/domains/subscriptions/routing.ts +19 -11
- package/src/domains/workflows/LICENSE.md +21 -0
- package/src/domains/workflows/index.ts +836 -0
- package/src/domains/workflows/runtime/advisory-challenge.ts +75 -0
- package/src/domains/workflows/runtime/advisory-evidence.ts +90 -0
- package/src/domains/workflows/runtime/advisory-schema.ts +83 -0
- package/src/domains/workflows/runtime/agent-attempt.ts +177 -0
- package/src/domains/workflows/runtime/agent-failure.ts +14 -0
- package/src/domains/workflows/runtime/agent-limits.ts +66 -0
- package/src/domains/workflows/runtime/agent-replay.ts +399 -0
- package/src/domains/workflows/runtime/agent-retry.ts +116 -0
- package/src/domains/workflows/runtime/agent-runner-types.ts +96 -0
- package/src/domains/workflows/runtime/agent-runner.ts +225 -0
- package/src/domains/workflows/runtime/agent-session-identity.ts +256 -0
- package/src/domains/workflows/runtime/agent-session-providers.ts +50 -0
- package/src/domains/workflows/runtime/agent-session.ts +382 -0
- package/src/domains/workflows/runtime/agent-skills.ts +270 -0
- package/src/domains/workflows/runtime/agent-workspace.ts +79 -0
- package/src/domains/workflows/runtime/background-workflow-tool.ts +75 -0
- package/src/domains/workflows/runtime/background-workflows.ts +492 -0
- package/src/domains/workflows/runtime/budget.ts +53 -0
- package/src/domains/workflows/runtime/cancellation.ts +87 -0
- package/src/domains/workflows/runtime/command-completions.ts +36 -0
- package/src/domains/workflows/runtime/concurrency.ts +403 -0
- package/src/domains/workflows/runtime/debug.ts +3 -0
- package/src/domains/workflows/runtime/diff-capture.ts +81 -0
- package/src/domains/workflows/runtime/discovery.ts +137 -0
- package/src/domains/workflows/runtime/dynamax-shortcuts.ts +122 -0
- package/src/domains/workflows/runtime/dynamax.ts +330 -0
- package/src/domains/workflows/runtime/engine.ts +579 -0
- package/src/domains/workflows/runtime/filesystem-error.ts +4 -0
- package/src/domains/workflows/runtime/finalizers.ts +66 -0
- package/src/domains/workflows/runtime/identity-canonicalization.ts +138 -0
- package/src/domains/workflows/runtime/identity-fingerprint.ts +15 -0
- package/src/domains/workflows/runtime/inline-workflow.ts +403 -0
- package/src/domains/workflows/runtime/journal.ts +313 -0
- package/src/domains/workflows/runtime/model-profiles.ts +310 -0
- package/src/domains/workflows/runtime/options.ts +157 -0
- package/src/domains/workflows/runtime/perf.ts +146 -0
- package/src/domains/workflows/runtime/pi-compat.ts +32 -0
- package/src/domains/workflows/runtime/process-runner.ts +260 -0
- package/src/domains/workflows/runtime/progress-types.ts +59 -0
- package/src/domains/workflows/runtime/progress.ts +400 -0
- package/src/domains/workflows/runtime/provider-usage-limit.ts +191 -0
- package/src/domains/workflows/runtime/replay-path-identity.ts +48 -0
- package/src/domains/workflows/runtime/research-contract.ts +89 -0
- package/src/domains/workflows/runtime/research-evidence.ts +289 -0
- package/src/domains/workflows/runtime/resume-context.ts +751 -0
- package/src/domains/workflows/runtime/review/code-review-orchestration.ts +24 -0
- package/src/domains/workflows/runtime/review/github-pr-comments.ts +249 -0
- package/src/domains/workflows/runtime/review/patch-validation.ts +120 -0
- package/src/domains/workflows/runtime/review/review-actions.ts +180 -0
- package/src/domains/workflows/runtime/review/review-budget.ts +45 -0
- package/src/domains/workflows/runtime/review/review-fix-workflow.ts +153 -0
- package/src/domains/workflows/runtime/review/review-format.ts +133 -0
- package/src/domains/workflows/runtime/review/review-handoff.ts +38 -0
- package/src/domains/workflows/runtime/review/review-issues.ts +87 -0
- package/src/domains/workflows/runtime/review/review-report.ts +42 -0
- package/src/domains/workflows/runtime/review/review-results-flow.ts +52 -0
- package/src/domains/workflows/runtime/review/review-results-viewer.ts +312 -0
- package/src/domains/workflows/runtime/review/review-session-coordinator.ts +153 -0
- package/src/domains/workflows/runtime/review/review-snapshot.ts +294 -0
- package/src/domains/workflows/runtime/review-diff-target.ts +130 -0
- package/src/domains/workflows/runtime/session-identity.ts +6 -0
- package/src/domains/workflows/runtime/structured-output.ts +32 -0
- package/src/domains/workflows/runtime/tool-capabilities.ts +44 -0
- package/src/domains/workflows/runtime/tool-source-identity.ts +150 -0
- package/src/domains/workflows/runtime/tree-fingerprint.ts +404 -0
- package/src/domains/workflows/runtime/types.ts +255 -0
- package/src/domains/workflows/runtime/ui/display-text.ts +28 -0
- package/src/domains/workflows/runtime/ui/dynamax-editor-decoration.ts +220 -0
- package/src/domains/workflows/runtime/ui/workflow-format.ts +165 -0
- package/src/domains/workflows/runtime/ui/workflow-inspector.ts +435 -0
- package/src/domains/workflows/runtime/ui/workflow-result-renderer.ts +171 -0
- package/src/domains/workflows/runtime/ui/workflow-viewer-layout.ts +51 -0
- package/src/domains/workflows/runtime/ui/workflow-widget.ts +78 -0
- package/src/domains/workflows/runtime/unknown-error.ts +8 -0
- package/src/domains/workflows/runtime/usage.ts +341 -0
- package/src/domains/workflows/runtime/workflow-advisory-utils.ts +245 -0
- package/src/domains/workflows/runtime/workflow-execution.ts +121 -0
- package/src/domains/workflows/runtime/workflow-module.ts +75 -0
- package/src/domains/workflows/runtime/workflow-run-background.ts +72 -0
- package/src/domains/workflows/runtime/workflow-run-controller.ts +342 -0
- package/src/domains/workflows/runtime/workflow-run-history.ts +164 -0
- package/src/domains/workflows/runtime/workflow-run-record.ts +636 -0
- package/src/domains/workflows/runtime/workflow-run-store.ts +176 -0
- package/src/domains/workflows/runtime/workflow-usage-limit-scheduler.ts +80 -0
- package/src/domains/workflows/runtime/workflows.ts +40 -0
- package/src/domains/workflows/runtime/worktree.ts +615 -0
- package/src/domains/workflows/workflows/code-review.ts +232 -0
- package/src/domains/workflows/workflows/diagnose.ts +154 -0
- package/src/domains/workflows/workflows/perf-review.ts +149 -0
- package/src/domains/workflows/workflows/refactor-scout.ts +143 -0
- package/src/domains/workflows/workflows/research.ts +169 -0
- package/src/services/usage-service.ts +6 -30
- package/src/ui/usage-bars.ts +1 -2
- package/src/core/codex/oauth.ts +0 -129
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
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";
|
|
8
|
+
import { createBudget } from "./budget.ts";
|
|
9
|
+
import { runAgent, type AgentExecutionOptions, type RunContext } from "./agent-runner.ts";
|
|
10
|
+
import { ProgressTracker } from "./progress.ts";
|
|
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";
|
|
18
|
+
import { WorkflowInspector } from "./ui/workflow-inspector.ts";
|
|
19
|
+
import { WORKFLOW_VIEWER_OVERLAY_OPTIONS } from "./ui/workflow-viewer-layout.ts";
|
|
20
|
+
import { createWorkflowJournal, createWorkflowRunId, pruneWorkflowJournals, workflowJournalPath } from "./journal.ts";
|
|
21
|
+
import { WorktreeRegistry } from "./worktree.ts";
|
|
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";
|
|
39
|
+
|
|
40
|
+
/** The workflow-facing slice of the progress tracker (satisfied by `ProgressTracker`). */
|
|
41
|
+
export interface WorkflowProgress {
|
|
42
|
+
phase(title: string): void;
|
|
43
|
+
log(message: string): void;
|
|
44
|
+
event(event: WorkflowProgressEvent): void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Engine state shared by the workflow and its individual agent calls. */
|
|
48
|
+
export type WorkflowRunContext = RunContext;
|
|
49
|
+
|
|
50
|
+
/** Per-context knobs threaded through `runWorkflowWithContext` (and re-derived for each sub-workflow). */
|
|
51
|
+
export interface WorkflowContextOptions {
|
|
52
|
+
/** Controller whose `.signal` equals `rc.signal`; aborts in-flight `parallel` siblings on failure. */
|
|
53
|
+
abortController: AbortController;
|
|
54
|
+
/** Optional eager-submission cap for `parallel`. */
|
|
55
|
+
submissionLimit?: number;
|
|
56
|
+
/** Resolve a sub-workflow reference for `api.workflow()`. Omit to disable composition. */
|
|
57
|
+
resolveWorkflow?: (ref: WorkflowRef) => Promise<LoadedWorkflow>;
|
|
58
|
+
/** Nesting depth. Sub-workflows run at depth >= 1, where `api.workflow()` rejects. */
|
|
59
|
+
depth?: number;
|
|
60
|
+
/** Prefix applied to phase titles so sub-workflow phases nest as "<name> ▸ <title>". */
|
|
61
|
+
progressPrefix?: string;
|
|
62
|
+
/** Namespace applied to child structured progress keys/lanes/logs. */
|
|
63
|
+
progressNamespace?: string;
|
|
64
|
+
}
|
|
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
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Run a workflow module: build the per-run primitives (binding agent/parallel/pipeline
|
|
78
|
+
* to a shared semaphore + progress tracker), invoke the workflow, return its result.
|
|
79
|
+
*/
|
|
80
|
+
export async function runWorkflow(
|
|
81
|
+
ctx: ExtensionContext,
|
|
82
|
+
mod: LoadedWorkflow,
|
|
83
|
+
args: string,
|
|
84
|
+
options: WorkflowRunOptions = {},
|
|
85
|
+
dependencies: WorkflowEngineDependencies = {},
|
|
86
|
+
): Promise<unknown> {
|
|
87
|
+
return await runResolvedWorkflow(ctx, mod, args, resolveWorkflowRunOptions(options), dependencies);
|
|
88
|
+
}
|
|
89
|
+
|
|
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> {
|
|
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 = {
|
|
102
|
+
snapshot: () => progress.snapshot(),
|
|
103
|
+
conversation: (agentId: number) => progress.conversation(agentId),
|
|
104
|
+
followUp: (agentId: number, message: string) => progress.followUp(agentId, message),
|
|
105
|
+
subscribe: (listener: () => void) => progress.subscribe(listener),
|
|
106
|
+
};
|
|
107
|
+
const perf = resolvedOptions.perfRecorder ?? createPerfRecorder(resolvedOptions.perf);
|
|
108
|
+
const usage = createWorkflowUsageRecorder((snapshot) => progress.updateUsage(snapshot));
|
|
109
|
+
const budget = createBudget(resolvedOptions.budget, usage);
|
|
110
|
+
const journalPath = workflowJournalPath(ctx.cwd, runId);
|
|
111
|
+
const resumePath = resolvedOptions.resumeFromRunId ? workflowJournalPath(ctx.cwd, resolvedOptions.resumeFromRunId) : undefined;
|
|
112
|
+
const runStore = new ProjectWorkflowRunStore(ctx.cwd);
|
|
113
|
+
let persistenceFailureReported = false;
|
|
114
|
+
const durableRun = new DurableWorkflowRun(
|
|
115
|
+
runStore,
|
|
116
|
+
createWorkflowRunRecord({
|
|
117
|
+
runId,
|
|
118
|
+
workflow: mod,
|
|
119
|
+
options: resolvedOptions,
|
|
120
|
+
progress: progress.snapshot(),
|
|
121
|
+
argumentsPresent: args.length > 0,
|
|
122
|
+
}),
|
|
123
|
+
(error) => {
|
|
124
|
+
if (persistenceFailureReported) return;
|
|
125
|
+
persistenceFailureReported = true;
|
|
126
|
+
progress.log(`run persistence failed: ${unknownErrorMessage(error)}`);
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
durableProgress = durableRun;
|
|
130
|
+
const worktrees = dependencies.worktrees ?? new WorktreeRegistry(ctx.cwd);
|
|
131
|
+
const runAbortController = new AbortController();
|
|
132
|
+
const unlinkContextAbortSignal = linkAbortSignal(ctx.signal, runAbortController);
|
|
133
|
+
const unlinkOptionAbortSignal = linkAbortSignal(resolvedOptions.signal, runAbortController);
|
|
134
|
+
const workflowOutcome = await captureOutcome(async () => {
|
|
135
|
+
await notifyLifecycleObserver(progress, "progress source callback", () => resolvedOptions.onProgressSource?.(progressSource));
|
|
136
|
+
if (resolvedOptions.inspect && ctx.hasUI && ctx.mode === "tui") {
|
|
137
|
+
let unsubscribe: (() => void) | undefined;
|
|
138
|
+
void ctx.ui
|
|
139
|
+
.custom<void>(
|
|
140
|
+
(tui, theme, _keybindings, done) => {
|
|
141
|
+
unsubscribe = progressSource.subscribe(() => tui.requestRender());
|
|
142
|
+
return new WorkflowInspector(
|
|
143
|
+
() => progress.snapshot(),
|
|
144
|
+
tui,
|
|
145
|
+
theme,
|
|
146
|
+
() => done(undefined),
|
|
147
|
+
undefined,
|
|
148
|
+
progressSource,
|
|
149
|
+
);
|
|
150
|
+
},
|
|
151
|
+
WORKFLOW_VIEWER_OVERLAY_OPTIONS,
|
|
152
|
+
)
|
|
153
|
+
.catch((error: unknown) => {
|
|
154
|
+
try {
|
|
155
|
+
progress.log(`inspector failed: ${unknownErrorMessage(error)}`);
|
|
156
|
+
} catch {
|
|
157
|
+
// Inspector reporting is detached and must never become another rejection.
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
.finally(() => unsubscribe?.());
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const journal = await createWorkflowJournal({ resumePath, writePath: journalPath });
|
|
164
|
+
durableRun.transition({ state: "running", progress: progress.snapshot() });
|
|
165
|
+
await durableRun.flush().catch(() => undefined);
|
|
166
|
+
await notifyLifecycleObserver(progress, "run metadata callback", () =>
|
|
167
|
+
resolvedOptions.onRunMetadata?.({
|
|
168
|
+
runId,
|
|
169
|
+
resumedFromRunId: resolvedOptions.resumeFromRunId,
|
|
170
|
+
journalPath,
|
|
171
|
+
recordPath: workflowRunRecordPath(ctx.cwd, runId),
|
|
172
|
+
}),
|
|
173
|
+
);
|
|
174
|
+
progress.log(resolvedOptions.resumeFromRunId ? `run id: ${runId} (resuming from ${resolvedOptions.resumeFromRunId})` : `run id: ${runId}`);
|
|
175
|
+
const modelProfiles = dependencies.modelProfiles ?? resolveWorkflowModelProfiles({
|
|
176
|
+
cwd: ctx.cwd,
|
|
177
|
+
modelRegistry: ctx.modelRegistry,
|
|
178
|
+
hostModel: ctx.model,
|
|
179
|
+
});
|
|
180
|
+
const rc: WorkflowRunContext = {
|
|
181
|
+
cwd: ctx.cwd,
|
|
182
|
+
hostModel: ctx.model,
|
|
183
|
+
modelRegistry: ctx.modelRegistry,
|
|
184
|
+
semaphore: new Semaphore(resolvedOptions.concurrency),
|
|
185
|
+
agentLimiter: new WorkflowAgentLimiter(resolvedOptions.maxAgents),
|
|
186
|
+
agentTimeoutMs: resolvedOptions.agentTimeoutMs,
|
|
187
|
+
agentRetries: resolvedOptions.agentRetries,
|
|
188
|
+
pauseOnProviderUsageLimit: resolvedOptions.background !== undefined,
|
|
189
|
+
resumeEditedWorkflow: resolvedOptions.resumeEditedWorkflow,
|
|
190
|
+
retryScheduler: dependencies.retryScheduler ?? defaultAgentRetryScheduler,
|
|
191
|
+
modelProfiles,
|
|
192
|
+
progress,
|
|
193
|
+
signal: runAbortController.signal,
|
|
194
|
+
perf,
|
|
195
|
+
usage,
|
|
196
|
+
budget,
|
|
197
|
+
journal,
|
|
198
|
+
worktrees,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// perf.total_ms wraps the whole tree: sub-workflows run inside this span via api.workflow().
|
|
202
|
+
return perf.time("workflow.total_ms", () =>
|
|
203
|
+
runWorkflowWithContext(rc, progress, mod, args, {
|
|
204
|
+
abortController: runAbortController,
|
|
205
|
+
submissionLimit: resolvedOptions.parallelSubmissionLimit ?? (resolvedOptions.concurrency === null ? undefined : resolvedOptions.concurrency * 2),
|
|
206
|
+
resolveWorkflow: resolvedOptions.resolveWorkflow,
|
|
207
|
+
depth: 0,
|
|
208
|
+
progressPrefix: "",
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const workflowPause = workflowOutcome.ok
|
|
214
|
+
? undefined
|
|
215
|
+
: backgroundPauseError(workflowOutcome.error, ctx.signal, resolvedOptions.signal);
|
|
216
|
+
const willPauseWorkflow = workflowPause !== undefined
|
|
217
|
+
&& (!(workflowPause instanceof WorkflowProviderUsageLimitError) || resolvedOptions.background !== undefined);
|
|
218
|
+
const preserveFailedWorktrees = !workflowOutcome.ok
|
|
219
|
+
&& !willPauseWorkflow
|
|
220
|
+
&& !ctx.signal?.aborted
|
|
221
|
+
&& !resolvedOptions.signal?.aborted;
|
|
222
|
+
if (preserveFailedWorktrees) worktrees.preserveRecoverable();
|
|
223
|
+
const finalizationOutcome = await captureOutcome(() =>
|
|
224
|
+
finalizeWorkflowRun({
|
|
225
|
+
cwd: ctx.cwd,
|
|
226
|
+
options: resolvedOptions,
|
|
227
|
+
perf,
|
|
228
|
+
usage,
|
|
229
|
+
progress,
|
|
230
|
+
runStore,
|
|
231
|
+
worktrees,
|
|
232
|
+
preserveWorktrees: preserveFailedWorktrees,
|
|
233
|
+
unlinkSignals: [unlinkContextAbortSignal, unlinkOptionAbortSignal],
|
|
234
|
+
}),
|
|
235
|
+
);
|
|
236
|
+
const retainedPaths = preserveFailedWorktrees ? worktrees.preservedPaths : [];
|
|
237
|
+
const terminalWorkflowError = workflowOutcome.ok
|
|
238
|
+
? undefined
|
|
239
|
+
: withRetainedWorktrees(workflowOutcome.error, retainedPaths);
|
|
240
|
+
|
|
241
|
+
if (workflowOutcome.ok && finalizationOutcome.ok) {
|
|
242
|
+
durableRun.transition({
|
|
243
|
+
state: "completed",
|
|
244
|
+
progress: progress.snapshot(),
|
|
245
|
+
usage: usage.snapshot(),
|
|
246
|
+
result: workflowOutcome.value,
|
|
247
|
+
});
|
|
248
|
+
} else if (!workflowOutcome.ok) {
|
|
249
|
+
const error = finalizationOutcome.ok
|
|
250
|
+
? terminalWorkflowError!
|
|
251
|
+
: combinedWorkflowError(terminalWorkflowError!, finalizationOutcome.error);
|
|
252
|
+
persistTerminalWorkflowError(error, workflowOutcome.error);
|
|
253
|
+
} else if (!finalizationOutcome.ok) {
|
|
254
|
+
persistTerminalWorkflowError(finalizationOutcome.error);
|
|
255
|
+
}
|
|
256
|
+
await durableRun.flush().catch(() => undefined);
|
|
257
|
+
|
|
258
|
+
function persistTerminalWorkflowError(error: unknown, pauseCandidate: unknown = error): void {
|
|
259
|
+
const pauseError = backgroundPauseError(pauseCandidate, ctx.signal, resolvedOptions.signal);
|
|
260
|
+
if (pauseError) {
|
|
261
|
+
if (pauseError instanceof WorkflowProviderUsageLimitError) {
|
|
262
|
+
if (resolvedOptions.background === undefined) {
|
|
263
|
+
durableRun.transition({
|
|
264
|
+
state: "failed",
|
|
265
|
+
progress: progress.snapshot(),
|
|
266
|
+
usage: usage.snapshot(),
|
|
267
|
+
error: pauseError,
|
|
268
|
+
});
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const providerPause = createProviderUsageLimitPauseRecord(
|
|
272
|
+
pauseError,
|
|
273
|
+
resolvedOptions,
|
|
274
|
+
mod.source.kind === "file" && args.length === 0,
|
|
275
|
+
);
|
|
276
|
+
durableRun.transition({
|
|
277
|
+
state: "paused",
|
|
278
|
+
progress: progress.snapshot(),
|
|
279
|
+
...providerPause,
|
|
280
|
+
});
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
durableRun.transition({
|
|
284
|
+
state: "paused",
|
|
285
|
+
progress: progress.snapshot(),
|
|
286
|
+
message: unknownErrorMessage(pauseError),
|
|
287
|
+
});
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
durableRun.transition({
|
|
291
|
+
state: ctx.signal?.aborted || resolvedOptions.signal?.aborted ? "stopped" : "failed",
|
|
292
|
+
progress: progress.snapshot(),
|
|
293
|
+
usage: usage.snapshot(),
|
|
294
|
+
error,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (workflowOutcome.ok) {
|
|
299
|
+
if (finalizationOutcome.ok) return workflowOutcome.value;
|
|
300
|
+
throw finalizationOutcome.error;
|
|
301
|
+
}
|
|
302
|
+
if (finalizationOutcome.ok) throw terminalWorkflowError;
|
|
303
|
+
throw combinedWorkflowError(terminalWorkflowError, finalizationOutcome.error);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function backgroundPauseError(error: unknown, ...signals: Array<AbortSignal | undefined>): unknown {
|
|
307
|
+
if (isWorkflowPauseError(error)) return error;
|
|
308
|
+
for (const signal of signals) {
|
|
309
|
+
if (!signal?.aborted) continue;
|
|
310
|
+
const reason = abortReason(signal);
|
|
311
|
+
if (isWorkflowPauseError(reason)) return reason;
|
|
312
|
+
}
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function captureOutcome<T>(operation: () => Promise<T>): Promise<Outcome<T>> {
|
|
317
|
+
try {
|
|
318
|
+
return { ok: true, value: await operation() };
|
|
319
|
+
} catch (error) {
|
|
320
|
+
return { ok: false, error };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
interface WorkflowFinalizationInput {
|
|
325
|
+
readonly cwd: string;
|
|
326
|
+
readonly options: ResolvedWorkflowRunOptions;
|
|
327
|
+
readonly perf: PerfSink;
|
|
328
|
+
readonly usage: WorkflowUsageSink;
|
|
329
|
+
readonly progress: ProgressTracker;
|
|
330
|
+
readonly runStore: WorkflowRunStore;
|
|
331
|
+
readonly worktrees: WorktreeRegistry;
|
|
332
|
+
readonly preserveWorktrees: boolean;
|
|
333
|
+
readonly unlinkSignals: readonly (() => void)[];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function finalizeWorkflowRun(input: WorkflowFinalizationInput): Promise<void> {
|
|
337
|
+
await runFinalizers(
|
|
338
|
+
[
|
|
339
|
+
{
|
|
340
|
+
name: "usage snapshot callback",
|
|
341
|
+
criticality: "best-effort",
|
|
342
|
+
run: () => input.options.onUsageSnapshot?.(input.usage.snapshot()),
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
name: "performance snapshot callback",
|
|
346
|
+
criticality: "best-effort",
|
|
347
|
+
run: async () => {
|
|
348
|
+
if (!input.options.perf) return;
|
|
349
|
+
const snapshot = input.perf.snapshot();
|
|
350
|
+
await input.options.onPerfSnapshot?.(snapshot);
|
|
351
|
+
input.progress.log(formatPerfSummary(snapshot));
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
...input.unlinkSignals.map((run, index) => ({
|
|
355
|
+
name: `abort signal unlink ${index + 1}`,
|
|
356
|
+
criticality: "required" as const,
|
|
357
|
+
run,
|
|
358
|
+
})),
|
|
359
|
+
input.preserveWorktrees
|
|
360
|
+
? {
|
|
361
|
+
name: "worktree cleanup and retention",
|
|
362
|
+
criticality: "required",
|
|
363
|
+
run: async () => {
|
|
364
|
+
await input.worktrees.removeUnpreserved();
|
|
365
|
+
for (const path of input.worktrees.preservedPaths) {
|
|
366
|
+
input.progress.log(`retained failed workflow worktree: ${path}`);
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
}
|
|
370
|
+
: {
|
|
371
|
+
name: "worktree cleanup",
|
|
372
|
+
criticality: "required",
|
|
373
|
+
run: () => input.worktrees.removeAll(),
|
|
374
|
+
},
|
|
375
|
+
{ name: "journal pruning", criticality: "best-effort", run: () => pruneWorkflowJournals(input.cwd) },
|
|
376
|
+
{ name: "run record pruning", criticality: "best-effort", run: () => input.runStore.prune() },
|
|
377
|
+
{ name: "progress completion", criticality: "best-effort", run: () => input.progress.done() },
|
|
378
|
+
{
|
|
379
|
+
name: "progress snapshot callback",
|
|
380
|
+
criticality: "best-effort",
|
|
381
|
+
run: () => input.options.onProgressSnapshot?.(input.progress.snapshot()),
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
name: "progress source clear",
|
|
385
|
+
criticality: "best-effort",
|
|
386
|
+
run: () => input.options.onProgressSource?.(undefined),
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
{
|
|
390
|
+
onBestEffortFailure: (failure) => {
|
|
391
|
+
try {
|
|
392
|
+
input.progress.log(`${failure.name} failed: ${unknownErrorMessage(failure.error)}`);
|
|
393
|
+
} catch {
|
|
394
|
+
// Observer failures must never replace the workflow outcome.
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function withRetainedWorktrees(error: unknown, paths: readonly string[]): unknown {
|
|
402
|
+
if (paths.length === 0) return error;
|
|
403
|
+
const locations = paths.map((path) => `- ${path}`).join("\n");
|
|
404
|
+
return new Error(
|
|
405
|
+
`${unknownErrorMessage(error)}\n\nFailed workflow worktree${paths.length === 1 ? "" : "s"} retained for recovery:\n${locations}`,
|
|
406
|
+
{ cause: error },
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function combinedWorkflowError(workflowError: unknown, finalizationError: unknown): AggregateError {
|
|
411
|
+
return new AggregateError(
|
|
412
|
+
[workflowError, finalizationError],
|
|
413
|
+
`Workflow failed: ${unknownErrorMessage(workflowError)}; finalization also failed: ${unknownErrorMessage(finalizationError)}`,
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function createWorkflowAgent(
|
|
418
|
+
rc: WorkflowRunContext,
|
|
419
|
+
scope: WorkflowScope,
|
|
420
|
+
mod: LoadedWorkflow,
|
|
421
|
+
resumeContext: AgentResumeBaseContext,
|
|
422
|
+
): WorkflowApi["agent"] {
|
|
423
|
+
function agent<S extends TSchema>(
|
|
424
|
+
prompt: string,
|
|
425
|
+
opts: AgentOptions<S> & { schema: S; isolation: "worktree" },
|
|
426
|
+
): Promise<IsolatedAgentResult<Static<S>>>;
|
|
427
|
+
function agent(prompt: string, opts: AgentOptions & { isolation: "worktree" }): Promise<IsolatedAgentResult<string>>;
|
|
428
|
+
function agent<S extends TSchema>(prompt: string, opts: AgentOptions<S> & { schema: S }): Promise<Static<S>>;
|
|
429
|
+
function agent(prompt: string, opts?: AgentOptions): Promise<string>;
|
|
430
|
+
function agent(prompt: string, agentOpts?: AgentOptions): Promise<unknown> {
|
|
431
|
+
const scopedOptions = scope.agentOptions(agentOpts);
|
|
432
|
+
const executionOptions: AgentExecutionOptions =
|
|
433
|
+
scopedOptions.isolation === "worktree" && mod.isolatedWorktreeBaseline !== undefined
|
|
434
|
+
? { ...scopedOptions, worktreeBaseline: mod.isolatedWorktreeBaseline }
|
|
435
|
+
: scopedOptions;
|
|
436
|
+
return runAgent(rc, prompt, executionOptions, resumeContext);
|
|
437
|
+
}
|
|
438
|
+
return agent;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Build the `WorkflowApi` from an existing run context and invoke the module. Reused for both
|
|
443
|
+
* the top-level run and every `api.workflow()` sub-step, so children share the parent's semaphore,
|
|
444
|
+
* abort signal, and perf sink. No setup/teardown of its own — that belongs to `runWorkflow`.
|
|
445
|
+
*/
|
|
446
|
+
export async function runWorkflowWithContext(
|
|
447
|
+
rc: WorkflowRunContext,
|
|
448
|
+
progress: WorkflowProgress,
|
|
449
|
+
mod: LoadedWorkflow,
|
|
450
|
+
args: string,
|
|
451
|
+
opts: WorkflowContextOptions,
|
|
452
|
+
): Promise<unknown> {
|
|
453
|
+
const depth = opts.depth ?? 0;
|
|
454
|
+
const scope = createWorkflowScope(progress, opts.progressPrefix ?? "", opts.progressNamespace);
|
|
455
|
+
const resumeContext: AgentResumeBaseContext = {
|
|
456
|
+
workflow: await captureWorkflowResumeContext(mod, rc.signal),
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
const agent = createWorkflowAgent(rc, scope, mod, resumeContext);
|
|
460
|
+
|
|
461
|
+
const workflow: WorkflowApi["workflow"] =
|
|
462
|
+
depth >= 1
|
|
463
|
+
? async () => {
|
|
464
|
+
throw new Error("workflow() can only nest one level deep");
|
|
465
|
+
}
|
|
466
|
+
: async (ref, childArgs) => {
|
|
467
|
+
if (!opts.resolveWorkflow) throw new Error("sub-workflows are not enabled in this context");
|
|
468
|
+
const childMod = await opts.resolveWorkflow(ref);
|
|
469
|
+
// A child controller isolates the child's failures from the parent (so the parent can
|
|
470
|
+
// try/catch a failed sub-workflow), while still cancelling the child when the run aborts.
|
|
471
|
+
const childController = new AbortController();
|
|
472
|
+
const unlink = linkAbortSignal(rc.signal, childController);
|
|
473
|
+
try {
|
|
474
|
+
return await runWorkflowWithContext({ ...rc, signal: childController.signal }, progress, childMod, childArgs ?? "", {
|
|
475
|
+
abortController: childController,
|
|
476
|
+
submissionLimit: opts.submissionLimit,
|
|
477
|
+
resolveWorkflow: opts.resolveWorkflow,
|
|
478
|
+
depth: depth + 1,
|
|
479
|
+
progressPrefix: `${childMod.meta.name} ▸ `,
|
|
480
|
+
progressNamespace: childMod.meta.name,
|
|
481
|
+
});
|
|
482
|
+
} finally {
|
|
483
|
+
unlink();
|
|
484
|
+
scope.restorePhase();
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
const api: WorkflowApi = {
|
|
489
|
+
agent,
|
|
490
|
+
workflow,
|
|
491
|
+
parallel: bindParallel({
|
|
492
|
+
signal: rc.signal,
|
|
493
|
+
abortController: opts.abortController,
|
|
494
|
+
limit: opts.submissionLimit,
|
|
495
|
+
}),
|
|
496
|
+
pipeline: bindPipeline({ signal: rc.signal, abortController: opts.abortController }),
|
|
497
|
+
phase: scope.phase,
|
|
498
|
+
log: scope.log,
|
|
499
|
+
progress: scope.event,
|
|
500
|
+
args,
|
|
501
|
+
cwd: rc.cwd,
|
|
502
|
+
budget: rc.budget,
|
|
503
|
+
signal: rc.signal,
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
throwIfAborted(rc.signal);
|
|
507
|
+
const result = await mod.default(api);
|
|
508
|
+
throwIfAborted(rc.signal);
|
|
509
|
+
return result;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
interface WorkflowScope {
|
|
513
|
+
agentOptions(opts: AgentOptions | undefined): AgentOptions;
|
|
514
|
+
phase(title: string): void;
|
|
515
|
+
log(message: string): void;
|
|
516
|
+
event(event: WorkflowProgressEvent): void;
|
|
517
|
+
restorePhase(): void;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function createWorkflowScope(progress: WorkflowProgress, prefix: string, namespace: string | undefined): WorkflowScope {
|
|
521
|
+
let currentPhase = `${prefix}Workflow`;
|
|
522
|
+
const display = (title: string) => `${prefix}${title}`;
|
|
523
|
+
return {
|
|
524
|
+
agentOptions(opts) {
|
|
525
|
+
const phase = opts?.phase ? display(opts.phase) : currentPhase;
|
|
526
|
+
return opts ? { ...opts, phase } : { phase };
|
|
527
|
+
},
|
|
528
|
+
phase(title) {
|
|
529
|
+
currentPhase = display(title);
|
|
530
|
+
progress.phase(currentPhase);
|
|
531
|
+
},
|
|
532
|
+
log(message) {
|
|
533
|
+
progress.log(namespace ? `${namespace}: ${message}` : message);
|
|
534
|
+
},
|
|
535
|
+
event(event) {
|
|
536
|
+
progress.event(namespace ? namespaceProgressEvent(namespace, event) : event);
|
|
537
|
+
},
|
|
538
|
+
restorePhase() {
|
|
539
|
+
progress.phase(currentPhase);
|
|
540
|
+
},
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function namespaceProgressEvent(namespace: string, event: WorkflowProgressEvent): WorkflowProgressEvent {
|
|
545
|
+
switch (event.type) {
|
|
546
|
+
case "counter":
|
|
547
|
+
return { ...event, key: `${namespace}.${event.key}` };
|
|
548
|
+
case "counter_delta":
|
|
549
|
+
return { ...event, key: `${namespace}.${event.key}` };
|
|
550
|
+
case "lane_item":
|
|
551
|
+
return { ...event, lane: `${namespace} ▸ ${event.lane}` };
|
|
552
|
+
case "summary":
|
|
553
|
+
return { ...event, key: `${namespace}.${event.key}` };
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function formatPerfSummary(snapshot: PerfSnapshot): string {
|
|
558
|
+
const parts = snapshot.aggregates
|
|
559
|
+
.filter((aggregate) => aggregate.count > 0)
|
|
560
|
+
.slice(0, 5)
|
|
561
|
+
.map((aggregate) => `${aggregate.name} ${Math.round(aggregate.total)}ms`);
|
|
562
|
+
return parts.length > 0 ? `perf: ${parts.join(", ")}` : "perf: no samples";
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async function notifyLifecycleObserver(
|
|
566
|
+
progress: ProgressTracker,
|
|
567
|
+
name: string,
|
|
568
|
+
notify: () => void | Promise<void>,
|
|
569
|
+
): Promise<void> {
|
|
570
|
+
try {
|
|
571
|
+
await notify();
|
|
572
|
+
} catch (error) {
|
|
573
|
+
try {
|
|
574
|
+
progress.log(`${name} failed: ${unknownErrorMessage(error)}`);
|
|
575
|
+
} catch {
|
|
576
|
+
// Lifecycle observers are advisory and must not replace the workflow outcome.
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
@@ -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
|
+
}
|