pi-workflow-engine 0.13.0 → 1.0.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 +106 -391
- package/.pi/extensions/pi-workflow-engine/src/advisory-challenge.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +8 -8
- 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 +5 -11
- 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 +93 -100
- 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-workflows.ts +33 -86
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +1 -5
- 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 +39 -98
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +37 -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/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/options.ts +97 -16
- package/.pi/extensions/pi-workflow-engine/src/perf.ts +43 -72
- 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 +27 -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 +2 -1
- 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 +6 -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 +21 -32
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +56 -19
- 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/USAGE.md +31 -17
- 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
|
@@ -61,7 +61,7 @@ export async function challengeFindings<T extends AdvisoryVerified>(
|
|
|
61
61
|
replacements.set(finding.candidateId, { ...finding, verdict: "NOT_SUBSTANTIATED", challenge: { status: "failed", challenge } });
|
|
62
62
|
const adjudication = await api.agent(
|
|
63
63
|
`Adjudicate the original finding, independent verifier evidence and falsification attempt below. Preserve unresolved conflict; do not force consensus. A missing counterexample alone cannot upgrade a plausible claim. Cite concrete evidence and observed test results; never invent experiments.\nContext:\n${context}\nOriginal and verifier:\n${JSON.stringify(finding)}\nChallenger:\n${JSON.stringify(challenge)}`,
|
|
64
|
-
{ label: `adjudicate:${finding.candidateId}`, phase: "Challenge", profile: "medium", tools: [], schema: AdjudicationSchema },
|
|
64
|
+
{ label: `adjudicate:${finding.candidateId}`, phase: "Challenge", profile: "medium", tools: [], skills: [], schema: AdjudicationSchema },
|
|
65
65
|
);
|
|
66
66
|
replacements.set(finding.candidateId, {
|
|
67
67
|
...finding,
|
|
@@ -3,13 +3,7 @@ import { Type, type Static } from "typebox";
|
|
|
3
3
|
import { AdvisorySeveritySchema, type IdentifiedAdvisoryCandidate, type AdvisoryCandidate, type AdvisoryReport } from "./advisory-schema.ts";
|
|
4
4
|
import type { WorkflowApi } from "./types.ts";
|
|
5
5
|
|
|
6
|
-
export
|
|
7
|
-
stage: string;
|
|
8
|
-
expected: number;
|
|
9
|
-
completed: number;
|
|
10
|
-
failed: number;
|
|
11
|
-
failures: { branch: string; reason: string; candidate?: AdvisoryCandidate }[];
|
|
12
|
-
}
|
|
6
|
+
export type AdvisoryStageCoverage = NonNullable<AdvisoryReport["coverage"]>[number];
|
|
13
7
|
|
|
14
8
|
/** Settled results are required here: a missing branch is not a negative finding. */
|
|
15
9
|
export async function collectAdvisoryStage<T>(
|
|
@@ -36,9 +30,15 @@ function normalizedSummary(summary: string): string {
|
|
|
36
30
|
return summary.toLowerCase().replace(/\s+/g, " ").trim().replace(/[.!?]+$/, "");
|
|
37
31
|
}
|
|
38
32
|
|
|
33
|
+
/** Repo-relative readings of a reported path, most literal first: without a stray `./`, then also without a git `a/`/`b/` prefix. */
|
|
34
|
+
export function repoPathCandidates(path: string): [literal: string, unprefixed: string] {
|
|
35
|
+
const literal = path.replace(/^\.\//, "");
|
|
36
|
+
return [literal, literal.replace(/^[ab]\//, "")];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
39
|
function candidateDedupKey(candidate: AdvisoryCandidate): string {
|
|
40
40
|
const location = candidate.locations[0];
|
|
41
|
-
return JSON.stringify([candidate.category, location
|
|
41
|
+
return JSON.stringify([candidate.category, location && repoPathCandidates(location.file)[1],
|
|
42
42
|
location?.symbol ?? location?.line ?? null, normalizedSummary(candidate.summary)]);
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -36,7 +36,6 @@ export const AdvisoryCandidatesSchema = Type.Object({
|
|
|
36
36
|
export const AdvisoryVerdictSchema = Type.Object({
|
|
37
37
|
verdict: AdvisoryVerifierVerdictSchema,
|
|
38
38
|
evidence: Type.Array(Type.String({ description: "Quoted or cited evidence supporting the verdict." })),
|
|
39
|
-
confidence: Type.Optional(AdvisoryConfidenceSchema),
|
|
40
39
|
});
|
|
41
40
|
|
|
42
41
|
export const AdvisoryFindingSchema = Type.Object({
|
|
@@ -81,3 +80,10 @@ export type AdvisoryReportWithStats = Static<typeof AdvisoryReportWithStatsSchem
|
|
|
81
80
|
export function isAdvisoryReport(value: unknown): value is AdvisoryReportWithStats {
|
|
82
81
|
return Value.Check(AdvisoryReportWithStatsSchema, value);
|
|
83
82
|
}
|
|
83
|
+
|
|
84
|
+
/** `file:line (symbol)`, omitting whichever of line and symbol is unknown. */
|
|
85
|
+
export function formatAdvisoryLocation(location: AdvisoryLocation): string {
|
|
86
|
+
const line = location.line != null ? `:${location.line}` : "";
|
|
87
|
+
const symbol = location.symbol ? ` (${location.symbol})` : "";
|
|
88
|
+
return `${location.file}${line}${symbol}`;
|
|
89
|
+
}
|
|
@@ -2,28 +2,23 @@ import type { Api, Model } from "@earendil-works/pi-ai";
|
|
|
2
2
|
import { assertWorkflowBudgetAvailable } from "./budget.ts";
|
|
3
3
|
import {
|
|
4
4
|
type AgentExecutionOptions,
|
|
5
|
-
type AgentRunTags,
|
|
6
5
|
type RunContext,
|
|
7
6
|
} from "./agent-runner-types.ts";
|
|
8
7
|
import {
|
|
9
8
|
captureReplayIdentity,
|
|
10
9
|
captureIsolatedRepositoryAfterSetup,
|
|
11
10
|
captureSharedRepositoryBeforeSetup,
|
|
12
|
-
isReplayEnabled,
|
|
13
11
|
lookupReplayResult,
|
|
14
|
-
|
|
12
|
+
validateMutationGuard,
|
|
15
13
|
validateReplayIdentity,
|
|
16
14
|
type AgentAttemptResult,
|
|
17
|
-
type
|
|
15
|
+
type AgentReplayContract,
|
|
18
16
|
type AgentReplayPlan,
|
|
17
|
+
type ArmedReplayPlan,
|
|
19
18
|
} from "./agent-replay.ts";
|
|
20
19
|
import { openAgentSession, promptAgentSession, type AgentSessionHandle } from "./agent-session.ts";
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
disposeAgentWorkspace,
|
|
24
|
-
type AgentWorkspace,
|
|
25
|
-
} from "./agent-workspace.ts";
|
|
26
|
-
import type { AgentResumeBaseContext, AgentResumeContext, RepositoryResumeContext } from "./resume-context.ts";
|
|
20
|
+
import { createAgentWorkspace, type AgentWorkspace } from "./agent-workspace.ts";
|
|
21
|
+
import type { AgentResumeBaseContext, RepositoryResumeContext } from "./resume-context.ts";
|
|
27
22
|
import { captureRepositoryMutationGuard } from "./resume-context.ts";
|
|
28
23
|
|
|
29
24
|
/** Execute one fully bracketed workspace/session attempt. Cleanup always precedes settlement. */
|
|
@@ -36,38 +31,33 @@ export async function executeAgentAttempt(input: {
|
|
|
36
31
|
readonly replay: AgentReplayPlan;
|
|
37
32
|
readonly label: string;
|
|
38
33
|
readonly rowId: number;
|
|
39
|
-
readonly
|
|
34
|
+
readonly phase: string;
|
|
40
35
|
readonly admitLiveAgent: () => void;
|
|
41
36
|
}): Promise<AgentAttemptResult> {
|
|
42
|
-
const { rc, prompt, opts, resumeBaseContext, model, replay, label, rowId,
|
|
37
|
+
const { rc, prompt, opts, resumeBaseContext, model, replay, label, rowId, phase, admitLiveAgent } = input;
|
|
38
|
+
let armed: ArmedReplayPlan | undefined;
|
|
43
39
|
let repositoryBefore: RepositoryResumeContext | undefined;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
evidence = { kind: "isolated", mutationGuard: mutationGuard.fingerprint };
|
|
52
|
-
}
|
|
53
|
-
} else {
|
|
54
|
-
repositoryBefore = await captureSharedRepositoryBeforeSetup(rc, prompt, opts, replay);
|
|
55
|
-
evidence = { kind: "shared" };
|
|
56
|
-
}
|
|
40
|
+
if (replay.kind === "isolated") {
|
|
41
|
+
const mutationGuard = await captureRepositoryMutationGuard(rc.cwd, rc.signal);
|
|
42
|
+
if (mutationGuard.kind === "verified") armed = { ...replay, mutationGuard: mutationGuard.fingerprint };
|
|
43
|
+
else rc.progress.log(`${label}: resume disabled for this call (${mutationGuard.reason})`);
|
|
44
|
+
} else if (replay.kind === "shared") {
|
|
45
|
+
armed = replay;
|
|
46
|
+
repositoryBefore = await captureSharedRepositoryBeforeSetup(rc, prompt, opts, replay);
|
|
57
47
|
}
|
|
58
48
|
let workspace: AgentWorkspace | undefined;
|
|
59
49
|
let handle: AgentSessionHandle | undefined;
|
|
60
50
|
|
|
61
51
|
try {
|
|
62
52
|
workspace = await createAgentWorkspace(rc, opts, label);
|
|
63
|
-
if (
|
|
53
|
+
if (armed?.kind === "isolated") {
|
|
64
54
|
if (workspace.kind !== "isolated") throw new Error("Isolated replay created a shared workspace.");
|
|
65
55
|
repositoryBefore = await captureIsolatedRepositoryAfterSetup(rc, prompt, opts, workspace);
|
|
66
56
|
}
|
|
67
|
-
handle = await openAgentSession({ rc, prompt, opts, cwd: workspace.cwd, model, label
|
|
57
|
+
handle = await openAgentSession({ rc, prompt, opts, cwd: workspace.cwd, model, label });
|
|
68
58
|
|
|
69
|
-
let
|
|
70
|
-
if (
|
|
59
|
+
let contract: AgentReplayContract | undefined;
|
|
60
|
+
if (armed && repositoryBefore) {
|
|
71
61
|
const capture = await captureReplayIdentity({
|
|
72
62
|
rc,
|
|
73
63
|
base: resumeBaseContext,
|
|
@@ -79,23 +69,12 @@ export async function executeAgentAttempt(input: {
|
|
|
79
69
|
if (capture.kind === "unverifiable") {
|
|
80
70
|
rc.progress.log(`${label}: resume disabled for this call (${capture.reason})`);
|
|
81
71
|
} else {
|
|
82
|
-
|
|
83
|
-
const cached = await lookupReplayResult({ rc, key:
|
|
72
|
+
contract = { ...armed, identity: capture.identity };
|
|
73
|
+
const cached = await lookupReplayResult({ rc, key: contract.key, identity: contract.identity, opts, workspace });
|
|
84
74
|
if (cached.hit) {
|
|
85
|
-
const
|
|
86
|
-
rc,
|
|
87
|
-
identity,
|
|
88
|
-
selectedSkills: handle.selectedSkills,
|
|
89
|
-
session: handle.session,
|
|
90
|
-
sessionCwd: workspace.cwd,
|
|
91
|
-
replay,
|
|
92
|
-
workspace,
|
|
93
|
-
});
|
|
94
|
-
const validation = contract.ok
|
|
95
|
-
? await validateReplayEvidence(rc, evidence)
|
|
96
|
-
: contract;
|
|
75
|
+
const validation = await validateReplayContract(rc, contract, handle, workspace);
|
|
97
76
|
if (validation.ok) {
|
|
98
|
-
return { kind: "cache-hit", result: cached.result,
|
|
77
|
+
return { kind: "cache-hit", result: cached.result, contract };
|
|
99
78
|
}
|
|
100
79
|
rc.progress.log(`${label}: cached result invalidated (${validation.reason})`);
|
|
101
80
|
} else if (cached.reason) {
|
|
@@ -113,35 +92,42 @@ export async function executeAgentAttempt(input: {
|
|
|
113
92
|
opts,
|
|
114
93
|
label,
|
|
115
94
|
rowId,
|
|
116
|
-
|
|
95
|
+
phase,
|
|
117
96
|
});
|
|
118
97
|
const result = await workspace.wrapResult(rawResult);
|
|
119
|
-
if (!
|
|
120
|
-
if (!isReplayEnabled(replay) || !evidence) throw new Error("Replay identity produced without complete replay evidence.");
|
|
98
|
+
if (!contract) return { kind: "live-unrecordable", result };
|
|
121
99
|
|
|
122
|
-
const
|
|
123
|
-
rc,
|
|
124
|
-
identity,
|
|
125
|
-
selectedSkills: handle.selectedSkills,
|
|
126
|
-
session: handle.session,
|
|
127
|
-
sessionCwd: workspace.cwd,
|
|
128
|
-
replay,
|
|
129
|
-
workspace,
|
|
130
|
-
});
|
|
131
|
-
const validation = contract.ok
|
|
132
|
-
? await validateReplayEvidence(rc, evidence)
|
|
133
|
-
: contract;
|
|
100
|
+
const validation = await validateReplayContract(rc, contract, handle, workspace);
|
|
134
101
|
if (validation.ok) {
|
|
135
|
-
return { kind: "live-recordable", result,
|
|
102
|
+
return { kind: "live-recordable", result, contract };
|
|
136
103
|
}
|
|
137
104
|
rc.progress.log(`${label}: read-only resume contract was not recorded (${validation.reason})`);
|
|
138
105
|
return { kind: "live-unrecordable", result };
|
|
139
106
|
} finally {
|
|
140
107
|
try {
|
|
141
108
|
const session = handle?.session;
|
|
142
|
-
if (session) rc.perf.timeSync("agent.dispose_ms", () => session.dispose()
|
|
109
|
+
if (session) rc.perf.timeSync("agent.dispose_ms", () => session.dispose());
|
|
143
110
|
} finally {
|
|
144
|
-
await
|
|
111
|
+
await workspace?.dispose();
|
|
145
112
|
}
|
|
146
113
|
}
|
|
147
114
|
}
|
|
115
|
+
|
|
116
|
+
/** Re-check a captured replay identity and its repository evidence against the session and workspace as they are now. */
|
|
117
|
+
async function validateReplayContract(
|
|
118
|
+
rc: RunContext,
|
|
119
|
+
contract: AgentReplayContract,
|
|
120
|
+
handle: AgentSessionHandle,
|
|
121
|
+
workspace: AgentWorkspace,
|
|
122
|
+
): Promise<{ readonly ok: true } | { readonly ok: false; readonly reason: string }> {
|
|
123
|
+
const identity = await validateReplayIdentity({
|
|
124
|
+
rc,
|
|
125
|
+
contract,
|
|
126
|
+
selectedSkills: handle.selectedSkills,
|
|
127
|
+
session: handle.session,
|
|
128
|
+
sessionCwd: workspace.cwd,
|
|
129
|
+
workspace,
|
|
130
|
+
});
|
|
131
|
+
if (!identity.ok || contract.kind === "shared") return identity;
|
|
132
|
+
return await validateMutationGuard(rc, contract.mutationGuard);
|
|
133
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
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
|
-
import type { AgentExecutionOptions, AgentRunnerSession,
|
|
4
|
+
import type { AgentExecutionOptions, AgentRunnerSession, RunContext } from "./agent-runner-types.ts";
|
|
5
5
|
import { captureEffectiveAgentSessionIdentity } from "./agent-session-identity.ts";
|
|
6
|
+
import { isRecord } from "./guards.ts";
|
|
6
7
|
import { FINAL_TOOL } from "./agent-session.ts";
|
|
7
8
|
import type { AgentWorkspace, IsolatedAgentWorkspace } from "./agent-workspace.ts";
|
|
8
9
|
import { captureAgentJournalKey } from "./journal.ts";
|
|
@@ -11,7 +12,7 @@ import {
|
|
|
11
12
|
captureRepositoryMutationGuard,
|
|
12
13
|
captureIsolatedRepositoryContext,
|
|
13
14
|
captureRepositoryResumeContext,
|
|
14
|
-
|
|
15
|
+
repositoryMismatchReason,
|
|
15
16
|
resumeContextMismatchReason,
|
|
16
17
|
type AgentResumeBaseContext,
|
|
17
18
|
type AgentResumeContext,
|
|
@@ -26,13 +27,17 @@ export type AgentReplayPlan =
|
|
|
26
27
|
| { readonly kind: "shared"; readonly key: string; readonly additionalInputs: readonly string[] }
|
|
27
28
|
| { readonly kind: "isolated"; readonly key: string };
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
| { readonly kind: "
|
|
30
|
+
/** An enabled replay plan with its main-workspace evidence: isolated replays carry the mutation guard taken before setup. */
|
|
31
|
+
export type ArmedReplayPlan =
|
|
32
|
+
| Extract<AgentReplayPlan, { readonly kind: "shared" }>
|
|
33
|
+
| (Extract<AgentReplayPlan, { readonly kind: "isolated" }> & { readonly mutationGuard: string });
|
|
34
|
+
|
|
35
|
+
/** An armed replay plan plus the resume identity captured for one attempt. */
|
|
36
|
+
export type AgentReplayContract = ArmedReplayPlan & { readonly identity: AgentResumeContext };
|
|
32
37
|
|
|
33
38
|
export type AgentAttemptResult =
|
|
34
|
-
| { readonly kind: "cache-hit"; readonly result: unknown; readonly
|
|
35
|
-
| { readonly kind: "live-recordable"; readonly result: unknown; readonly
|
|
39
|
+
| { readonly kind: "cache-hit"; readonly result: unknown; readonly contract: AgentReplayContract }
|
|
40
|
+
| { readonly kind: "live-recordable"; readonly result: unknown; readonly contract: AgentReplayContract }
|
|
36
41
|
| { readonly kind: "live-unrecordable"; readonly result: unknown };
|
|
37
42
|
|
|
38
43
|
export type AgentAttemptSettlement =
|
|
@@ -129,28 +134,28 @@ export async function lookupReplayResult(input: {
|
|
|
129
134
|
|
|
130
135
|
export async function validateReplayIdentity(input: {
|
|
131
136
|
readonly rc: RunContext;
|
|
132
|
-
readonly
|
|
137
|
+
readonly contract: AgentReplayContract;
|
|
133
138
|
readonly selectedSkills: readonly Skill[];
|
|
134
139
|
readonly session: AgentRunnerSession;
|
|
135
140
|
readonly sessionCwd: string;
|
|
136
|
-
readonly replay: Extract<AgentReplayPlan, { readonly kind: "shared" | "isolated" }>;
|
|
137
141
|
readonly workspace: AgentWorkspace;
|
|
138
142
|
}): Promise<{ readonly ok: true } | { readonly ok: false; readonly reason: string }> {
|
|
139
|
-
const
|
|
143
|
+
const { identity } = input.contract;
|
|
144
|
+
const repository = identity.repository.state === "inaccessible"
|
|
140
145
|
? INACCESSIBLE_REPOSITORY_CONTEXT
|
|
141
|
-
: await captureCurrentRepository(input.rc, input.
|
|
146
|
+
: await captureCurrentRepository(input.rc, input.contract, input.workspace);
|
|
142
147
|
if (repository.kind === "unverifiable") return { ok: false, reason: repository.reason };
|
|
143
148
|
|
|
144
149
|
const current = await captureVerifiedReplayIdentity({
|
|
145
150
|
rc: input.rc,
|
|
146
151
|
repository,
|
|
147
|
-
workflow:
|
|
152
|
+
workflow: identity.workflow,
|
|
148
153
|
selectedSkills: input.selectedSkills,
|
|
149
154
|
session: input.session,
|
|
150
155
|
sessionCwd: input.sessionCwd,
|
|
151
156
|
});
|
|
152
157
|
if (current.kind === "unverifiable") return { ok: false, reason: current.reason };
|
|
153
|
-
const mismatch = resumeContextMismatchReason(
|
|
158
|
+
const mismatch = resumeContextMismatchReason(identity, current.identity, {
|
|
154
159
|
allowWorkflowSourceMismatch: input.rc.resumeEditedWorkflow,
|
|
155
160
|
});
|
|
156
161
|
return mismatch ? { ok: false, reason: mismatch } : { ok: true };
|
|
@@ -159,23 +164,16 @@ export async function validateReplayIdentity(input: {
|
|
|
159
164
|
export async function settleAgentAttempt(input: {
|
|
160
165
|
readonly rc: RunContext;
|
|
161
166
|
readonly label: string;
|
|
162
|
-
readonly tags: AgentRunTags;
|
|
163
|
-
readonly replay: AgentReplayPlan;
|
|
164
167
|
readonly outcome: AgentAttemptResult;
|
|
165
168
|
}): Promise<AgentAttemptSettlement> {
|
|
166
|
-
const { rc, label,
|
|
169
|
+
const { rc, label, outcome } = input;
|
|
167
170
|
if (outcome.kind === "live-unrecordable") {
|
|
168
171
|
recordAgentResultSource(rc, "live");
|
|
169
172
|
return { kind: "done", result: outcome.result };
|
|
170
173
|
}
|
|
171
|
-
if (!isReplayEnabled(replay)) throw new Error("Replayable agent outcome produced without an enabled replay plan.");
|
|
172
174
|
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
outcome.identity,
|
|
176
|
-
replay,
|
|
177
|
-
outcome.evidence,
|
|
178
|
-
);
|
|
175
|
+
const { contract } = outcome;
|
|
176
|
+
const repository = await validateRepositoryAfterCleanup(rc, contract);
|
|
179
177
|
if (!repository.ok) {
|
|
180
178
|
if (outcome.kind === "cache-hit") {
|
|
181
179
|
rc.progress.log(`${label}: cached result invalidated after cleanup (${repository.reason})`);
|
|
@@ -185,10 +183,10 @@ export async function settleAgentAttempt(input: {
|
|
|
185
183
|
return { kind: "done", result: outcome.result };
|
|
186
184
|
}
|
|
187
185
|
|
|
188
|
-
await recordJournalResult(rc, label,
|
|
186
|
+
await recordJournalResult(rc, label, contract.key, outcome.result, contract.identity);
|
|
189
187
|
if (outcome.kind === "cache-hit") {
|
|
190
188
|
rc.progress.log(`${label}: using cached result from workflow journal`);
|
|
191
|
-
rc.perf.counter("agent.cache_hit"
|
|
189
|
+
rc.perf.counter("agent.cache_hit");
|
|
192
190
|
recordAgentResultSource(rc, "cached");
|
|
193
191
|
} else {
|
|
194
192
|
recordAgentResultSource(rc, "live");
|
|
@@ -236,12 +234,12 @@ async function captureVerifiedReplayIdentity(input: {
|
|
|
236
234
|
}
|
|
237
235
|
return {
|
|
238
236
|
kind: "verified",
|
|
239
|
-
identity:
|
|
240
|
-
|
|
241
|
-
input.repository,
|
|
242
|
-
effectiveSession.identity,
|
|
243
|
-
skills.skills,
|
|
244
|
-
|
|
237
|
+
identity: {
|
|
238
|
+
workflow: input.workflow,
|
|
239
|
+
repository: input.repository,
|
|
240
|
+
session: effectiveSession.identity,
|
|
241
|
+
skills: skills.skills,
|
|
242
|
+
},
|
|
245
243
|
};
|
|
246
244
|
}
|
|
247
245
|
|
|
@@ -259,7 +257,7 @@ async function validateCachedResult(
|
|
|
259
257
|
if (!patch.ok) {
|
|
260
258
|
return {
|
|
261
259
|
ok: false,
|
|
262
|
-
reason: `cached isolated patch does not apply to the current baseline (${
|
|
260
|
+
reason: `cached isolated patch does not apply to the current baseline (${patch.error})`,
|
|
263
261
|
};
|
|
264
262
|
}
|
|
265
263
|
result = isolated.wrapper.result;
|
|
@@ -271,7 +269,6 @@ async function validateCachedResult(
|
|
|
271
269
|
? { ok: true }
|
|
272
270
|
: { ok: false, reason: "cached text result is not a string" };
|
|
273
271
|
}
|
|
274
|
-
if (result === null) return { ok: true };
|
|
275
272
|
try {
|
|
276
273
|
return Value.Check(opts.schema, result)
|
|
277
274
|
? { ok: true }
|
|
@@ -287,63 +284,45 @@ function isolatedCachedResult(value: unknown):
|
|
|
287
284
|
readonly wrapper: { readonly result: unknown; readonly patch: string; readonly changed: boolean };
|
|
288
285
|
}
|
|
289
286
|
| { readonly ok: false; readonly reason: string } {
|
|
290
|
-
if (
|
|
287
|
+
if (!isRecord(value)) {
|
|
291
288
|
return { ok: false, reason: "cached isolated result is not an object" };
|
|
292
289
|
}
|
|
293
|
-
|
|
294
|
-
if (!("result" in candidate) || typeof candidate.patch !== "string" || typeof candidate.changed !== "boolean") {
|
|
290
|
+
if (!("result" in value) || typeof value.patch !== "string" || typeof value.changed !== "boolean") {
|
|
295
291
|
return { ok: false, reason: "cached isolated result has an invalid wrapper" };
|
|
296
292
|
}
|
|
297
|
-
const wrapper = { result:
|
|
293
|
+
const wrapper = { result: value.result, patch: value.patch, changed: value.changed };
|
|
298
294
|
return { ok: true, wrapper };
|
|
299
295
|
}
|
|
300
296
|
|
|
301
297
|
async function validateRepositoryAfterCleanup(
|
|
302
298
|
rc: RunContext,
|
|
303
|
-
|
|
304
|
-
replay: Extract<AgentReplayPlan, { readonly kind: "shared" | "isolated" }>,
|
|
305
|
-
evidence: AgentReplayEvidence,
|
|
299
|
+
contract: AgentReplayContract,
|
|
306
300
|
): Promise<{ readonly ok: true } | { readonly ok: false; readonly reason: string }> {
|
|
307
|
-
if (
|
|
308
|
-
return evidence.kind === "isolated"
|
|
309
|
-
? await validateReplayEvidence(rc, evidence)
|
|
310
|
-
: { ok: false, reason: "isolated replay omitted its main-workspace mutation guard" };
|
|
311
|
-
}
|
|
301
|
+
if (contract.kind === "isolated") return await validateMutationGuard(rc, contract.mutationGuard);
|
|
312
302
|
|
|
313
|
-
const repository = identity.repository.state === "inaccessible"
|
|
303
|
+
const repository = contract.identity.repository.state === "inaccessible"
|
|
314
304
|
? INACCESSIBLE_REPOSITORY_CONTEXT
|
|
315
|
-
: await captureRepositoryResumeContext(rc.cwd,
|
|
305
|
+
: await captureRepositoryResumeContext(rc.cwd, contract.additionalInputs, rc.signal);
|
|
316
306
|
if (repository.kind === "unverifiable") return { ok: false, reason: repository.reason };
|
|
317
|
-
const mismatch =
|
|
318
|
-
|
|
319
|
-
return evidence.kind === "shared"
|
|
320
|
-
? { ok: true }
|
|
321
|
-
: { ok: false, reason: "shared replay carried isolated mutation evidence" };
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
export async function validateReplayEvidence(
|
|
325
|
-
rc: RunContext,
|
|
326
|
-
evidence: AgentReplayEvidence,
|
|
327
|
-
): Promise<{ readonly ok: true } | { readonly ok: false; readonly reason: string }> {
|
|
328
|
-
return evidence.kind === "shared"
|
|
329
|
-
? { ok: true }
|
|
330
|
-
: await validateRepositoryMutationGuard(rc, evidence.mutationGuard);
|
|
307
|
+
const mismatch = repositoryMismatchReason(contract.identity.repository, repository);
|
|
308
|
+
return mismatch ? { ok: false, reason: mismatch } : { ok: true };
|
|
331
309
|
}
|
|
332
310
|
|
|
333
|
-
|
|
311
|
+
/** Check that the main workspace is unchanged since an isolated replay took its mutation guard. */
|
|
312
|
+
export async function validateMutationGuard(
|
|
334
313
|
rc: RunContext,
|
|
335
|
-
|
|
314
|
+
mutationGuard: string,
|
|
336
315
|
): Promise<{ readonly ok: true } | { readonly ok: false; readonly reason: string }> {
|
|
337
316
|
const current = await captureRepositoryMutationGuard(rc.cwd, rc.signal);
|
|
338
317
|
if (current.kind === "unverifiable") return { ok: false, reason: current.reason };
|
|
339
|
-
return current.fingerprint ===
|
|
318
|
+
return current.fingerprint === mutationGuard
|
|
340
319
|
? { ok: true }
|
|
341
320
|
: { ok: false, reason: "main workspace changed outside the isolated worktree" };
|
|
342
321
|
}
|
|
343
322
|
|
|
344
323
|
async function captureCurrentRepository(
|
|
345
324
|
rc: RunContext,
|
|
346
|
-
replay:
|
|
325
|
+
replay: ArmedReplayPlan,
|
|
347
326
|
workspace: AgentWorkspace,
|
|
348
327
|
): Promise<RepositoryResumeContext> {
|
|
349
328
|
if (replay.kind === "shared") return await captureRepositoryResumeContext(rc.cwd, replay.additionalInputs, rc.signal);
|
|
@@ -375,8 +354,8 @@ function declaresNoWorkspaceCapability(prompt: string, opts: AgentExecutionOptio
|
|
|
375
354
|
opts.tools !== undefined &&
|
|
376
355
|
opts.tools.length === 0 &&
|
|
377
356
|
(opts.toolHints?.length ?? 0) === 0 &&
|
|
378
|
-
|
|
379
|
-
extractSkillSelectorsFromText(prompt).length === 0
|
|
357
|
+
// Explicit skills suppress prompt inference, matching resolveAgentSkillRequest.
|
|
358
|
+
(opts.skills !== undefined ? opts.skills.length === 0 : extractSkillSelectorsFromText(prompt).length === 0)
|
|
380
359
|
);
|
|
381
360
|
}
|
|
382
361
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isRetryableAssistantError, type AssistantMessage } from "@earendil-works/pi-ai";
|
|
2
2
|
import { abortReason, throwIfAborted } from "./cancellation.ts";
|
|
3
3
|
import {
|
|
4
|
+
lastAssistantError,
|
|
4
5
|
providerUsageLimitFromMessages,
|
|
5
6
|
type WorkflowProviderUsageLimitError,
|
|
6
7
|
} from "./provider-usage-limit.ts";
|
|
@@ -75,42 +76,18 @@ export function providerErrorFromMessages(
|
|
|
75
76
|
messages: readonly unknown[],
|
|
76
77
|
options: { readonly pauseOnUsageLimit?: boolean } = {},
|
|
77
78
|
): WorkflowProviderError | WorkflowProviderUsageLimitError | undefined {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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"
|
|
79
|
+
if (options.pauseOnUsageLimit) {
|
|
80
|
+
const usageLimit = providerUsageLimitFromMessages(messages);
|
|
81
|
+
if (usageLimit) return usageLimit;
|
|
82
|
+
}
|
|
83
|
+
const failure = lastAssistantError(messages);
|
|
84
|
+
if (!failure) return undefined;
|
|
85
|
+
return new WorkflowProviderError(
|
|
86
|
+
failure.errorMessage || "Provider session ended with an unspecified error.",
|
|
87
|
+
{
|
|
88
|
+
stopReason: "error",
|
|
89
|
+
retryable: isRetryableAssistantError(failure.message as AssistantMessage),
|
|
90
|
+
...failure.details,
|
|
91
|
+
},
|
|
111
92
|
);
|
|
112
93
|
}
|
|
113
|
-
|
|
114
|
-
function stringDetail(value: unknown): string | undefined {
|
|
115
|
-
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
116
|
-
}
|
|
@@ -29,18 +29,18 @@ export type AgentRunnerSession = Pick<
|
|
|
29
29
|
| "getActiveToolNames"
|
|
30
30
|
| "getToolDefinition"
|
|
31
31
|
| "setActiveToolsByName"
|
|
32
|
-
| "setAutoRetryEnabled"
|
|
33
32
|
| "getLastAssistantText"
|
|
34
33
|
>;
|
|
35
34
|
|
|
36
35
|
export type CreateAgentSession = (options: CreateAgentSessionOptions) => Promise<{ session: AgentRunnerSession }>;
|
|
37
36
|
|
|
37
|
+
/** Every row update after `agentQueued` addresses that row by its id; labels are display text, not unique keys. */
|
|
38
38
|
export interface AgentProgress {
|
|
39
39
|
agentQueued(phase: string | undefined, label: string): number;
|
|
40
|
-
agentStart(
|
|
41
|
-
agentTool(
|
|
42
|
-
agentDone(
|
|
43
|
-
agentFailed(
|
|
40
|
+
agentStart(id: number): void;
|
|
41
|
+
agentTool(id: number, tool: string): void;
|
|
42
|
+
agentDone(id: number): void;
|
|
43
|
+
agentFailed(id: number, error: unknown): void;
|
|
44
44
|
event(event: WorkflowProgressEvent): void;
|
|
45
45
|
log(message: string): void;
|
|
46
46
|
}
|
|
@@ -83,9 +83,3 @@ export type RunContext = RunContextBase & (
|
|
|
83
83
|
export type AgentExecutionOptions = AgentOptions & {
|
|
84
84
|
readonly worktreeBaseline?: WorktreeBaseline;
|
|
85
85
|
};
|
|
86
|
-
|
|
87
|
-
export interface AgentRunTags {
|
|
88
|
-
readonly [key: string]: string | number;
|
|
89
|
-
readonly label: string;
|
|
90
|
-
readonly phase: string;
|
|
91
|
-
}
|