@jameslovespancakes/pi-plus 1.0.10 → 1.0.12
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/README.md +44 -20
- package/config/skills/workflow-code-review-actions/SKILL.md +44 -0
- package/package.json +6 -3
- package/src/core/accounts/oauth-pool.ts +102 -0
- package/src/core/accounts/registry.ts +8 -25
- package/src/core/accounts/routing.ts +122 -0
- package/src/core/anthropic/client-identity.ts +9 -64
- package/src/core/anthropic/quota.ts +8 -53
- package/src/core/anthropic/routing.ts +40 -115
- package/src/core/anthropic/store.ts +7 -9
- package/src/core/catalog/quality.ts +33 -15
- package/src/core/codex/quota.ts +8 -2
- package/src/core/codex/store.ts +12 -9
- package/src/core/config.ts +4 -17
- package/src/core/store.ts +13 -12
- package/src/domains/models/provider-picker.ts +1 -1
- package/src/domains/setup/index.ts +23 -23
- package/src/domains/subscriptions/accounts-picker.ts +18 -37
- package/src/domains/subscriptions/accounts.ts +23 -29
- package/src/domains/subscriptions/footer.ts +48 -29
- package/src/domains/subscriptions/index.ts +11 -27
- package/src/domains/subscriptions/provider.ts +38 -103
- package/src/domains/subscriptions/providers/anthropic.ts +5 -17
- package/src/domains/subscriptions/providers/codex.ts +171 -4
- package/src/domains/subscriptions/providers/hosted.ts +18 -0
- package/src/domains/subscriptions/providers/oauth-pool.ts +273 -0
- package/src/domains/subscriptions/routing.ts +19 -11
- package/src/domains/workflows/LICENSE.md +21 -0
- package/src/domains/workflows/index.ts +836 -0
- package/src/domains/workflows/runtime/advisory-challenge.ts +75 -0
- package/src/domains/workflows/runtime/advisory-evidence.ts +90 -0
- package/src/domains/workflows/runtime/advisory-schema.ts +83 -0
- package/src/domains/workflows/runtime/agent-attempt.ts +147 -0
- package/src/domains/workflows/runtime/agent-limits.ts +43 -0
- package/src/domains/workflows/runtime/agent-replay.ts +399 -0
- package/src/domains/workflows/runtime/agent-retry.ts +116 -0
- package/src/domains/workflows/runtime/agent-runner-types.ts +96 -0
- package/src/domains/workflows/runtime/agent-runner.ts +203 -0
- package/src/domains/workflows/runtime/agent-session-identity.ts +256 -0
- package/src/domains/workflows/runtime/agent-session-providers.ts +50 -0
- package/src/domains/workflows/runtime/agent-session.ts +382 -0
- package/src/domains/workflows/runtime/agent-skills.ts +270 -0
- package/src/domains/workflows/runtime/agent-workspace.ts +103 -0
- package/src/domains/workflows/runtime/background-workflow-tool.ts +75 -0
- package/src/domains/workflows/runtime/background-workflows.ts +492 -0
- package/src/domains/workflows/runtime/budget.ts +53 -0
- package/src/domains/workflows/runtime/cancellation.ts +87 -0
- package/src/domains/workflows/runtime/command-completions.ts +36 -0
- package/src/domains/workflows/runtime/concurrency.ts +403 -0
- package/src/domains/workflows/runtime/debug.ts +3 -0
- package/src/domains/workflows/runtime/diff-capture.ts +81 -0
- package/src/domains/workflows/runtime/discovery.ts +137 -0
- package/src/domains/workflows/runtime/dynamax-shortcuts.ts +122 -0
- package/src/domains/workflows/runtime/dynamax.ts +330 -0
- package/src/domains/workflows/runtime/engine.ts +543 -0
- package/src/domains/workflows/runtime/filesystem-error.ts +4 -0
- package/src/domains/workflows/runtime/finalizers.ts +66 -0
- package/src/domains/workflows/runtime/identity-canonicalization.ts +138 -0
- package/src/domains/workflows/runtime/identity-fingerprint.ts +15 -0
- package/src/domains/workflows/runtime/inline-workflow.ts +403 -0
- package/src/domains/workflows/runtime/journal.ts +313 -0
- package/src/domains/workflows/runtime/model-profiles.ts +310 -0
- package/src/domains/workflows/runtime/options.ts +157 -0
- package/src/domains/workflows/runtime/perf.ts +146 -0
- package/src/domains/workflows/runtime/pi-compat.ts +32 -0
- package/src/domains/workflows/runtime/process-runner.ts +260 -0
- package/src/domains/workflows/runtime/progress-types.ts +59 -0
- package/src/domains/workflows/runtime/progress.ts +400 -0
- package/src/domains/workflows/runtime/provider-usage-limit.ts +191 -0
- package/src/domains/workflows/runtime/replay-path-identity.ts +48 -0
- package/src/domains/workflows/runtime/research-contract.ts +89 -0
- package/src/domains/workflows/runtime/research-evidence.ts +289 -0
- package/src/domains/workflows/runtime/resume-context.ts +751 -0
- package/src/domains/workflows/runtime/review/code-review-orchestration.ts +24 -0
- package/src/domains/workflows/runtime/review/github-pr-comments.ts +249 -0
- package/src/domains/workflows/runtime/review/patch-validation.ts +120 -0
- package/src/domains/workflows/runtime/review/review-actions.ts +180 -0
- package/src/domains/workflows/runtime/review/review-budget.ts +45 -0
- package/src/domains/workflows/runtime/review/review-fix-workflow.ts +153 -0
- package/src/domains/workflows/runtime/review/review-format.ts +133 -0
- package/src/domains/workflows/runtime/review/review-handoff.ts +38 -0
- package/src/domains/workflows/runtime/review/review-issues.ts +87 -0
- package/src/domains/workflows/runtime/review/review-report.ts +42 -0
- package/src/domains/workflows/runtime/review/review-results-flow.ts +52 -0
- package/src/domains/workflows/runtime/review/review-results-viewer.ts +312 -0
- package/src/domains/workflows/runtime/review/review-session-coordinator.ts +153 -0
- package/src/domains/workflows/runtime/review/review-snapshot.ts +294 -0
- package/src/domains/workflows/runtime/review-diff-target.ts +130 -0
- package/src/domains/workflows/runtime/session-identity.ts +6 -0
- package/src/domains/workflows/runtime/structured-output.ts +32 -0
- package/src/domains/workflows/runtime/tool-capabilities.ts +44 -0
- package/src/domains/workflows/runtime/tool-source-identity.ts +150 -0
- package/src/domains/workflows/runtime/tree-fingerprint.ts +404 -0
- package/src/domains/workflows/runtime/types.ts +255 -0
- package/src/domains/workflows/runtime/ui/display-text.ts +13 -0
- package/src/domains/workflows/runtime/ui/dynamax-editor-decoration.ts +220 -0
- package/src/domains/workflows/runtime/ui/workflow-format.ts +165 -0
- package/src/domains/workflows/runtime/ui/workflow-inspector.ts +398 -0
- package/src/domains/workflows/runtime/ui/workflow-result-renderer.ts +171 -0
- package/src/domains/workflows/runtime/ui/workflow-viewer-layout.ts +51 -0
- package/src/domains/workflows/runtime/ui/workflow-widget.ts +78 -0
- package/src/domains/workflows/runtime/unknown-error.ts +8 -0
- package/src/domains/workflows/runtime/usage.ts +341 -0
- package/src/domains/workflows/runtime/workflow-advisory-utils.ts +245 -0
- package/src/domains/workflows/runtime/workflow-execution.ts +121 -0
- package/src/domains/workflows/runtime/workflow-module.ts +75 -0
- package/src/domains/workflows/runtime/workflow-run-background.ts +72 -0
- package/src/domains/workflows/runtime/workflow-run-controller.ts +342 -0
- package/src/domains/workflows/runtime/workflow-run-history.ts +162 -0
- package/src/domains/workflows/runtime/workflow-run-record.ts +635 -0
- package/src/domains/workflows/runtime/workflow-run-store.ts +176 -0
- package/src/domains/workflows/runtime/workflow-usage-limit-scheduler.ts +80 -0
- package/src/domains/workflows/runtime/workflows.ts +40 -0
- package/src/domains/workflows/runtime/worktree.ts +581 -0
- package/src/domains/workflows/workflows/code-review.ts +232 -0
- package/src/domains/workflows/workflows/diagnose.ts +154 -0
- package/src/domains/workflows/workflows/perf-review.ts +149 -0
- package/src/domains/workflows/workflows/refactor-scout.ts +143 -0
- package/src/domains/workflows/workflows/research.ts +169 -0
- package/src/services/usage-service.ts +6 -30
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { assertWorkflowBudgetAvailable } from "./budget.ts";
|
|
2
|
+
import { WorkflowAgentTimeoutError } from "./agent-limits.ts";
|
|
3
|
+
import { abortReason, linkAbortSignal, throwIfAborted } from "./cancellation.ts";
|
|
4
|
+
import { executeAgentAttempt } from "./agent-attempt.ts";
|
|
5
|
+
import {
|
|
6
|
+
createAgentReplayPlan,
|
|
7
|
+
isReplayEnabled,
|
|
8
|
+
settleAgentAttempt,
|
|
9
|
+
type AgentAttemptResult,
|
|
10
|
+
type AgentReplayPlan,
|
|
11
|
+
} from "./agent-replay.ts";
|
|
12
|
+
import {
|
|
13
|
+
resolveAgentModel,
|
|
14
|
+
type ResolvedAgentModel,
|
|
15
|
+
} from "./agent-session.ts";
|
|
16
|
+
import type {
|
|
17
|
+
AgentExecutionOptions,
|
|
18
|
+
AgentRunTags,
|
|
19
|
+
RunContext,
|
|
20
|
+
} from "./agent-runner-types.ts";
|
|
21
|
+
import type { AgentResumeBaseContext } from "./resume-context.ts";
|
|
22
|
+
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
23
|
+
import {
|
|
24
|
+
agentRetryDelayMs,
|
|
25
|
+
WorkflowProviderError,
|
|
26
|
+
} from "./agent-retry.ts";
|
|
27
|
+
import { resolveAgentModelProfile } from "./model-profiles.ts";
|
|
28
|
+
|
|
29
|
+
export type {
|
|
30
|
+
AgentExecutionOptions,
|
|
31
|
+
AgentProgress,
|
|
32
|
+
AgentRunnerSession,
|
|
33
|
+
CreateAgentSession,
|
|
34
|
+
RunContext,
|
|
35
|
+
} from "./agent-runner-types.ts";
|
|
36
|
+
export { resolveAgentModel } from "./agent-session.ts";
|
|
37
|
+
export type { ResolvedAgentModel, ResolvedAgentModelRequest } from "./agent-session.ts";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Run one subagent to completion in an isolated in-memory session.
|
|
41
|
+
*
|
|
42
|
+
* Session construction, replay validation, and workspace cleanup live in focused
|
|
43
|
+
* modules; this boundary owns admission, progress, and bounded cache/provider retries.
|
|
44
|
+
*/
|
|
45
|
+
export async function runAgent(
|
|
46
|
+
rc: RunContext,
|
|
47
|
+
prompt: string,
|
|
48
|
+
opts: AgentExecutionOptions,
|
|
49
|
+
resumeBaseContext: AgentResumeBaseContext,
|
|
50
|
+
): Promise<unknown> {
|
|
51
|
+
if (typeof prompt !== "string") {
|
|
52
|
+
throw new Error(`agent() prompt must be a string; received ${describeAgentPrompt(prompt)}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const label = opts.label ?? "agent";
|
|
56
|
+
const phase = opts.phase ?? "Workflow";
|
|
57
|
+
const tags: AgentRunTags = { label, phase };
|
|
58
|
+
|
|
59
|
+
return await rc.perf.time("agent.total_ms", async () => {
|
|
60
|
+
throwIfAborted(rc.signal);
|
|
61
|
+
const routing = resolveAgentRouting(rc, opts, label);
|
|
62
|
+
const effectiveOpts = routing.thinkingLevel === opts.thinkingLevel
|
|
63
|
+
? opts
|
|
64
|
+
: { ...opts, thinkingLevel: routing.thinkingLevel };
|
|
65
|
+
const replay = createAgentReplayPlan(prompt, effectiveOpts);
|
|
66
|
+
if (!isReplayEnabled(replay)) assertWorkflowBudgetAvailable(rc.budget);
|
|
67
|
+
|
|
68
|
+
const modelLabel = describeAgentModel(routing.model);
|
|
69
|
+
const rowId = rc.progress.agentQueued(opts.phase, label, modelLabel);
|
|
70
|
+
rc.progress.agentMessage(rowId, "task", prompt);
|
|
71
|
+
const liveScope = createAgentLiveScope(rc, label);
|
|
72
|
+
try {
|
|
73
|
+
return await rc.semaphore.run(
|
|
74
|
+
async () => {
|
|
75
|
+
const agentRc: RunContext = { ...rc, signal: liveScope.signal };
|
|
76
|
+
throwIfAborted(agentRc.signal);
|
|
77
|
+
if (!isReplayEnabled(replay)) assertWorkflowBudgetAvailable(agentRc.budget);
|
|
78
|
+
rc.progress.agentStart(opts.phase, label, rowId, modelLabel);
|
|
79
|
+
if (replay.kind === "disabled") {
|
|
80
|
+
rc.progress.log(`${label}: resume disabled for this call (${replay.reason})`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let attemptPlan: AgentReplayPlan = replay;
|
|
84
|
+
let providerRetries = 0;
|
|
85
|
+
while (true) {
|
|
86
|
+
let outcome: AgentAttemptResult;
|
|
87
|
+
try {
|
|
88
|
+
outcome = await executeAgentAttempt({
|
|
89
|
+
rc: agentRc,
|
|
90
|
+
prompt,
|
|
91
|
+
opts: effectiveOpts,
|
|
92
|
+
resumeBaseContext,
|
|
93
|
+
model: routing.model,
|
|
94
|
+
replay: attemptPlan,
|
|
95
|
+
label,
|
|
96
|
+
rowId,
|
|
97
|
+
tags,
|
|
98
|
+
admitLiveAgent: liveScope.admit,
|
|
99
|
+
});
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (!(error instanceof WorkflowProviderError) || !error.retryable) throw error;
|
|
102
|
+
if (providerRetries >= agentRc.agentRetries) {
|
|
103
|
+
if (agentRc.agentRetries > 0) {
|
|
104
|
+
rc.progress.log(`${label}: transient provider failure after ${providerRetries} retries; giving up`);
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
assertWorkflowBudgetAvailable(agentRc.budget);
|
|
110
|
+
providerRetries++;
|
|
111
|
+
const delayMs = agentRetryDelayMs(providerRetries);
|
|
112
|
+
rc.progress.log(`${label}: transient provider failure; retry ${providerRetries}/${agentRc.agentRetries} in ${delayMs}ms`);
|
|
113
|
+
rc.perf.counter("agent.provider_retry", 1, tags);
|
|
114
|
+
rc.perf.observe("agent.provider_retry_delay_ms", delayMs, tags);
|
|
115
|
+
attemptPlan = { kind: "off" };
|
|
116
|
+
await agentRc.retryScheduler.sleep(delayMs, agentRc.signal);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const settlement = await settleAgentAttempt({ rc: agentRc, label, tags, replay: attemptPlan, outcome });
|
|
120
|
+
if (settlement.kind === "retry-live") {
|
|
121
|
+
attemptPlan = { kind: "off" };
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
rc.progress.agentDone(label, rowId);
|
|
125
|
+
return settlement.result;
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
onQueueWaitMs: (durationMs) => rc.perf.observe("agent.queue_wait_ms", durationMs, tags),
|
|
130
|
+
signal: rc.signal,
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
let failure = error;
|
|
135
|
+
if (liveScope.signal.aborted) failure = abortReason(liveScope.signal);
|
|
136
|
+
if (rc.signal?.aborted) failure = abortReason(rc.signal);
|
|
137
|
+
rc.progress.agentFailed(label, failure, rowId);
|
|
138
|
+
rc.progress.log(`${label} failed: ${unknownErrorMessage(failure)}`);
|
|
139
|
+
throw failure;
|
|
140
|
+
} finally {
|
|
141
|
+
liveScope.dispose();
|
|
142
|
+
}
|
|
143
|
+
}, tags);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function createAgentLiveScope(rc: RunContext, label: string) {
|
|
147
|
+
const controller = new AbortController();
|
|
148
|
+
const unlink = linkAbortSignal(rc.signal, controller);
|
|
149
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
150
|
+
let timerStarted = false;
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
signal: controller.signal,
|
|
154
|
+
admit() {
|
|
155
|
+
rc.agentLimiter.admit(controller.signal);
|
|
156
|
+
if (timerStarted || rc.agentTimeoutMs === null) return;
|
|
157
|
+
timerStarted = true;
|
|
158
|
+
const timeoutMs = rc.agentTimeoutMs;
|
|
159
|
+
timer = setTimeout(
|
|
160
|
+
() => controller.abort(new WorkflowAgentTimeoutError(label, timeoutMs)),
|
|
161
|
+
timeoutMs,
|
|
162
|
+
);
|
|
163
|
+
},
|
|
164
|
+
dispose() {
|
|
165
|
+
if (timer) clearTimeout(timer);
|
|
166
|
+
unlink();
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function resolveAgentRouting(
|
|
172
|
+
rc: RunContext,
|
|
173
|
+
opts: AgentExecutionOptions,
|
|
174
|
+
label: string,
|
|
175
|
+
): { readonly model: ResolvedAgentModel["model"]; readonly thinkingLevel: AgentExecutionOptions["thinkingLevel"] } {
|
|
176
|
+
try {
|
|
177
|
+
return resolveAgentModelProfile(
|
|
178
|
+
{
|
|
179
|
+
request: opts,
|
|
180
|
+
profiles: rc.modelProfiles,
|
|
181
|
+
resolveExplicitModel: (modelRef) => resolveAgentModel(modelRef, rc.modelRegistry, rc.hostModel).model,
|
|
182
|
+
hostModel: rc.hostModel,
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
rc.progress.agentFailed(label, error);
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** "provider/id" for progress rows so every agent shows the model it routed to. */
|
|
192
|
+
function describeAgentModel(model: unknown): string | undefined {
|
|
193
|
+
if (!model || typeof model !== "object") return undefined;
|
|
194
|
+
const candidate = model as { provider?: unknown; id?: unknown };
|
|
195
|
+
if (typeof candidate.id !== "string") return undefined;
|
|
196
|
+
return typeof candidate.provider === "string" ? `${candidate.provider}/${candidate.id}` : candidate.id;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function describeAgentPrompt(value: unknown): string {
|
|
200
|
+
if (value === null) return "null";
|
|
201
|
+
if (Array.isArray(value)) return "array";
|
|
202
|
+
return typeof value;
|
|
203
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { VERSION } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { throwIfAborted } from "./cancellation.ts";
|
|
4
|
+
import { hashIdentity, inspectableFunctionSource } from "./identity-fingerprint.ts";
|
|
5
|
+
import { normalizeWorkspaceReferences } from "./replay-path-identity.ts";
|
|
6
|
+
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
7
|
+
import {
|
|
8
|
+
captureEffectiveToolSourceIdentity,
|
|
9
|
+
type EffectiveToolSourceIdentity,
|
|
10
|
+
type EffectiveToolSourceInfoLike,
|
|
11
|
+
type ToolSourceFingerprintCache,
|
|
12
|
+
} from "./tool-source-identity.ts";
|
|
13
|
+
export type { EffectiveToolSourceIdentity, EffectiveToolSourceInfoLike } from "./tool-source-identity.ts";
|
|
14
|
+
|
|
15
|
+
export interface EffectiveAgentModelLike {
|
|
16
|
+
readonly provider: string;
|
|
17
|
+
readonly id: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface EffectiveToolInfoLike {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly description: string;
|
|
23
|
+
readonly parameters: unknown;
|
|
24
|
+
readonly promptGuidelines?: readonly string[];
|
|
25
|
+
readonly sourceInfo: EffectiveToolSourceInfoLike;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Only execution-relevant fields are required; installed pi ToolDefinition values satisfy this shape. */
|
|
29
|
+
export interface EffectiveToolDefinitionLike {
|
|
30
|
+
readonly name: string;
|
|
31
|
+
readonly description: string;
|
|
32
|
+
readonly parameters: unknown;
|
|
33
|
+
readonly promptSnippet?: string;
|
|
34
|
+
readonly promptGuidelines?: readonly string[];
|
|
35
|
+
readonly executionMode?: string;
|
|
36
|
+
readonly prepareArguments?: unknown;
|
|
37
|
+
readonly execute: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Structural subset of AgentSession so lightweight fakes can verify replay identity behavior. */
|
|
41
|
+
export interface EffectiveAgentSessionLike {
|
|
42
|
+
readonly systemPrompt: string;
|
|
43
|
+
readonly model: EffectiveAgentModelLike | undefined;
|
|
44
|
+
readonly thinkingLevel: string;
|
|
45
|
+
getActiveToolNames(): readonly string[];
|
|
46
|
+
getAllTools(): readonly EffectiveToolInfoLike[];
|
|
47
|
+
getToolDefinition(name: string): EffectiveToolDefinitionLike | undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface EffectiveToolIdentity {
|
|
51
|
+
readonly name: string;
|
|
52
|
+
readonly definitionFingerprint: string;
|
|
53
|
+
readonly implementationFingerprint: string;
|
|
54
|
+
readonly source: EffectiveToolSourceIdentity;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface EffectiveAgentSessionIdentity {
|
|
58
|
+
readonly fingerprint: string;
|
|
59
|
+
readonly runtimeVersion: string;
|
|
60
|
+
readonly systemPromptFingerprint: string;
|
|
61
|
+
readonly model: EffectiveAgentModelLike;
|
|
62
|
+
readonly thinkingLevel: string;
|
|
63
|
+
/** Ordered exactly as the effective AgentSession reports its active tools. */
|
|
64
|
+
readonly tools: readonly EffectiveToolIdentity[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type EffectiveAgentSessionIdentityCapture =
|
|
68
|
+
| { readonly kind: "verified"; readonly identity: EffectiveAgentSessionIdentity }
|
|
69
|
+
| { readonly kind: "unverifiable"; readonly reason: string };
|
|
70
|
+
|
|
71
|
+
export interface EffectiveAgentSessionIdentityOptions {
|
|
72
|
+
/** Stable main-workspace root used as the logical path namespace across replay attempts. */
|
|
73
|
+
readonly workspaceRoot: string;
|
|
74
|
+
/** Effective cwd used to create the session, which may be a randomized disposable worktree. */
|
|
75
|
+
readonly sessionCwd: string;
|
|
76
|
+
/** Override only for tests or a host embedding a version-compatible coding-agent runtime. */
|
|
77
|
+
readonly runtimeVersion?: string;
|
|
78
|
+
readonly signal?: AbortSignal;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface IdentityComponents {
|
|
82
|
+
readonly runtimeVersion: string;
|
|
83
|
+
readonly systemPromptFingerprint: string;
|
|
84
|
+
readonly model: EffectiveAgentModelLike;
|
|
85
|
+
readonly thinkingLevel: string;
|
|
86
|
+
readonly tools: readonly EffectiveToolIdentity[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Capture the post-creation AgentSession state that determines whether a cached
|
|
91
|
+
* agent result is safe to replay. Any missing or opaque executable identity
|
|
92
|
+
* fails closed instead of producing a partial key.
|
|
93
|
+
*/
|
|
94
|
+
export async function captureEffectiveAgentSessionIdentity(
|
|
95
|
+
session: EffectiveAgentSessionLike,
|
|
96
|
+
options: EffectiveAgentSessionIdentityOptions,
|
|
97
|
+
): Promise<EffectiveAgentSessionIdentityCapture> {
|
|
98
|
+
try {
|
|
99
|
+
throwIfAborted(options.signal);
|
|
100
|
+
const workspaceRoot = resolve(nonEmptyString(options.workspaceRoot, "workspace root"));
|
|
101
|
+
const sessionCwd = resolve(nonEmptyString(options.sessionCwd, "session cwd"));
|
|
102
|
+
const runtimeVersion = nonEmptyString(options.runtimeVersion ?? VERSION, "coding-agent runtime version");
|
|
103
|
+
const systemPrompt = stringValue(session.systemPrompt, "effective system prompt");
|
|
104
|
+
const thinkingLevel = nonEmptyString(session.thinkingLevel, "effective thinking level");
|
|
105
|
+
const model = captureModel(session.model);
|
|
106
|
+
const activeToolNames = captureActiveToolNames(session.getActiveToolNames());
|
|
107
|
+
const toolInfoByName = indexToolInfo(session.getAllTools());
|
|
108
|
+
const sourceCaptures: ToolSourceFingerprintCache = new Map();
|
|
109
|
+
const tools: EffectiveToolIdentity[] = [];
|
|
110
|
+
|
|
111
|
+
for (const name of activeToolNames) {
|
|
112
|
+
throwIfAborted(options.signal);
|
|
113
|
+
const info = toolInfoByName.get(name);
|
|
114
|
+
if (!info) throw new Error(`active tool "${name}" is absent from the tool registry`);
|
|
115
|
+
const definition = session.getToolDefinition(name);
|
|
116
|
+
if (!definition) throw new Error(`active tool "${name}" has no executable definition`);
|
|
117
|
+
tools.push(
|
|
118
|
+
await captureToolIdentity(name, info, definition, {
|
|
119
|
+
runtimeVersion,
|
|
120
|
+
signal: options.signal,
|
|
121
|
+
sessionCwd,
|
|
122
|
+
sourceCaptures,
|
|
123
|
+
workspaceRoot,
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const components: IdentityComponents = {
|
|
129
|
+
runtimeVersion,
|
|
130
|
+
systemPromptFingerprint: hashIdentity(
|
|
131
|
+
normalizeWorkspaceReferences(systemPrompt, { sessionCwd, workspaceRoot }),
|
|
132
|
+
),
|
|
133
|
+
model,
|
|
134
|
+
thinkingLevel,
|
|
135
|
+
tools,
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
kind: "verified",
|
|
139
|
+
identity: {
|
|
140
|
+
fingerprint: hashIdentity(components),
|
|
141
|
+
...components,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
} catch (error) {
|
|
145
|
+
throwIfAborted(options.signal);
|
|
146
|
+
return { kind: "unverifiable", reason: unknownErrorMessage(error) };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
interface ToolCaptureOptions {
|
|
151
|
+
readonly runtimeVersion: string;
|
|
152
|
+
readonly sessionCwd: string;
|
|
153
|
+
readonly workspaceRoot: string;
|
|
154
|
+
readonly sourceCaptures: ToolSourceFingerprintCache;
|
|
155
|
+
readonly signal?: AbortSignal;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function captureToolIdentity(
|
|
159
|
+
name: string,
|
|
160
|
+
info: EffectiveToolInfoLike,
|
|
161
|
+
definition: EffectiveToolDefinitionLike,
|
|
162
|
+
options: ToolCaptureOptions,
|
|
163
|
+
): Promise<EffectiveToolIdentity> {
|
|
164
|
+
if (nonEmptyString(info.name, `registry name for active tool "${name}"`) !== name) {
|
|
165
|
+
throw new Error(`tool registry returned a mismatched definition for "${name}"`);
|
|
166
|
+
}
|
|
167
|
+
if (nonEmptyString(definition.name, `definition name for active tool "${name}"`) !== name) {
|
|
168
|
+
throw new Error(`tool definition returned a mismatched definition for "${name}"`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const registryGuidelines = stringArray(info.promptGuidelines ?? [], `prompt guidelines for active tool "${name}"`);
|
|
172
|
+
const definitionGuidelines = stringArray(definition.promptGuidelines ?? [], `definition prompt guidelines for active tool "${name}"`);
|
|
173
|
+
const executeSource = inspectableFunctionSource(definition.execute, `execute handler for active tool "${name}"`);
|
|
174
|
+
const prepareArgumentsSource =
|
|
175
|
+
definition.prepareArguments === undefined
|
|
176
|
+
? null
|
|
177
|
+
: inspectableFunctionSource(definition.prepareArguments, `argument-preparation handler for active tool "${name}"`);
|
|
178
|
+
const source = await captureEffectiveToolSourceIdentity(info.sourceInfo, {
|
|
179
|
+
runtimeVersion: options.runtimeVersion,
|
|
180
|
+
sessionCwd: options.sessionCwd,
|
|
181
|
+
workspaceRoot: options.workspaceRoot,
|
|
182
|
+
cache: options.sourceCaptures,
|
|
183
|
+
signal: options.signal,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
name,
|
|
188
|
+
definitionFingerprint: hashIdentity({
|
|
189
|
+
registry: {
|
|
190
|
+
description: stringValue(info.description, `description for active tool "${name}"`),
|
|
191
|
+
parameters: info.parameters,
|
|
192
|
+
promptGuidelines: registryGuidelines,
|
|
193
|
+
},
|
|
194
|
+
definition: {
|
|
195
|
+
description: stringValue(definition.description, `definition description for active tool "${name}"`),
|
|
196
|
+
parameters: definition.parameters,
|
|
197
|
+
promptSnippet: optionalString(definition.promptSnippet, `prompt snippet for active tool "${name}"`),
|
|
198
|
+
promptGuidelines: definitionGuidelines,
|
|
199
|
+
executionMode: optionalString(definition.executionMode, `execution mode for active tool "${name}"`),
|
|
200
|
+
},
|
|
201
|
+
}),
|
|
202
|
+
implementationFingerprint: hashIdentity({ executeSource, prepareArgumentsSource }),
|
|
203
|
+
source,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function captureModel(model: EffectiveAgentModelLike | undefined): EffectiveAgentModelLike {
|
|
208
|
+
if (!model) throw new Error("effective model is unavailable");
|
|
209
|
+
return {
|
|
210
|
+
provider: nonEmptyString(model.provider, "effective model provider"),
|
|
211
|
+
id: nonEmptyString(model.id, "effective model id"),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function captureActiveToolNames(names: readonly string[]): readonly string[] {
|
|
216
|
+
if (!Array.isArray(names)) throw new Error("active tool names are unavailable");
|
|
217
|
+
const seen = new Set<string>();
|
|
218
|
+
return names.map((value, index) => {
|
|
219
|
+
const name = nonEmptyString(value, `active tool name at index ${index}`);
|
|
220
|
+
if (seen.has(name)) throw new Error(`active tool list contains duplicate "${name}" entries`);
|
|
221
|
+
seen.add(name);
|
|
222
|
+
return name;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function indexToolInfo(tools: readonly EffectiveToolInfoLike[]): ReadonlyMap<string, EffectiveToolInfoLike> {
|
|
227
|
+
if (!Array.isArray(tools)) throw new Error("tool registry is unavailable");
|
|
228
|
+
const indexed = new Map<string, EffectiveToolInfoLike>();
|
|
229
|
+
for (const [index, tool] of tools.entries()) {
|
|
230
|
+
const name = nonEmptyString(tool.name, `tool registry name at index ${index}`);
|
|
231
|
+
if (indexed.has(name)) throw new Error(`tool registry contains duplicate "${name}" entries`);
|
|
232
|
+
indexed.set(name, tool);
|
|
233
|
+
}
|
|
234
|
+
return indexed;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function stringArray(value: readonly string[], label: string): readonly string[] {
|
|
238
|
+
if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) throw new Error(`${label} must be a string array`);
|
|
239
|
+
return [...value];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function optionalString(value: string | undefined, label: string): string | null {
|
|
243
|
+
if (value === undefined) return null;
|
|
244
|
+
return stringValue(value, label);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function nonEmptyString(value: string, label: string): string {
|
|
248
|
+
const result = stringValue(value, label);
|
|
249
|
+
if (result.length === 0) throw new Error(`${label} is empty`);
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function stringValue(value: string, label: string): string {
|
|
254
|
+
if (typeof value !== "string") throw new Error(`${label} is not a string`);
|
|
255
|
+
return value;
|
|
256
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ModelRegistry, ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
type HostProviderRegistry = Pick<
|
|
5
|
+
ModelRegistry,
|
|
6
|
+
| "getApiKeyForProvider"
|
|
7
|
+
| "getProviderAuthStatus"
|
|
8
|
+
| "getRegisteredProviderConfig"
|
|
9
|
+
| "getRegisteredProviderIds"
|
|
10
|
+
| "isUsingOAuth"
|
|
11
|
+
>;
|
|
12
|
+
|
|
13
|
+
/** Mirror the host's live provider and selected-model auth state into a child runtime. */
|
|
14
|
+
export async function synchronizeWorkflowModelRuntime(input: {
|
|
15
|
+
readonly host: HostProviderRegistry;
|
|
16
|
+
readonly child: ModelRuntime;
|
|
17
|
+
readonly selectedModel: Model<Api> | undefined;
|
|
18
|
+
readonly removeChildOnlyProviders: boolean;
|
|
19
|
+
}): Promise<void> {
|
|
20
|
+
const { host, child, selectedModel, removeChildOnlyProviders } = input;
|
|
21
|
+
const hostProviderIds = new Set(host.getRegisteredProviderIds());
|
|
22
|
+
|
|
23
|
+
if (removeChildOnlyProviders) {
|
|
24
|
+
for (const providerId of child.getRegisteredProviderIds()) {
|
|
25
|
+
if (!hostProviderIds.has(providerId)) child.unregisterProvider(providerId);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const providerId of hostProviderIds) {
|
|
30
|
+
const config = host.getRegisteredProviderConfig(providerId);
|
|
31
|
+
if (!config) continue;
|
|
32
|
+
child.unregisterProvider(providerId);
|
|
33
|
+
child.registerProvider(providerId, config);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const selectedProvider = selectedModel?.provider;
|
|
37
|
+
if (!selectedModel || !selectedProvider) return;
|
|
38
|
+
|
|
39
|
+
const hostAuth = host.getProviderAuthStatus(selectedProvider);
|
|
40
|
+
const childAuth = child.getProviderAuthStatus(selectedProvider);
|
|
41
|
+
if (hostAuth.configured && !childAuth.configured && host.isUsingOAuth(selectedModel)) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Workflow subagents cannot inherit OAuth credentials for "${selectedProvider}" from a host-only credential store; configure OAuth in Pi's shared agent directory.`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if (hostAuth.source === "runtime" || (hostAuth.configured && !childAuth.configured)) {
|
|
47
|
+
const apiKey = await host.getApiKeyForProvider(selectedProvider);
|
|
48
|
+
if (apiKey !== undefined) await child.setRuntimeApiKey(selectedProvider, apiKey);
|
|
49
|
+
}
|
|
50
|
+
}
|