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.
- package/.pi/extensions/pi-workflow-engine/index.ts +418 -213
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +11 -4
- package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +147 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-limits.ts +35 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +399 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +116 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +91 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +154 -530
- package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +256 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +368 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +87 -39
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +89 -0
- package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +75 -0
- package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +492 -0
- package/.pi/extensions/pi-workflow-engine/src/budget.ts +10 -3
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +55 -3
- package/.pi/extensions/pi-workflow-engine/src/command-completions.ts +36 -0
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +230 -61
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +38 -55
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +70 -17
- package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +59 -13
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +123 -63
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +357 -75
- package/.pi/extensions/pi-workflow-engine/src/filesystem-error.ts +4 -0
- package/.pi/extensions/pi-workflow-engine/src/finalizers.ts +66 -0
- package/.pi/extensions/pi-workflow-engine/src/identity-canonicalization.ts +138 -0
- package/.pi/extensions/pi-workflow-engine/src/identity-fingerprint.ts +15 -0
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +10 -5
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +156 -66
- package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +167 -0
- package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +310 -0
- package/.pi/extensions/pi-workflow-engine/src/options.ts +96 -6
- package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +32 -0
- package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +203 -39
- package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +74 -122
- package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +189 -0
- package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +48 -0
- package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +89 -0
- package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +289 -0
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +701 -0
- package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +49 -0
- package/.pi/extensions/pi-workflow-engine/src/review/github-pr-comments.ts +75 -16
- package/.pi/extensions/pi-workflow-engine/src/review/review-actions.ts +83 -17
- package/.pi/extensions/pi-workflow-engine/src/review/review-budget.ts +45 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +117 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +14 -66
- package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +38 -12
- package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +42 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +18 -22
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +128 -56
- package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +153 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +294 -0
- package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +130 -0
- package/.pi/extensions/pi-workflow-engine/src/session-identity.ts +6 -0
- package/.pi/extensions/pi-workflow-engine/src/structured-output.ts +32 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +44 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +150 -0
- package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +401 -0
- package/.pi/extensions/pi-workflow-engine/src/types.ts +87 -25
- package/.pi/extensions/pi-workflow-engine/src/ui/dynamax-editor-decoration.ts +220 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +26 -6
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +88 -46
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +7 -70
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-viewer-layout.ts +51 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +12 -67
- package/.pi/extensions/pi-workflow-engine/src/unknown-error.ts +8 -0
- package/.pi/extensions/pi-workflow-engine/src/usage.ts +124 -16
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +23 -3
- package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +121 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +19 -1
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +72 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +354 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +162 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +632 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +172 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +80 -0
- package/.pi/extensions/pi-workflow-engine/src/workflows.ts +30 -3
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +321 -43
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +62 -64
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +17 -17
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +15 -16
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +15 -15
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +170 -0
- package/README.md +82 -207
- package/USAGE.md +295 -21
- package/package.json +9 -9
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import {
|
|
3
|
+
createAgentSessionFromServices,
|
|
4
|
+
createAgentSessionServices,
|
|
5
|
+
defineTool,
|
|
6
|
+
SessionManager,
|
|
7
|
+
type CreateAgentSessionOptions,
|
|
8
|
+
type ModelRegistry,
|
|
9
|
+
type Skill,
|
|
10
|
+
type ToolDefinition,
|
|
11
|
+
} from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { prepareAgentSkillResources } from "./agent-skills.ts";
|
|
13
|
+
import type {
|
|
14
|
+
AgentExecutionOptions,
|
|
15
|
+
AgentRunTags,
|
|
16
|
+
AgentRunnerSession,
|
|
17
|
+
RunContext,
|
|
18
|
+
} from "./agent-runner-types.ts";
|
|
19
|
+
import { raceWithAbort, throwIfAborted } from "./cancellation.ts";
|
|
20
|
+
import {
|
|
21
|
+
MAX_SCHEMA_REPAIR_ATTEMPTS,
|
|
22
|
+
WorkflowStructuredOutputError,
|
|
23
|
+
} from "./structured-output.ts";
|
|
24
|
+
import { providerErrorFromMessages } from "./agent-retry.ts";
|
|
25
|
+
import { synchronizeWorkflowModelRuntime } from "./agent-session-providers.ts";
|
|
26
|
+
import { matchesAgentToolHint, WorkflowToolHintUnavailableError } from "./tool-capabilities.ts";
|
|
27
|
+
import type { AgentToolHint } from "./types.ts";
|
|
28
|
+
|
|
29
|
+
export const FINAL_TOOL = "final_answer";
|
|
30
|
+
|
|
31
|
+
const SCHEMA_REPROMPT =
|
|
32
|
+
`You ended your turn without calling the ${FINAL_TOOL} tool, so no result was recorded. ` +
|
|
33
|
+
`Call ${FINAL_TOOL} now with your final answer as its arguments. Do not reply with plain text.`;
|
|
34
|
+
const DEFAULT_SEARCH_BASE_TOOLS = ["read", "bash", "grep", "find", "ls"];
|
|
35
|
+
|
|
36
|
+
export interface ResolvedAgentModelRequest {
|
|
37
|
+
readonly ref: string;
|
|
38
|
+
readonly provider: string;
|
|
39
|
+
readonly id: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ResolvedAgentModel {
|
|
43
|
+
readonly model: Model<Api> | undefined;
|
|
44
|
+
readonly requested: ResolvedAgentModelRequest | undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface AgentSessionHandle {
|
|
48
|
+
readonly session: AgentRunnerSession;
|
|
49
|
+
readonly selectedSkills: readonly Skill[];
|
|
50
|
+
hasStructuredResult(): boolean;
|
|
51
|
+
structuredResult(): unknown;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function resolveAgentModel(
|
|
55
|
+
modelRef: string | undefined,
|
|
56
|
+
modelRegistry: Pick<ModelRegistry, "find">,
|
|
57
|
+
hostModel: Model<Api> | undefined,
|
|
58
|
+
): ResolvedAgentModel {
|
|
59
|
+
if (modelRef === undefined) return { model: hostModel, requested: undefined };
|
|
60
|
+
|
|
61
|
+
const parsed = parseAgentModelRef(modelRef);
|
|
62
|
+
const found = modelRegistry.find(parsed.provider, parsed.id);
|
|
63
|
+
if (!found) {
|
|
64
|
+
throw new Error(`Agent model "${modelRef}" not found (resolved as ${parsed.provider}/${parsed.id}).`);
|
|
65
|
+
}
|
|
66
|
+
return { model: found, requested: parsed };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function openAgentSession(input: {
|
|
70
|
+
readonly rc: RunContext;
|
|
71
|
+
readonly prompt: string;
|
|
72
|
+
readonly opts: AgentExecutionOptions;
|
|
73
|
+
readonly cwd: string;
|
|
74
|
+
readonly model: Model<Api> | undefined;
|
|
75
|
+
readonly label: string;
|
|
76
|
+
readonly tags: AgentRunTags;
|
|
77
|
+
}): Promise<AgentSessionHandle> {
|
|
78
|
+
const { rc, prompt, opts, cwd, model, label, tags } = input;
|
|
79
|
+
let captured = false;
|
|
80
|
+
let structuredResult: unknown;
|
|
81
|
+
const customTools: ToolDefinition[] = opts.schema
|
|
82
|
+
? [
|
|
83
|
+
defineTool({
|
|
84
|
+
name: FINAL_TOOL,
|
|
85
|
+
label: "Final Answer",
|
|
86
|
+
description:
|
|
87
|
+
"Return your final structured answer. This MUST be your last action — do not write a normal reply after calling it.",
|
|
88
|
+
parameters: opts.schema,
|
|
89
|
+
async execute(_toolCallId, params) {
|
|
90
|
+
captured = true;
|
|
91
|
+
structuredResult = params;
|
|
92
|
+
return { content: [{ type: "text", text: "Recorded." }], details: params, terminate: true };
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
95
|
+
]
|
|
96
|
+
: [];
|
|
97
|
+
let session: AgentRunnerSession | undefined;
|
|
98
|
+
try {
|
|
99
|
+
throwIfAborted(rc.signal);
|
|
100
|
+
const resourceInput = { rc, prompt, opts, cwd, model, customTools, label };
|
|
101
|
+
const resources = rc.createSession
|
|
102
|
+
? await prepareAgentSessionResources(resourceInput)
|
|
103
|
+
: await rc.perf.time(
|
|
104
|
+
"agent.session_resources_ms",
|
|
105
|
+
() => prepareAgentSessionResources(resourceInput),
|
|
106
|
+
tags,
|
|
107
|
+
);
|
|
108
|
+
const toolSelection = buildToolSelection(opts, resources.selectedSkills.length > 0);
|
|
109
|
+
session = (
|
|
110
|
+
await rc.perf.time(
|
|
111
|
+
"agent.create_session_ms",
|
|
112
|
+
() => resources.createSession(toolSelection.sessionOptions),
|
|
113
|
+
tags,
|
|
114
|
+
)
|
|
115
|
+
).session;
|
|
116
|
+
session.setAutoRetryEnabled(false);
|
|
117
|
+
const matchedToolHints = toolSelection.toolHints.length === 0
|
|
118
|
+
? new Set<AgentToolHint>()
|
|
119
|
+
: applyDynamicToolHints(session, toolSelection);
|
|
120
|
+
if (opts.requireToolHints) {
|
|
121
|
+
const missing = toolSelection.toolHints.filter((hint) => !matchedToolHints.has(hint));
|
|
122
|
+
if (missing.length > 0) throw new WorkflowToolHintUnavailableError(missing);
|
|
123
|
+
}
|
|
124
|
+
throwIfAborted(rc.signal);
|
|
125
|
+
return {
|
|
126
|
+
session,
|
|
127
|
+
selectedSkills: resources.selectedSkills,
|
|
128
|
+
hasStructuredResult: () => captured,
|
|
129
|
+
structuredResult: () => structuredResult,
|
|
130
|
+
};
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (session) {
|
|
133
|
+
const activeSession = session;
|
|
134
|
+
session = undefined;
|
|
135
|
+
rc.perf.timeSync("agent.dispose_ms", () => activeSession.dispose(), tags);
|
|
136
|
+
}
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function promptAgentSession(input: {
|
|
142
|
+
readonly rc: RunContext;
|
|
143
|
+
readonly handle: AgentSessionHandle;
|
|
144
|
+
readonly prompt: string;
|
|
145
|
+
readonly opts: AgentExecutionOptions;
|
|
146
|
+
readonly label: string;
|
|
147
|
+
readonly rowId: number;
|
|
148
|
+
readonly tags: AgentRunTags;
|
|
149
|
+
}): Promise<unknown> {
|
|
150
|
+
const { rc, handle, prompt, opts, label, rowId, tags } = input;
|
|
151
|
+
const { session } = handle;
|
|
152
|
+
const unsubscribe = session.subscribe((event) => {
|
|
153
|
+
if (event.type === "tool_execution_start" && event.toolName !== undefined && event.toolName !== FINAL_TOOL) {
|
|
154
|
+
rc.progress.agentTool(label, event.toolName, rowId);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const finalPrompt = opts.schema
|
|
160
|
+
? `${prompt}\n\nWhen finished, return your result by calling the ${FINAL_TOOL} tool.`
|
|
161
|
+
: prompt;
|
|
162
|
+
throwIfAborted(rc.signal);
|
|
163
|
+
const unlinkPromptAbort = linkSessionAbort(rc.signal, session);
|
|
164
|
+
try {
|
|
165
|
+
try {
|
|
166
|
+
const promptSession = async (text: string) => {
|
|
167
|
+
await rc.perf.time("agent.prompt_ms", () => raceWithAbort(() => session.prompt(text), rc.signal), tags);
|
|
168
|
+
const failure = providerErrorFromMessages(session.messages, {
|
|
169
|
+
pauseOnUsageLimit: rc.pauseOnProviderUsageLimit,
|
|
170
|
+
});
|
|
171
|
+
if (failure) throw failure;
|
|
172
|
+
};
|
|
173
|
+
await promptSession(finalPrompt);
|
|
174
|
+
if (opts.schema) {
|
|
175
|
+
for (let attempt = 0; !handle.hasStructuredResult() && attempt < MAX_SCHEMA_REPAIR_ATTEMPTS; attempt++) {
|
|
176
|
+
throwIfAborted(rc.signal);
|
|
177
|
+
session.setActiveToolsByName([FINAL_TOOL]);
|
|
178
|
+
rc.progress.log(`${label}: no final answer; re-prompting (${attempt + 1}/${MAX_SCHEMA_REPAIR_ATTEMPTS})`);
|
|
179
|
+
rc.perf.counter("agent.structured_reprompt", 1, tags);
|
|
180
|
+
await promptSession(SCHEMA_REPROMPT);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
throwIfAborted(rc.signal);
|
|
185
|
+
return rc.perf.timeSync(
|
|
186
|
+
"agent.extract_result_ms",
|
|
187
|
+
() => {
|
|
188
|
+
if (!opts.schema) {
|
|
189
|
+
return session.getLastAssistantText() ?? "";
|
|
190
|
+
}
|
|
191
|
+
if (!handle.hasStructuredResult()) {
|
|
192
|
+
rc.progress.log(`${label}: no structured answer returned`);
|
|
193
|
+
rc.perf.counter("agent.structured_missing", 1, tags);
|
|
194
|
+
throw new WorkflowStructuredOutputError(label, MAX_SCHEMA_REPAIR_ATTEMPTS);
|
|
195
|
+
}
|
|
196
|
+
return handle.structuredResult();
|
|
197
|
+
},
|
|
198
|
+
tags,
|
|
199
|
+
);
|
|
200
|
+
} finally {
|
|
201
|
+
rc.usage.recordAgentSession({ label, phase: tags.phase, messages: session.messages });
|
|
202
|
+
}
|
|
203
|
+
} finally {
|
|
204
|
+
unlinkPromptAbort();
|
|
205
|
+
}
|
|
206
|
+
} finally {
|
|
207
|
+
unsubscribe();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function parseAgentModelRef(modelRef: string): ResolvedAgentModelRequest {
|
|
212
|
+
const normalized = modelRef.trim();
|
|
213
|
+
if (normalized.length === 0) {
|
|
214
|
+
throw new Error('Invalid agent model ref: expected a bare model id or "provider/id".');
|
|
215
|
+
}
|
|
216
|
+
if (normalized !== modelRef) {
|
|
217
|
+
throw new Error(`Invalid agent model ref "${modelRef}": remove leading or trailing whitespace.`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const slash = modelRef.indexOf("/");
|
|
221
|
+
if (slash === -1) return { ref: modelRef, provider: "anthropic", id: modelRef };
|
|
222
|
+
const provider = modelRef.slice(0, slash);
|
|
223
|
+
const id = modelRef.slice(slash + 1);
|
|
224
|
+
if (provider.length === 0 || id.length === 0 || id.startsWith("/")) {
|
|
225
|
+
throw new Error(`Invalid agent model ref "${modelRef}": expected "provider/id".`);
|
|
226
|
+
}
|
|
227
|
+
return { ref: modelRef, provider, id };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
interface AgentSessionResources {
|
|
231
|
+
readonly selectedSkills: readonly Skill[];
|
|
232
|
+
createSession(sessionOptions: ToolSessionOptions): Promise<{ session: AgentRunnerSession }>;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function prepareAgentSessionResources(input: {
|
|
236
|
+
readonly rc: RunContext;
|
|
237
|
+
readonly prompt: string;
|
|
238
|
+
readonly opts: AgentExecutionOptions;
|
|
239
|
+
readonly cwd: string;
|
|
240
|
+
readonly model: Model<Api> | undefined;
|
|
241
|
+
readonly customTools: ToolDefinition[];
|
|
242
|
+
readonly label: string;
|
|
243
|
+
}): Promise<AgentSessionResources> {
|
|
244
|
+
const { rc, prompt, opts, cwd, model, customTools, label } = input;
|
|
245
|
+
const commonSessionOptions = (sessionOptions: ToolSessionOptions) => ({
|
|
246
|
+
model,
|
|
247
|
+
thinkingLevel: opts.thinkingLevel,
|
|
248
|
+
noTools: sessionOptions.noTools,
|
|
249
|
+
tools: sessionOptions.tools,
|
|
250
|
+
customTools,
|
|
251
|
+
sessionManager: SessionManager.inMemory(cwd),
|
|
252
|
+
});
|
|
253
|
+
const createSession = rc.createSession;
|
|
254
|
+
if (createSession) {
|
|
255
|
+
return {
|
|
256
|
+
selectedSkills: [],
|
|
257
|
+
createSession: (sessionOptions) =>
|
|
258
|
+
createSession({
|
|
259
|
+
cwd,
|
|
260
|
+
...commonSessionOptions(sessionOptions),
|
|
261
|
+
}),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const skillOptions = {
|
|
266
|
+
prompt,
|
|
267
|
+
skills: opts.skills,
|
|
268
|
+
log: (message: string) => rc.progress.log(`${label}: ${message}`),
|
|
269
|
+
};
|
|
270
|
+
const preparedSkills = prepareAgentSkillResources(skillOptions);
|
|
271
|
+
const services = await createAgentSessionServices({
|
|
272
|
+
cwd,
|
|
273
|
+
resourceLoaderOptions: preparedSkills.resourceLoaderOptions,
|
|
274
|
+
});
|
|
275
|
+
await synchronizeWorkflowModelRuntime({
|
|
276
|
+
host: rc.modelRegistry,
|
|
277
|
+
child: services.modelRuntime,
|
|
278
|
+
selectedModel: model,
|
|
279
|
+
// Shared-cwd sessions mirror live removals; isolated cwd sessions retain target-only providers.
|
|
280
|
+
removeChildOnlyProviders: cwd === rc.cwd,
|
|
281
|
+
});
|
|
282
|
+
for (const diagnostic of services.diagnostics) {
|
|
283
|
+
rc.progress.log(`${label}: session ${diagnostic.type}: ${diagnostic.message}`);
|
|
284
|
+
}
|
|
285
|
+
const selectedSkills = preparedSkills.resolve(services.resourceLoader);
|
|
286
|
+
return {
|
|
287
|
+
selectedSkills,
|
|
288
|
+
createSession: (sessionOptions) =>
|
|
289
|
+
createAgentSessionFromServices({
|
|
290
|
+
services,
|
|
291
|
+
...commonSessionOptions(sessionOptions),
|
|
292
|
+
}),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function linkSessionAbort(signal: AbortSignal | undefined, session: AgentRunnerSession): () => void {
|
|
297
|
+
if (!signal) return () => {};
|
|
298
|
+
const onAbort = () => {
|
|
299
|
+
void session.abort().catch(() => undefined);
|
|
300
|
+
};
|
|
301
|
+
if (signal.aborted) {
|
|
302
|
+
onAbort();
|
|
303
|
+
return () => {};
|
|
304
|
+
}
|
|
305
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
306
|
+
return () => signal.removeEventListener("abort", onAbort);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
type ToolSessionOptions = Pick<CreateAgentSessionOptions, "tools" | "noTools">;
|
|
310
|
+
|
|
311
|
+
interface ToolSelection {
|
|
312
|
+
readonly sessionOptions: ToolSessionOptions;
|
|
313
|
+
readonly activeTools?: readonly string[];
|
|
314
|
+
readonly toolHints: NonNullable<AgentExecutionOptions["toolHints"]>;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function buildToolSelection(opts: AgentExecutionOptions, skillsEnabled: boolean): ToolSelection {
|
|
318
|
+
const toolHints = opts.toolHints ?? [];
|
|
319
|
+
const fallback = toolHints.includes("search")
|
|
320
|
+
? DEFAULT_SEARCH_BASE_TOOLS
|
|
321
|
+
: toolHints.includes("external-search")
|
|
322
|
+
? ["read"]
|
|
323
|
+
: undefined;
|
|
324
|
+
const activeTools = buildToolList(opts, skillsEnabled, fallback);
|
|
325
|
+
if (toolHints.length === 0) {
|
|
326
|
+
return {
|
|
327
|
+
sessionOptions: { tools: activeTools ? [...activeTools] : undefined },
|
|
328
|
+
activeTools,
|
|
329
|
+
toolHints,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
sessionOptions: { noTools: "builtin" },
|
|
335
|
+
activeTools,
|
|
336
|
+
toolHints,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function buildToolList(
|
|
341
|
+
opts: AgentExecutionOptions,
|
|
342
|
+
skillsEnabled: boolean,
|
|
343
|
+
fallback?: readonly string[],
|
|
344
|
+
): string[] | undefined {
|
|
345
|
+
const configured = opts.tools ?? fallback;
|
|
346
|
+
const allow = configured ? [...configured] : undefined;
|
|
347
|
+
if (!allow) return undefined;
|
|
348
|
+
if (skillsEnabled && !allow.includes("read")) allow.push("read");
|
|
349
|
+
if (opts.schema && !allow.includes(FINAL_TOOL)) allow.push(FINAL_TOOL);
|
|
350
|
+
return allow;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function applyDynamicToolHints(
|
|
354
|
+
session: AgentRunnerSession,
|
|
355
|
+
selection: ToolSelection,
|
|
356
|
+
): ReadonlySet<AgentToolHint> {
|
|
357
|
+
const active = new Set(selection.activeTools ?? []);
|
|
358
|
+
const matched = new Set<AgentToolHint>();
|
|
359
|
+
for (const tool of session.getAllTools()) {
|
|
360
|
+
for (const hint of selection.toolHints) {
|
|
361
|
+
if (!matchesAgentToolHint(tool, hint)) continue;
|
|
362
|
+
matched.add(hint);
|
|
363
|
+
active.add(tool.name);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
session.setActiveToolsByName([...active]);
|
|
367
|
+
return matched;
|
|
368
|
+
}
|