pi-workflow-engine 0.10.3 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/.pi/extensions/pi-workflow-engine/index.ts +418 -213
  2. package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +11 -4
  3. package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +147 -0
  4. package/.pi/extensions/pi-workflow-engine/src/agent-limits.ts +35 -0
  5. package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +399 -0
  6. package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +116 -0
  7. package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +91 -0
  8. package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +154 -530
  9. package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +256 -0
  10. package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +50 -0
  11. package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +368 -0
  12. package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +87 -39
  13. package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +89 -0
  14. package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +75 -0
  15. package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +492 -0
  16. package/.pi/extensions/pi-workflow-engine/src/budget.ts +10 -3
  17. package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +55 -3
  18. package/.pi/extensions/pi-workflow-engine/src/command-completions.ts +36 -0
  19. package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +230 -61
  20. package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +38 -55
  21. package/.pi/extensions/pi-workflow-engine/src/discovery.ts +70 -17
  22. package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +59 -13
  23. package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +123 -63
  24. package/.pi/extensions/pi-workflow-engine/src/engine.ts +357 -75
  25. package/.pi/extensions/pi-workflow-engine/src/filesystem-error.ts +4 -0
  26. package/.pi/extensions/pi-workflow-engine/src/finalizers.ts +66 -0
  27. package/.pi/extensions/pi-workflow-engine/src/identity-canonicalization.ts +138 -0
  28. package/.pi/extensions/pi-workflow-engine/src/identity-fingerprint.ts +15 -0
  29. package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +10 -5
  30. package/.pi/extensions/pi-workflow-engine/src/journal.ts +156 -66
  31. package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +167 -0
  32. package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +310 -0
  33. package/.pi/extensions/pi-workflow-engine/src/options.ts +96 -6
  34. package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +32 -0
  35. package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +203 -39
  36. package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +50 -0
  37. package/.pi/extensions/pi-workflow-engine/src/progress.ts +74 -122
  38. package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +189 -0
  39. package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +48 -0
  40. package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +89 -0
  41. package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +289 -0
  42. package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +701 -0
  43. package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +49 -0
  44. package/.pi/extensions/pi-workflow-engine/src/review/github-pr-comments.ts +75 -16
  45. package/.pi/extensions/pi-workflow-engine/src/review/review-actions.ts +83 -17
  46. package/.pi/extensions/pi-workflow-engine/src/review/review-budget.ts +45 -0
  47. package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +117 -0
  48. package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +14 -66
  49. package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +38 -12
  50. package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +42 -0
  51. package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +18 -22
  52. package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +128 -56
  53. package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +153 -0
  54. package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +294 -0
  55. package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +130 -0
  56. package/.pi/extensions/pi-workflow-engine/src/session-identity.ts +6 -0
  57. package/.pi/extensions/pi-workflow-engine/src/structured-output.ts +32 -0
  58. package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +44 -0
  59. package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +150 -0
  60. package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +401 -0
  61. package/.pi/extensions/pi-workflow-engine/src/types.ts +87 -25
  62. package/.pi/extensions/pi-workflow-engine/src/ui/dynamax-editor-decoration.ts +220 -0
  63. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +26 -6
  64. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +88 -46
  65. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +7 -70
  66. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-viewer-layout.ts +51 -0
  67. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +12 -67
  68. package/.pi/extensions/pi-workflow-engine/src/unknown-error.ts +8 -0
  69. package/.pi/extensions/pi-workflow-engine/src/usage.ts +124 -16
  70. package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +23 -3
  71. package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +121 -0
  72. package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +19 -1
  73. package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +72 -0
  74. package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +354 -0
  75. package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +162 -0
  76. package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +632 -0
  77. package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +172 -0
  78. package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +80 -0
  79. package/.pi/extensions/pi-workflow-engine/src/workflows.ts +30 -3
  80. package/.pi/extensions/pi-workflow-engine/src/worktree.ts +321 -43
  81. package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +62 -64
  82. package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +17 -17
  83. package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +15 -16
  84. package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +15 -15
  85. package/.pi/extensions/pi-workflow-engine/workflows/research.ts +170 -0
  86. package/README.md +82 -207
  87. package/USAGE.md +295 -21
  88. package/package.json +9 -9
@@ -1,4 +1,5 @@
1
1
  import { Type, type Static } from "typebox";
2
+ import { Value } from "typebox/value";
2
3
 
3
4
  export const AdvisorySeveritySchema = Type.Union([Type.Literal("low"), Type.Literal("medium"), Type.Literal("high")], {
4
5
  description: "Impact level of an advisory finding.",
@@ -53,12 +54,18 @@ export const AdvisoryReportSchema = Type.Object({
53
54
  nextSteps: Type.Array(Type.String({ description: "Concrete follow-up commands, inspections, or decisions." })),
54
55
  });
55
56
 
56
- export type AdvisorySeverity = Static<typeof AdvisorySeveritySchema>;
57
- export type AdvisoryConfidence = Static<typeof AdvisoryConfidenceSchema>;
58
- export type AdvisoryVerifierVerdict = Static<typeof AdvisoryVerifierVerdictSchema>;
57
+ export const AdvisoryReportWithStatsSchema = Type.Object({
58
+ ...AdvisoryReportSchema.properties,
59
+ stats: Type.Optional(Type.Record(Type.String(), Type.Union([Type.String(), Type.Number()]))),
60
+ });
61
+
59
62
  export type AdvisoryLocation = Static<typeof AdvisoryLocationSchema>;
60
63
  export type AdvisoryCandidate = Static<typeof AdvisoryCandidateSchema>;
61
- export type AdvisoryCandidates = Static<typeof AdvisoryCandidatesSchema>;
62
64
  export type AdvisoryVerdict = Static<typeof AdvisoryVerdictSchema>;
63
65
  export type AdvisoryFinding = Static<typeof AdvisoryFindingSchema>;
64
66
  export type AdvisoryReport = Static<typeof AdvisoryReportSchema>;
67
+ export type AdvisoryReportWithStats = Static<typeof AdvisoryReportWithStatsSchema>;
68
+
69
+ export function isAdvisoryReport(value: unknown): value is AdvisoryReportWithStats {
70
+ return Value.Check(AdvisoryReportWithStatsSchema, value);
71
+ }
@@ -0,0 +1,147 @@
1
+ import type { Api, Model } from "@earendil-works/pi-ai";
2
+ import { assertWorkflowBudgetAvailable } from "./budget.ts";
3
+ import {
4
+ type AgentExecutionOptions,
5
+ type AgentRunTags,
6
+ type RunContext,
7
+ } from "./agent-runner-types.ts";
8
+ import {
9
+ captureReplayIdentity,
10
+ captureIsolatedRepositoryAfterSetup,
11
+ captureSharedRepositoryBeforeSetup,
12
+ isReplayEnabled,
13
+ lookupReplayResult,
14
+ validateReplayEvidence,
15
+ validateReplayIdentity,
16
+ type AgentAttemptResult,
17
+ type AgentReplayEvidence,
18
+ type AgentReplayPlan,
19
+ } from "./agent-replay.ts";
20
+ import { openAgentSession, promptAgentSession, type AgentSessionHandle } from "./agent-session.ts";
21
+ import {
22
+ createAgentWorkspace,
23
+ disposeAgentWorkspace,
24
+ type AgentWorkspace,
25
+ } from "./agent-workspace.ts";
26
+ import type { AgentResumeBaseContext, AgentResumeContext, RepositoryResumeContext } from "./resume-context.ts";
27
+ import { captureRepositoryMutationGuard } from "./resume-context.ts";
28
+
29
+ /** Execute one fully bracketed workspace/session attempt. Cleanup always precedes settlement. */
30
+ export async function executeAgentAttempt(input: {
31
+ readonly rc: RunContext;
32
+ readonly prompt: string;
33
+ readonly opts: AgentExecutionOptions;
34
+ readonly resumeBaseContext: AgentResumeBaseContext;
35
+ readonly model: Model<Api> | undefined;
36
+ readonly replay: AgentReplayPlan;
37
+ readonly label: string;
38
+ readonly rowId: number;
39
+ readonly tags: AgentRunTags;
40
+ readonly admitLiveAgent: () => void;
41
+ }): Promise<AgentAttemptResult> {
42
+ const { rc, prompt, opts, resumeBaseContext, model, replay, label, rowId, tags, admitLiveAgent } = input;
43
+ let repositoryBefore: RepositoryResumeContext | undefined;
44
+ let evidence: AgentReplayEvidence | undefined;
45
+ if (isReplayEnabled(replay)) {
46
+ if (replay.kind === "isolated") {
47
+ const mutationGuard = await captureRepositoryMutationGuard(rc.cwd, rc.signal);
48
+ if (mutationGuard.kind === "unverifiable") {
49
+ repositoryBefore = mutationGuard;
50
+ } else {
51
+ evidence = { kind: "isolated", mutationGuard: mutationGuard.fingerprint };
52
+ }
53
+ } else {
54
+ repositoryBefore = await captureSharedRepositoryBeforeSetup(rc, prompt, opts, replay);
55
+ evidence = { kind: "shared" };
56
+ }
57
+ }
58
+ let workspace: AgentWorkspace | undefined;
59
+ let handle: AgentSessionHandle | undefined;
60
+
61
+ try {
62
+ workspace = await createAgentWorkspace(rc, opts, label);
63
+ if (replay.kind === "isolated" && evidence?.kind === "isolated") {
64
+ if (workspace.kind !== "isolated") throw new Error("Isolated replay created a shared workspace.");
65
+ repositoryBefore = await captureIsolatedRepositoryAfterSetup(rc, prompt, opts, workspace);
66
+ }
67
+ handle = await openAgentSession({ rc, prompt, opts, cwd: workspace.cwd, model, label, tags });
68
+
69
+ let identity: AgentResumeContext | undefined;
70
+ if (isReplayEnabled(replay) && repositoryBefore && evidence) {
71
+ const capture = await captureReplayIdentity({
72
+ rc,
73
+ base: resumeBaseContext,
74
+ repository: repositoryBefore,
75
+ selectedSkills: handle.selectedSkills,
76
+ session: handle.session,
77
+ sessionCwd: workspace.cwd,
78
+ });
79
+ if (capture.kind === "unverifiable") {
80
+ rc.progress.log(`${label}: resume disabled for this call (${capture.reason})`);
81
+ } else {
82
+ identity = capture.identity;
83
+ const cached = await lookupReplayResult({ rc, key: replay.key, identity, opts, workspace });
84
+ if (cached.hit) {
85
+ const contract = await validateReplayIdentity({
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;
97
+ if (validation.ok) {
98
+ return { kind: "cache-hit", result: cached.result, identity, evidence };
99
+ }
100
+ rc.progress.log(`${label}: cached result invalidated (${validation.reason})`);
101
+ } else if (cached.reason) {
102
+ rc.progress.log(`${label}: cached result invalidated (${cached.reason})`);
103
+ }
104
+ }
105
+ }
106
+
107
+ assertWorkflowBudgetAvailable(rc.budget);
108
+ admitLiveAgent();
109
+ const rawResult = await promptAgentSession({
110
+ rc,
111
+ handle,
112
+ prompt,
113
+ opts,
114
+ label,
115
+ rowId,
116
+ tags,
117
+ });
118
+ const result = await workspace.wrapResult(rawResult);
119
+ if (!identity) return { kind: "live-unrecordable", result };
120
+ if (!isReplayEnabled(replay) || !evidence) throw new Error("Replay identity produced without complete replay evidence.");
121
+
122
+ const contract = await validateReplayIdentity({
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;
134
+ if (validation.ok) {
135
+ return { kind: "live-recordable", result, identity, evidence };
136
+ }
137
+ rc.progress.log(`${label}: read-only resume contract was not recorded (${validation.reason})`);
138
+ return { kind: "live-unrecordable", result };
139
+ } finally {
140
+ try {
141
+ const session = handle?.session;
142
+ if (session) rc.perf.timeSync("agent.dispose_ms", () => session.dispose(), tags);
143
+ } finally {
144
+ await disposeAgentWorkspace(rc, label, workspace);
145
+ }
146
+ }
147
+ }
@@ -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
+ }
@@ -0,0 +1,399 @@
1
+ import type { Skill } from "@earendil-works/pi-coding-agent";
2
+ import { Value } from "typebox/value";
3
+ import { captureAgentSkillIdentities, extractSkillSelectorsFromText } from "./agent-skills.ts";
4
+ import type { AgentExecutionOptions, AgentRunnerSession, AgentRunTags, RunContext } from "./agent-runner-types.ts";
5
+ import { captureEffectiveAgentSessionIdentity } from "./agent-session-identity.ts";
6
+ import { FINAL_TOOL } from "./agent-session.ts";
7
+ import type { AgentWorkspace, IsolatedAgentWorkspace } from "./agent-workspace.ts";
8
+ import { captureAgentJournalKey } from "./journal.ts";
9
+ import { unknownErrorMessage } from "./unknown-error.ts";
10
+ import {
11
+ captureRepositoryMutationGuard,
12
+ captureIsolatedRepositoryContext,
13
+ captureRepositoryResumeContext,
14
+ createAgentResumeContext,
15
+ resumeContextMismatchReason,
16
+ type AgentResumeBaseContext,
17
+ type AgentResumeContext,
18
+ type RepositoryResumeContext,
19
+ type VerifiedRepositoryResumeContext,
20
+ type VerifiedWorkflowResumeContext,
21
+ } from "./resume-context.ts";
22
+
23
+ export type AgentReplayPlan =
24
+ | { readonly kind: "off" }
25
+ | { readonly kind: "disabled"; readonly reason: string }
26
+ | { readonly kind: "shared"; readonly key: string; readonly additionalInputs: readonly string[] }
27
+ | { readonly kind: "isolated"; readonly key: string };
28
+
29
+ export type AgentReplayEvidence =
30
+ | { readonly kind: "shared" }
31
+ | { readonly kind: "isolated"; readonly mutationGuard: string };
32
+
33
+ export type AgentAttemptResult =
34
+ | { readonly kind: "cache-hit"; readonly result: unknown; readonly identity: AgentResumeContext; readonly evidence: AgentReplayEvidence }
35
+ | { readonly kind: "live-recordable"; readonly result: unknown; readonly identity: AgentResumeContext; readonly evidence: AgentReplayEvidence }
36
+ | { readonly kind: "live-unrecordable"; readonly result: unknown };
37
+
38
+ export type AgentAttemptSettlement =
39
+ | { readonly kind: "done"; readonly result: unknown }
40
+ | { readonly kind: "retry-live" };
41
+
42
+ type ReplayIdentityCapture =
43
+ | { readonly kind: "verified"; readonly identity: AgentResumeContext }
44
+ | { readonly kind: "unverifiable"; readonly reason: string };
45
+
46
+ const INACCESSIBLE_REPOSITORY_CONTEXT: VerifiedRepositoryResumeContext = {
47
+ kind: "verified",
48
+ state: "inaccessible",
49
+ workingTreeFingerprint: "no-workspace-capability",
50
+ };
51
+
52
+ export function createAgentReplayPlan(
53
+ prompt: string,
54
+ opts: AgentExecutionOptions,
55
+ ): AgentReplayPlan {
56
+ if (opts.resume !== undefined && opts.resume !== "read-only" && opts.resume !== "off") {
57
+ return { kind: "disabled", reason: "resume policy must be read-only or off" };
58
+ }
59
+ const isolated = opts.isolation === "worktree";
60
+ if (opts.resume === "off" || (!isolated && opts.resume !== "read-only")) return { kind: "off" };
61
+ const additionalInputs = inspectResumeInputs(opts);
62
+ if (typeof additionalInputs === "string") return { kind: "disabled", reason: additionalInputs };
63
+ const capture = captureAgentJournalKey(prompt, opts, opts.worktreeBaseline);
64
+ if (capture.kind === "unverifiable") return { kind: "disabled", reason: capture.reason };
65
+ return isolated
66
+ ? { kind: "isolated", key: capture.key }
67
+ : { kind: "shared", key: capture.key, additionalInputs };
68
+ }
69
+
70
+ export function isReplayEnabled(
71
+ replay: AgentReplayPlan,
72
+ ): replay is Extract<AgentReplayPlan, { readonly kind: "shared" | "isolated" }> {
73
+ return replay.kind === "shared" || replay.kind === "isolated";
74
+ }
75
+
76
+ export async function captureSharedRepositoryBeforeSetup(
77
+ rc: RunContext,
78
+ prompt: string,
79
+ opts: AgentExecutionOptions,
80
+ replay: Extract<AgentReplayPlan, { readonly kind: "shared" }>,
81
+ ): Promise<RepositoryResumeContext> {
82
+ if (declaresNoWorkspaceCapability(prompt, opts)) return INACCESSIBLE_REPOSITORY_CONTEXT;
83
+ return await captureRepositoryResumeContext(rc.cwd, replay.additionalInputs, rc.signal);
84
+ }
85
+
86
+ export async function captureIsolatedRepositoryAfterSetup(
87
+ rc: RunContext,
88
+ prompt: string,
89
+ opts: AgentExecutionOptions,
90
+ workspace: IsolatedAgentWorkspace,
91
+ ): Promise<RepositoryResumeContext> {
92
+ if (declaresNoWorkspaceCapability(prompt, opts)) return INACCESSIBLE_REPOSITORY_CONTEXT;
93
+ return await captureIsolatedRepositoryContext(workspace.cwd, workspace.baselineOid, rc.signal);
94
+ }
95
+
96
+ export async function captureReplayIdentity(input: {
97
+ readonly rc: RunContext;
98
+ readonly base: AgentResumeBaseContext;
99
+ readonly repository: RepositoryResumeContext;
100
+ readonly selectedSkills: readonly Skill[];
101
+ readonly session: AgentRunnerSession;
102
+ readonly sessionCwd: string;
103
+ }): Promise<ReplayIdentityCapture> {
104
+ if (input.repository.kind === "unverifiable") return input.repository;
105
+ if (input.base.workflow.kind === "unverifiable") return input.base.workflow;
106
+ return await captureVerifiedReplayIdentity({
107
+ ...input,
108
+ repository: input.repository,
109
+ workflow: input.base.workflow,
110
+ });
111
+ }
112
+
113
+ export async function lookupReplayResult(input: {
114
+ readonly rc: RunContext;
115
+ readonly key: string;
116
+ readonly identity: AgentResumeContext;
117
+ readonly opts: AgentExecutionOptions;
118
+ readonly workspace: AgentWorkspace;
119
+ }): Promise<{ readonly hit: true; readonly result: unknown } | { readonly hit: false; readonly reason?: string }> {
120
+ const cached = input.rc.journal.lookup(input.key, input.identity, {
121
+ allowWorkflowSourceMismatch: input.rc.resumeEditedWorkflow,
122
+ });
123
+ if (!cached.hit) return cached;
124
+ const validation = await validateCachedResult(cached.value, input.opts, input.rc, input.workspace);
125
+ return validation.ok
126
+ ? { hit: true, result: cached.value }
127
+ : { hit: false, reason: validation.reason };
128
+ }
129
+
130
+ export async function validateReplayIdentity(input: {
131
+ readonly rc: RunContext;
132
+ readonly identity: AgentResumeContext;
133
+ readonly selectedSkills: readonly Skill[];
134
+ readonly session: AgentRunnerSession;
135
+ readonly sessionCwd: string;
136
+ readonly replay: Extract<AgentReplayPlan, { readonly kind: "shared" | "isolated" }>;
137
+ readonly workspace: AgentWorkspace;
138
+ }): Promise<{ readonly ok: true } | { readonly ok: false; readonly reason: string }> {
139
+ const repository = input.identity.repository.state === "inaccessible"
140
+ ? INACCESSIBLE_REPOSITORY_CONTEXT
141
+ : await captureCurrentRepository(input.rc, input.replay, input.workspace);
142
+ if (repository.kind === "unverifiable") return { ok: false, reason: repository.reason };
143
+
144
+ const current = await captureVerifiedReplayIdentity({
145
+ rc: input.rc,
146
+ repository,
147
+ workflow: input.identity.workflow,
148
+ selectedSkills: input.selectedSkills,
149
+ session: input.session,
150
+ sessionCwd: input.sessionCwd,
151
+ });
152
+ if (current.kind === "unverifiable") return { ok: false, reason: current.reason };
153
+ const mismatch = resumeContextMismatchReason(input.identity, current.identity, {
154
+ allowWorkflowSourceMismatch: input.rc.resumeEditedWorkflow,
155
+ });
156
+ return mismatch ? { ok: false, reason: mismatch } : { ok: true };
157
+ }
158
+
159
+ export async function settleAgentAttempt(input: {
160
+ readonly rc: RunContext;
161
+ readonly label: string;
162
+ readonly tags: AgentRunTags;
163
+ readonly replay: AgentReplayPlan;
164
+ readonly outcome: AgentAttemptResult;
165
+ }): Promise<AgentAttemptSettlement> {
166
+ const { rc, label, replay, outcome } = input;
167
+ if (outcome.kind === "live-unrecordable") {
168
+ recordAgentResultSource(rc, "live");
169
+ return { kind: "done", result: outcome.result };
170
+ }
171
+ if (!isReplayEnabled(replay)) throw new Error("Replayable agent outcome produced without an enabled replay plan.");
172
+
173
+ const repository = await validateRepositoryAfterCleanup(
174
+ rc,
175
+ outcome.identity,
176
+ replay,
177
+ outcome.evidence,
178
+ );
179
+ if (!repository.ok) {
180
+ if (outcome.kind === "cache-hit") {
181
+ rc.progress.log(`${label}: cached result invalidated after cleanup (${repository.reason})`);
182
+ return { kind: "retry-live" };
183
+ }
184
+ rc.progress.log(`${label}: read-only resume contract was not recorded (${repository.reason})`);
185
+ return { kind: "done", result: outcome.result };
186
+ }
187
+
188
+ await recordJournalResult(rc, label, replay.key, outcome.result, outcome.identity);
189
+ if (outcome.kind === "cache-hit") {
190
+ rc.progress.log(`${label}: using cached result from workflow journal`);
191
+ rc.perf.counter("agent.cache_hit", 1, input.tags);
192
+ recordAgentResultSource(rc, "cached");
193
+ } else {
194
+ recordAgentResultSource(rc, "live");
195
+ }
196
+ return { kind: "done", result: outcome.result };
197
+ }
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
+
209
+ async function captureVerifiedReplayIdentity(input: {
210
+ readonly rc: RunContext;
211
+ readonly repository: VerifiedRepositoryResumeContext;
212
+ readonly workflow: VerifiedWorkflowResumeContext;
213
+ readonly selectedSkills: readonly Skill[];
214
+ readonly session: AgentRunnerSession;
215
+ readonly sessionCwd: string;
216
+ }): Promise<ReplayIdentityCapture> {
217
+ const [skills, effectiveSession] = await Promise.all([
218
+ captureAgentSkillIdentities(input.selectedSkills, {
219
+ sessionCwd: input.sessionCwd,
220
+ workspaceRoot: input.rc.cwd,
221
+ signal: input.rc.signal,
222
+ }),
223
+ captureEffectiveAgentSessionIdentity(input.session, {
224
+ sessionCwd: input.sessionCwd,
225
+ workspaceRoot: input.rc.cwd,
226
+ signal: input.rc.signal,
227
+ }),
228
+ ]);
229
+ if (skills.kind === "unverifiable") return skills;
230
+ if (effectiveSession.kind === "unverifiable") return effectiveSession;
231
+ if (
232
+ input.repository.state === "inaccessible" &&
233
+ (skills.skills.length > 0 || effectiveSession.identity.tools.some((tool) => tool.name !== FINAL_TOOL))
234
+ ) {
235
+ return { kind: "unverifiable", reason: "session unexpectedly retained workspace-observing capabilities" };
236
+ }
237
+ return {
238
+ kind: "verified",
239
+ identity: createAgentResumeContext(
240
+ { workflow: input.workflow },
241
+ input.repository,
242
+ effectiveSession.identity,
243
+ skills.skills,
244
+ ),
245
+ };
246
+ }
247
+
248
+ async function validateCachedResult(
249
+ value: unknown,
250
+ opts: AgentExecutionOptions,
251
+ rc: RunContext,
252
+ workspace: AgentWorkspace,
253
+ ): Promise<{ readonly ok: true } | { readonly ok: false; readonly reason: string }> {
254
+ let result: unknown;
255
+ if (opts.isolation === "worktree") {
256
+ const isolated = isolatedCachedResult(value);
257
+ if (!isolated.ok) return isolated;
258
+ const patch = await rc.worktrees.validatePatch(workspace.cwd, isolated.wrapper, rc.signal);
259
+ if (!patch.ok) {
260
+ return {
261
+ ok: false,
262
+ reason: `cached isolated patch does not apply to the current baseline (${(patch.error ?? patch.stderr.trim()) || "unknown error"})`,
263
+ };
264
+ }
265
+ result = isolated.wrapper.result;
266
+ } else {
267
+ result = value;
268
+ }
269
+ if (!opts.schema) {
270
+ return typeof result === "string"
271
+ ? { ok: true }
272
+ : { ok: false, reason: "cached text result is not a string" };
273
+ }
274
+ if (result === null) return { ok: true };
275
+ try {
276
+ return Value.Check(opts.schema, result)
277
+ ? { ok: true }
278
+ : { ok: false, reason: "cached structured result does not match the current schema" };
279
+ } catch (error) {
280
+ return { ok: false, reason: `cached structured result could not be validated (${unknownErrorMessage(error)})` };
281
+ }
282
+ }
283
+
284
+ function isolatedCachedResult(value: unknown):
285
+ | {
286
+ readonly ok: true;
287
+ readonly wrapper: { readonly result: unknown; readonly patch: string; readonly changed: boolean };
288
+ }
289
+ | { readonly ok: false; readonly reason: string } {
290
+ if (typeof value !== "object" || value === null) {
291
+ return { ok: false, reason: "cached isolated result is not an object" };
292
+ }
293
+ const candidate = value as { readonly result?: unknown; readonly patch?: unknown; readonly changed?: unknown };
294
+ if (!("result" in candidate) || typeof candidate.patch !== "string" || typeof candidate.changed !== "boolean") {
295
+ return { ok: false, reason: "cached isolated result has an invalid wrapper" };
296
+ }
297
+ const wrapper = { result: candidate.result, patch: candidate.patch, changed: candidate.changed };
298
+ return { ok: true, wrapper };
299
+ }
300
+
301
+ async function validateRepositoryAfterCleanup(
302
+ rc: RunContext,
303
+ identity: AgentResumeContext,
304
+ replay: Extract<AgentReplayPlan, { readonly kind: "shared" | "isolated" }>,
305
+ evidence: AgentReplayEvidence,
306
+ ): Promise<{ readonly ok: true } | { readonly ok: false; readonly reason: string }> {
307
+ if (replay.kind === "isolated") {
308
+ return evidence.kind === "isolated"
309
+ ? await validateReplayEvidence(rc, evidence)
310
+ : { ok: false, reason: "isolated replay omitted its main-workspace mutation guard" };
311
+ }
312
+
313
+ const repository = identity.repository.state === "inaccessible"
314
+ ? INACCESSIBLE_REPOSITORY_CONTEXT
315
+ : await captureRepositoryResumeContext(rc.cwd, replay.additionalInputs, rc.signal);
316
+ if (repository.kind === "unverifiable") return { ok: false, reason: repository.reason };
317
+ const mismatch = resumeContextMismatchReason(identity, { ...identity, repository });
318
+ if (mismatch) return { ok: false, reason: mismatch };
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);
331
+ }
332
+
333
+ export async function validateRepositoryMutationGuard(
334
+ rc: RunContext,
335
+ expected: string,
336
+ ): Promise<{ readonly ok: true } | { readonly ok: false; readonly reason: string }> {
337
+ const current = await captureRepositoryMutationGuard(rc.cwd, rc.signal);
338
+ if (current.kind === "unverifiable") return { ok: false, reason: current.reason };
339
+ return current.fingerprint === expected
340
+ ? { ok: true }
341
+ : { ok: false, reason: "main workspace changed outside the isolated worktree" };
342
+ }
343
+
344
+ async function captureCurrentRepository(
345
+ rc: RunContext,
346
+ replay: Extract<AgentReplayPlan, { readonly kind: "shared" | "isolated" }>,
347
+ workspace: AgentWorkspace,
348
+ ): Promise<RepositoryResumeContext> {
349
+ if (replay.kind === "shared") return await captureRepositoryResumeContext(rc.cwd, replay.additionalInputs, rc.signal);
350
+ if (workspace.kind !== "isolated") {
351
+ return { kind: "unverifiable", reason: "isolated replay lost its isolated workspace baseline" };
352
+ }
353
+ return await captureIsolatedRepositoryContext(workspace.cwd, workspace.baselineOid, rc.signal);
354
+ }
355
+
356
+ async function recordJournalResult(
357
+ rc: RunContext,
358
+ label: string,
359
+ key: string,
360
+ result: unknown,
361
+ identity: AgentResumeContext,
362
+ ): Promise<void> {
363
+ try {
364
+ const recorded = await rc.journal.record(key, result, identity);
365
+ if (!recorded.ok) {
366
+ rc.progress.log(`${label}: workflow journal write failed (${recorded.error}); future resume may be incomplete`);
367
+ }
368
+ } catch (error) {
369
+ rc.progress.log(`${label}: workflow journal write failed (${unknownErrorMessage(error)}); future resume may be incomplete`);
370
+ }
371
+ }
372
+
373
+ function declaresNoWorkspaceCapability(prompt: string, opts: AgentExecutionOptions): boolean {
374
+ return (
375
+ opts.tools !== undefined &&
376
+ opts.tools.length === 0 &&
377
+ (opts.toolHints?.length ?? 0) === 0 &&
378
+ (opts.skills?.length ?? 0) === 0 &&
379
+ extractSkillSelectorsFromText(prompt).length === 0
380
+ );
381
+ }
382
+
383
+ function inspectResumeInputs(opts: AgentExecutionOptions): readonly string[] | string {
384
+ try {
385
+ const inputs = opts.resumeInputs;
386
+ if (inputs === undefined) return [];
387
+ if (!Array.isArray(inputs)) return "read-only resume inputs must be an array";
388
+ if (inputs.length > 128) return "read-only resume accepts at most 128 repository inputs";
389
+ const inspected: string[] = [];
390
+ for (let index = 0; index < inputs.length; index++) {
391
+ const value = inputs[index];
392
+ if (typeof value !== "string") return "read-only resume inputs must be strings";
393
+ inspected.push(value);
394
+ }
395
+ return inspected;
396
+ } catch {
397
+ return "read-only resume inputs could not be inspected";
398
+ }
399
+ }