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
|
@@ -1,560 +1,188 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
abort(): Promise<void>;
|
|
39
|
-
getAllTools?(): readonly AgentRunnerToolInfo[];
|
|
40
|
-
setActiveToolsByName?(toolNames: readonly string[]): void;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export type CreateAgentSession = (options: CreateAgentSessionOptions) => Promise<{ session: AgentRunnerSession }>;
|
|
44
|
-
|
|
45
|
-
export interface AgentProgress {
|
|
46
|
-
agentQueued(phase: string | undefined, label: string): number;
|
|
47
|
-
agentStart(phase: string | undefined, label: string, id?: number): void;
|
|
48
|
-
agentTool(label: string, tool: string, id?: number): void;
|
|
49
|
-
agentDone(label: string, id?: number): void;
|
|
50
|
-
agentFailed(label: string, error: unknown, id?: number): void;
|
|
51
|
-
log(message: string): void;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Shared per-run context threaded into every agent() call. */
|
|
55
|
-
export interface RunContext {
|
|
56
|
-
cwd: string;
|
|
57
|
-
hostModel: Model<Api> | undefined;
|
|
58
|
-
modelRegistry: Pick<ModelRegistry, "find">;
|
|
59
|
-
semaphore: Semaphore;
|
|
60
|
-
progress: AgentProgress;
|
|
61
|
-
signal: AbortSignal | undefined;
|
|
62
|
-
perf: PerfSink;
|
|
63
|
-
usage: WorkflowUsageSink;
|
|
64
|
-
budget: WorkflowBudget;
|
|
65
|
-
journal: WorkflowJournal;
|
|
66
|
-
worktrees: WorktreeRegistry;
|
|
67
|
-
createSession?: CreateAgentSession;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export interface ResolvedAgentModelRequest {
|
|
71
|
-
readonly ref: string;
|
|
72
|
-
readonly provider: string;
|
|
73
|
-
readonly id: string;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface ResolvedAgentModel {
|
|
77
|
-
readonly model: Model<Api> | undefined;
|
|
78
|
-
readonly requested: ResolvedAgentModelRequest | undefined;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function parseAgentModelRef(modelRef: string): ResolvedAgentModelRequest {
|
|
82
|
-
const normalized = modelRef.trim();
|
|
83
|
-
if (normalized.length === 0) {
|
|
84
|
-
throw new Error('Invalid agent model ref: expected a bare model id or "provider/id".');
|
|
85
|
-
}
|
|
86
|
-
if (normalized !== modelRef) {
|
|
87
|
-
throw new Error(`Invalid agent model ref "${modelRef}": remove leading or trailing whitespace.`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const slash = modelRef.indexOf("/");
|
|
91
|
-
if (slash === -1) {
|
|
92
|
-
return { ref: modelRef, provider: "anthropic", id: modelRef };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const provider = modelRef.slice(0, slash);
|
|
96
|
-
const id = modelRef.slice(slash + 1);
|
|
97
|
-
if (provider.length === 0 || id.length === 0 || id.startsWith("/")) {
|
|
98
|
-
throw new Error(`Invalid agent model ref "${modelRef}": expected "provider/id".`);
|
|
99
|
-
}
|
|
100
|
-
return { ref: modelRef, provider, id };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Resolve a workflow agent model reference.
|
|
105
|
-
*
|
|
106
|
-
* Omitted refs inherit the host/session default model. Explicit refs are strict:
|
|
107
|
-
* bare model ids keep the original Anthropic shorthand for compatibility, and
|
|
108
|
-
* provider-qualified refs split only on the first slash so provider routers such
|
|
109
|
-
* as OpenRouter can use ids that contain additional slashes.
|
|
110
|
-
*/
|
|
111
|
-
export function resolveAgentModel(
|
|
112
|
-
modelRef: string | undefined,
|
|
113
|
-
modelRegistry: Pick<ModelRegistry, "find">,
|
|
114
|
-
hostModel: Model<Api> | undefined,
|
|
115
|
-
): ResolvedAgentModel {
|
|
116
|
-
if (modelRef === undefined) {
|
|
117
|
-
return { model: hostModel, requested: undefined };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const parsed = parseAgentModelRef(modelRef);
|
|
121
|
-
const found = modelRegistry.find(parsed.provider, parsed.id);
|
|
122
|
-
if (!found) {
|
|
123
|
-
throw new Error(`Agent model "${modelRef}" not found (resolved as ${parsed.provider}/${parsed.id}).`);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return { model: found, requested: parsed };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** Pull the last assistant message's plain text out of a finished session. */
|
|
130
|
-
function lastAssistantText(state: { readonly messages: readonly unknown[] }): string {
|
|
131
|
-
const messages = state.messages;
|
|
132
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
133
|
-
const message = messages[i];
|
|
134
|
-
if (hasRole(message, "assistant")) {
|
|
135
|
-
return messageTextContent(message).trim();
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return "";
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function hasRole(value: unknown, role: string): value is { role: string } {
|
|
142
|
-
return typeof value === "object" && value !== null && "role" in value && value.role === role;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function messageTextContent(message: unknown): string {
|
|
146
|
-
if (!hasContentArray(message)) return "";
|
|
147
|
-
return message.content
|
|
148
|
-
.filter(isTextPart)
|
|
149
|
-
.map((part) => part.text)
|
|
150
|
-
.join("");
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function hasContentArray(value: unknown): value is { content: unknown[] } {
|
|
154
|
-
return typeof value === "object" && value !== null && "content" in value && Array.isArray(value.content);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function isTextPart(value: unknown): value is { type: "text"; text: string } {
|
|
158
|
-
return typeof value === "object" && value !== null && "type" in value && value.type === "text" && "text" in value && typeof value.text === "string";
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async function defaultCreateSession(options: CreateAgentSessionOptions): Promise<{ session: AgentRunnerSession }> {
|
|
162
|
-
const { session } = await createAgentSession(options);
|
|
163
|
-
return { session };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function linkSessionAbort(signal: AbortSignal | undefined, session: AgentRunnerSession): () => void {
|
|
167
|
-
if (!signal) return () => {};
|
|
168
|
-
if (signal.aborted) {
|
|
169
|
-
void session.abort();
|
|
170
|
-
return () => {};
|
|
171
|
-
}
|
|
172
|
-
const onAbort = () => {
|
|
173
|
-
void session.abort();
|
|
174
|
-
};
|
|
175
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
176
|
-
return () => signal.removeEventListener("abort", onAbort);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
type ToolSessionOptions = Pick<CreateAgentSessionOptions, "tools" | "excludeTools">;
|
|
180
|
-
|
|
181
|
-
interface ToolSelection {
|
|
182
|
-
readonly sessionOptions: ToolSessionOptions;
|
|
183
|
-
readonly fallbackSessionOptions: ToolSessionOptions;
|
|
184
|
-
readonly activeTools?: readonly string[];
|
|
185
|
-
readonly toolHints: NonNullable<AgentOptions["toolHints"]>;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
interface DynamicToolSession extends AgentRunnerSession {
|
|
189
|
-
getAllTools(): readonly AgentRunnerToolInfo[];
|
|
190
|
-
setActiveToolsByName(toolNames: readonly string[]): void;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function buildToolList(opts: AgentOptions, skillsEnabled: boolean, fallback?: readonly string[]): string[] | undefined {
|
|
194
|
-
const allow = opts.tools ? [...opts.tools] : fallback ? [...fallback] : undefined;
|
|
195
|
-
if (!allow) return undefined;
|
|
196
|
-
if (skillsEnabled && !allow.includes("read")) allow.push("read");
|
|
197
|
-
if (opts.schema && !allow.includes(FINAL_TOOL)) allow.push(FINAL_TOOL);
|
|
198
|
-
return allow;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function buildToolSelection(opts: AgentOptions, skillsEnabled: boolean): ToolSelection {
|
|
202
|
-
const toolHints = opts.toolHints ?? [];
|
|
203
|
-
const fallback = toolHints.includes("search") ? DEFAULT_SEARCH_BASE_TOOLS : undefined;
|
|
204
|
-
const activeTools = buildToolList(opts, skillsEnabled, fallback);
|
|
205
|
-
const strictSessionOptions = { tools: activeTools ? [...activeTools] : undefined };
|
|
206
|
-
|
|
207
|
-
if (toolHints.length === 0) {
|
|
208
|
-
return { sessionOptions: strictSessionOptions, fallbackSessionOptions: strictSessionOptions, activeTools, toolHints };
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const explicitlyActive = new Set(activeTools ?? []);
|
|
212
|
-
return {
|
|
213
|
-
sessionOptions: { excludeTools: BUILTIN_TOOL_NAMES.filter((name) => !explicitlyActive.has(name)) },
|
|
214
|
-
// If dynamic tool APIs are unavailable, fail closed to the concrete tools we
|
|
215
|
-
// already know instead of keeping every extension/custom tool active.
|
|
216
|
-
fallbackSessionOptions: { tools: activeTools ? [...activeTools] : [] },
|
|
217
|
-
activeTools,
|
|
218
|
-
toolHints,
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function hasDynamicToolApis(session: AgentRunnerSession): session is DynamicToolSession {
|
|
223
|
-
return typeof session.getAllTools === "function" && typeof session.setActiveToolsByName === "function";
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function applyDynamicToolHints(session: AgentRunnerSession, selection: ToolSelection): boolean {
|
|
227
|
-
if (selection.toolHints.length === 0) return true;
|
|
228
|
-
if (!hasDynamicToolApis(session)) return false;
|
|
229
|
-
|
|
230
|
-
const active = new Set(selection.activeTools ?? []);
|
|
231
|
-
for (const tool of session.getAllTools()) {
|
|
232
|
-
if (matchesToolHints(tool, selection.toolHints)) active.add(tool.name);
|
|
233
|
-
}
|
|
234
|
-
session.setActiveToolsByName([...active]);
|
|
235
|
-
return true;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function matchesToolHints(tool: AgentRunnerToolInfo, toolHints: readonly string[]): boolean {
|
|
239
|
-
return toolHints.some((hint) => hint === "search" && isSearchLikeTool(tool));
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function isSearchLikeTool(tool: AgentRunnerToolInfo): boolean {
|
|
243
|
-
if (isClearlyMutatingToolName(tool.name)) return false;
|
|
244
|
-
const name = tool.name.toLowerCase();
|
|
245
|
-
if (name.includes("grep") || name.includes("find") || name.includes("search") || name === "rg" || name.includes("ripgrep")) return true;
|
|
246
|
-
const description = tool.description?.toLowerCase() ?? "";
|
|
247
|
-
return /\b(?:grep|find|search|ripgrep|rg|structural search|code search)\b/.test(description);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function isClearlyMutatingToolName(name: string): boolean {
|
|
251
|
-
return /(?:^|[-_])(?:edit|write|replace|patch|apply|delete|remove|move|rename|create|commit|push)(?:$|[-_])/.test(name.toLowerCase());
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function shouldCreateSkillResourceLoader(rc: RunContext, prompt: string, opts: AgentOptions): boolean {
|
|
255
|
-
if (!rc.createSession) return true;
|
|
256
|
-
if (opts.skills !== undefined) return true;
|
|
257
|
-
return extractSkillSelectorsFromText(prompt).length > 0;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Refuse to start a new subagent once the run is over budget. Called before queueing and
|
|
262
|
-
* again after a queued agent acquires a concurrency slot, so an exhausted run stops spending
|
|
263
|
-
* immediately rather than starting stale queued work.
|
|
264
|
-
*
|
|
265
|
-
* Policy: accept overshoot (matches the built-in Workflow tool). `budget.spent()` counts
|
|
266
|
-
* only COMPLETED agents (usage is recorded on session dispose), so agents admitted together
|
|
267
|
-
* can each pass this check and then collectively overshoot `budget.total` — we tolerate that
|
|
268
|
-
* rather than reserving per-agent estimates. An uncapped run (`budget.total === null`,
|
|
269
|
-
* `remaining()` is Infinity) never throws.
|
|
270
|
-
*
|
|
271
|
-
* Heads-up: a throw here is a recoverable branch failure inside `parallel`/`pipeline`, so the
|
|
272
|
-
* over-budget slot becomes null while already-started siblings can finish. The intended primary
|
|
273
|
-
* usage is the loop guard `while (budget.total && budget.remaining() > N) { await agent(...) }`,
|
|
274
|
-
* where this throw is only a backstop.
|
|
275
|
-
*/
|
|
276
|
-
function ensureWithinBudget(budget: WorkflowBudget): void {
|
|
277
|
-
if (budget.total !== null && budget.remaining() <= 0) {
|
|
278
|
-
throw new WorkflowBudgetExceededError(budget.total, budget.spent());
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
interface AgentWorkspace {
|
|
283
|
-
readonly cwd: string;
|
|
284
|
-
wrapResult(result: unknown): Promise<unknown>;
|
|
285
|
-
dispose(): Promise<void>;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
async function createAgentWorkspace(rc: RunContext, opts: AgentOptions, label: string): Promise<AgentWorkspace> {
|
|
289
|
-
if (opts.isolation !== "worktree") {
|
|
290
|
-
return {
|
|
291
|
-
cwd: rc.cwd,
|
|
292
|
-
async wrapResult(result) {
|
|
293
|
-
return result;
|
|
294
|
-
},
|
|
295
|
-
async dispose() {},
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const probe = await rc.worktrees.probe(rc.signal);
|
|
300
|
-
if (!probe.ok) {
|
|
301
|
-
throw new Error(`Failed to check git worktree availability for isolated agent: ${probe.error ?? "unknown git error"}`);
|
|
302
|
-
}
|
|
303
|
-
if (!probe.inside) {
|
|
304
|
-
throw new Error("Agent requested worktree isolation, but the workflow cwd is not inside a git work tree.");
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const added = await rc.worktrees.add(rc.signal);
|
|
308
|
-
if ("error" in added) throw new Error(`Failed to create isolated worktree: ${added.error}`);
|
|
309
|
-
const worktreePath = added.path;
|
|
310
|
-
rc.progress.log(`${label}: using isolated worktree ${worktreePath}`);
|
|
311
|
-
|
|
312
|
-
return {
|
|
313
|
-
cwd: worktreePath,
|
|
314
|
-
async wrapResult(result) {
|
|
315
|
-
return await isolatedResult(rc, worktreePath, result);
|
|
316
|
-
},
|
|
317
|
-
async dispose() {
|
|
318
|
-
const removed = await rc.worktrees.remove(worktreePath);
|
|
319
|
-
if (!removed.ok) {
|
|
320
|
-
rc.progress.log(`${label}: failed to remove isolated worktree (${removed.error ?? (removed.stderr.trim() || "unknown error")})`);
|
|
321
|
-
}
|
|
322
|
-
},
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
async function disposeAgentWorkspace(rc: RunContext, label: string, workspace: AgentWorkspace | undefined): Promise<void> {
|
|
327
|
-
if (!workspace) return;
|
|
328
|
-
try {
|
|
329
|
-
await workspace.dispose();
|
|
330
|
-
} catch (error) {
|
|
331
|
-
rc.progress.log(`${label}: failed to dispose isolated workspace (${errorMessage(error)})`);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
async function recordJournalResult(rc: RunContext, label: string, key: string, value: unknown): Promise<void> {
|
|
336
|
-
try {
|
|
337
|
-
const recorded = await rc.journal.record(key, value);
|
|
338
|
-
if (!recorded.ok) {
|
|
339
|
-
rc.progress.log(`${label}: workflow journal write failed (${recorded.error}); future resume may be incomplete`);
|
|
340
|
-
}
|
|
341
|
-
} catch (error) {
|
|
342
|
-
rc.progress.log(`${label}: workflow journal write failed (${errorMessage(error)}); future resume may be incomplete`);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
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";
|
|
345
38
|
|
|
346
39
|
/**
|
|
347
40
|
* Run one subagent to completion in an isolated in-memory session.
|
|
348
41
|
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
* call it; pi validates the args against the schema, hands them to `execute`, and
|
|
352
|
-
* `terminate: true` ends the turn with no extra LLM round-trip. We capture those
|
|
353
|
-
* validated args in a closure — that captured object is the structured result, so
|
|
354
|
-
* no event-stream parsing is needed.
|
|
42
|
+
* Session construction, replay validation, and workspace cleanup live in focused
|
|
43
|
+
* modules; this boundary owns admission, progress, and bounded cache/provider retries.
|
|
355
44
|
*/
|
|
356
|
-
export async function runAgent(
|
|
45
|
+
export async function runAgent(
|
|
46
|
+
rc: RunContext,
|
|
47
|
+
prompt: string,
|
|
48
|
+
opts: AgentExecutionOptions,
|
|
49
|
+
resumeBaseContext: AgentResumeBaseContext,
|
|
50
|
+
): Promise<unknown> {
|
|
357
51
|
if (typeof prompt !== "string") {
|
|
358
52
|
throw new Error(`agent() prompt must be a string; received ${describeAgentPrompt(prompt)}`);
|
|
359
53
|
}
|
|
360
54
|
|
|
361
55
|
const label = opts.label ?? "agent";
|
|
362
56
|
const phase = opts.phase ?? "Workflow";
|
|
363
|
-
const tags = { label, phase };
|
|
57
|
+
const tags: AgentRunTags = { label, phase };
|
|
364
58
|
|
|
365
59
|
return await rc.perf.time("agent.total_ms", async () => {
|
|
366
60
|
throwIfAborted(rc.signal);
|
|
367
|
-
const
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
375
|
-
// Stop spending the moment the run is over budget — before queueing.
|
|
376
|
-
ensureWithinBudget(rc.budget);
|
|
377
|
-
// Track queued agents before acquiring a global concurrency slot.
|
|
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
|
+
|
|
378
68
|
const rowId = rc.progress.agentQueued(opts.phase, label);
|
|
379
|
-
|
|
69
|
+
const liveScope = createAgentLiveScope(rc, label);
|
|
380
70
|
try {
|
|
381
71
|
return await rc.semaphore.run(
|
|
382
72
|
async () => {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
let session: AgentRunnerSession | undefined;
|
|
391
|
-
let usageRecorded = false;
|
|
392
|
-
let unsubscribe: (() => void) | undefined;
|
|
393
|
-
let workspace: AgentWorkspace | undefined;
|
|
394
|
-
const recordUsage = (activeSession: AgentRunnerSession): void => {
|
|
395
|
-
if (usageRecorded) return;
|
|
396
|
-
usageRecorded = true;
|
|
397
|
-
rc.usage.recordAgentSession({ label, phase, messages: activeSession.state.messages });
|
|
398
|
-
};
|
|
399
|
-
const customTools: ToolDefinition[] = opts.schema
|
|
400
|
-
? [
|
|
401
|
-
defineTool({
|
|
402
|
-
name: FINAL_TOOL,
|
|
403
|
-
label: "Final Answer",
|
|
404
|
-
description:
|
|
405
|
-
"Return your final structured answer. This MUST be your last action — do not write a normal reply after calling it.",
|
|
406
|
-
parameters: opts.schema,
|
|
407
|
-
async execute(_toolCallId, params) {
|
|
408
|
-
captured = params;
|
|
409
|
-
return { content: [{ type: "text", text: "Recorded." }], details: params, terminate: true };
|
|
410
|
-
},
|
|
411
|
-
}),
|
|
412
|
-
]
|
|
413
|
-
: [];
|
|
414
|
-
|
|
415
|
-
try {
|
|
416
|
-
workspace = await createAgentWorkspace(rc, opts, label);
|
|
417
|
-
const agentCwd = workspace.cwd;
|
|
73
|
+
const agentRc: RunContext = { ...rc, signal: liveScope.signal };
|
|
74
|
+
throwIfAborted(agentRc.signal);
|
|
75
|
+
if (!isReplayEnabled(replay)) assertWorkflowBudgetAvailable(agentRc.budget);
|
|
76
|
+
rc.progress.agentStart(opts.phase, label, rowId);
|
|
77
|
+
if (replay.kind === "disabled") {
|
|
78
|
+
rc.progress.log(`${label}: resume disabled for this call (${replay.reason})`);
|
|
79
|
+
}
|
|
418
80
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
81
|
+
let attemptPlan: AgentReplayPlan = replay;
|
|
82
|
+
let providerRetries = 0;
|
|
83
|
+
while (true) {
|
|
84
|
+
let outcome: AgentAttemptResult;
|
|
85
|
+
try {
|
|
86
|
+
outcome = await executeAgentAttempt({
|
|
87
|
+
rc: agentRc,
|
|
88
|
+
prompt,
|
|
89
|
+
opts: effectiveOpts,
|
|
90
|
+
resumeBaseContext,
|
|
91
|
+
model: routing.model,
|
|
92
|
+
replay: attemptPlan,
|
|
93
|
+
label,
|
|
94
|
+
rowId,
|
|
430
95
|
tags,
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
"agent.create_session_ms",
|
|
442
|
-
() =>
|
|
443
|
-
createSessionForRun({
|
|
444
|
-
cwd: agentCwd,
|
|
445
|
-
model,
|
|
446
|
-
modelRegistry: rc.modelRegistry as ModelRegistry,
|
|
447
|
-
thinkingLevel: opts.thinkingLevel,
|
|
448
|
-
tools: sessionOptions.tools,
|
|
449
|
-
excludeTools: sessionOptions.excludeTools,
|
|
450
|
-
customTools,
|
|
451
|
-
resourceLoader: skillSetup?.resourceLoader,
|
|
452
|
-
sessionManager: SessionManager.inMemory(agentCwd),
|
|
453
|
-
}),
|
|
454
|
-
tags,
|
|
455
|
-
);
|
|
456
|
-
|
|
457
|
-
throwIfAborted(rc.signal);
|
|
458
|
-
let created = await createSubagentSession(toolSelection.sessionOptions);
|
|
459
|
-
session = created.session;
|
|
460
|
-
let activeSession = session;
|
|
461
|
-
if (!applyDynamicToolHints(activeSession, toolSelection)) {
|
|
462
|
-
rc.progress.log(`${label}: dynamic tool hints unavailable; falling back to concrete tools only`);
|
|
463
|
-
rc.perf.counter("agent.tool_hint_fallback", 1, tags);
|
|
464
|
-
const dynamicSession = activeSession;
|
|
465
|
-
session = undefined;
|
|
466
|
-
rc.perf.timeSync("agent.dispose_ms", () => dynamicSession.dispose(), tags);
|
|
467
|
-
throwIfAborted(rc.signal);
|
|
468
|
-
created = await createSubagentSession(toolSelection.fallbackSessionOptions);
|
|
469
|
-
session = created.session;
|
|
470
|
-
activeSession = session;
|
|
471
|
-
}
|
|
472
|
-
throwIfAborted(rc.signal);
|
|
96
|
+
admitLiveAgent: liveScope.admit,
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (!(error instanceof WorkflowProviderError) || !error.retryable) throw error;
|
|
100
|
+
if (providerRetries >= agentRc.agentRetries) {
|
|
101
|
+
if (agentRc.agentRetries > 0) {
|
|
102
|
+
rc.progress.log(`${label}: transient provider failure after ${providerRetries} retries; giving up`);
|
|
103
|
+
}
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
473
106
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
107
|
+
assertWorkflowBudgetAvailable(agentRc.budget);
|
|
108
|
+
providerRetries++;
|
|
109
|
+
const delayMs = agentRetryDelayMs(providerRetries);
|
|
110
|
+
rc.progress.log(`${label}: transient provider failure; retry ${providerRetries}/${agentRc.agentRetries} in ${delayMs}ms`);
|
|
111
|
+
rc.perf.counter("agent.provider_retry", 1, tags);
|
|
112
|
+
rc.perf.observe("agent.provider_retry_delay_ms", delayMs, tags);
|
|
113
|
+
attemptPlan = { kind: "off" };
|
|
114
|
+
await agentRc.retryScheduler.sleep(delayMs, agentRc.signal);
|
|
115
|
+
continue;
|
|
477
116
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
? `${promptedWithSkills}\n\nWhen finished, return your result by calling the ${FINAL_TOOL} tool.`
|
|
483
|
-
: promptedWithSkills;
|
|
484
|
-
throwIfAborted(rc.signal);
|
|
485
|
-
const unlinkPromptAbort = linkSessionAbort(rc.signal, activeSession);
|
|
486
|
-
try {
|
|
487
|
-
await rc.perf.time("agent.prompt_ms", () => activeSession.prompt(finalPrompt), tags);
|
|
488
|
-
if (opts.schema) {
|
|
489
|
-
for (let attempt = 0; captured === null && attempt < SCHEMA_REPROMPT_ATTEMPTS; attempt++) {
|
|
490
|
-
throwIfAborted(rc.signal);
|
|
491
|
-
rc.progress.log(`${label}: no final answer; re-prompting (${attempt + 1}/${SCHEMA_REPROMPT_ATTEMPTS})`);
|
|
492
|
-
rc.perf.counter("agent.structured_reprompt", 1, tags);
|
|
493
|
-
await rc.perf.time("agent.prompt_ms", () => activeSession.prompt(SCHEMA_REPROMPT), tags);
|
|
494
|
-
}
|
|
117
|
+
const settlement = await settleAgentAttempt({ rc: agentRc, label, tags, replay: attemptPlan, outcome });
|
|
118
|
+
if (settlement.kind === "retry-live") {
|
|
119
|
+
attemptPlan = { kind: "off" };
|
|
120
|
+
continue;
|
|
495
121
|
}
|
|
496
|
-
|
|
497
|
-
|
|
122
|
+
rc.progress.agentDone(label, rowId);
|
|
123
|
+
return settlement.result;
|
|
498
124
|
}
|
|
499
|
-
recordUsage(activeSession);
|
|
500
|
-
throwIfAborted(rc.signal);
|
|
501
|
-
|
|
502
|
-
const rawResult = rc.perf.timeSync(
|
|
503
|
-
"agent.extract_result_ms",
|
|
504
|
-
() => {
|
|
505
|
-
if (opts.schema) {
|
|
506
|
-
if (captured === null) {
|
|
507
|
-
rc.progress.log(`${label}: no structured answer returned`);
|
|
508
|
-
rc.perf.counter("agent.structured_missing", 1, tags);
|
|
509
|
-
}
|
|
510
|
-
return captured;
|
|
511
|
-
}
|
|
512
|
-
return lastAssistantText(activeSession.state);
|
|
513
|
-
},
|
|
514
|
-
tags,
|
|
515
|
-
);
|
|
516
|
-
const result = await workspace.wrapResult(rawResult);
|
|
517
|
-
await recordJournalResult(rc, label, journalKey, result);
|
|
518
|
-
return result;
|
|
519
|
-
} catch (error) {
|
|
520
|
-
failed = true;
|
|
521
|
-
failureHandled = true;
|
|
522
|
-
rc.progress.agentFailed(label, error, rowId);
|
|
523
|
-
rc.progress.log(`${label} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
524
|
-
throw error;
|
|
525
|
-
} finally {
|
|
526
|
-
const disposable = session;
|
|
527
|
-
if (disposable) {
|
|
528
|
-
recordUsage(disposable);
|
|
529
|
-
rc.perf.timeSync(
|
|
530
|
-
"agent.dispose_ms",
|
|
531
|
-
() => {
|
|
532
|
-
unsubscribe?.();
|
|
533
|
-
disposable.dispose();
|
|
534
|
-
},
|
|
535
|
-
tags,
|
|
536
|
-
);
|
|
537
|
-
}
|
|
538
|
-
await disposeAgentWorkspace(rc, label, workspace);
|
|
539
|
-
if (!failed) rc.progress.agentDone(label, rowId);
|
|
540
|
-
}
|
|
541
125
|
},
|
|
542
|
-
{
|
|
126
|
+
{
|
|
127
|
+
onQueueWaitMs: (durationMs) => rc.perf.observe("agent.queue_wait_ms", durationMs, tags),
|
|
128
|
+
signal: rc.signal,
|
|
129
|
+
},
|
|
543
130
|
);
|
|
544
131
|
} catch (error) {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
132
|
+
let failure = error;
|
|
133
|
+
if (liveScope.signal.aborted) failure = abortReason(liveScope.signal);
|
|
134
|
+
if (rc.signal?.aborted) failure = abortReason(rc.signal);
|
|
135
|
+
rc.progress.agentFailed(label, failure, rowId);
|
|
136
|
+
rc.progress.log(`${label} failed: ${unknownErrorMessage(failure)}`);
|
|
137
|
+
throw failure;
|
|
138
|
+
} finally {
|
|
139
|
+
liveScope.dispose();
|
|
550
140
|
}
|
|
551
141
|
}, tags);
|
|
552
142
|
}
|
|
553
143
|
|
|
554
|
-
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
144
|
+
function createAgentLiveScope(rc: RunContext, label: string) {
|
|
145
|
+
const controller = new AbortController();
|
|
146
|
+
const unlink = linkAbortSignal(rc.signal, controller);
|
|
147
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
148
|
+
let timerStarted = false;
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
signal: controller.signal,
|
|
152
|
+
admit() {
|
|
153
|
+
rc.agentLimiter.admit(controller.signal);
|
|
154
|
+
if (timerStarted) return;
|
|
155
|
+
timerStarted = true;
|
|
156
|
+
timer = setTimeout(
|
|
157
|
+
() => controller.abort(new WorkflowAgentTimeoutError(label, rc.agentTimeoutMs)),
|
|
158
|
+
rc.agentTimeoutMs,
|
|
159
|
+
);
|
|
160
|
+
},
|
|
161
|
+
dispose() {
|
|
162
|
+
if (timer) clearTimeout(timer);
|
|
163
|
+
unlink();
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function resolveAgentRouting(
|
|
169
|
+
rc: RunContext,
|
|
170
|
+
opts: AgentExecutionOptions,
|
|
171
|
+
label: string,
|
|
172
|
+
): { readonly model: ResolvedAgentModel["model"]; readonly thinkingLevel: AgentExecutionOptions["thinkingLevel"] } {
|
|
173
|
+
try {
|
|
174
|
+
return resolveAgentModelProfile(
|
|
175
|
+
{
|
|
176
|
+
request: opts,
|
|
177
|
+
profiles: rc.modelProfiles,
|
|
178
|
+
resolveExplicitModel: (modelRef) => resolveAgentModel(modelRef, rc.modelRegistry, rc.hostModel).model,
|
|
179
|
+
hostModel: rc.hostModel,
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
rc.progress.agentFailed(label, error);
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
558
186
|
}
|
|
559
187
|
|
|
560
188
|
function describeAgentPrompt(value: unknown): string {
|
|
@@ -562,7 +190,3 @@ function describeAgentPrompt(value: unknown): string {
|
|
|
562
190
|
if (Array.isArray(value)) return "array";
|
|
563
191
|
return typeof value;
|
|
564
192
|
}
|
|
565
|
-
|
|
566
|
-
function errorMessage(error: unknown): string {
|
|
567
|
-
return error instanceof Error ? error.message : String(error);
|
|
568
|
-
}
|