pi-workflow-engine 0.11.0 → 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 +255 -97
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +0 -4
- package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +3 -1
- package/.pi/extensions/pi-workflow-engine/src/agent-limits.ts +35 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +27 -4
- package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +116 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +49 -45
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +95 -26
- package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +158 -203
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +48 -42
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +11 -3
- 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 +3 -3
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +38 -0
- package/.pi/extensions/pi-workflow-engine/src/command-completions.ts +36 -0
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +70 -4
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +16 -130
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +9 -5
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +109 -60
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +166 -18
- package/.pi/extensions/pi-workflow-engine/src/filesystem-error.ts +4 -0
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +2 -2
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +12 -6
- 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 +88 -1
- package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +32 -0
- package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +72 -117
- package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +189 -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 +74 -38
- package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +49 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +2 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +11 -30
- package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +5 -5
- package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +3 -4
- 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 +1 -4
- package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +1 -4
- package/.pi/extensions/pi-workflow-engine/src/types.ts +49 -15
- 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 +2 -1
- 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/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 +1 -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 +2 -0
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +40 -3
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +18 -42
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +10 -17
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +8 -15
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +8 -15
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +170 -0
- package/README.md +72 -141
- package/USAGE.md +211 -17
- package/package.json +1 -1
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Static, TSchema } from "typebox";
|
|
2
3
|
import { bindParallel, bindPipeline, Semaphore } from "./concurrency.ts";
|
|
3
|
-
import {
|
|
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
9
|
import { runAgent, type AgentExecutionOptions, type RunContext } from "./agent-runner.ts";
|
|
6
10
|
import { ProgressTracker } from "./progress.ts";
|
|
7
11
|
import { createPerfRecorder, type PerfSink, type PerfSnapshot } from "./perf.ts";
|
|
8
12
|
import { createWorkflowUsageRecorder, type WorkflowUsageSink } from "./usage.ts";
|
|
9
|
-
import {
|
|
10
|
-
|
|
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
22
|
import { runFinalizers } from "./finalizers.ts";
|
|
@@ -17,6 +25,17 @@ import {
|
|
|
17
25
|
captureWorkflowResumeContext,
|
|
18
26
|
type AgentResumeBaseContext,
|
|
19
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";
|
|
20
39
|
|
|
21
40
|
/** The workflow-facing slice of the progress tracker (satisfied by `ProgressTracker`). */
|
|
22
41
|
export interface WorkflowProgress {
|
|
@@ -50,6 +69,8 @@ type Outcome<T> =
|
|
|
50
69
|
|
|
51
70
|
export interface WorkflowEngineDependencies {
|
|
52
71
|
readonly worktrees?: WorktreeRegistry;
|
|
72
|
+
readonly retryScheduler?: AgentRetryScheduler;
|
|
73
|
+
readonly modelProfiles?: ResolvedWorkflowModelProfiles;
|
|
53
74
|
}
|
|
54
75
|
|
|
55
76
|
/**
|
|
@@ -74,25 +95,44 @@ export async function runResolvedWorkflow(
|
|
|
74
95
|
resolvedOptions: ResolvedWorkflowRunOptions,
|
|
75
96
|
dependencies: WorkflowEngineDependencies = {},
|
|
76
97
|
): Promise<unknown> {
|
|
77
|
-
const
|
|
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));
|
|
78
101
|
const progressSource = { snapshot: () => progress.snapshot() };
|
|
79
102
|
const perf = resolvedOptions.perfRecorder ?? createPerfRecorder(resolvedOptions.perf);
|
|
80
|
-
const usage = createWorkflowUsageRecorder();
|
|
103
|
+
const usage = createWorkflowUsageRecorder((snapshot) => progress.updateUsage(snapshot));
|
|
81
104
|
const budget = createBudget(resolvedOptions.budget, usage);
|
|
82
|
-
const runId = resolvedOptions.runId ?? createWorkflowRunId();
|
|
83
105
|
const journalPath = workflowJournalPath(ctx.cwd, runId);
|
|
84
106
|
const resumePath = resolvedOptions.resumeFromRunId ? workflowJournalPath(ctx.cwd, resolvedOptions.resumeFromRunId) : undefined;
|
|
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;
|
|
85
125
|
const worktrees = dependencies.worktrees ?? new WorktreeRegistry(ctx.cwd);
|
|
86
126
|
const runAbortController = new AbortController();
|
|
87
127
|
const unlinkContextAbortSignal = linkAbortSignal(ctx.signal, runAbortController);
|
|
88
128
|
const unlinkOptionAbortSignal = linkAbortSignal(resolvedOptions.signal, runAbortController);
|
|
89
129
|
const workflowOutcome = await captureOutcome(async () => {
|
|
90
130
|
await notifyLifecycleObserver(progress, "progress source callback", () => resolvedOptions.onProgressSource?.(progressSource));
|
|
91
|
-
if (resolvedOptions.inspect && ctx.hasUI) {
|
|
131
|
+
if (resolvedOptions.inspect && ctx.hasUI && ctx.mode === "tui") {
|
|
92
132
|
void ctx.ui
|
|
93
133
|
.custom<void>(
|
|
94
134
|
(tui, theme, _keybindings, done) => new WorkflowInspector(() => progress.snapshot(), tui, theme, () => done(undefined)),
|
|
95
|
-
|
|
135
|
+
WORKFLOW_VIEWER_OVERLAY_OPTIONS,
|
|
96
136
|
)
|
|
97
137
|
.catch((error: unknown) => {
|
|
98
138
|
try {
|
|
@@ -104,15 +144,34 @@ export async function runResolvedWorkflow(
|
|
|
104
144
|
}
|
|
105
145
|
|
|
106
146
|
const journal = await createWorkflowJournal({ resumePath, writePath: journalPath });
|
|
147
|
+
durableRun.transition({ state: "running", progress: progress.snapshot() });
|
|
148
|
+
await durableRun.flush().catch(() => undefined);
|
|
107
149
|
await notifyLifecycleObserver(progress, "run metadata callback", () =>
|
|
108
|
-
resolvedOptions.onRunMetadata?.({
|
|
150
|
+
resolvedOptions.onRunMetadata?.({
|
|
151
|
+
runId,
|
|
152
|
+
resumedFromRunId: resolvedOptions.resumeFromRunId,
|
|
153
|
+
journalPath,
|
|
154
|
+
recordPath: workflowRunRecordPath(ctx.cwd, runId),
|
|
155
|
+
}),
|
|
109
156
|
);
|
|
110
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
|
+
});
|
|
111
163
|
const rc: WorkflowRunContext = {
|
|
112
164
|
cwd: ctx.cwd,
|
|
113
165
|
hostModel: ctx.model,
|
|
114
166
|
modelRegistry: ctx.modelRegistry,
|
|
115
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,
|
|
116
175
|
progress,
|
|
117
176
|
signal: runAbortController.signal,
|
|
118
177
|
perf,
|
|
@@ -141,11 +200,69 @@ export async function runResolvedWorkflow(
|
|
|
141
200
|
perf,
|
|
142
201
|
usage,
|
|
143
202
|
progress,
|
|
203
|
+
runStore,
|
|
144
204
|
worktrees,
|
|
145
205
|
unlinkSignals: [unlinkContextAbortSignal, unlinkOptionAbortSignal],
|
|
146
206
|
}),
|
|
147
207
|
);
|
|
148
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;
|
|
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;
|
|
250
|
+
}
|
|
251
|
+
durableRun.transition({
|
|
252
|
+
state: "paused",
|
|
253
|
+
progress: progress.snapshot(),
|
|
254
|
+
message: unknownErrorMessage(pauseError),
|
|
255
|
+
});
|
|
256
|
+
return;
|
|
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
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
149
266
|
if (workflowOutcome.ok) {
|
|
150
267
|
if (finalizationOutcome.ok) return workflowOutcome.value;
|
|
151
268
|
throw finalizationOutcome.error;
|
|
@@ -154,6 +271,16 @@ export async function runResolvedWorkflow(
|
|
|
154
271
|
throw combinedWorkflowError(workflowOutcome.error, finalizationOutcome.error);
|
|
155
272
|
}
|
|
156
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
|
+
|
|
157
284
|
async function captureOutcome<T>(operation: () => Promise<T>): Promise<Outcome<T>> {
|
|
158
285
|
try {
|
|
159
286
|
return { ok: true, value: await operation() };
|
|
@@ -168,6 +295,7 @@ interface WorkflowFinalizationInput {
|
|
|
168
295
|
readonly perf: PerfSink;
|
|
169
296
|
readonly usage: WorkflowUsageSink;
|
|
170
297
|
readonly progress: ProgressTracker;
|
|
298
|
+
readonly runStore: WorkflowRunStore;
|
|
171
299
|
readonly worktrees: WorktreeRegistry;
|
|
172
300
|
readonly unlinkSignals: readonly (() => void)[];
|
|
173
301
|
}
|
|
@@ -201,6 +329,7 @@ async function finalizeWorkflowRun(input: WorkflowFinalizationInput): Promise<vo
|
|
|
201
329
|
run: () => input.worktrees.removeAll(),
|
|
202
330
|
},
|
|
203
331
|
{ name: "journal pruning", criticality: "best-effort", run: () => pruneWorkflowJournals(input.cwd) },
|
|
332
|
+
{ name: "run record pruning", criticality: "best-effort", run: () => input.runStore.prune() },
|
|
204
333
|
{ name: "progress completion", criticality: "best-effort", run: () => input.progress.done() },
|
|
205
334
|
{
|
|
206
335
|
name: "progress snapshot callback",
|
|
@@ -232,6 +361,30 @@ function combinedWorkflowError(workflowError: unknown, finalizationError: unknow
|
|
|
232
361
|
);
|
|
233
362
|
}
|
|
234
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;
|
|
386
|
+
}
|
|
387
|
+
|
|
235
388
|
/**
|
|
236
389
|
* Build the `WorkflowApi` from an existing run context and invoke the module. Reused for both
|
|
237
390
|
* the top-level run and every `api.workflow()` sub-step, so children share the parent's semaphore,
|
|
@@ -250,14 +403,7 @@ export async function runWorkflowWithContext(
|
|
|
250
403
|
workflow: await captureWorkflowResumeContext(mod, rc.signal),
|
|
251
404
|
};
|
|
252
405
|
|
|
253
|
-
const agent = (
|
|
254
|
-
const scopedOptions = scope.agentOptions(agentOpts);
|
|
255
|
-
const executionOptions: AgentExecutionOptions =
|
|
256
|
-
scopedOptions.isolation === "worktree" && mod.isolatedWorktreeBaseline !== undefined
|
|
257
|
-
? { ...scopedOptions, worktreeBaseline: mod.isolatedWorktreeBaseline }
|
|
258
|
-
: scopedOptions;
|
|
259
|
-
return runAgent(rc, prompt, executionOptions, resumeContext);
|
|
260
|
-
}) as WorkflowApi["agent"];
|
|
406
|
+
const agent = createWorkflowAgent(rc, scope, mod, resumeContext);
|
|
261
407
|
|
|
262
408
|
const workflow: WorkflowApi["workflow"] =
|
|
263
409
|
depth >= 1
|
|
@@ -305,7 +451,9 @@ export async function runWorkflowWithContext(
|
|
|
305
451
|
};
|
|
306
452
|
|
|
307
453
|
throwIfAborted(rc.signal);
|
|
308
|
-
|
|
454
|
+
const result = await mod.default(api);
|
|
455
|
+
throwIfAborted(rc.signal);
|
|
456
|
+
return result;
|
|
309
457
|
}
|
|
310
458
|
|
|
311
459
|
interface WorkflowScope {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import type { LoadedWorkflow, WorkflowApi } from "./types.ts";
|
|
4
|
+
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
4
5
|
import { parseWorkflowMeta } from "./workflow-module.ts";
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -138,8 +139,7 @@ function compileExecutor(defaultExpression: string): InlineWorkflowExecutor {
|
|
|
138
139
|
try {
|
|
139
140
|
return new AsyncFunction("api", "Type", `"use strict";\nconst workflow = ${defaultExpression};\nreturn await workflow(api);`);
|
|
140
141
|
} catch (error) {
|
|
141
|
-
|
|
142
|
-
throw new InlineWorkflowCompileError(`inline workflow default export did not compile: ${message}`);
|
|
142
|
+
throw new InlineWorkflowCompileError(`inline workflow default export did not compile: ${unknownErrorMessage(error)}`);
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
isAgentResumeContext,
|
|
10
10
|
resumeContextMismatchReason,
|
|
11
11
|
type AgentResumeContext,
|
|
12
|
+
type ResumeContextComparisonOptions,
|
|
12
13
|
} from "./resume-context.ts";
|
|
13
14
|
|
|
14
15
|
export const WORKFLOW_RUNS_DIR = join(".pi", ".workflow-runs");
|
|
@@ -47,7 +48,7 @@ export type AgentJournalKeyCapture =
|
|
|
47
48
|
| { readonly kind: "unverifiable"; readonly reason: string };
|
|
48
49
|
|
|
49
50
|
export interface WorkflowJournal {
|
|
50
|
-
lookup(key: string, identity: AgentResumeContext): JournalLookup;
|
|
51
|
+
lookup(key: string, identity: AgentResumeContext, options?: ResumeContextComparisonOptions): JournalLookup;
|
|
51
52
|
record(key: string, result: unknown, identity: AgentResumeContext): Promise<JournalRecordResult>;
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -60,7 +61,7 @@ export function createWorkflowRunId(): string {
|
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
export function workflowJournalPath(cwd: string, runId: string): string {
|
|
63
|
-
return join(cwd, WORKFLOW_RUNS_DIR, `${
|
|
64
|
+
return join(cwd, WORKFLOW_RUNS_DIR, `${validateWorkflowRunId(runId)}.jsonl`);
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
export function agentJournalKey(prompt: string, opts: AgentOptions = {}, worktreeBaseline?: WorktreeBaseline): string {
|
|
@@ -110,6 +111,7 @@ function captureAgentCallHash(
|
|
|
110
111
|
thinkingLevel: opts.thinkingLevel,
|
|
111
112
|
tools: opts.tools,
|
|
112
113
|
toolHints: opts.toolHints,
|
|
114
|
+
requireToolHints: opts.requireToolHints === true ? true : undefined,
|
|
113
115
|
skills: opts.skills,
|
|
114
116
|
schema: opts.schema,
|
|
115
117
|
isolation: opts.isolation,
|
|
@@ -170,6 +172,8 @@ export async function createWorkflowJournal(options: {
|
|
|
170
172
|
readonly writePath: string;
|
|
171
173
|
}): Promise<WorkflowJournal> {
|
|
172
174
|
const priorEntries = options.resumePath ? await loadJournalEntries(options.resumePath, { required: true }) : [];
|
|
175
|
+
await mkdir(dirname(options.writePath), { recursive: true });
|
|
176
|
+
await appendFile(options.writePath, "", "utf8");
|
|
173
177
|
return createMemoryBackedJournal(priorEntries, options.writePath);
|
|
174
178
|
}
|
|
175
179
|
|
|
@@ -182,7 +186,7 @@ export function createMemoryBackedJournal(priorEntries: readonly JournalEntry[]
|
|
|
182
186
|
}
|
|
183
187
|
|
|
184
188
|
return {
|
|
185
|
-
lookup(key, identity) {
|
|
189
|
+
lookup(key, identity, options) {
|
|
186
190
|
const entries = priorByKey.get(key);
|
|
187
191
|
if (!entries || entries.length === 0) return { hit: false };
|
|
188
192
|
|
|
@@ -197,10 +201,12 @@ export function createMemoryBackedJournal(priorEntries: readonly JournalEntry[]
|
|
|
197
201
|
: "journal entry predates replay contract v2",
|
|
198
202
|
};
|
|
199
203
|
}
|
|
200
|
-
const matches = current.filter(
|
|
204
|
+
const matches = current.filter(
|
|
205
|
+
(entry) => resumeContextMismatchReason(entry.identity, identity, options) === undefined,
|
|
206
|
+
);
|
|
201
207
|
if (matches.length === 1) return { hit: true, value: matches[0]!.result };
|
|
202
208
|
if (matches.length > 1) return { hit: false, reason: "multiple cached entries match this agent call" };
|
|
203
|
-
return { hit: false, reason: resumeContextMismatchReason(current[0]!.identity, identity) };
|
|
209
|
+
return { hit: false, reason: resumeContextMismatchReason(current[0]!.identity, identity, options) };
|
|
204
210
|
},
|
|
205
211
|
async record(key, result, identity) {
|
|
206
212
|
const entry: JournalEntryV2 = { version: 2, key, result, identity };
|
|
@@ -248,7 +254,7 @@ export async function pruneWorkflowJournals(cwd: string, keep = WORKFLOW_JOURNAL
|
|
|
248
254
|
await Promise.all(stale.map((entry) => rm(entry.path, { force: true }).catch(() => undefined)));
|
|
249
255
|
}
|
|
250
256
|
|
|
251
|
-
function
|
|
257
|
+
export function validateWorkflowRunId(runId: string): string {
|
|
252
258
|
const trimmed = runId.trim();
|
|
253
259
|
if (!/^[A-Za-z0-9_.-]+$/.test(trimmed)) {
|
|
254
260
|
throw new Error("Workflow run id must contain only letters, numbers, dots, underscores, or hyphens.");
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
3
|
+
import {
|
|
4
|
+
clearWorkflowModelProfile,
|
|
5
|
+
isWorkflowModelProfileName,
|
|
6
|
+
isWorkflowThinkingLevel,
|
|
7
|
+
resolveWorkflowModelProfiles,
|
|
8
|
+
setWorkflowModelProfile,
|
|
9
|
+
workflowModelProfilePaths,
|
|
10
|
+
WORKFLOW_MODEL_PROFILE_NAMES,
|
|
11
|
+
WORKFLOW_THINKING_LEVELS,
|
|
12
|
+
type ResolvedWorkflowModelProfiles,
|
|
13
|
+
type WorkflowModelProfilePaths,
|
|
14
|
+
type WorkflowModelProfileName,
|
|
15
|
+
} from "./model-profiles.ts";
|
|
16
|
+
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
17
|
+
import { completeCurrentArgument, splitArgumentPrefix } from "./command-completions.ts";
|
|
18
|
+
|
|
19
|
+
type ConfigScope = "user" | "project";
|
|
20
|
+
|
|
21
|
+
type WorkflowModelsCommand =
|
|
22
|
+
| { readonly kind: "status" }
|
|
23
|
+
| {
|
|
24
|
+
readonly kind: "set";
|
|
25
|
+
readonly scope: ConfigScope;
|
|
26
|
+
readonly profile: WorkflowModelProfileName;
|
|
27
|
+
readonly model: string;
|
|
28
|
+
readonly thinkingLevel?: Parameters<typeof setWorkflowModelProfile>[0]["profile"]["thinkingLevel"];
|
|
29
|
+
}
|
|
30
|
+
| { readonly kind: "clear"; readonly scope: ConfigScope; readonly profile: WorkflowModelProfileName }
|
|
31
|
+
| { readonly kind: "error"; readonly message: string };
|
|
32
|
+
|
|
33
|
+
const USAGE =
|
|
34
|
+
"Usage: /workflow:models [status | set <small|medium|big> <provider/model> [thinkingLevel] [--user|--project] | clear <small|medium|big> [--user|--project]]";
|
|
35
|
+
const MODEL_COMMAND_ACTIONS = [
|
|
36
|
+
{ value: "status", description: "Show resolved workflow model profiles" },
|
|
37
|
+
{ value: "set", description: "Configure an exact model for a profile" },
|
|
38
|
+
{ value: "clear", description: "Remove a configured profile" },
|
|
39
|
+
] as const;
|
|
40
|
+
const MODEL_COMMAND_SCOPES = [
|
|
41
|
+
{ value: "--user", description: "Write the user-level workflow model config" },
|
|
42
|
+
{ value: "--project", description: "Write the project-level workflow model config" },
|
|
43
|
+
] as const;
|
|
44
|
+
|
|
45
|
+
export function registerWorkflowModelProfileCommand(pi: ExtensionAPI): void {
|
|
46
|
+
pi.registerCommand("workflow:models", {
|
|
47
|
+
description: "Inspect or configure exact small, medium, and big workflow model profiles",
|
|
48
|
+
getArgumentCompletions: workflowModelProfileArgumentCompletions,
|
|
49
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
50
|
+
const command = parseWorkflowModelsCommand(args);
|
|
51
|
+
if (command.kind === "error") {
|
|
52
|
+
ctx.ui.notify(`${command.message}\n${USAGE}`, "warning");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const paths = workflowModelProfilePaths(ctx.cwd);
|
|
58
|
+
if (command.kind === "set") {
|
|
59
|
+
setWorkflowModelProfile({
|
|
60
|
+
configPath: paths[command.scope],
|
|
61
|
+
name: command.profile,
|
|
62
|
+
profile: {
|
|
63
|
+
model: command.model,
|
|
64
|
+
...(command.thinkingLevel === undefined ? {} : { thinkingLevel: command.thinkingLevel }),
|
|
65
|
+
},
|
|
66
|
+
modelRegistry: ctx.modelRegistry,
|
|
67
|
+
});
|
|
68
|
+
} else if (command.kind === "clear") {
|
|
69
|
+
clearWorkflowModelProfile(paths[command.scope], command.profile);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const profiles = resolveWorkflowModelProfiles({
|
|
73
|
+
cwd: ctx.cwd,
|
|
74
|
+
modelRegistry: ctx.modelRegistry,
|
|
75
|
+
hostModel: ctx.model,
|
|
76
|
+
paths,
|
|
77
|
+
});
|
|
78
|
+
const prefix = command.kind === "status"
|
|
79
|
+
? "Workflow model profiles"
|
|
80
|
+
: `${command.profile} ${command.kind === "set" ? "updated" : "cleared"} in ${command.scope} config`;
|
|
81
|
+
ctx.ui.notify(`${prefix}\n${formatWorkflowModelProfiles(profiles, paths)}`, "info");
|
|
82
|
+
} catch (error) {
|
|
83
|
+
ctx.ui.notify(unknownErrorMessage(error), "error");
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function workflowModelProfileArgumentCompletions(argumentPrefix: string): AutocompleteItem[] | null {
|
|
90
|
+
const parts = splitArgumentPrefix(argumentPrefix);
|
|
91
|
+
const completedWithoutScopes = parts.completed.filter((token) => token !== "--user" && token !== "--project");
|
|
92
|
+
const action = completedWithoutScopes[0];
|
|
93
|
+
const scopes = parts.completed.some((token) => token === "--user" || token === "--project")
|
|
94
|
+
? []
|
|
95
|
+
: MODEL_COMMAND_SCOPES;
|
|
96
|
+
|
|
97
|
+
if (completedWithoutScopes.length === 0) {
|
|
98
|
+
return completeCurrentArgument(argumentPrefix, MODEL_COMMAND_ACTIONS);
|
|
99
|
+
}
|
|
100
|
+
if (action === "status") return null;
|
|
101
|
+
if (completedWithoutScopes.length === 1) {
|
|
102
|
+
return completeCurrentArgument(
|
|
103
|
+
argumentPrefix,
|
|
104
|
+
WORKFLOW_MODEL_PROFILE_NAMES.map((value) => ({ value, description: `${value} workflow model profile` })),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
if (action === "clear") return completeCurrentArgument(argumentPrefix, scopes);
|
|
108
|
+
if (action !== "set") return null;
|
|
109
|
+
|
|
110
|
+
if (completedWithoutScopes.length === 2) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
if (completedWithoutScopes.length === 3) {
|
|
114
|
+
return completeCurrentArgument(argumentPrefix, [
|
|
115
|
+
...WORKFLOW_THINKING_LEVELS.map((value) => ({ value, description: "Thinking level" })),
|
|
116
|
+
...scopes,
|
|
117
|
+
]);
|
|
118
|
+
}
|
|
119
|
+
return completeCurrentArgument(argumentPrefix, scopes);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function parseWorkflowModelsCommand(args: string): WorkflowModelsCommand {
|
|
123
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
124
|
+
if (tokens.length === 0 || (tokens.length === 1 && tokens[0] === "status")) return { kind: "status" };
|
|
125
|
+
|
|
126
|
+
const userScope = tokens.includes("--user");
|
|
127
|
+
const projectScope = tokens.includes("--project");
|
|
128
|
+
if (userScope && projectScope) return { kind: "error", message: "Choose either --user or --project, not both." };
|
|
129
|
+
const scope: ConfigScope = projectScope ? "project" : "user";
|
|
130
|
+
const positional = tokens.filter((token) => token !== "--user" && token !== "--project");
|
|
131
|
+
const [action, profile, model, thinkingLevel, ...extra] = positional;
|
|
132
|
+
if (extra.length > 0) return { kind: "error", message: "Too many /workflow:models arguments." };
|
|
133
|
+
if (!profile || !isWorkflowModelProfileName(profile)) {
|
|
134
|
+
return { kind: "error", message: "Profile must be small, medium, or big." };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (action === "clear") {
|
|
138
|
+
if (model !== undefined) return { kind: "error", message: "The clear action accepts only a profile and optional scope." };
|
|
139
|
+
return { kind: "clear", scope, profile };
|
|
140
|
+
}
|
|
141
|
+
if (action !== "set") return { kind: "error", message: `Unknown /workflow:models action "${action ?? ""}".` };
|
|
142
|
+
if (!model) return { kind: "error", message: "The set action requires an exact provider/model identity." };
|
|
143
|
+
if (thinkingLevel !== undefined && !isWorkflowThinkingLevel(thinkingLevel)) {
|
|
144
|
+
return { kind: "error", message: "Thinking level must be off, minimal, low, medium, high, xhigh, or max." };
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
kind: "set",
|
|
148
|
+
scope,
|
|
149
|
+
profile,
|
|
150
|
+
model,
|
|
151
|
+
...(thinkingLevel === undefined ? {} : { thinkingLevel }),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function formatWorkflowModelProfiles(
|
|
156
|
+
profiles: ResolvedWorkflowModelProfiles,
|
|
157
|
+
paths: WorkflowModelProfilePaths,
|
|
158
|
+
): string {
|
|
159
|
+
const lines = WORKFLOW_MODEL_PROFILE_NAMES.map((name) => {
|
|
160
|
+
const profile = profiles[name];
|
|
161
|
+
const model = profile.model ? `${profile.model.provider}/${profile.model.id}` : "(no host model)";
|
|
162
|
+
const thinking = profile.thinkingLevel ?? "host";
|
|
163
|
+
const source = profile.source === "host" ? "host fallback" : `${profile.source}: ${profile.configPath}`;
|
|
164
|
+
return `${name}: ${model} · thinking ${thinking} · ${source}`;
|
|
165
|
+
});
|
|
166
|
+
return [...lines, `user: ${paths.user}`, `project: ${paths.project} (overrides user)`].join("\n");
|
|
167
|
+
}
|