pi-workflow-engine 0.10.2 → 0.11.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 +199 -120
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +11 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +145 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +376 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +87 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +84 -529
- package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +256 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +413 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +43 -1
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +81 -0
- package/.pi/extensions/pi-workflow-engine/src/budget.ts +7 -0
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +17 -3
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +164 -61
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +117 -20
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +64 -15
- package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +59 -13
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +14 -3
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +208 -74
- 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 +8 -3
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +148 -64
- package/.pi/extensions/pi-workflow-engine/src/options.ts +9 -6
- package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +203 -39
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +2 -5
- package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +48 -0
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +665 -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 +17 -22
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +143 -52
- 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 +295 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +153 -0
- package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +404 -0
- package/.pi/extensions/pi-workflow-engine/src/types.ts +38 -10
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +5 -69
- package/.pi/extensions/pi-workflow-engine/src/unknown-error.ts +8 -0
- 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/workflows.ts +28 -3
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +282 -41
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +48 -26
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +8 -1
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +8 -2
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +8 -1
- package/README.md +170 -226
- package/USAGE.md +92 -12
- 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,413 @@
|
|
|
1
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import {
|
|
3
|
+
createAgentSession,
|
|
4
|
+
defineTool,
|
|
5
|
+
SessionManager,
|
|
6
|
+
type CreateAgentSessionOptions,
|
|
7
|
+
type ModelRegistry,
|
|
8
|
+
type Skill,
|
|
9
|
+
type ToolDefinition,
|
|
10
|
+
} from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import {
|
|
12
|
+
appendSkillReminder,
|
|
13
|
+
createAgentSkillResourceLoader,
|
|
14
|
+
extractSkillSelectorsFromText,
|
|
15
|
+
} from "./agent-skills.ts";
|
|
16
|
+
import type {
|
|
17
|
+
AgentExecutionOptions,
|
|
18
|
+
AgentRunTags,
|
|
19
|
+
AgentRunnerSession,
|
|
20
|
+
AgentRunnerToolInfo,
|
|
21
|
+
RunContext,
|
|
22
|
+
} from "./agent-runner-types.ts";
|
|
23
|
+
import {
|
|
24
|
+
captureEffectiveAgentSessionIdentity,
|
|
25
|
+
type EffectiveAgentSessionIdentity,
|
|
26
|
+
type EffectiveToolInfoLike,
|
|
27
|
+
} from "./agent-session-identity.ts";
|
|
28
|
+
import { throwIfAborted } from "./cancellation.ts";
|
|
29
|
+
|
|
30
|
+
export const FINAL_TOOL = "final_answer";
|
|
31
|
+
|
|
32
|
+
const SCHEMA_REPROMPT_ATTEMPTS = 1;
|
|
33
|
+
const SCHEMA_REPROMPT =
|
|
34
|
+
`You ended your turn without calling the ${FINAL_TOOL} tool, so no result was recorded. ` +
|
|
35
|
+
`Call ${FINAL_TOOL} now with your final answer as its arguments. Do not reply with plain text.`;
|
|
36
|
+
const BUILTIN_TOOL_NAMES = ["read", "bash", "edit", "write", "grep", "find", "ls"] as const;
|
|
37
|
+
const DEFAULT_SEARCH_BASE_TOOLS = ["read", "bash", "grep", "find", "ls"];
|
|
38
|
+
|
|
39
|
+
export interface ResolvedAgentModelRequest {
|
|
40
|
+
readonly ref: string;
|
|
41
|
+
readonly provider: string;
|
|
42
|
+
readonly id: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ResolvedAgentModel {
|
|
46
|
+
readonly model: Model<Api> | undefined;
|
|
47
|
+
readonly requested: ResolvedAgentModelRequest | undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface AgentSessionHandle {
|
|
51
|
+
readonly session: AgentRunnerSession;
|
|
52
|
+
readonly selectedSkills: readonly Skill[];
|
|
53
|
+
structuredResult(): unknown;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type EffectiveSessionCapture =
|
|
57
|
+
| { readonly kind: "verified"; readonly identity: EffectiveAgentSessionIdentity }
|
|
58
|
+
| { readonly kind: "unverifiable"; readonly reason: string };
|
|
59
|
+
|
|
60
|
+
export function resolveAgentModel(
|
|
61
|
+
modelRef: string | undefined,
|
|
62
|
+
modelRegistry: Pick<ModelRegistry, "find">,
|
|
63
|
+
hostModel: Model<Api> | undefined,
|
|
64
|
+
): ResolvedAgentModel {
|
|
65
|
+
if (modelRef === undefined) return { model: hostModel, requested: undefined };
|
|
66
|
+
|
|
67
|
+
const parsed = parseAgentModelRef(modelRef);
|
|
68
|
+
const found = modelRegistry.find(parsed.provider, parsed.id);
|
|
69
|
+
if (!found) {
|
|
70
|
+
throw new Error(`Agent model "${modelRef}" not found (resolved as ${parsed.provider}/${parsed.id}).`);
|
|
71
|
+
}
|
|
72
|
+
return { model: found, requested: parsed };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function openAgentSession(input: {
|
|
76
|
+
readonly rc: RunContext;
|
|
77
|
+
readonly prompt: string;
|
|
78
|
+
readonly opts: AgentExecutionOptions;
|
|
79
|
+
readonly cwd: string;
|
|
80
|
+
readonly model: Model<Api> | undefined;
|
|
81
|
+
readonly label: string;
|
|
82
|
+
readonly tags: AgentRunTags;
|
|
83
|
+
}): Promise<AgentSessionHandle> {
|
|
84
|
+
const { rc, prompt, opts, cwd, model, label, tags } = input;
|
|
85
|
+
const skillSetup = shouldCreateSkillResourceLoader(rc, prompt, opts)
|
|
86
|
+
? await rc.perf.time(
|
|
87
|
+
"agent.skills_ms",
|
|
88
|
+
() =>
|
|
89
|
+
createAgentSkillResourceLoader({
|
|
90
|
+
cwd,
|
|
91
|
+
prompt,
|
|
92
|
+
skills: opts.skills,
|
|
93
|
+
log: (message) => rc.progress.log(`${label}: ${message}`),
|
|
94
|
+
}),
|
|
95
|
+
tags,
|
|
96
|
+
)
|
|
97
|
+
: undefined;
|
|
98
|
+
const selectedSkills = skillSetup?.selectedSkills ?? [];
|
|
99
|
+
const toolSelection = buildToolSelection(opts, selectedSkills.length > 0);
|
|
100
|
+
let captured: unknown = null;
|
|
101
|
+
const customTools: ToolDefinition[] = opts.schema
|
|
102
|
+
? [
|
|
103
|
+
defineTool({
|
|
104
|
+
name: FINAL_TOOL,
|
|
105
|
+
label: "Final Answer",
|
|
106
|
+
description:
|
|
107
|
+
"Return your final structured answer. This MUST be your last action — do not write a normal reply after calling it.",
|
|
108
|
+
parameters: opts.schema,
|
|
109
|
+
async execute(_toolCallId, params) {
|
|
110
|
+
captured = params;
|
|
111
|
+
return { content: [{ type: "text", text: "Recorded." }], details: params, terminate: true };
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
]
|
|
115
|
+
: [];
|
|
116
|
+
const createSessionForRun = rc.createSession ?? defaultCreateSession;
|
|
117
|
+
const createSubagentSession = (sessionOptions: ToolSessionOptions) =>
|
|
118
|
+
rc.perf.time(
|
|
119
|
+
"agent.create_session_ms",
|
|
120
|
+
() =>
|
|
121
|
+
createSessionForRun({
|
|
122
|
+
cwd,
|
|
123
|
+
model,
|
|
124
|
+
modelRegistry: rc.modelRegistry as ModelRegistry,
|
|
125
|
+
thinkingLevel: opts.thinkingLevel,
|
|
126
|
+
tools: sessionOptions.tools,
|
|
127
|
+
excludeTools: sessionOptions.excludeTools,
|
|
128
|
+
customTools,
|
|
129
|
+
resourceLoader: skillSetup?.resourceLoader,
|
|
130
|
+
sessionManager: SessionManager.inMemory(cwd),
|
|
131
|
+
}),
|
|
132
|
+
tags,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
let session: AgentRunnerSession | undefined;
|
|
136
|
+
try {
|
|
137
|
+
throwIfAborted(rc.signal);
|
|
138
|
+
session = (await createSubagentSession(toolSelection.sessionOptions)).session;
|
|
139
|
+
const dynamicToolHintsApplied = toolSelection.toolHints.length === 0 || applyDynamicToolHints(session, toolSelection);
|
|
140
|
+
if (!dynamicToolHintsApplied) {
|
|
141
|
+
rc.progress.log(`${label}: dynamic tool hints unavailable; falling back to concrete tools only`);
|
|
142
|
+
rc.perf.counter("agent.tool_hint_fallback", 1, tags);
|
|
143
|
+
const dynamicSession = session;
|
|
144
|
+
session = undefined;
|
|
145
|
+
rc.perf.timeSync("agent.dispose_ms", () => dynamicSession.dispose(), tags);
|
|
146
|
+
throwIfAborted(rc.signal);
|
|
147
|
+
session = (await createSubagentSession(toolSelection.fallbackSessionOptions)).session;
|
|
148
|
+
}
|
|
149
|
+
throwIfAborted(rc.signal);
|
|
150
|
+
return { session, selectedSkills, structuredResult: () => captured };
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (session) {
|
|
153
|
+
const activeSession = session;
|
|
154
|
+
session = undefined;
|
|
155
|
+
rc.perf.timeSync("agent.dispose_ms", () => activeSession.dispose(), tags);
|
|
156
|
+
}
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function promptAgentSession(input: {
|
|
162
|
+
readonly rc: RunContext;
|
|
163
|
+
readonly handle: AgentSessionHandle;
|
|
164
|
+
readonly prompt: string;
|
|
165
|
+
readonly opts: AgentExecutionOptions;
|
|
166
|
+
readonly label: string;
|
|
167
|
+
readonly rowId: number;
|
|
168
|
+
readonly tags: AgentRunTags;
|
|
169
|
+
}): Promise<unknown> {
|
|
170
|
+
const { rc, handle, prompt, opts, label, rowId, tags } = input;
|
|
171
|
+
const { session, selectedSkills } = handle;
|
|
172
|
+
const unsubscribe = session.subscribe((event) => {
|
|
173
|
+
if (event.type === "tool_execution_start" && event.toolName !== undefined && event.toolName !== FINAL_TOOL) {
|
|
174
|
+
rc.progress.agentTool(label, event.toolName, rowId);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const promptedWithSkills = appendSkillReminder(prompt, selectedSkills);
|
|
180
|
+
const finalPrompt = opts.schema
|
|
181
|
+
? `${promptedWithSkills}\n\nWhen finished, return your result by calling the ${FINAL_TOOL} tool.`
|
|
182
|
+
: promptedWithSkills;
|
|
183
|
+
throwIfAborted(rc.signal);
|
|
184
|
+
const unlinkPromptAbort = linkSessionAbort(rc.signal, session);
|
|
185
|
+
try {
|
|
186
|
+
try {
|
|
187
|
+
await rc.perf.time("agent.prompt_ms", () => session.prompt(finalPrompt), tags);
|
|
188
|
+
if (opts.schema) {
|
|
189
|
+
for (let attempt = 0; handle.structuredResult() === null && attempt < SCHEMA_REPROMPT_ATTEMPTS; attempt++) {
|
|
190
|
+
throwIfAborted(rc.signal);
|
|
191
|
+
rc.progress.log(`${label}: no final answer; re-prompting (${attempt + 1}/${SCHEMA_REPROMPT_ATTEMPTS})`);
|
|
192
|
+
rc.perf.counter("agent.structured_reprompt", 1, tags);
|
|
193
|
+
await rc.perf.time("agent.prompt_ms", () => session.prompt(SCHEMA_REPROMPT), tags);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
throwIfAborted(rc.signal);
|
|
198
|
+
return rc.perf.timeSync(
|
|
199
|
+
"agent.extract_result_ms",
|
|
200
|
+
() => {
|
|
201
|
+
if (!opts.schema) return lastAssistantText(session.state);
|
|
202
|
+
const result = handle.structuredResult();
|
|
203
|
+
if (result === null) {
|
|
204
|
+
rc.progress.log(`${label}: no structured answer returned`);
|
|
205
|
+
rc.perf.counter("agent.structured_missing", 1, tags);
|
|
206
|
+
}
|
|
207
|
+
return result;
|
|
208
|
+
},
|
|
209
|
+
tags,
|
|
210
|
+
);
|
|
211
|
+
} finally {
|
|
212
|
+
rc.usage.recordAgentSession({ label, phase: tags.phase, messages: session.state.messages });
|
|
213
|
+
}
|
|
214
|
+
} finally {
|
|
215
|
+
unlinkPromptAbort();
|
|
216
|
+
}
|
|
217
|
+
} finally {
|
|
218
|
+
unsubscribe();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function captureEffectiveSession(
|
|
223
|
+
session: AgentRunnerSession,
|
|
224
|
+
sessionCwd: string,
|
|
225
|
+
workspaceRoot: string,
|
|
226
|
+
signal: AbortSignal | undefined,
|
|
227
|
+
): Promise<EffectiveSessionCapture> {
|
|
228
|
+
const state = session.state;
|
|
229
|
+
if (typeof state.systemPrompt !== "string") {
|
|
230
|
+
return { kind: "unverifiable", reason: "effective system prompt is unavailable" };
|
|
231
|
+
}
|
|
232
|
+
if (!state.model) return { kind: "unverifiable", reason: "effective model is unavailable" };
|
|
233
|
+
if (typeof state.thinkingLevel !== "string") {
|
|
234
|
+
return { kind: "unverifiable", reason: "effective thinking level is unavailable" };
|
|
235
|
+
}
|
|
236
|
+
if (!session.getActiveToolNames || !session.getAllTools || !session.getToolDefinition) {
|
|
237
|
+
return { kind: "unverifiable", reason: "effective tool APIs are unavailable" };
|
|
238
|
+
}
|
|
239
|
+
const toolInfos = effectiveToolInfos(session.getAllTools());
|
|
240
|
+
if (toolInfos.kind === "unverifiable") return toolInfos;
|
|
241
|
+
return await captureEffectiveAgentSessionIdentity(
|
|
242
|
+
{
|
|
243
|
+
systemPrompt: state.systemPrompt,
|
|
244
|
+
model: state.model,
|
|
245
|
+
thinkingLevel: state.thinkingLevel,
|
|
246
|
+
getActiveToolNames: () => session.getActiveToolNames!(),
|
|
247
|
+
getAllTools: () => toolInfos.tools,
|
|
248
|
+
getToolDefinition: (name) => session.getToolDefinition!(name),
|
|
249
|
+
},
|
|
250
|
+
{ sessionCwd, workspaceRoot, signal },
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function parseAgentModelRef(modelRef: string): ResolvedAgentModelRequest {
|
|
255
|
+
const normalized = modelRef.trim();
|
|
256
|
+
if (normalized.length === 0) {
|
|
257
|
+
throw new Error('Invalid agent model ref: expected a bare model id or "provider/id".');
|
|
258
|
+
}
|
|
259
|
+
if (normalized !== modelRef) {
|
|
260
|
+
throw new Error(`Invalid agent model ref "${modelRef}": remove leading or trailing whitespace.`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const slash = modelRef.indexOf("/");
|
|
264
|
+
if (slash === -1) return { ref: modelRef, provider: "anthropic", id: modelRef };
|
|
265
|
+
const provider = modelRef.slice(0, slash);
|
|
266
|
+
const id = modelRef.slice(slash + 1);
|
|
267
|
+
if (provider.length === 0 || id.length === 0 || id.startsWith("/")) {
|
|
268
|
+
throw new Error(`Invalid agent model ref "${modelRef}": expected "provider/id".`);
|
|
269
|
+
}
|
|
270
|
+
return { ref: modelRef, provider, id };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function defaultCreateSession(options: CreateAgentSessionOptions): Promise<{ session: AgentRunnerSession }> {
|
|
274
|
+
const { session } = await createAgentSession(options);
|
|
275
|
+
return { session };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function linkSessionAbort(signal: AbortSignal | undefined, session: AgentRunnerSession): () => void {
|
|
279
|
+
if (!signal) return () => {};
|
|
280
|
+
if (signal.aborted) {
|
|
281
|
+
void session.abort();
|
|
282
|
+
return () => {};
|
|
283
|
+
}
|
|
284
|
+
const onAbort = () => {
|
|
285
|
+
void session.abort();
|
|
286
|
+
};
|
|
287
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
288
|
+
return () => signal.removeEventListener("abort", onAbort);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
type ToolSessionOptions = Pick<CreateAgentSessionOptions, "tools" | "excludeTools">;
|
|
292
|
+
|
|
293
|
+
interface ToolSelection {
|
|
294
|
+
readonly sessionOptions: ToolSessionOptions;
|
|
295
|
+
readonly fallbackSessionOptions: ToolSessionOptions;
|
|
296
|
+
readonly activeTools?: readonly string[];
|
|
297
|
+
readonly toolHints: NonNullable<AgentExecutionOptions["toolHints"]>;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
interface DynamicToolSession extends AgentRunnerSession {
|
|
301
|
+
getAllTools(): readonly AgentRunnerToolInfo[];
|
|
302
|
+
setActiveToolsByName(toolNames: readonly string[]): void;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function buildToolSelection(opts: AgentExecutionOptions, skillsEnabled: boolean): ToolSelection {
|
|
306
|
+
const toolHints = opts.toolHints ?? [];
|
|
307
|
+
const fallback = toolHints.includes("search") ? DEFAULT_SEARCH_BASE_TOOLS : undefined;
|
|
308
|
+
const activeTools = buildToolList(opts, skillsEnabled, fallback);
|
|
309
|
+
const strictSessionOptions = { tools: activeTools ? [...activeTools] : undefined };
|
|
310
|
+
if (toolHints.length === 0) {
|
|
311
|
+
return { sessionOptions: strictSessionOptions, fallbackSessionOptions: strictSessionOptions, activeTools, toolHints };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const explicitlyActive = new Set(activeTools ?? []);
|
|
315
|
+
return {
|
|
316
|
+
sessionOptions: { excludeTools: BUILTIN_TOOL_NAMES.filter((name) => !explicitlyActive.has(name)) },
|
|
317
|
+
fallbackSessionOptions: { tools: activeTools ? [...activeTools] : [] },
|
|
318
|
+
activeTools,
|
|
319
|
+
toolHints,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function buildToolList(
|
|
324
|
+
opts: AgentExecutionOptions,
|
|
325
|
+
skillsEnabled: boolean,
|
|
326
|
+
fallback?: readonly string[],
|
|
327
|
+
): string[] | undefined {
|
|
328
|
+
const configured = opts.tools ?? fallback;
|
|
329
|
+
const allow = configured ? [...configured] : undefined;
|
|
330
|
+
if (!allow) return undefined;
|
|
331
|
+
if (skillsEnabled && !allow.includes("read")) allow.push("read");
|
|
332
|
+
if (opts.schema && !allow.includes(FINAL_TOOL)) allow.push(FINAL_TOOL);
|
|
333
|
+
return allow;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function applyDynamicToolHints(session: AgentRunnerSession, selection: ToolSelection): boolean {
|
|
337
|
+
if (!hasDynamicToolApis(session)) return false;
|
|
338
|
+
|
|
339
|
+
const active = new Set(selection.activeTools ?? []);
|
|
340
|
+
for (const tool of session.getAllTools()) {
|
|
341
|
+
if (selection.toolHints.some((hint) => hint === "search" && isSearchLikeTool(tool))) active.add(tool.name);
|
|
342
|
+
}
|
|
343
|
+
session.setActiveToolsByName([...active]);
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function hasDynamicToolApis(session: AgentRunnerSession): session is DynamicToolSession {
|
|
348
|
+
return typeof session.getAllTools === "function" && typeof session.setActiveToolsByName === "function";
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function isSearchLikeTool(tool: AgentRunnerToolInfo): boolean {
|
|
352
|
+
if (/(?:^|[-_])(?:edit|write|replace|patch|apply|delete|remove|move|rename|create|commit|push)(?:$|[-_])/.test(tool.name.toLowerCase())) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
const name = tool.name.toLowerCase();
|
|
356
|
+
if (name.includes("grep") || name.includes("find") || name.includes("search") || name === "rg" || name.includes("ripgrep")) return true;
|
|
357
|
+
return /\b(?:grep|find|search|ripgrep|rg|structural search|code search)\b/.test(tool.description?.toLowerCase() ?? "");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function shouldCreateSkillResourceLoader(rc: RunContext, prompt: string, opts: AgentExecutionOptions): boolean {
|
|
361
|
+
return !rc.createSession || opts.skills !== undefined || extractSkillSelectorsFromText(prompt).length > 0;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function effectiveToolInfos(
|
|
365
|
+
tools: readonly AgentRunnerToolInfo[],
|
|
366
|
+
): { readonly kind: "verified"; readonly tools: readonly EffectiveToolInfoLike[] } | { readonly kind: "unverifiable"; readonly reason: string } {
|
|
367
|
+
const normalized: EffectiveToolInfoLike[] = [];
|
|
368
|
+
for (const tool of tools) {
|
|
369
|
+
if (typeof tool.description !== "string" || tool.parameters === undefined || !tool.sourceInfo) {
|
|
370
|
+
return { kind: "unverifiable", reason: `active tool metadata is incomplete for ${tool.name}` };
|
|
371
|
+
}
|
|
372
|
+
normalized.push({
|
|
373
|
+
name: tool.name,
|
|
374
|
+
description: tool.description,
|
|
375
|
+
parameters: tool.parameters,
|
|
376
|
+
promptGuidelines: tool.promptGuidelines,
|
|
377
|
+
sourceInfo: tool.sourceInfo,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
return { kind: "verified", tools: normalized };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function lastAssistantText(state: { readonly messages: readonly unknown[] }): string {
|
|
384
|
+
for (let index = state.messages.length - 1; index >= 0; index--) {
|
|
385
|
+
const message = state.messages[index];
|
|
386
|
+
if (hasRole(message, "assistant")) return messageTextContent(message).trim();
|
|
387
|
+
}
|
|
388
|
+
return "";
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function hasRole(value: unknown, role: string): value is { role: string } {
|
|
392
|
+
return typeof value === "object" && value !== null && "role" in value && value.role === role;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function messageTextContent(message: unknown): string {
|
|
396
|
+
if (!hasContentArray(message)) return "";
|
|
397
|
+
return message.content.filter(isTextPart).map((part) => part.text).join("");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function hasContentArray(value: unknown): value is { content: unknown[] } {
|
|
401
|
+
return typeof value === "object" && value !== null && "content" in value && Array.isArray(value.content);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function isTextPart(value: unknown): value is { type: "text"; text: string } {
|
|
405
|
+
return (
|
|
406
|
+
typeof value === "object" &&
|
|
407
|
+
value !== null &&
|
|
408
|
+
"type" in value &&
|
|
409
|
+
value.type === "text" &&
|
|
410
|
+
"text" in value &&
|
|
411
|
+
typeof value.text === "string"
|
|
412
|
+
);
|
|
413
|
+
}
|