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
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { Type, type Static } from "typebox";
|
|
3
|
+
import { AdvisorySeveritySchema, type IdentifiedAdvisoryCandidate, type AdvisoryCandidate, type AdvisoryReport } from "./advisory-schema.ts";
|
|
4
|
+
import type { WorkflowApi } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
export interface AdvisoryStageCoverage {
|
|
7
|
+
stage: string;
|
|
8
|
+
expected: number;
|
|
9
|
+
completed: number;
|
|
10
|
+
failed: number;
|
|
11
|
+
failures: { branch: string; reason: string; candidate?: AdvisoryCandidate }[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Settled results are required here: a missing branch is not a negative finding. */
|
|
15
|
+
export async function collectAdvisoryStage<T>(
|
|
16
|
+
api: Pick<WorkflowApi, "parallel">,
|
|
17
|
+
stage: string,
|
|
18
|
+
branches: { id: string; candidate?: AdvisoryCandidate; run(): Promise<T> }[],
|
|
19
|
+
coverage: AdvisoryStageCoverage[],
|
|
20
|
+
): Promise<T[]> {
|
|
21
|
+
const results = await api.parallel(branches.map((branch) => branch.run), { settled: true });
|
|
22
|
+
const failures = results.flatMap((result, index) => result.ok ? [] : [{ branch: branches[index]!.id, reason: result.error.message, ...(branches[index]!.candidate ? { candidate: branches[index]!.candidate } : {}) }]);
|
|
23
|
+
coverage.push({ stage, expected: branches.length, completed: results.length - failures.length, failed: failures.length, failures });
|
|
24
|
+
return results.flatMap((result) => result.ok ? [result.value] : []);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function identifyCandidates(candidates: readonly AdvisoryCandidate[], lens: string): IdentifiedAdvisoryCandidate[] {
|
|
28
|
+
return candidates.map((candidate, index) => {
|
|
29
|
+
const candidateId = createHash("sha256").update(JSON.stringify([lens, index, candidate])).digest("hex").slice(0, 20);
|
|
30
|
+
// Identities are assigned by the workflow, never accepted from a finder.
|
|
31
|
+
return { ...candidate, candidateId, sourceCandidateIds: [candidateId] };
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizedSummary(summary: string): string {
|
|
36
|
+
return summary.toLowerCase().replace(/\s+/g, " ").trim().replace(/[.!?]+$/, "");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function candidateDedupKey(candidate: AdvisoryCandidate): string {
|
|
40
|
+
const location = candidate.locations[0];
|
|
41
|
+
return JSON.stringify([candidate.category, location?.file.replace(/^\.\//, "").replace(/^[ab]\//, ""),
|
|
42
|
+
location?.symbol ?? location?.line ?? null, normalizedSummary(candidate.summary)]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Collapse only equivalent claims at the same anchor; keep every discovery ID and location. */
|
|
46
|
+
export function dedupeCandidates<T extends IdentifiedAdvisoryCandidate>(candidates: readonly T[]): T[] {
|
|
47
|
+
const seen = new Map<string, T>();
|
|
48
|
+
for (const candidate of candidates) {
|
|
49
|
+
const key = candidateDedupKey(candidate);
|
|
50
|
+
const previous = seen.get(key);
|
|
51
|
+
if (!previous) seen.set(key, { ...candidate });
|
|
52
|
+
else {
|
|
53
|
+
previous.sourceCandidateIds = [...new Set([...previous.sourceCandidateIds, ...candidate.sourceCandidateIds])];
|
|
54
|
+
previous.locations = uniqueLocations([...previous.locations, ...candidate.locations]);
|
|
55
|
+
previous.impact = [...new Set([previous.impact, candidate.impact])].join("\n");
|
|
56
|
+
previous.discoveryEvidence = [...new Set([...(previous.discoveryEvidence ?? []), ...(candidate.discoveryEvidence ?? [])])];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return [...seen.values()];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function uniqueLocations(locations: AdvisoryCandidate["locations"]): AdvisoryCandidate["locations"] {
|
|
63
|
+
return [...new Map(locations.map((location) => [JSON.stringify(location), location])).values()];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Synthesis selects evidence records. It has no fields with which to replace their evidence.
|
|
67
|
+
export const AdvisorySynthesisSchema = Type.Object({
|
|
68
|
+
summary: Type.String(),
|
|
69
|
+
findings: Type.Array(Type.Object({
|
|
70
|
+
sourceCandidateIds: Type.Array(Type.String(), { minItems: 1 }),
|
|
71
|
+
severity: AdvisorySeveritySchema,
|
|
72
|
+
recommendation: Type.String(),
|
|
73
|
+
})),
|
|
74
|
+
nextSteps: Type.Array(Type.String()),
|
|
75
|
+
});
|
|
76
|
+
export type AdvisorySynthesis = Static<typeof AdvisorySynthesisSchema>;
|
|
77
|
+
export const SYNTHESIS_ID_INSTRUCTIONS = "Select or merge findings only by sourceCandidateIds shown below. Return severity and an advisory recommendation per selection. Never select an ID absent from these verified records. ";
|
|
78
|
+
|
|
79
|
+
export function withAdvisoryCoverage<T extends AdvisoryReport>(report: T, coverage: AdvisoryStageCoverage[]) {
|
|
80
|
+
const incomplete = coverage.some((stage) => stage.failed > 0);
|
|
81
|
+
const gaps = coverage.flatMap((stage) => stage.failures.map((failure) => `${stage.stage}/${failure.branch}: ${failure.reason}`));
|
|
82
|
+
return {
|
|
83
|
+
...report,
|
|
84
|
+
status: incomplete ? "incomplete" as const : "complete" as const,
|
|
85
|
+
summary: incomplete ? `Incomplete review: ${gaps.length} branch(es) failed. ${report.findings.length} finding(s) available; no clean conclusion is possible.` : report.summary,
|
|
86
|
+
nextSteps: incomplete ? ["Inspect failed branches and rerun missing work.", ...(report.findings.length > 0 ? report.nextSteps : [])] : report.nextSteps,
|
|
87
|
+
coverage,
|
|
88
|
+
gaps,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -9,7 +9,7 @@ export const AdvisoryConfidenceSchema = Type.Union([Type.Literal("low"), Type.Li
|
|
|
9
9
|
description: "Confidence that the finding is actionable.",
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
-
export const AdvisoryVerifierVerdictSchema = Type.Union([Type.Literal("CONFIRMED"), Type.Literal("PLAUSIBLE"), Type.Literal("REFUTED")], {
|
|
12
|
+
export const AdvisoryVerifierVerdictSchema = Type.Union([Type.Literal("CONFIRMED"), Type.Literal("PLAUSIBLE"), Type.Literal("REFUTED"), Type.Literal("NOT_SUBSTANTIATED")], {
|
|
13
13
|
description: "Verifier judgment for a candidate finding.",
|
|
14
14
|
});
|
|
15
15
|
|
|
@@ -20,6 +20,8 @@ export const AdvisoryLocationSchema = Type.Object({
|
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
export const AdvisoryCandidateSchema = Type.Object({
|
|
23
|
+
discoveryEvidence: Type.Optional(Type.Array(Type.String())),
|
|
24
|
+
reviewAnchor: Type.Optional(AdvisoryLocationSchema),
|
|
23
25
|
summary: Type.String({ description: "One-line candidate finding or hypothesis." }),
|
|
24
26
|
category: Type.String({ description: "Workflow-specific category such as bug, duplication, root-cause, or io." }),
|
|
25
27
|
locations: Type.Array(AdvisoryLocationSchema, { description: "Relevant code or configuration locations." }),
|
|
@@ -38,6 +40,9 @@ export const AdvisoryVerdictSchema = Type.Object({
|
|
|
38
40
|
});
|
|
39
41
|
|
|
40
42
|
export const AdvisoryFindingSchema = Type.Object({
|
|
43
|
+
verdict: Type.Optional(AdvisoryVerifierVerdictSchema),
|
|
44
|
+
sourceCandidateIds: Type.Optional(Type.Array(Type.String())),
|
|
45
|
+
reviewAnchor: Type.Optional(AdvisoryLocationSchema),
|
|
41
46
|
summary: Type.String({ description: "One-line final finding." }),
|
|
42
47
|
category: Type.String({ description: "Workflow-specific category for grouping and display." }),
|
|
43
48
|
severity: AdvisorySeveritySchema,
|
|
@@ -49,6 +54,12 @@ export const AdvisoryFindingSchema = Type.Object({
|
|
|
49
54
|
});
|
|
50
55
|
|
|
51
56
|
export const AdvisoryReportSchema = Type.Object({
|
|
57
|
+
status: Type.Optional(Type.Union([Type.Literal("complete"), Type.Literal("incomplete")])),
|
|
58
|
+
gaps: Type.Optional(Type.Array(Type.String())),
|
|
59
|
+
coverage: Type.Optional(Type.Array(Type.Object({
|
|
60
|
+
stage: Type.String(), expected: Type.Number(), completed: Type.Number(), failed: Type.Number(),
|
|
61
|
+
failures: Type.Array(Type.Object({ branch: Type.String(), reason: Type.String(), candidate: Type.Optional(AdvisoryCandidateSchema) })),
|
|
62
|
+
}))),
|
|
52
63
|
summary: Type.String({ description: "Short overall advisory summary." }),
|
|
53
64
|
findings: Type.Array(AdvisoryFindingSchema, { description: "Verified and ranked advisory findings." }),
|
|
54
65
|
nextSteps: Type.Array(Type.String({ description: "Concrete follow-up commands, inspections, or decisions." })),
|
|
@@ -59,12 +70,9 @@ export const AdvisoryReportWithStatsSchema = Type.Object({
|
|
|
59
70
|
stats: Type.Optional(Type.Record(Type.String(), Type.Union([Type.String(), Type.Number()]))),
|
|
60
71
|
});
|
|
61
72
|
|
|
62
|
-
export type AdvisorySeverity = Static<typeof AdvisorySeveritySchema>;
|
|
63
|
-
export type AdvisoryConfidence = Static<typeof AdvisoryConfidenceSchema>;
|
|
64
|
-
export type AdvisoryVerifierVerdict = Static<typeof AdvisoryVerifierVerdictSchema>;
|
|
65
73
|
export type AdvisoryLocation = Static<typeof AdvisoryLocationSchema>;
|
|
66
74
|
export type AdvisoryCandidate = Static<typeof AdvisoryCandidateSchema>;
|
|
67
|
-
export type
|
|
75
|
+
export type IdentifiedAdvisoryCandidate = AdvisoryCandidate & { candidateId: string; sourceCandidateIds: string[] };
|
|
68
76
|
export type AdvisoryVerdict = Static<typeof AdvisoryVerdictSchema>;
|
|
69
77
|
export type AdvisoryFinding = Static<typeof AdvisoryFindingSchema>;
|
|
70
78
|
export type AdvisoryReport = Static<typeof AdvisoryReportSchema>;
|
|
@@ -37,8 +37,9 @@ export async function executeAgentAttempt(input: {
|
|
|
37
37
|
readonly label: string;
|
|
38
38
|
readonly rowId: number;
|
|
39
39
|
readonly tags: AgentRunTags;
|
|
40
|
+
readonly admitLiveAgent: () => void;
|
|
40
41
|
}): Promise<AgentAttemptResult> {
|
|
41
|
-
const { rc, prompt, opts, resumeBaseContext, model, replay, label, rowId, tags } = input;
|
|
42
|
+
const { rc, prompt, opts, resumeBaseContext, model, replay, label, rowId, tags, admitLiveAgent } = input;
|
|
42
43
|
let repositoryBefore: RepositoryResumeContext | undefined;
|
|
43
44
|
let evidence: AgentReplayEvidence | undefined;
|
|
44
45
|
if (isReplayEnabled(replay)) {
|
|
@@ -104,6 +105,7 @@ export async function executeAgentAttempt(input: {
|
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
assertWorkflowBudgetAvailable(rc.budget);
|
|
108
|
+
admitLiveAgent();
|
|
107
109
|
const rawResult = await promptAgentSession({
|
|
108
110
|
rc,
|
|
109
111
|
handle,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { throwIfAborted } from "./cancellation.ts";
|
|
2
|
+
|
|
3
|
+
export class WorkflowAgentLimitError extends Error {
|
|
4
|
+
override readonly name = "WorkflowAgentLimitError";
|
|
5
|
+
readonly code = "WORKFLOW_AGENT_LIMIT_EXCEEDED";
|
|
6
|
+
|
|
7
|
+
constructor(readonly maxAgents: number) {
|
|
8
|
+
super(`Workflow live-agent limit of ${maxAgents} has been reached; no new model request was started.`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class WorkflowAgentTimeoutError extends Error {
|
|
13
|
+
override readonly name = "WorkflowAgentTimeoutError";
|
|
14
|
+
readonly code = "WORKFLOW_AGENT_TIMEOUT";
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
readonly label: string,
|
|
18
|
+
readonly timeoutMs: number,
|
|
19
|
+
) {
|
|
20
|
+
super(`Workflow agent "${label}" exceeded its ${timeoutMs}ms duration limit.`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Shared run-level admission counter. Cache hits do not call admit(). */
|
|
25
|
+
export class WorkflowAgentLimiter {
|
|
26
|
+
private admitted = 0;
|
|
27
|
+
|
|
28
|
+
constructor(readonly maxAgents: number) {}
|
|
29
|
+
|
|
30
|
+
admit(signal: AbortSignal | undefined): void {
|
|
31
|
+
throwIfAborted(signal);
|
|
32
|
+
if (this.admitted >= this.maxAgents) throw new WorkflowAgentLimitError(this.maxAgents);
|
|
33
|
+
this.admitted++;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -2,7 +2,8 @@ import type { Skill } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { Value } from "typebox/value";
|
|
3
3
|
import { captureAgentSkillIdentities, extractSkillSelectorsFromText } from "./agent-skills.ts";
|
|
4
4
|
import type { AgentExecutionOptions, AgentRunnerSession, AgentRunTags, RunContext } from "./agent-runner-types.ts";
|
|
5
|
-
import {
|
|
5
|
+
import { captureEffectiveAgentSessionIdentity } from "./agent-session-identity.ts";
|
|
6
|
+
import { FINAL_TOOL } from "./agent-session.ts";
|
|
6
7
|
import type { AgentWorkspace, IsolatedAgentWorkspace } from "./agent-workspace.ts";
|
|
7
8
|
import { captureAgentJournalKey } from "./journal.ts";
|
|
8
9
|
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
@@ -116,7 +117,9 @@ export async function lookupReplayResult(input: {
|
|
|
116
117
|
readonly opts: AgentExecutionOptions;
|
|
117
118
|
readonly workspace: AgentWorkspace;
|
|
118
119
|
}): Promise<{ readonly hit: true; readonly result: unknown } | { readonly hit: false; readonly reason?: string }> {
|
|
119
|
-
const cached = input.rc.journal.lookup(input.key, input.identity
|
|
120
|
+
const cached = input.rc.journal.lookup(input.key, input.identity, {
|
|
121
|
+
allowWorkflowSourceMismatch: input.rc.resumeEditedWorkflow,
|
|
122
|
+
});
|
|
120
123
|
if (!cached.hit) return cached;
|
|
121
124
|
const validation = await validateCachedResult(cached.value, input.opts, input.rc, input.workspace);
|
|
122
125
|
return validation.ok
|
|
@@ -147,7 +150,9 @@ export async function validateReplayIdentity(input: {
|
|
|
147
150
|
sessionCwd: input.sessionCwd,
|
|
148
151
|
});
|
|
149
152
|
if (current.kind === "unverifiable") return { ok: false, reason: current.reason };
|
|
150
|
-
const mismatch = resumeContextMismatchReason(input.identity, current.identity
|
|
153
|
+
const mismatch = resumeContextMismatchReason(input.identity, current.identity, {
|
|
154
|
+
allowWorkflowSourceMismatch: input.rc.resumeEditedWorkflow,
|
|
155
|
+
});
|
|
151
156
|
return mismatch ? { ok: false, reason: mismatch } : { ok: true };
|
|
152
157
|
}
|
|
153
158
|
|
|
@@ -160,6 +165,7 @@ export async function settleAgentAttempt(input: {
|
|
|
160
165
|
}): Promise<AgentAttemptSettlement> {
|
|
161
166
|
const { rc, label, replay, outcome } = input;
|
|
162
167
|
if (outcome.kind === "live-unrecordable") {
|
|
168
|
+
recordAgentResultSource(rc, "live");
|
|
163
169
|
return { kind: "done", result: outcome.result };
|
|
164
170
|
}
|
|
165
171
|
if (!isReplayEnabled(replay)) throw new Error("Replayable agent outcome produced without an enabled replay plan.");
|
|
@@ -183,10 +189,23 @@ export async function settleAgentAttempt(input: {
|
|
|
183
189
|
if (outcome.kind === "cache-hit") {
|
|
184
190
|
rc.progress.log(`${label}: using cached result from workflow journal`);
|
|
185
191
|
rc.perf.counter("agent.cache_hit", 1, input.tags);
|
|
192
|
+
recordAgentResultSource(rc, "cached");
|
|
193
|
+
} else {
|
|
194
|
+
recordAgentResultSource(rc, "live");
|
|
186
195
|
}
|
|
187
196
|
return { kind: "done", result: outcome.result };
|
|
188
197
|
}
|
|
189
198
|
|
|
199
|
+
function recordAgentResultSource(rc: RunContext, source: "cached" | "live"): void {
|
|
200
|
+
if (!rc.resumeEditedWorkflow) return;
|
|
201
|
+
rc.progress.event({
|
|
202
|
+
type: "counter_delta",
|
|
203
|
+
key: `resume.${source}`,
|
|
204
|
+
label: source === "cached" ? "Cached agents" : "Live agents",
|
|
205
|
+
delta: 1,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
190
209
|
async function captureVerifiedReplayIdentity(input: {
|
|
191
210
|
readonly rc: RunContext;
|
|
192
211
|
readonly repository: VerifiedRepositoryResumeContext;
|
|
@@ -201,7 +220,11 @@ async function captureVerifiedReplayIdentity(input: {
|
|
|
201
220
|
workspaceRoot: input.rc.cwd,
|
|
202
221
|
signal: input.rc.signal,
|
|
203
222
|
}),
|
|
204
|
-
|
|
223
|
+
captureEffectiveAgentSessionIdentity(input.session, {
|
|
224
|
+
sessionCwd: input.sessionCwd,
|
|
225
|
+
workspaceRoot: input.rc.cwd,
|
|
226
|
+
signal: input.rc.signal,
|
|
227
|
+
}),
|
|
205
228
|
]);
|
|
206
229
|
if (skills.kind === "unverifiable") return skills;
|
|
207
230
|
if (effectiveSession.kind === "unverifiable") return effectiveSession;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { isRetryableAssistantError, type AssistantMessage } from "@earendil-works/pi-ai";
|
|
2
|
+
import { abortReason, throwIfAborted } from "./cancellation.ts";
|
|
3
|
+
import {
|
|
4
|
+
providerUsageLimitFromMessages,
|
|
5
|
+
type WorkflowProviderUsageLimitError,
|
|
6
|
+
} from "./provider-usage-limit.ts";
|
|
7
|
+
|
|
8
|
+
export const AGENT_RETRY_BASE_DELAY_MS = 1_000;
|
|
9
|
+
export const AGENT_RETRY_MAX_DELAY_MS = 30_000;
|
|
10
|
+
export const WORKFLOW_PROVIDER_ERROR_CODE = "WORKFLOW_PROVIDER_ERROR";
|
|
11
|
+
|
|
12
|
+
export interface AgentRetryScheduler {
|
|
13
|
+
sleep(delayMs: number, signal: AbortSignal | undefined): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ProviderErrorDetails {
|
|
17
|
+
readonly stopReason: "error";
|
|
18
|
+
readonly retryable: boolean;
|
|
19
|
+
readonly provider?: string;
|
|
20
|
+
readonly model?: string;
|
|
21
|
+
readonly api?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Provider failure reconstructed from pi's terminal assistant-message metadata. */
|
|
25
|
+
export class WorkflowProviderError extends Error {
|
|
26
|
+
override readonly name = "WorkflowProviderError";
|
|
27
|
+
readonly code = WORKFLOW_PROVIDER_ERROR_CODE;
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
message: string,
|
|
31
|
+
readonly details: ProviderErrorDetails,
|
|
32
|
+
) {
|
|
33
|
+
super(message);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get retryable(): boolean {
|
|
37
|
+
return this.details.retryable;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
toJSON(): {
|
|
41
|
+
readonly name: string;
|
|
42
|
+
readonly message: string;
|
|
43
|
+
readonly code: string;
|
|
44
|
+
readonly details: ProviderErrorDetails;
|
|
45
|
+
} {
|
|
46
|
+
return { name: this.name, message: this.message, code: this.code, details: this.details };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const defaultAgentRetryScheduler: AgentRetryScheduler = {
|
|
51
|
+
async sleep(delayMs, signal) {
|
|
52
|
+
throwIfAborted(signal);
|
|
53
|
+
await new Promise<void>((resolve, reject) => {
|
|
54
|
+
const cleanup = () => signal?.removeEventListener("abort", onAbort);
|
|
55
|
+
const timer = setTimeout(() => {
|
|
56
|
+
cleanup();
|
|
57
|
+
resolve();
|
|
58
|
+
}, delayMs);
|
|
59
|
+
const onAbort = () => {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
cleanup();
|
|
62
|
+
reject(abortReason(signal));
|
|
63
|
+
};
|
|
64
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
65
|
+
if (signal?.aborted) onAbort();
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export function agentRetryDelayMs(retryAttempt: number): number {
|
|
71
|
+
return Math.min(AGENT_RETRY_MAX_DELAY_MS, AGENT_RETRY_BASE_DELAY_MS * 2 ** Math.max(0, retryAttempt - 1));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function providerErrorFromMessages(
|
|
75
|
+
messages: readonly unknown[],
|
|
76
|
+
options: { readonly pauseOnUsageLimit?: boolean } = {},
|
|
77
|
+
): WorkflowProviderError | WorkflowProviderUsageLimitError | undefined {
|
|
78
|
+
const usageLimit = providerUsageLimitFromMessages(messages);
|
|
79
|
+
if (usageLimit && options.pauseOnUsageLimit) return usageLimit;
|
|
80
|
+
const message = messages.findLast(isAssistantMessage);
|
|
81
|
+
if (!message || message.stopReason !== "error") return undefined;
|
|
82
|
+
const errorMessage = typeof message.errorMessage === "string" && message.errorMessage.length > 0
|
|
83
|
+
? message.errorMessage
|
|
84
|
+
: "Provider session ended with an unspecified error.";
|
|
85
|
+
const retryable = isRetryableAssistantError(message as AssistantMessage);
|
|
86
|
+
const provider = stringDetail(message.provider);
|
|
87
|
+
const model = stringDetail(message.model);
|
|
88
|
+
const api = stringDetail(message.api);
|
|
89
|
+
return new WorkflowProviderError(errorMessage, {
|
|
90
|
+
stopReason: "error",
|
|
91
|
+
retryable,
|
|
92
|
+
...(provider ? { provider } : {}),
|
|
93
|
+
...(model ? { model } : {}),
|
|
94
|
+
...(api ? { api } : {}),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isAssistantMessage(value: unknown): value is {
|
|
99
|
+
readonly role: "assistant";
|
|
100
|
+
readonly stopReason?: unknown;
|
|
101
|
+
readonly errorMessage?: unknown;
|
|
102
|
+
readonly provider?: unknown;
|
|
103
|
+
readonly model?: unknown;
|
|
104
|
+
readonly api?: unknown;
|
|
105
|
+
} {
|
|
106
|
+
return (
|
|
107
|
+
typeof value === "object" &&
|
|
108
|
+
value !== null &&
|
|
109
|
+
"role" in value &&
|
|
110
|
+
value.role === "assistant"
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function stringDetail(value: unknown): string | undefined {
|
|
115
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
116
|
+
}
|
|
@@ -1,52 +1,37 @@
|
|
|
1
1
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
AgentSession,
|
|
4
|
+
CreateAgentSessionOptions,
|
|
5
|
+
ModelRegistry,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
3
7
|
import type { WorkflowBudget } from "./budget.ts";
|
|
4
8
|
import type { Semaphore } from "./concurrency.ts";
|
|
9
|
+
import type { WorkflowAgentLimiter } from "./agent-limits.ts";
|
|
10
|
+
import type { AgentRetryScheduler } from "./agent-retry.ts";
|
|
11
|
+
import type { ResolvedWorkflowModelProfiles } from "./model-profiles.ts";
|
|
5
12
|
import type { WorkflowJournal } from "./journal.ts";
|
|
6
13
|
import type { PerfSink } from "./perf.ts";
|
|
7
|
-
import type { AgentOptions } from "./types.ts";
|
|
14
|
+
import type { AgentOptions, WorkflowProgressEvent } from "./types.ts";
|
|
8
15
|
import type { WorkflowUsageSink } from "./usage.ts";
|
|
9
16
|
import type { WorktreeBaseline, WorktreeRegistry } from "./worktree.ts";
|
|
10
|
-
import type {
|
|
11
|
-
EffectiveAgentModelLike,
|
|
12
|
-
EffectiveToolDefinitionLike,
|
|
13
|
-
} from "./agent-session-identity.ts";
|
|
14
|
-
|
|
15
|
-
export interface AgentRunnerEvent {
|
|
16
|
-
readonly type: string;
|
|
17
|
-
readonly toolName?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface AgentRunnerToolInfo {
|
|
21
|
-
readonly name: string;
|
|
22
|
-
readonly description?: string;
|
|
23
|
-
readonly parameters?: unknown;
|
|
24
|
-
readonly promptGuidelines?: readonly string[];
|
|
25
|
-
readonly sourceInfo?: {
|
|
26
|
-
readonly path: string;
|
|
27
|
-
readonly source: string;
|
|
28
|
-
readonly scope: string;
|
|
29
|
-
readonly origin: string;
|
|
30
|
-
readonly baseDir?: string;
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
17
|
|
|
34
|
-
export
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
18
|
+
export type AgentRunnerSession = Pick<
|
|
19
|
+
AgentSession,
|
|
20
|
+
| "messages"
|
|
21
|
+
| "systemPrompt"
|
|
22
|
+
| "model"
|
|
23
|
+
| "thinkingLevel"
|
|
24
|
+
| "prompt"
|
|
25
|
+
| "subscribe"
|
|
26
|
+
| "dispose"
|
|
27
|
+
| "abort"
|
|
28
|
+
| "getAllTools"
|
|
29
|
+
| "getActiveToolNames"
|
|
30
|
+
| "getToolDefinition"
|
|
31
|
+
| "setActiveToolsByName"
|
|
32
|
+
| "setAutoRetryEnabled"
|
|
33
|
+
| "getLastAssistantText"
|
|
34
|
+
>;
|
|
50
35
|
|
|
51
36
|
export type CreateAgentSession = (options: CreateAgentSessionOptions) => Promise<{ session: AgentRunnerSession }>;
|
|
52
37
|
|
|
@@ -56,15 +41,21 @@ export interface AgentProgress {
|
|
|
56
41
|
agentTool(label: string, tool: string, id?: number): void;
|
|
57
42
|
agentDone(label: string, id?: number): void;
|
|
58
43
|
agentFailed(label: string, error: unknown, id?: number): void;
|
|
44
|
+
event(event: WorkflowProgressEvent): void;
|
|
59
45
|
log(message: string): void;
|
|
60
46
|
}
|
|
61
47
|
|
|
62
|
-
|
|
63
|
-
export interface RunContext {
|
|
48
|
+
interface RunContextBase {
|
|
64
49
|
cwd: string;
|
|
65
50
|
hostModel: Model<Api> | undefined;
|
|
66
|
-
modelRegistry: Pick<ModelRegistry, "find">;
|
|
67
51
|
semaphore: Semaphore;
|
|
52
|
+
agentLimiter: WorkflowAgentLimiter;
|
|
53
|
+
agentTimeoutMs: number;
|
|
54
|
+
agentRetries: number;
|
|
55
|
+
pauseOnProviderUsageLimit?: boolean;
|
|
56
|
+
resumeEditedWorkflow?: boolean;
|
|
57
|
+
retryScheduler: AgentRetryScheduler;
|
|
58
|
+
modelProfiles: ResolvedWorkflowModelProfiles;
|
|
68
59
|
progress: AgentProgress;
|
|
69
60
|
signal: AbortSignal | undefined;
|
|
70
61
|
perf: PerfSink;
|
|
@@ -72,9 +63,22 @@ export interface RunContext {
|
|
|
72
63
|
budget: WorkflowBudget;
|
|
73
64
|
journal: WorkflowJournal;
|
|
74
65
|
worktrees: WorktreeRegistry;
|
|
75
|
-
createSession?: CreateAgentSession;
|
|
76
66
|
}
|
|
77
67
|
|
|
68
|
+
/** Shared per-run context threaded into every agent() call. */
|
|
69
|
+
export type RunContext = RunContextBase & (
|
|
70
|
+
| {
|
|
71
|
+
/** Production sessions inherit the host's live provider and auth state. */
|
|
72
|
+
modelRegistry: ModelRegistry;
|
|
73
|
+
createSession?: undefined;
|
|
74
|
+
}
|
|
75
|
+
| {
|
|
76
|
+
/** Injected test sessions bypass Pi's resource loader and therefore do not resolve skills. */
|
|
77
|
+
modelRegistry: Pick<ModelRegistry, "find">;
|
|
78
|
+
createSession: CreateAgentSession;
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
|
|
78
82
|
/** Runtime-only options. The authored WorkflowApi exposes only AgentOptions. */
|
|
79
83
|
export type AgentExecutionOptions = AgentOptions & {
|
|
80
84
|
readonly worktreeBaseline?: WorktreeBaseline;
|