pi-workflow-engine 0.13.0 → 1.1.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 +152 -416
- package/.pi/extensions/pi-workflow-engine/src/advisory-challenge.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +17 -14
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +7 -1
- package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +48 -62
- package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +46 -67
- package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +14 -37
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +12 -13
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +15 -21
- package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +13 -43
- package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +17 -5
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +131 -117
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +11 -43
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +4 -18
- package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +3 -0
- package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +33 -86
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +1 -5
- package/.pi/extensions/pi-workflow-engine/src/classify.ts +107 -0
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +25 -80
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +16 -23
- package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +1 -4
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +58 -117
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +41 -80
- package/.pi/extensions/pi-workflow-engine/src/finalizers.ts +2 -10
- package/.pi/extensions/pi-workflow-engine/src/git.ts +55 -0
- package/.pi/extensions/pi-workflow-engine/src/guards.ts +8 -0
- package/.pi/extensions/pi-workflow-engine/src/host-tools.ts +76 -0
- package/.pi/extensions/pi-workflow-engine/src/identity-canonicalization.ts +7 -19
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +171 -106
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +0 -12
- package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +37 -8
- package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +15 -66
- package/.pi/extensions/pi-workflow-engine/src/model-ref.ts +19 -0
- package/.pi/extensions/pi-workflow-engine/src/options.ts +97 -16
- package/.pi/extensions/pi-workflow-engine/src/perf.ts +43 -72
- package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +2 -2
- package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +14 -4
- package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +2 -0
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +79 -80
- package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +28 -20
- package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +2 -7
- package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +0 -1
- package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +29 -31
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +75 -139
- package/.pi/extensions/pi-workflow-engine/src/review/github-pr-comments.ts +14 -31
- package/.pi/extensions/pi-workflow-engine/src/review/patch-validation.ts +3 -4
- package/.pi/extensions/pi-workflow-engine/src/review/review-actions.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-diff-lines.ts +54 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +7 -9
- package/.pi/extensions/pi-workflow-engine/src/review/review-format.ts +51 -117
- package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +8 -13
- package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +3 -7
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +158 -241
- package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +28 -38
- package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +35 -44
- package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +13 -14
- package/.pi/extensions/pi-workflow-engine/src/text.ts +29 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +17 -4
- package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +16 -28
- package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +34 -14
- package/.pi/extensions/pi-workflow-engine/src/types.ts +30 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/kit.ts +193 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/research-result-renderer.ts +177 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/result-layout.ts +209 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +17 -44
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +703 -235
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-picker.ts +48 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +239 -129
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-runs-browser.ts +461 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-tool-renderer.ts +91 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +316 -98
- package/.pi/extensions/pi-workflow-engine/src/usage.ts +41 -34
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +162 -22
- package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +19 -24
- package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +17 -37
- package/.pi/extensions/pi-workflow-engine/src/workflow-result-context.ts +79 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +4 -14
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +77 -63
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +42 -29
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +15 -14
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +27 -31
- package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +1 -5
- package/.pi/extensions/pi-workflow-engine/src/workflows.ts +3 -6
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +48 -110
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +64 -93
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +25 -67
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +19 -53
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +21 -49
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +74 -66
- package/README.md +1 -1
- package/USAGE.md +79 -20
- package/package.json +1 -2
- package/skills/workflow-code-review-actions/SKILL.md +12 -23
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +0 -81
- package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +0 -24
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +0 -52
- package/.pi/settings.json +0 -3
|
@@ -62,12 +62,13 @@ export function resolveAgentSkillRequest(prompt: string, explicitSkills: unknown
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export function extractSkillSelectorsFromText(text: string): string[] {
|
|
65
|
-
const selectors
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
const selectors = [
|
|
66
|
+
...Array.from(text.matchAll(SLASH_SKILL_PATTERN), (match) => match[1] ?? ""),
|
|
67
|
+
...[...text.matchAll(SKILLS_FIELD_PATTERN), ...text.matchAll(VERB_SKILL_LIST_PATTERN)]
|
|
68
|
+
.flatMap((match) => parseSkillList(match[1] ?? "")),
|
|
69
|
+
...Array.from(text.matchAll(VERB_NAMED_SKILL_PATTERN), (match) => match[2] ?? ""),
|
|
70
|
+
].map(normalizeSkillSelector);
|
|
71
|
+
return [...new Set(selectors.filter((value) => value && !isGenericSkillWord(value)))];
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
export function normalizeSkillSelector(value: string): string {
|
|
@@ -179,23 +180,6 @@ export async function captureAgentSkillIdentities(
|
|
|
179
180
|
return { kind: "verified", skills: identities };
|
|
180
181
|
}
|
|
181
182
|
|
|
182
|
-
function collectPatternMatches(text: string, pattern: RegExp, selectors: string[], captureIndex: number): void {
|
|
183
|
-
pattern.lastIndex = 0;
|
|
184
|
-
for (let match = pattern.exec(text); match !== null; match = pattern.exec(text)) {
|
|
185
|
-
const value = match[captureIndex];
|
|
186
|
-
if (value !== undefined) selectors.push(value);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function collectListPatternMatches(text: string, pattern: RegExp, selectors: string[]): void {
|
|
191
|
-
pattern.lastIndex = 0;
|
|
192
|
-
for (let match = pattern.exec(text); match !== null; match = pattern.exec(text)) {
|
|
193
|
-
const value = match[1];
|
|
194
|
-
if (value === undefined) continue;
|
|
195
|
-
selectors.push(...parseSkillList(value));
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
183
|
function parseSkillList(fragment: string): string[] {
|
|
200
184
|
const scoped = fragment.replace(PURPOSE_BOUNDARY_PATTERN, "");
|
|
201
185
|
return scoped
|
|
@@ -208,9 +192,7 @@ function normalizeExplicitSkillSelectors(value: unknown): string[] {
|
|
|
208
192
|
if (!Array.isArray(value)) {
|
|
209
193
|
throw new Error("Invalid subagent skills option: expected an array of skill names.");
|
|
210
194
|
}
|
|
211
|
-
const
|
|
212
|
-
const result: string[] = [];
|
|
213
|
-
for (const item of value) {
|
|
195
|
+
const selectors = value.map((item: unknown) => {
|
|
214
196
|
if (typeof item !== "string") {
|
|
215
197
|
throw new Error("Invalid subagent skills option: every skill name must be a string.");
|
|
216
198
|
}
|
|
@@ -218,11 +200,9 @@ function normalizeExplicitSkillSelectors(value: unknown): string[] {
|
|
|
218
200
|
if (!normalized || isGenericSkillWord(normalized)) {
|
|
219
201
|
throw new Error(`Invalid subagent skills option: \"${item}\" is not a valid skill name.`);
|
|
220
202
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
return result;
|
|
203
|
+
return normalized;
|
|
204
|
+
});
|
|
205
|
+
return [...new Set(selectors)];
|
|
226
206
|
}
|
|
227
207
|
|
|
228
208
|
function logSkillDiagnostics(log: ((message: string) => void) | undefined, diagnostics: readonly ResourceDiagnostic[], requested: boolean): void {
|
|
@@ -233,18 +213,6 @@ function logSkillDiagnostics(log: ((message: string) => void) | undefined, diagn
|
|
|
233
213
|
}
|
|
234
214
|
}
|
|
235
215
|
|
|
236
|
-
function uniqueSelectors(values: Iterable<string>): string[] {
|
|
237
|
-
const seen = new Set<string>();
|
|
238
|
-
const result: string[] = [];
|
|
239
|
-
for (const value of values) {
|
|
240
|
-
const normalized = normalizeSkillSelector(value);
|
|
241
|
-
if (!normalized || isGenericSkillWord(normalized) || seen.has(normalized)) continue;
|
|
242
|
-
seen.add(normalized);
|
|
243
|
-
result.push(normalized);
|
|
244
|
-
}
|
|
245
|
-
return result;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
216
|
function isGenericSkillWord(value: string): boolean {
|
|
249
217
|
return value === "skill" || value === "skills" || value === "the" || value === "this" || value === "these" || value === "that";
|
|
250
218
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { AgentExecutionOptions, AgentProgress } from "./agent-runner-types.ts";
|
|
2
|
-
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
3
2
|
import type { WorktreeRegistry } from "./worktree.ts";
|
|
4
3
|
|
|
5
4
|
interface AgentWorkspaceBase {
|
|
@@ -45,7 +44,7 @@ export async function createAgentWorkspace(
|
|
|
45
44
|
|
|
46
45
|
const probe = await rc.worktrees.probe(rc.signal);
|
|
47
46
|
if (!probe.ok) {
|
|
48
|
-
throw new Error(`Failed to check git worktree availability for isolated agent: ${probe.error
|
|
47
|
+
throw new Error(`Failed to check git worktree availability for isolated agent: ${probe.error}`);
|
|
49
48
|
}
|
|
50
49
|
if (!probe.inside) {
|
|
51
50
|
throw new Error("Agent requested worktree isolation, but the workflow cwd is not inside a git work tree.");
|
|
@@ -59,11 +58,11 @@ export async function createAgentWorkspace(
|
|
|
59
58
|
if (opts.candidatePatch.baselineOid !== added.baselineOid) throw new Error("Candidate baseline differs from evaluator baseline");
|
|
60
59
|
if (opts.candidatePatch.patch.trim()) {
|
|
61
60
|
const applied = await rc.worktrees.applyPatch(worktreePath, opts.candidatePatch.patch, rc.signal);
|
|
62
|
-
if (!applied.ok) throw new Error(`Candidate patch could not be applied: ${applied.error
|
|
61
|
+
if (!applied.ok) throw new Error(`Candidate patch could not be applied: ${applied.error}`);
|
|
63
62
|
}
|
|
64
63
|
} catch (error) {
|
|
65
64
|
const removed = await rc.worktrees.remove(worktreePath);
|
|
66
|
-
if (!removed.ok) rc.progress.log(`${label}: evaluator setup cleanup failed: ${removed.error
|
|
65
|
+
if (!removed.ok) rc.progress.log(`${label}: evaluator setup cleanup failed: ${removed.error}`);
|
|
67
66
|
throw error;
|
|
68
67
|
}
|
|
69
68
|
}
|
|
@@ -82,22 +81,9 @@ export async function createAgentWorkspace(
|
|
|
82
81
|
const removed = await rc.worktrees.remove(worktreePath);
|
|
83
82
|
if (!removed.ok) {
|
|
84
83
|
rc.progress.log(
|
|
85
|
-
`${label}: failed to remove isolated worktree (${removed.error
|
|
84
|
+
`${label}: failed to remove isolated worktree (${removed.error})`,
|
|
86
85
|
);
|
|
87
86
|
}
|
|
88
87
|
},
|
|
89
88
|
};
|
|
90
89
|
}
|
|
91
|
-
|
|
92
|
-
export async function disposeAgentWorkspace(
|
|
93
|
-
rc: Pick<AgentWorkspaceContext, "progress">,
|
|
94
|
-
label: string,
|
|
95
|
-
workspace: AgentWorkspace | undefined,
|
|
96
|
-
): Promise<void> {
|
|
97
|
-
if (!workspace) return;
|
|
98
|
-
try {
|
|
99
|
-
await workspace.dispose();
|
|
100
|
-
} catch (error) {
|
|
101
|
-
rc.progress.log(`${label}: failed to dispose isolated workspace (${unknownErrorMessage(error)})`);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
@@ -7,6 +7,7 @@ import { unknownErrorMessage } from "./unknown-error.ts";
|
|
|
7
7
|
export interface BackgroundWorkflowToolResult {
|
|
8
8
|
readonly content: Array<{ readonly type: "text"; readonly text: string }>;
|
|
9
9
|
readonly details: Readonly<Record<string, unknown>>;
|
|
10
|
+
readonly isError?: true;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export interface BackgroundWorkflowToolStartInput {
|
|
@@ -25,6 +26,7 @@ export function backgroundUnavailableResult(mode: ExtensionContext["mode"]): Bac
|
|
|
25
26
|
text: `Background workflows are unavailable in ${mode} mode because pi exits after the prompt. Run synchronously or use TUI/RPC mode.`,
|
|
26
27
|
}],
|
|
27
28
|
details: { error: "background_unavailable", mode },
|
|
29
|
+
isError: true,
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
32
|
|
|
@@ -63,6 +65,7 @@ export async function startBackgroundWorkflowTool(
|
|
|
63
65
|
return {
|
|
64
66
|
content: [{ type: "text", text: `Background workflow did not start: ${message}` }],
|
|
65
67
|
details: { error: "background_start_failed", message, runId },
|
|
68
|
+
isError: true,
|
|
66
69
|
};
|
|
67
70
|
}
|
|
68
71
|
return {
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { SessionManager, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { WorkflowAbortError, WorkflowPauseError } from "./cancellation.ts";
|
|
3
|
+
import { isRecord } from "./guards.ts";
|
|
3
4
|
import type { WorkflowBackgroundOrigin } from "./types.ts";
|
|
4
5
|
import {
|
|
6
|
+
stopWorkflowRunRecord,
|
|
5
7
|
transitionWorkflowRun,
|
|
6
8
|
type WorkflowRunRecord,
|
|
7
9
|
type WorkflowRunState,
|
|
8
10
|
} from "./workflow-run-record.ts";
|
|
9
|
-
import { updateWorkflowRunDelivery } from "./workflow-run-background.ts";
|
|
11
|
+
import { updateWorkflowRunDelivery, type WorkflowRunDelivery } from "./workflow-run-background.ts";
|
|
10
12
|
import { ProjectWorkflowRunStore, type WorkflowRunStore } from "./workflow-run-store.ts";
|
|
13
|
+
import { truncateText } from "./text.ts";
|
|
14
|
+
import { workflowResultSummary } from "./workflow-execution.ts";
|
|
11
15
|
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
12
|
-
import { emptyWorkflowUsageTotals } from "./usage.ts";
|
|
13
16
|
|
|
14
17
|
const BACKGROUND_DELIVERY_CUSTOM_TYPE = "workflow-result";
|
|
15
|
-
const BACKGROUND_WIDGET_KEY = "workflow-background";
|
|
16
18
|
const SHUTDOWN_WAIT_MS = 5_000;
|
|
17
19
|
const SUMMARY_LIMIT = 500;
|
|
18
20
|
|
|
@@ -25,7 +27,10 @@ export interface BackgroundWorkflowStartInput {
|
|
|
25
27
|
|
|
26
28
|
export interface BackgroundWorkflowResultDetails {
|
|
27
29
|
readonly name: string;
|
|
28
|
-
|
|
30
|
+
/** The retained result of a completed run, so the delivery renders like a foreground result; else `{ summary }`. */
|
|
31
|
+
readonly result: unknown;
|
|
32
|
+
readonly summary: string;
|
|
33
|
+
readonly startedAt?: number;
|
|
29
34
|
readonly completedAt: number;
|
|
30
35
|
readonly usage: WorkflowRunRecord["usage"];
|
|
31
36
|
readonly runId: string;
|
|
@@ -56,7 +61,7 @@ type BackgroundWorkflowSettledListener = (ctx: ExtensionContext, runId: string)
|
|
|
56
61
|
/** Owns background runs for one extension instance and routes their durable completion delivery. */
|
|
57
62
|
export class BackgroundWorkflowCoordinator {
|
|
58
63
|
private readonly active = new Map<string, ActiveBackgroundRun>();
|
|
59
|
-
private
|
|
64
|
+
private settledListener: BackgroundWorkflowSettledListener | undefined;
|
|
60
65
|
private readonly pendingDelivery = new Map<string, Set<string>>();
|
|
61
66
|
private readonly shuttingDown = new Set<string>();
|
|
62
67
|
private readonly storeForCwd: (cwd: string) => WorkflowRunStore;
|
|
@@ -82,13 +87,8 @@ export class BackgroundWorkflowCoordinator {
|
|
|
82
87
|
let accepted = false;
|
|
83
88
|
let deliveryScheduled = false;
|
|
84
89
|
let settled = false;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
let rejectStarted: ((error: unknown) => void) | undefined;
|
|
88
|
-
const started = new Promise<void>((resolve, reject) => {
|
|
89
|
-
resolveStarted = resolve;
|
|
90
|
-
rejectStarted = reject;
|
|
91
|
-
});
|
|
90
|
+
// Settles once: a rejection after the run published its metadata is ignored.
|
|
91
|
+
const started = Promise.withResolvers<void>();
|
|
92
92
|
|
|
93
93
|
const scheduleDelivery = (): void => {
|
|
94
94
|
if (deliveryScheduled || !accepted || !settled || this.shuttingDown.has(sessionId)) return;
|
|
@@ -100,18 +100,13 @@ export class BackgroundWorkflowCoordinator {
|
|
|
100
100
|
|
|
101
101
|
const settledPromise = (async () => {
|
|
102
102
|
try {
|
|
103
|
-
await input.run(controller.signal, () =>
|
|
104
|
-
|
|
105
|
-
startedSignalled = true;
|
|
106
|
-
resolveStarted?.();
|
|
107
|
-
});
|
|
108
|
-
if (!startedSignalled) rejectStarted?.(new Error("Background workflow ended before publishing run metadata."));
|
|
103
|
+
await input.run(controller.signal, () => started.resolve());
|
|
104
|
+
started.reject(new Error("Background workflow ended before publishing run metadata."));
|
|
109
105
|
} catch (error) {
|
|
110
|
-
|
|
106
|
+
started.reject(error);
|
|
111
107
|
} finally {
|
|
112
108
|
settled = true;
|
|
113
109
|
this.active.delete(input.runId);
|
|
114
|
-
this.updateBackgroundSurface(input.ctx);
|
|
115
110
|
await this.notifyRunSettled(input.ctx, input.runId);
|
|
116
111
|
scheduleDelivery();
|
|
117
112
|
}
|
|
@@ -124,7 +119,7 @@ export class BackgroundWorkflowCoordinator {
|
|
|
124
119
|
isSettled: () => settled,
|
|
125
120
|
});
|
|
126
121
|
|
|
127
|
-
await started;
|
|
122
|
+
await started.promise;
|
|
128
123
|
const record = await this.storeForCwd(input.ctx.cwd).load(input.runId);
|
|
129
124
|
if (record?.state !== "running" || !record.background || record.background.origin.sessionId !== sessionId) {
|
|
130
125
|
controller.abort(new WorkflowAbortError("Background workflow could not persist its origin metadata."));
|
|
@@ -132,7 +127,6 @@ export class BackgroundWorkflowCoordinator {
|
|
|
132
127
|
}
|
|
133
128
|
|
|
134
129
|
accepted = true;
|
|
135
|
-
this.updateBackgroundSurface(input.ctx);
|
|
136
130
|
scheduleDelivery();
|
|
137
131
|
}
|
|
138
132
|
|
|
@@ -145,9 +139,8 @@ export class BackgroundWorkflowCoordinator {
|
|
|
145
139
|
);
|
|
146
140
|
}
|
|
147
141
|
|
|
148
|
-
onRunSettled(listener: BackgroundWorkflowSettledListener):
|
|
149
|
-
this.
|
|
150
|
-
return () => this.settledListeners.delete(listener);
|
|
142
|
+
onRunSettled(listener: BackgroundWorkflowSettledListener): void {
|
|
143
|
+
this.settledListener = listener;
|
|
151
144
|
}
|
|
152
145
|
|
|
153
146
|
async stop(ctx: ExtensionContext, runId: string): Promise<WorkflowRunRecord> {
|
|
@@ -243,7 +236,6 @@ export class BackgroundWorkflowCoordinator {
|
|
|
243
236
|
this.log(`[workflow:${runId}] failed to force paused state during shutdown: ${unknownErrorMessage(error)}`);
|
|
244
237
|
}
|
|
245
238
|
}
|
|
246
|
-
if (ctx.hasUI) ctx.ui.setWidget(BACKGROUND_WIDGET_KEY, undefined);
|
|
247
239
|
}
|
|
248
240
|
|
|
249
241
|
private async queueOrDeliver(ctx: ExtensionContext, runId: string): Promise<void> {
|
|
@@ -262,12 +254,10 @@ export class BackgroundWorkflowCoordinator {
|
|
|
262
254
|
}
|
|
263
255
|
|
|
264
256
|
private async notifyRunSettled(ctx: ExtensionContext, runId: string): Promise<void> {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
269
|
-
this.log(`[workflow:${runId}] background settlement listener failed: ${unknownErrorMessage(error)}`);
|
|
270
|
-
}
|
|
257
|
+
try {
|
|
258
|
+
await this.settledListener?.(ctx, runId);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
this.log(`[workflow:${runId}] background settlement listener failed: ${unknownErrorMessage(error)}`);
|
|
271
261
|
}
|
|
272
262
|
}
|
|
273
263
|
|
|
@@ -292,23 +282,6 @@ export class BackgroundWorkflowCoordinator {
|
|
|
292
282
|
this.pendingDelivery.set(sessionId, pending);
|
|
293
283
|
}
|
|
294
284
|
|
|
295
|
-
private updateBackgroundSurface(ctx: ExtensionContext): void {
|
|
296
|
-
if (!ctx.hasUI) return;
|
|
297
|
-
const sessionId = ctx.sessionManager.getSessionId();
|
|
298
|
-
const runs = [...this.active.entries()]
|
|
299
|
-
.filter(([, run]) => run.sessionId === sessionId)
|
|
300
|
-
.map(([runId, run]) => ({ runId, name: run.name }));
|
|
301
|
-
if (runs.length === 0) {
|
|
302
|
-
ctx.ui.setWidget(BACKGROUND_WIDGET_KEY, undefined);
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
ctx.ui.setWidget(
|
|
306
|
-
BACKGROUND_WIDGET_KEY,
|
|
307
|
-
[formatBackgroundActivity(runs, ctx.ui.theme)],
|
|
308
|
-
{ placement: "aboveEditor" },
|
|
309
|
-
);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
285
|
private async deliver(ctx: ExtensionContext, runId: string): Promise<boolean> {
|
|
313
286
|
const store = this.storeForCwd(ctx.cwd);
|
|
314
287
|
const record = await store.load(runId);
|
|
@@ -333,25 +306,18 @@ export class BackgroundWorkflowCoordinator {
|
|
|
333
306
|
}
|
|
334
307
|
}
|
|
335
308
|
|
|
336
|
-
function formatBackgroundActivity(
|
|
337
|
-
runs: readonly { readonly runId: string; readonly name: string }[],
|
|
338
|
-
theme: ExtensionContext["ui"]["theme"],
|
|
339
|
-
): string {
|
|
340
|
-
const visible = runs.slice(0, 2).map((run) => `${run.name} ${run.runId.slice(0, 8)}`);
|
|
341
|
-
const hidden = runs.length - visible.length;
|
|
342
|
-
const suffix = hidden > 0 ? ` · +${hidden} more` : "";
|
|
343
|
-
return `${theme.fg("accent", "●")} ${theme.bold("Background workflows")} ${theme.fg("dim", `· ${visible.join(" · ")}${suffix}`)}`;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
309
|
export function backgroundOrigin(ctx: Pick<ExtensionContext, "sessionManager">, requestedAt = Date.now()): WorkflowBackgroundOrigin {
|
|
347
310
|
return { sessionId: ctx.sessionManager.getSessionId(), requestedAt };
|
|
348
311
|
}
|
|
349
312
|
|
|
350
313
|
export function backgroundResultDetails(record: WorkflowRunRecord): BackgroundWorkflowResultDetails {
|
|
351
314
|
if (!isDeliverableState(record.state)) throw new Error(`Workflow run ${record.runId} has not finished or paused.`);
|
|
315
|
+
const summary = backgroundSummary(record);
|
|
352
316
|
return {
|
|
353
317
|
name: record.workflow.name,
|
|
354
|
-
result: { summary
|
|
318
|
+
result: record.state === "completed" && record.result.kind === "value" ? record.result.value : { summary },
|
|
319
|
+
summary,
|
|
320
|
+
startedAt: record.startedAt,
|
|
355
321
|
completedAt: record.endedAt ?? record.updatedAt,
|
|
356
322
|
usage: record.usage,
|
|
357
323
|
runId: record.runId,
|
|
@@ -362,14 +328,12 @@ export function backgroundResultDetails(record: WorkflowRunRecord): BackgroundWo
|
|
|
362
328
|
}
|
|
363
329
|
|
|
364
330
|
function backgroundSummary(record: WorkflowRunRecord): string {
|
|
365
|
-
if (record.state !== "completed") return
|
|
331
|
+
if (record.state !== "completed") return truncateText(`Workflow ${record.state}: ${record.message}`, SUMMARY_LIMIT);
|
|
366
332
|
if (record.result.kind === "unavailable") {
|
|
367
|
-
return
|
|
333
|
+
return truncateText(`Workflow completed; retained result is unavailable: ${record.result.reason}`, SUMMARY_LIMIT);
|
|
368
334
|
}
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
if (isRecord(value) && typeof value.summary === "string") return boundedSummary(value.summary);
|
|
372
|
-
return "Workflow completed. Open run history for the retained result.";
|
|
335
|
+
const summary = workflowResultSummary(record.result.value);
|
|
336
|
+
return summary === undefined ? "Workflow completed. Open run history for the retained result." : truncateText(summary, SUMMARY_LIMIT);
|
|
373
337
|
}
|
|
374
338
|
|
|
375
339
|
function formatBackgroundDelivery(details: BackgroundWorkflowResultDetails): string {
|
|
@@ -379,7 +343,7 @@ function formatBackgroundDelivery(details: BackgroundWorkflowResultDetails): str
|
|
|
379
343
|
`Run ID: ${details.runId}`,
|
|
380
344
|
`State: ${details.status}`,
|
|
381
345
|
"",
|
|
382
|
-
details.
|
|
346
|
+
details.summary,
|
|
383
347
|
].join("\n");
|
|
384
348
|
}
|
|
385
349
|
|
|
@@ -404,7 +368,7 @@ function isDeliverableState(state: WorkflowRunState): state is "completed" | "fa
|
|
|
404
368
|
async function markDelivery(
|
|
405
369
|
store: WorkflowRunStore,
|
|
406
370
|
runId: string,
|
|
407
|
-
delivery:
|
|
371
|
+
delivery: WorkflowRunDelivery,
|
|
408
372
|
): Promise<void> {
|
|
409
373
|
const latest = await store.load(runId);
|
|
410
374
|
if (!latest?.background || latest.background.delivery.state !== "pending") return;
|
|
@@ -424,16 +388,7 @@ async function forcePausedRecord(store: WorkflowRunStore, runId: string): Promis
|
|
|
424
388
|
async function forceStoppedRecord(store: WorkflowRunStore, runId: string): Promise<void> {
|
|
425
389
|
const record = await store.load(runId);
|
|
426
390
|
if (!record || (record.state !== "queued" && record.state !== "running")) return;
|
|
427
|
-
await store.save(
|
|
428
|
-
state: "stopped",
|
|
429
|
-
progress: record.progress,
|
|
430
|
-
usage: record.usage ?? record.progress.usage ?? {
|
|
431
|
-
agents: [],
|
|
432
|
-
totals: emptyWorkflowUsageTotals(),
|
|
433
|
-
assistantMessages: 0,
|
|
434
|
-
},
|
|
435
|
-
error: new WorkflowAbortError("Workflow stopped by user."),
|
|
436
|
-
}));
|
|
391
|
+
await store.save(stopWorkflowRunRecord(record));
|
|
437
392
|
}
|
|
438
393
|
|
|
439
394
|
async function reconcileInterruptedRun(
|
|
@@ -482,11 +437,3 @@ async function waitForRuns(runs: readonly Promise<void>[], timeoutMs: number): P
|
|
|
482
437
|
if (timer) clearTimeout(timer);
|
|
483
438
|
}
|
|
484
439
|
}
|
|
485
|
-
|
|
486
|
-
function boundedSummary(value: string): string {
|
|
487
|
-
return value.length <= SUMMARY_LIMIT ? value : `${value.slice(0, SUMMARY_LIMIT - 1)}…`;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
491
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
492
|
-
}
|
|
@@ -27,7 +27,7 @@ export function throwIfAborted(signal: AbortSignal | undefined): void {
|
|
|
27
27
|
export function isFatalWorkflowError(error: unknown, signal: AbortSignal | undefined): boolean {
|
|
28
28
|
if (signal?.aborted) return true;
|
|
29
29
|
if (safeInstanceOf(error, WorkflowAbortError)) return true;
|
|
30
|
-
if (
|
|
30
|
+
if (safeInstanceOf(error, DOMException)) {
|
|
31
31
|
try {
|
|
32
32
|
return error.name === "AbortError";
|
|
33
33
|
} catch {
|
|
@@ -67,10 +67,6 @@ export async function raceWithAbort<T>(operation: () => Promise<T>, signal: Abor
|
|
|
67
67
|
};
|
|
68
68
|
const onAbort = () => finish(() => reject(abortReason(signal)));
|
|
69
69
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
70
|
-
if (signal.aborted) {
|
|
71
|
-
onAbort();
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
70
|
void operation().then(
|
|
75
71
|
(value) => finish(() => resolve(value)),
|
|
76
72
|
(error: unknown) => finish(() => reject(error)),
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { ClassifierAnswer, ClassifierApi, ClassifierContext, ClassifierModel } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { RunContext } from "./agent-runner-types.ts";
|
|
4
|
+
import { parseAgentModelRef } from "./model-ref.ts";
|
|
5
|
+
import { assertWorkflowBudgetAvailable } from "./budget.ts";
|
|
6
|
+
import { throwIfAborted } from "./cancellation.ts";
|
|
7
|
+
import type { ClassifyOptions } from "./types.ts";
|
|
8
|
+
|
|
9
|
+
type ClassifierRegistry = Pick<ModelRegistry, "classify" | "getAvailableOfType" | "getModelOfType">;
|
|
10
|
+
|
|
11
|
+
/** No classifier model is configured, or the requested one is unknown. Recoverable inside `parallel`. */
|
|
12
|
+
export class WorkflowClassifierUnavailableError extends Error {
|
|
13
|
+
readonly code = "WORKFLOW_CLASSIFIER_UNAVAILABLE";
|
|
14
|
+
|
|
15
|
+
constructor(message: string) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "WorkflowClassifierUnavailableError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The classifier answered with an error or no answer. Recoverable inside `parallel`. */
|
|
22
|
+
export class WorkflowClassifierError extends Error {
|
|
23
|
+
readonly code = "WORKFLOW_CLASSIFIER_FAILED";
|
|
24
|
+
|
|
25
|
+
constructor(message: string) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "WorkflowClassifierError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* `api.classify()`: answer typed questions about JSON state with a classifier model through the host's
|
|
33
|
+
* credentials, without a chat session. It shares the run's concurrency cap, budget, and per-agent timeout,
|
|
34
|
+
* records usage like an agent, and is not journaled, so a resumed run asks again.
|
|
35
|
+
*/
|
|
36
|
+
export async function runClassifier(
|
|
37
|
+
rc: RunContext,
|
|
38
|
+
context: ClassifierContext,
|
|
39
|
+
opts: ClassifyOptions = {},
|
|
40
|
+
): Promise<Record<string, ClassifierAnswer>> {
|
|
41
|
+
const registry = rc.modelRegistry;
|
|
42
|
+
return await rc.semaphore.run(async () => {
|
|
43
|
+
assertWorkflowBudgetAvailable(rc.budget);
|
|
44
|
+
const model = await cachedClassifierModel(rc, registry, opts.model);
|
|
45
|
+
const label = opts.label ?? `classify:${model.provider}/${model.id}`;
|
|
46
|
+
// A stalled classifier must not hold a concurrency slot past the limit agents get.
|
|
47
|
+
const timeout = AbortSignal.timeout(rc.agentTimeoutMs);
|
|
48
|
+
const result = await registry.classify(model, context, { signal: rc.signal ? AbortSignal.any([rc.signal, timeout]) : timeout });
|
|
49
|
+
// One classifier response is one model response: recording it like one keeps per-model usage and the budget
|
|
50
|
+
// whole. Record before the abort check, since the provider charged for a call that returned as the run stopped.
|
|
51
|
+
rc.usage.recordAgentSession({
|
|
52
|
+
label,
|
|
53
|
+
phase: opts.phase,
|
|
54
|
+
messages: [{ role: "assistant", provider: result.provider, model: result.model, usage: result.usage }],
|
|
55
|
+
});
|
|
56
|
+
throwIfAborted(rc.signal);
|
|
57
|
+
if (result.stopReason !== "stop" && timeout.aborted) {
|
|
58
|
+
throw new WorkflowClassifierError(`${label}: classifier timed out after ${rc.agentTimeoutMs}ms`);
|
|
59
|
+
}
|
|
60
|
+
if (result.stopReason !== "stop") {
|
|
61
|
+
throw new WorkflowClassifierError(`${label}: classifier ${result.stopReason}${result.errorMessage ? `: ${result.errorMessage}` : ""}`);
|
|
62
|
+
}
|
|
63
|
+
return result.answers;
|
|
64
|
+
}, { signal: rc.signal });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** One credential lookup per route and run; a route that failed to resolve keeps failing for the run. */
|
|
68
|
+
function cachedClassifierModel(
|
|
69
|
+
rc: RunContext,
|
|
70
|
+
registry: ClassifierRegistry,
|
|
71
|
+
ref: string | readonly string[] | undefined,
|
|
72
|
+
): Promise<ClassifierModel<ClassifierApi>> {
|
|
73
|
+
const key = JSON.stringify(ref ?? null);
|
|
74
|
+
const cached = rc.classifierRoutes?.get(key);
|
|
75
|
+
if (cached) return cached;
|
|
76
|
+
const resolved = resolveClassifierModel(registry, ref);
|
|
77
|
+
rc.classifierRoutes?.set(key, resolved);
|
|
78
|
+
return resolved;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function resolveClassifierModel(
|
|
82
|
+
registry: ClassifierRegistry,
|
|
83
|
+
ref: string | readonly string[] | undefined,
|
|
84
|
+
): Promise<ClassifierModel<ClassifierApi>> {
|
|
85
|
+
if (ref !== undefined && typeof ref !== "string") {
|
|
86
|
+
const available = await registry.getAvailableOfType("classifier");
|
|
87
|
+
const preferred = ref.map((candidate) => parseAgentModelRef(candidate))
|
|
88
|
+
.map(({ provider, id }) => available.find((model) => model.provider === provider && model.id === id))
|
|
89
|
+
.find((model) => model !== undefined);
|
|
90
|
+
if (!preferred) throw new WorkflowClassifierUnavailableError(`None of the classifier models ${ref.join(", ")} has working credentials.`);
|
|
91
|
+
return preferred;
|
|
92
|
+
}
|
|
93
|
+
if (ref === undefined) {
|
|
94
|
+
const [available] = await registry.getAvailableOfType("classifier");
|
|
95
|
+
if (!available) {
|
|
96
|
+
throw new WorkflowClassifierUnavailableError(
|
|
97
|
+
"No classifier model has working credentials. Configure one (for example TypeSafe Jev via OpenRouter) or pass { model: \"provider/id\" }.",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return available;
|
|
101
|
+
}
|
|
102
|
+
// Same ref rules as agent({ model }): malformed refs throw, bare ids mean Anthropic.
|
|
103
|
+
const { provider, id } = parseAgentModelRef(ref);
|
|
104
|
+
const model = registry.getModelOfType("classifier", provider, id);
|
|
105
|
+
if (!model) throw new WorkflowClassifierUnavailableError(`Classifier model "${ref}" not found (resolved as ${provider}/${id}).`);
|
|
106
|
+
return model;
|
|
107
|
+
}
|