pi-workflow-engine 0.11.0 → 0.13.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-challenge.ts +75 -0
- package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +90 -0
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +13 -5
- 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 +26 -4
- 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 +82 -10
- 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 +13 -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 +127 -41
- package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +24 -0
- package/.pi/extensions/pi-workflow-engine/src/review/patch-validation.ts +120 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +43 -7
- package/.pi/extensions/pi-workflow-engine/src/review/review-format.ts +2 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +1 -1
- 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 +52 -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 +7 -3
- 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 +151 -178
- 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 +47 -3
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +65 -162
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +48 -149
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +28 -77
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +28 -77
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +170 -0
- package/README.md +72 -141
- package/USAGE.md +270 -17
- package/package.json +1 -1
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import type
|
|
3
|
-
import type { KeyId } from "@earendil-works/pi-tui";
|
|
2
|
+
import { CustomEditor, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { EditorComponent, KeyId } from "@earendil-works/pi-tui";
|
|
4
|
+
import { completeCurrentArgument } from "./command-completions.ts";
|
|
4
5
|
import { resolveDynamaxShortcuts, type DynamaxShortcuts } from "./dynamax-shortcuts.ts";
|
|
6
|
+
import { sessionKey } from "./session-identity.ts";
|
|
7
|
+
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
8
|
+
import {
|
|
9
|
+
decorateDynamaxEditor,
|
|
10
|
+
resolveDynamaxEffect,
|
|
11
|
+
type DynamaxAnimationScheduler,
|
|
12
|
+
type DynamaxEditorDecoration,
|
|
13
|
+
type DynamaxEffect,
|
|
14
|
+
} from "./ui/dynamax-editor-decoration.ts";
|
|
5
15
|
|
|
6
16
|
export interface DynamaxState {
|
|
7
17
|
sticky: boolean;
|
|
@@ -15,13 +25,34 @@ export interface DynamaxRuntime {
|
|
|
15
25
|
}
|
|
16
26
|
|
|
17
27
|
export type DynamaxRuntimeStore = Map<string, DynamaxRuntime>;
|
|
28
|
+
|
|
18
29
|
export interface DynamaxRegistrationOptions {
|
|
19
30
|
openInspector?: (ctx: ExtensionContext) => Promise<void> | void;
|
|
31
|
+
effect?: DynamaxEffect;
|
|
32
|
+
animationScheduler?: DynamaxAnimationScheduler;
|
|
20
33
|
}
|
|
21
34
|
|
|
22
35
|
export const DYNAMAX_TOKEN_PATTERN = /(^|[^A-Za-z0-9_])dynamax([^A-Za-z0-9_]|$)/i;
|
|
23
36
|
export const DYNAMAX_STATUS_KEY = "dynamax";
|
|
24
|
-
|
|
37
|
+
|
|
38
|
+
type EditorFactory = NonNullable<ReturnType<ExtensionContext["ui"]["getEditorComponent"]>>;
|
|
39
|
+
|
|
40
|
+
interface DynamaxEditorInstallation {
|
|
41
|
+
factory: EditorFactory;
|
|
42
|
+
previousFactory: EditorFactory | undefined;
|
|
43
|
+
decorations: Set<DynamaxEditorDecoration>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function disposeDynamaxDecorations(decorations: Set<DynamaxEditorDecoration>): void {
|
|
47
|
+
for (const decoration of decorations) decoration.dispose();
|
|
48
|
+
decorations.clear();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const DYNAMAX_ACTION_COMPLETIONS = [
|
|
52
|
+
{ value: "on", description: "Keep Dynamax enabled for this session" },
|
|
53
|
+
{ value: "off", description: "Disable sticky and pending Dynamax modes" },
|
|
54
|
+
{ value: "status", description: "Show the current Dynamax state" },
|
|
55
|
+
] as const;
|
|
25
56
|
|
|
26
57
|
export const ADAPTIVE_WORKFLOW_GUIDANCE = `
|
|
27
58
|
Adaptive multi-pass workflows are optional. Use a simple single-pass fan-out when it is sufficient. When the first pass may expose gaps, conflicts, weak claims, or missing evidence:
|
|
@@ -40,7 +71,7 @@ The user has opted into dynamax multi-agent orchestration. The workflow tool is
|
|
|
40
71
|
Inline workflow rules:
|
|
41
72
|
- Use the injected Type object for schemas, for example Type.Object({ ok: Type.Boolean() }); do not import typebox.
|
|
42
73
|
- Do not use import statements or dynamic import() in inline workflow scripts.
|
|
43
|
-
- Set
|
|
74
|
+
- Set profile to "small", "medium", or "big" on each agent() call so routing remains explicit; use model/thinkingLevel only for an intentional override.
|
|
44
75
|
- Subagents receive no skills by default. Add \`skills: ["skill-name"]\` per agent only when that stage should load that skill; grant the smallest useful set.
|
|
45
76
|
- Provide exactly one of workflow.name or workflow.script.
|
|
46
77
|
|
|
@@ -57,12 +88,8 @@ export function createDynamaxRuntime(): DynamaxRuntime {
|
|
|
57
88
|
return { state: createDynamaxState() };
|
|
58
89
|
}
|
|
59
90
|
|
|
60
|
-
export function dynamaxSessionKey(ctx: Pick<ExtensionContext, "sessionManager">): string {
|
|
61
|
-
return ctx.sessionManager.getSessionFile() ?? ctx.sessionManager.getSessionId();
|
|
62
|
-
}
|
|
63
|
-
|
|
64
91
|
export function getDynamaxRuntime(store: DynamaxRuntimeStore, ctx: Pick<ExtensionContext, "sessionManager">): DynamaxRuntime {
|
|
65
|
-
const key =
|
|
92
|
+
const key = sessionKey(ctx);
|
|
66
93
|
const existing = store.get(key);
|
|
67
94
|
if (existing) return existing;
|
|
68
95
|
const runtime = createDynamaxRuntime();
|
|
@@ -78,12 +105,8 @@ export function markDynamaxOneShot(state: DynamaxState): void {
|
|
|
78
105
|
state.oneShotPending = true;
|
|
79
106
|
}
|
|
80
107
|
|
|
81
|
-
export function
|
|
82
|
-
state.sticky =
|
|
83
|
-
if (!sticky) {
|
|
84
|
-
state.oneShotPending = false;
|
|
85
|
-
state.turnActive = false;
|
|
86
|
-
}
|
|
108
|
+
export function enableDynamaxSticky(state: DynamaxState): void {
|
|
109
|
+
state.sticky = true;
|
|
87
110
|
}
|
|
88
111
|
|
|
89
112
|
export function clearDynamax(state: DynamaxState): void {
|
|
@@ -92,12 +115,6 @@ export function clearDynamax(state: DynamaxState): void {
|
|
|
92
115
|
state.turnActive = false;
|
|
93
116
|
}
|
|
94
117
|
|
|
95
|
-
export function consumeDynamaxOneShot(state: DynamaxState): boolean {
|
|
96
|
-
const pending = state.oneShotPending;
|
|
97
|
-
state.oneShotPending = false;
|
|
98
|
-
return pending;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
118
|
export function isDynamaxActive(state: DynamaxState): boolean {
|
|
102
119
|
return state.sticky || state.oneShotPending || state.turnActive;
|
|
103
120
|
}
|
|
@@ -139,23 +156,84 @@ export function registerDynamax(
|
|
|
139
156
|
options: DynamaxRegistrationOptions = {},
|
|
140
157
|
): void {
|
|
141
158
|
const runtimes: DynamaxRuntimeStore = new Map();
|
|
159
|
+
const effect = options.effect ?? resolveDynamaxEffect();
|
|
160
|
+
let editorInstallation: DynamaxEditorInstallation | undefined;
|
|
142
161
|
const openInspector =
|
|
143
162
|
options.openInspector ??
|
|
144
163
|
((ctx: ExtensionContext): void => {
|
|
145
164
|
ctx.ui.notify("No workflow inspector is available yet", "warning");
|
|
146
165
|
});
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
166
|
+
const installEditor = (ctx: ExtensionContext): void => {
|
|
167
|
+
if (ctx.mode !== "tui" || effect === "off") return;
|
|
168
|
+
const previousFactory = ctx.ui.getEditorComponent();
|
|
169
|
+
if (editorInstallation && editorInstallation.factory === previousFactory) return;
|
|
170
|
+
|
|
171
|
+
const decorations = new Set<DynamaxEditorDecoration>();
|
|
172
|
+
const installation: DynamaxEditorInstallation = {
|
|
173
|
+
previousFactory,
|
|
174
|
+
decorations,
|
|
175
|
+
factory: (tui, theme, keybindings) => {
|
|
176
|
+
const decorate = (editor: EditorComponent): EditorComponent => {
|
|
177
|
+
const decoration = decorateDynamaxEditor(editor, () => tui.requestRender(), {
|
|
178
|
+
effect,
|
|
179
|
+
scheduler: options.animationScheduler,
|
|
180
|
+
isActive: () => editorInstallation === installation && ctx.ui.getEditorComponent() === installation.factory,
|
|
181
|
+
});
|
|
182
|
+
decorations.add(decoration);
|
|
183
|
+
return decoration.editor;
|
|
184
|
+
};
|
|
185
|
+
if (!previousFactory) return decorate(new CustomEditor(tui, theme, keybindings));
|
|
186
|
+
|
|
187
|
+
let editor: EditorComponent;
|
|
188
|
+
try {
|
|
189
|
+
editor = previousFactory(tui, theme, keybindings);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
ctx.ui.notify(
|
|
192
|
+
`Dynamax could not compose the existing custom editor (${unknownErrorMessage(error)}); using pi's stock-compatible CustomEditor so highlighting stays enabled`,
|
|
193
|
+
"warning",
|
|
194
|
+
);
|
|
195
|
+
installation.previousFactory = undefined;
|
|
196
|
+
return decorate(new CustomEditor(tui, theme, keybindings));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
return decorate(editor);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
ctx.ui.notify(
|
|
203
|
+
`Dynamax could not decorate the existing custom editor (${unknownErrorMessage(error)}); using pi's stock-compatible CustomEditor so highlighting stays enabled`,
|
|
204
|
+
"warning",
|
|
205
|
+
);
|
|
206
|
+
return decorate(new CustomEditor(tui, theme, keybindings));
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
ctx.ui.setEditorComponent(installation.factory);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
disposeDynamaxDecorations(decorations);
|
|
215
|
+
ctx.ui.notify(`Dynamax highlighting could not be installed: ${unknownErrorMessage(error)}`, "error");
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
editorInstallation = installation;
|
|
219
|
+
};
|
|
220
|
+
const uninstallEditor = (ctx: ExtensionContext): void => {
|
|
221
|
+
const installation = editorInstallation;
|
|
222
|
+
if (!installation) return;
|
|
223
|
+
editorInstallation = undefined;
|
|
224
|
+
disposeDynamaxDecorations(installation.decorations);
|
|
225
|
+
if (ctx.mode === "tui" && ctx.ui.getEditorComponent() === installation.factory) {
|
|
226
|
+
ctx.ui.setEditorComponent(installation.previousFactory);
|
|
151
227
|
}
|
|
152
228
|
};
|
|
153
229
|
|
|
154
230
|
pi.on("session_start", (_event, ctx) => {
|
|
231
|
+
installEditor(ctx);
|
|
155
232
|
updateDynamaxSurfaces(ctx, getDynamaxRuntime(runtimes, ctx), shortcuts);
|
|
156
233
|
});
|
|
157
234
|
|
|
158
235
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
236
|
+
uninstallEditor(ctx);
|
|
159
237
|
clearDynamaxSurfaces(ctx);
|
|
160
238
|
});
|
|
161
239
|
|
|
@@ -177,26 +255,6 @@ export function registerDynamax(
|
|
|
177
255
|
return { systemPrompt };
|
|
178
256
|
});
|
|
179
257
|
|
|
180
|
-
pi.on("agent_start", (_event, ctx) => {
|
|
181
|
-
refreshIfVisible(ctx);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
pi.on("message_start", (_event, ctx) => {
|
|
185
|
-
refreshIfVisible(ctx);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
pi.on("message_update", (_event, ctx) => {
|
|
189
|
-
refreshIfVisible(ctx);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
pi.on("message_end", (_event, ctx) => {
|
|
193
|
-
refreshIfVisible(ctx);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
pi.on("turn_end", (_event, ctx) => {
|
|
197
|
-
refreshIfVisible(ctx);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
258
|
pi.on("agent_end", (_event, ctx) => {
|
|
201
259
|
const runtime = getDynamaxRuntime(runtimes, ctx);
|
|
202
260
|
runtime.runningWorkflow = undefined;
|
|
@@ -205,23 +263,17 @@ export function registerDynamax(
|
|
|
205
263
|
});
|
|
206
264
|
|
|
207
265
|
pi.on("tool_execution_start", (event, ctx) => {
|
|
266
|
+
if (event.toolName !== "workflow") return undefined;
|
|
208
267
|
const runtime = getDynamaxRuntime(runtimes, ctx);
|
|
209
|
-
|
|
210
|
-
runtime.runningWorkflow = workflowLabel(event.args);
|
|
211
|
-
}
|
|
268
|
+
runtime.runningWorkflow = workflowLabel(event.args);
|
|
212
269
|
updateDynamaxSurfaces(ctx, runtime, shortcuts);
|
|
213
270
|
return undefined;
|
|
214
271
|
});
|
|
215
272
|
|
|
216
|
-
pi.on("tool_execution_update", (_event, ctx) => {
|
|
217
|
-
refreshIfVisible(ctx);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
273
|
pi.on("tool_execution_end", (event, ctx) => {
|
|
274
|
+
if (event.toolName !== "workflow") return undefined;
|
|
221
275
|
const runtime = getDynamaxRuntime(runtimes, ctx);
|
|
222
|
-
|
|
223
|
-
runtime.runningWorkflow = undefined;
|
|
224
|
-
}
|
|
276
|
+
runtime.runningWorkflow = undefined;
|
|
225
277
|
updateDynamaxSurfaces(ctx, runtime, shortcuts);
|
|
226
278
|
return undefined;
|
|
227
279
|
});
|
|
@@ -244,6 +296,7 @@ export function registerDynamax(
|
|
|
244
296
|
|
|
245
297
|
pi.registerCommand("workflow:dynamax", {
|
|
246
298
|
description: "Toggle Dynamax workflow orchestration: /workflow:dynamax [on|off|status]",
|
|
299
|
+
getArgumentCompletions: (argumentPrefix) => completeCurrentArgument(argumentPrefix, DYNAMAX_ACTION_COMPLETIONS),
|
|
247
300
|
handler: async (args, ctx) => {
|
|
248
301
|
const runtime = getDynamaxRuntime(runtimes, ctx);
|
|
249
302
|
const action = args.trim().toLowerCase();
|
|
@@ -252,7 +305,7 @@ export function registerDynamax(
|
|
|
252
305
|
return;
|
|
253
306
|
}
|
|
254
307
|
if (action === "on") {
|
|
255
|
-
|
|
308
|
+
enableDynamaxSticky(runtime.state);
|
|
256
309
|
updateDynamaxSurfaces(ctx, runtime, shortcuts);
|
|
257
310
|
ctx.ui.notify("Dynamax workflow orchestration is on for this session", "info");
|
|
258
311
|
return;
|
|
@@ -274,8 +327,6 @@ export function registerDynamax(
|
|
|
274
327
|
|
|
275
328
|
export function updateDynamaxSurfaces(ctx: Pick<ExtensionContext, "hasUI" | "ui">, runtime: DynamaxRuntime, shortcuts: DynamaxShortcuts): void {
|
|
276
329
|
if (!ctx.hasUI) return;
|
|
277
|
-
ctx.ui.setWidget(DYNAMAX_WIDGET_KEY, undefined);
|
|
278
|
-
ctx.ui.setStatus(DYNAMAX_WIDGET_KEY, undefined);
|
|
279
330
|
if (!isDynamaxActive(runtime.state) && !runtime.runningWorkflow) {
|
|
280
331
|
clearDynamaxSurfaces(ctx);
|
|
281
332
|
return;
|
|
@@ -287,9 +338,7 @@ export function updateDynamaxSurfaces(ctx: Pick<ExtensionContext, "hasUI" | "ui"
|
|
|
287
338
|
|
|
288
339
|
export function clearDynamaxSurfaces(ctx: Pick<ExtensionContext, "hasUI" | "ui">): void {
|
|
289
340
|
if (!ctx.hasUI) return;
|
|
290
|
-
ctx.ui.setWidget(DYNAMAX_WIDGET_KEY, undefined);
|
|
291
341
|
ctx.ui.setStatus(DYNAMAX_STATUS_KEY, undefined);
|
|
292
|
-
ctx.ui.setStatus(DYNAMAX_WIDGET_KEY, undefined);
|
|
293
342
|
}
|
|
294
343
|
|
|
295
344
|
function workflowLabel(args: unknown): string {
|
|
@@ -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,9 +111,11 @@ 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,
|
|
118
|
+
candidatePatch: opts.candidatePatch ? { baselineOid: opts.candidatePatch.baselineOid, hash: createHash("sha256").update(opts.candidatePatch.patch).digest("hex") } : undefined,
|
|
116
119
|
worktreeBaseline: worktreeBaseline
|
|
117
120
|
? {
|
|
118
121
|
ref: worktreeBaseline.ref,
|
|
@@ -170,6 +173,8 @@ export async function createWorkflowJournal(options: {
|
|
|
170
173
|
readonly writePath: string;
|
|
171
174
|
}): Promise<WorkflowJournal> {
|
|
172
175
|
const priorEntries = options.resumePath ? await loadJournalEntries(options.resumePath, { required: true }) : [];
|
|
176
|
+
await mkdir(dirname(options.writePath), { recursive: true });
|
|
177
|
+
await appendFile(options.writePath, "", "utf8");
|
|
173
178
|
return createMemoryBackedJournal(priorEntries, options.writePath);
|
|
174
179
|
}
|
|
175
180
|
|
|
@@ -182,7 +187,7 @@ export function createMemoryBackedJournal(priorEntries: readonly JournalEntry[]
|
|
|
182
187
|
}
|
|
183
188
|
|
|
184
189
|
return {
|
|
185
|
-
lookup(key, identity) {
|
|
190
|
+
lookup(key, identity, options) {
|
|
186
191
|
const entries = priorByKey.get(key);
|
|
187
192
|
if (!entries || entries.length === 0) return { hit: false };
|
|
188
193
|
|
|
@@ -197,10 +202,12 @@ export function createMemoryBackedJournal(priorEntries: readonly JournalEntry[]
|
|
|
197
202
|
: "journal entry predates replay contract v2",
|
|
198
203
|
};
|
|
199
204
|
}
|
|
200
|
-
const matches = current.filter(
|
|
205
|
+
const matches = current.filter(
|
|
206
|
+
(entry) => resumeContextMismatchReason(entry.identity, identity, options) === undefined,
|
|
207
|
+
);
|
|
201
208
|
if (matches.length === 1) return { hit: true, value: matches[0]!.result };
|
|
202
209
|
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) };
|
|
210
|
+
return { hit: false, reason: resumeContextMismatchReason(current[0]!.identity, identity, options) };
|
|
204
211
|
},
|
|
205
212
|
async record(key, result, identity) {
|
|
206
213
|
const entry: JournalEntryV2 = { version: 2, key, result, identity };
|
|
@@ -248,7 +255,7 @@ export async function pruneWorkflowJournals(cwd: string, keep = WORKFLOW_JOURNAL
|
|
|
248
255
|
await Promise.all(stale.map((entry) => rm(entry.path, { force: true }).catch(() => undefined)));
|
|
249
256
|
}
|
|
250
257
|
|
|
251
|
-
function
|
|
258
|
+
export function validateWorkflowRunId(runId: string): string {
|
|
252
259
|
const trimmed = runId.trim();
|
|
253
260
|
if (!/^[A-Za-z0-9_.-]+$/.test(trimmed)) {
|
|
254
261
|
throw new Error("Workflow run id must contain only letters, numbers, dots, underscores, or hyphens.");
|