pi-workflow-engine 0.12.0 → 1.0.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 +106 -391
- package/.pi/extensions/pi-workflow-engine/src/advisory-challenge.ts +75 -0
- package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +90 -0
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +20 -2
- package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +48 -62
- package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +46 -67
- package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +14 -37
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +5 -11
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +15 -21
- package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +13 -43
- package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +17 -5
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +93 -100
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +11 -43
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +18 -18
- package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +33 -86
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +1 -5
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +37 -86
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +16 -23
- package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +1 -4
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +39 -98
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +37 -80
- package/.pi/extensions/pi-workflow-engine/src/finalizers.ts +2 -10
- package/.pi/extensions/pi-workflow-engine/src/git.ts +55 -0
- package/.pi/extensions/pi-workflow-engine/src/guards.ts +8 -0
- package/.pi/extensions/pi-workflow-engine/src/identity-canonicalization.ts +7 -19
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +171 -106
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +1 -12
- package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +37 -8
- package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +15 -66
- package/.pi/extensions/pi-workflow-engine/src/options.ts +97 -16
- package/.pi/extensions/pi-workflow-engine/src/perf.ts +43 -72
- package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +14 -4
- package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +2 -0
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +79 -80
- package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +28 -20
- package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +2 -7
- package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +0 -1
- package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +27 -31
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +128 -142
- package/.pi/extensions/pi-workflow-engine/src/review/github-pr-comments.ts +14 -31
- package/.pi/extensions/pi-workflow-engine/src/review/patch-validation.ts +119 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-actions.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-diff-lines.ts +54 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +41 -7
- package/.pi/extensions/pi-workflow-engine/src/review/review-format.ts +51 -116
- package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +9 -14
- package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +3 -7
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +158 -241
- package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +28 -38
- package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +35 -44
- package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +13 -14
- package/.pi/extensions/pi-workflow-engine/src/text.ts +29 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +2 -1
- package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +16 -28
- package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +34 -14
- package/.pi/extensions/pi-workflow-engine/src/types.ts +9 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/kit.ts +193 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/research-result-renderer.ts +177 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/result-layout.ts +209 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +17 -44
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +703 -235
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-picker.ts +48 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +239 -126
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-runs-browser.ts +461 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-tool-renderer.ts +91 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +316 -98
- package/.pi/extensions/pi-workflow-engine/src/usage.ts +21 -32
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +178 -188
- package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +19 -24
- package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +17 -37
- package/.pi/extensions/pi-workflow-engine/src/workflow-result-context.ts +79 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +4 -14
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +77 -63
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +42 -29
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +15 -14
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +27 -31
- package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +1 -5
- package/.pi/extensions/pi-workflow-engine/src/workflows.ts +3 -6
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +55 -110
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +101 -203
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +51 -187
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +33 -109
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +36 -106
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +74 -66
- package/USAGE.md +90 -17
- package/package.json +1 -2
- package/skills/workflow-code-review-actions/SKILL.md +12 -23
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +0 -81
- package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +0 -49
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +0 -52
- package/.pi/settings.json +0 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
1
|
+
import type { Api, AssistantMessage, Model } from "@earendil-works/pi-ai";
|
|
2
2
|
import {
|
|
3
3
|
createAgentSessionFromServices,
|
|
4
4
|
createAgentSessionServices,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
import { prepareAgentSkillResources } from "./agent-skills.ts";
|
|
13
13
|
import type {
|
|
14
14
|
AgentExecutionOptions,
|
|
15
|
-
|
|
15
|
+
AgentProgress,
|
|
16
16
|
AgentRunnerSession,
|
|
17
17
|
RunContext,
|
|
18
18
|
} from "./agent-runner-types.ts";
|
|
@@ -24,7 +24,8 @@ import {
|
|
|
24
24
|
import { providerErrorFromMessages } from "./agent-retry.ts";
|
|
25
25
|
import { synchronizeWorkflowModelRuntime } from "./agent-session-providers.ts";
|
|
26
26
|
import { matchesAgentToolHint, WorkflowToolHintUnavailableError } from "./tool-capabilities.ts";
|
|
27
|
-
import
|
|
27
|
+
import { truncateText } from "./text.ts";
|
|
28
|
+
import { WORKFLOW_TOOL_NAME, type AgentToolHint } from "./types.ts";
|
|
28
29
|
|
|
29
30
|
export const FINAL_TOOL = "final_answer";
|
|
30
31
|
|
|
@@ -32,17 +33,12 @@ const SCHEMA_REPROMPT =
|
|
|
32
33
|
`You ended your turn without calling the ${FINAL_TOOL} tool, so no result was recorded. ` +
|
|
33
34
|
`Call ${FINAL_TOOL} now with your final answer as its arguments. Do not reply with plain text.`;
|
|
34
35
|
const DEFAULT_SEARCH_BASE_TOOLS = ["read", "bash", "grep", "find", "ls"];
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
export interface ResolvedAgentModel {
|
|
43
|
-
readonly model: Model<Api> | undefined;
|
|
44
|
-
readonly requested: ResolvedAgentModelRequest | undefined;
|
|
45
|
-
}
|
|
36
|
+
const RETRY_REASON_CHARS = 160;
|
|
37
|
+
const RETRY_DISABLED_WARNING =
|
|
38
|
+
"pi auto-retry is disabled (retry.enabled: false in pi settings), so subagents will not retry failed provider turns. " +
|
|
39
|
+
"pi-workflow-engine 0.12.0 and 0.13.0 wrote that setting into ~/.pi/agent/settings.json; remove it unless you disabled retry on purpose.";
|
|
40
|
+
/** Runs (keyed by their shared progress sink) already warned about disabled pi auto-retry. */
|
|
41
|
+
const retryDisabledWarnedRuns = new WeakSet<AgentProgress>();
|
|
46
42
|
|
|
47
43
|
export interface AgentSessionHandle {
|
|
48
44
|
readonly session: AgentRunnerSession;
|
|
@@ -51,19 +47,14 @@ export interface AgentSessionHandle {
|
|
|
51
47
|
structuredResult(): unknown;
|
|
52
48
|
}
|
|
53
49
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
modelRegistry: Pick<ModelRegistry, "find">,
|
|
57
|
-
hostModel: Model<Api> | undefined,
|
|
58
|
-
): ResolvedAgentModel {
|
|
59
|
-
if (modelRef === undefined) return { model: hostModel, requested: undefined };
|
|
60
|
-
|
|
50
|
+
/** Resolve an explicit `agent({ model })` ref; unknown refs fail instead of falling back to another model. */
|
|
51
|
+
export function resolveAgentModel(modelRef: string, modelRegistry: Pick<ModelRegistry, "find">): Model<Api> {
|
|
61
52
|
const parsed = parseAgentModelRef(modelRef);
|
|
62
53
|
const found = modelRegistry.find(parsed.provider, parsed.id);
|
|
63
54
|
if (!found) {
|
|
64
55
|
throw new Error(`Agent model "${modelRef}" not found (resolved as ${parsed.provider}/${parsed.id}).`);
|
|
65
56
|
}
|
|
66
|
-
return
|
|
57
|
+
return found;
|
|
67
58
|
}
|
|
68
59
|
|
|
69
60
|
export async function openAgentSession(input: {
|
|
@@ -73,9 +64,8 @@ export async function openAgentSession(input: {
|
|
|
73
64
|
readonly cwd: string;
|
|
74
65
|
readonly model: Model<Api> | undefined;
|
|
75
66
|
readonly label: string;
|
|
76
|
-
readonly tags: AgentRunTags;
|
|
77
67
|
}): Promise<AgentSessionHandle> {
|
|
78
|
-
const { rc, prompt, opts, cwd, model, label
|
|
68
|
+
const { rc, prompt, opts, cwd, model, label } = input;
|
|
79
69
|
let captured = false;
|
|
80
70
|
let structuredResult: unknown;
|
|
81
71
|
const customTools: ToolDefinition[] = opts.schema
|
|
@@ -97,26 +87,22 @@ export async function openAgentSession(input: {
|
|
|
97
87
|
let session: AgentRunnerSession | undefined;
|
|
98
88
|
try {
|
|
99
89
|
throwIfAborted(rc.signal);
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
"agent.session_resources_ms",
|
|
105
|
-
() => prepareAgentSessionResources(resourceInput),
|
|
106
|
-
tags,
|
|
107
|
-
);
|
|
90
|
+
const resources = await rc.perf.time(
|
|
91
|
+
"agent.session_resources_ms",
|
|
92
|
+
() => prepareAgentSessionResources({ rc, prompt, opts, cwd, model, customTools, label }),
|
|
93
|
+
);
|
|
108
94
|
const toolSelection = buildToolSelection(opts, resources.selectedSkills.length > 0);
|
|
109
95
|
session = (
|
|
110
96
|
await rc.perf.time(
|
|
111
97
|
"agent.create_session_ms",
|
|
112
98
|
() => resources.createSession(toolSelection.sessionOptions),
|
|
113
|
-
tags,
|
|
114
99
|
)
|
|
115
100
|
).session;
|
|
116
|
-
session.setAutoRetryEnabled(false);
|
|
117
101
|
const matchedToolHints = toolSelection.toolHints.length === 0
|
|
118
102
|
? new Set<AgentToolHint>()
|
|
119
103
|
: applyDynamicToolHints(session, toolSelection);
|
|
104
|
+
// A nested run would get its own concurrency cap and budget, so subagents may start one only when allowlisted.
|
|
105
|
+
if (!opts.tools?.includes(WORKFLOW_TOOL_NAME)) withholdWorkflowTool(session);
|
|
120
106
|
if (opts.requireToolHints) {
|
|
121
107
|
const missing = toolSelection.toolHints.filter((hint) => !matchedToolHints.has(hint));
|
|
122
108
|
if (missing.length > 0) throw new WorkflowToolHintUnavailableError(missing);
|
|
@@ -129,11 +115,8 @@ export async function openAgentSession(input: {
|
|
|
129
115
|
structuredResult: () => structuredResult,
|
|
130
116
|
};
|
|
131
117
|
} catch (error) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
session = undefined;
|
|
135
|
-
rc.perf.timeSync("agent.dispose_ms", () => activeSession.dispose(), tags);
|
|
136
|
-
}
|
|
118
|
+
const created = session;
|
|
119
|
+
if (created) rc.perf.timeSync("agent.dispose_ms", () => created.dispose());
|
|
137
120
|
throw error;
|
|
138
121
|
}
|
|
139
122
|
}
|
|
@@ -145,70 +128,79 @@ export async function promptAgentSession(input: {
|
|
|
145
128
|
readonly opts: AgentExecutionOptions;
|
|
146
129
|
readonly label: string;
|
|
147
130
|
readonly rowId: number;
|
|
148
|
-
readonly
|
|
131
|
+
readonly phase: string;
|
|
149
132
|
}): Promise<unknown> {
|
|
150
|
-
const { rc, handle, prompt, opts, label, rowId,
|
|
133
|
+
const { rc, handle, prompt, opts, label, rowId, phase } = input;
|
|
151
134
|
const { session } = handle;
|
|
135
|
+
throwIfAborted(rc.signal);
|
|
136
|
+
// pi drops a retried turn's failed message from session.messages; keep it so usage covers every attempt.
|
|
137
|
+
const retriedFailures: AssistantMessage[] = [];
|
|
138
|
+
let lastFailure: AssistantMessage | undefined;
|
|
152
139
|
const unsubscribe = session.subscribe((event) => {
|
|
153
|
-
if (event.type === "tool_execution_start" && event.toolName !==
|
|
154
|
-
rc.progress.agentTool(
|
|
140
|
+
if (event.type === "tool_execution_start" && event.toolName !== FINAL_TOOL) {
|
|
141
|
+
rc.progress.agentTool(rowId, event.toolName);
|
|
142
|
+
} else if (event.type === "message_end" && event.message.role === "assistant" && event.message.stopReason === "error") {
|
|
143
|
+
lastFailure = event.message;
|
|
144
|
+
} else if (event.type === "auto_retry_start") {
|
|
145
|
+
if (lastFailure) retriedFailures.push(lastFailure);
|
|
146
|
+
lastFailure = undefined;
|
|
147
|
+
const reason = truncateText(event.errorMessage, RETRY_REASON_CHARS);
|
|
148
|
+
rc.progress.log(
|
|
149
|
+
`${label}: transient provider failure (${reason}); retrying turn ${event.attempt}/${event.maxAttempts} in ${event.delayMs}ms`,
|
|
150
|
+
);
|
|
151
|
+
rc.perf.counter("agent.turn_retry");
|
|
155
152
|
}
|
|
156
153
|
});
|
|
154
|
+
const unlinkPromptAbort = linkSessionAbort(rc.signal, session);
|
|
155
|
+
const promptSession = async (text: string) => {
|
|
156
|
+
await rc.perf.time("agent.prompt_ms", () => raceWithAbort(() => session.prompt(text), rc.signal));
|
|
157
|
+
const failure = providerErrorFromMessages(session.messages, {
|
|
158
|
+
pauseOnUsageLimit: rc.pauseOnProviderUsageLimit,
|
|
159
|
+
});
|
|
160
|
+
if (failure) throw failure;
|
|
161
|
+
};
|
|
157
162
|
|
|
158
163
|
try {
|
|
159
|
-
|
|
160
|
-
? `${prompt}\n\nWhen finished, return your result by calling the ${FINAL_TOOL} tool.`
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
164
|
+
await promptSession(
|
|
165
|
+
opts.schema ? `${prompt}\n\nWhen finished, return your result by calling the ${FINAL_TOOL} tool.` : prompt,
|
|
166
|
+
);
|
|
167
|
+
if (opts.schema && !handle.hasStructuredResult()) {
|
|
168
|
+
const activeTools = session.getActiveToolNames();
|
|
169
|
+
for (let attempt = 0; !handle.hasStructuredResult() && attempt < MAX_SCHEMA_REPAIR_ATTEMPTS; attempt++) {
|
|
184
170
|
throwIfAborted(rc.signal);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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 });
|
|
171
|
+
session.setActiveToolsByName([FINAL_TOOL]);
|
|
172
|
+
rc.progress.log(`${label}: no final answer; re-prompting (${attempt + 1}/${MAX_SCHEMA_REPAIR_ATTEMPTS})`);
|
|
173
|
+
rc.perf.counter("agent.structured_reprompt");
|
|
174
|
+
await promptSession(SCHEMA_REPROMPT);
|
|
202
175
|
}
|
|
203
|
-
|
|
204
|
-
|
|
176
|
+
// Narrowing also rebuilds pi's system prompt; restore both so replay validation sees the captured identity.
|
|
177
|
+
session.setActiveToolsByName(activeTools);
|
|
205
178
|
}
|
|
179
|
+
|
|
180
|
+
throwIfAborted(rc.signal);
|
|
181
|
+
return rc.perf.timeSync(
|
|
182
|
+
"agent.extract_result_ms",
|
|
183
|
+
() => {
|
|
184
|
+
if (!opts.schema) {
|
|
185
|
+
return session.getLastAssistantText() ?? "";
|
|
186
|
+
}
|
|
187
|
+
if (!handle.hasStructuredResult()) {
|
|
188
|
+
rc.progress.log(`${label}: no structured answer returned`);
|
|
189
|
+
rc.perf.counter("agent.structured_missing");
|
|
190
|
+
throw new WorkflowStructuredOutputError(label, MAX_SCHEMA_REPAIR_ATTEMPTS);
|
|
191
|
+
}
|
|
192
|
+
return handle.structuredResult();
|
|
193
|
+
},
|
|
194
|
+
);
|
|
206
195
|
} finally {
|
|
196
|
+
// Detach first: recording publishes usage to the UI and must not be able to skip listener cleanup.
|
|
197
|
+
unlinkPromptAbort();
|
|
207
198
|
unsubscribe();
|
|
199
|
+
rc.usage.recordAgentSession({ label, phase, messages: [...retriedFailures, ...session.messages] });
|
|
208
200
|
}
|
|
209
201
|
}
|
|
210
202
|
|
|
211
|
-
function parseAgentModelRef(modelRef: string):
|
|
203
|
+
function parseAgentModelRef(modelRef: string): { readonly provider: string; readonly id: string } {
|
|
212
204
|
const normalized = modelRef.trim();
|
|
213
205
|
if (normalized.length === 0) {
|
|
214
206
|
throw new Error('Invalid agent model ref: expected a bare model id or "provider/id".');
|
|
@@ -218,13 +210,13 @@ function parseAgentModelRef(modelRef: string): ResolvedAgentModelRequest {
|
|
|
218
210
|
}
|
|
219
211
|
|
|
220
212
|
const slash = modelRef.indexOf("/");
|
|
221
|
-
if (slash === -1) return {
|
|
213
|
+
if (slash === -1) return { provider: "anthropic", id: modelRef };
|
|
222
214
|
const provider = modelRef.slice(0, slash);
|
|
223
215
|
const id = modelRef.slice(slash + 1);
|
|
224
216
|
if (provider.length === 0 || id.length === 0 || id.startsWith("/")) {
|
|
225
217
|
throw new Error(`Invalid agent model ref "${modelRef}": expected "provider/id".`);
|
|
226
218
|
}
|
|
227
|
-
return {
|
|
219
|
+
return { provider, id };
|
|
228
220
|
}
|
|
229
221
|
|
|
230
222
|
interface AgentSessionResources {
|
|
@@ -282,6 +274,10 @@ async function prepareAgentSessionResources(input: {
|
|
|
282
274
|
for (const diagnostic of services.diagnostics) {
|
|
283
275
|
rc.progress.log(`${label}: session ${diagnostic.type}: ${diagnostic.message}`);
|
|
284
276
|
}
|
|
277
|
+
if (!services.settingsManager.getRetryEnabled() && !retryDisabledWarnedRuns.has(rc.progress)) {
|
|
278
|
+
retryDisabledWarnedRuns.add(rc.progress);
|
|
279
|
+
rc.progress.log(RETRY_DISABLED_WARNING);
|
|
280
|
+
}
|
|
285
281
|
const selectedSkills = preparedSkills.resolve(services.resourceLoader);
|
|
286
282
|
return {
|
|
287
283
|
selectedSkills,
|
|
@@ -322,16 +318,8 @@ function buildToolSelection(opts: AgentExecutionOptions, skillsEnabled: boolean)
|
|
|
322
318
|
? ["read"]
|
|
323
319
|
: undefined;
|
|
324
320
|
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
321
|
return {
|
|
334
|
-
sessionOptions: { noTools: "builtin" },
|
|
322
|
+
sessionOptions: toolHints.length === 0 ? { tools: activeTools } : { noTools: "builtin" },
|
|
335
323
|
activeTools,
|
|
336
324
|
toolHints,
|
|
337
325
|
};
|
|
@@ -350,11 +338,16 @@ function buildToolList(
|
|
|
350
338
|
return allow;
|
|
351
339
|
}
|
|
352
340
|
|
|
341
|
+
function withholdWorkflowTool(session: AgentRunnerSession): void {
|
|
342
|
+
const active = session.getActiveToolNames();
|
|
343
|
+
if (active.includes(WORKFLOW_TOOL_NAME)) session.setActiveToolsByName(active.filter((name) => name !== WORKFLOW_TOOL_NAME));
|
|
344
|
+
}
|
|
345
|
+
|
|
353
346
|
function applyDynamicToolHints(
|
|
354
347
|
session: AgentRunnerSession,
|
|
355
348
|
selection: ToolSelection,
|
|
356
349
|
): ReadonlySet<AgentToolHint> {
|
|
357
|
-
const active = new Set(selection.activeTools
|
|
350
|
+
const active = new Set(selection.activeTools);
|
|
358
351
|
const matched = new Set<AgentToolHint>();
|
|
359
352
|
for (const tool of session.getAllTools()) {
|
|
360
353
|
for (const hint of selection.toolHints) {
|
|
@@ -62,12 +62,13 @@ export function resolveAgentSkillRequest(prompt: string, explicitSkills: unknown
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export function extractSkillSelectorsFromText(text: string): string[] {
|
|
65
|
-
const selectors
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
const selectors = [
|
|
66
|
+
...Array.from(text.matchAll(SLASH_SKILL_PATTERN), (match) => match[1] ?? ""),
|
|
67
|
+
...[...text.matchAll(SKILLS_FIELD_PATTERN), ...text.matchAll(VERB_SKILL_LIST_PATTERN)]
|
|
68
|
+
.flatMap((match) => parseSkillList(match[1] ?? "")),
|
|
69
|
+
...Array.from(text.matchAll(VERB_NAMED_SKILL_PATTERN), (match) => match[2] ?? ""),
|
|
70
|
+
].map(normalizeSkillSelector);
|
|
71
|
+
return [...new Set(selectors.filter((value) => value && !isGenericSkillWord(value)))];
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
export function normalizeSkillSelector(value: string): string {
|
|
@@ -179,23 +180,6 @@ export async function captureAgentSkillIdentities(
|
|
|
179
180
|
return { kind: "verified", skills: identities };
|
|
180
181
|
}
|
|
181
182
|
|
|
182
|
-
function collectPatternMatches(text: string, pattern: RegExp, selectors: string[], captureIndex: number): void {
|
|
183
|
-
pattern.lastIndex = 0;
|
|
184
|
-
for (let match = pattern.exec(text); match !== null; match = pattern.exec(text)) {
|
|
185
|
-
const value = match[captureIndex];
|
|
186
|
-
if (value !== undefined) selectors.push(value);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function collectListPatternMatches(text: string, pattern: RegExp, selectors: string[]): void {
|
|
191
|
-
pattern.lastIndex = 0;
|
|
192
|
-
for (let match = pattern.exec(text); match !== null; match = pattern.exec(text)) {
|
|
193
|
-
const value = match[1];
|
|
194
|
-
if (value === undefined) continue;
|
|
195
|
-
selectors.push(...parseSkillList(value));
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
183
|
function parseSkillList(fragment: string): string[] {
|
|
200
184
|
const scoped = fragment.replace(PURPOSE_BOUNDARY_PATTERN, "");
|
|
201
185
|
return scoped
|
|
@@ -208,9 +192,7 @@ function normalizeExplicitSkillSelectors(value: unknown): string[] {
|
|
|
208
192
|
if (!Array.isArray(value)) {
|
|
209
193
|
throw new Error("Invalid subagent skills option: expected an array of skill names.");
|
|
210
194
|
}
|
|
211
|
-
const
|
|
212
|
-
const result: string[] = [];
|
|
213
|
-
for (const item of value) {
|
|
195
|
+
const selectors = value.map((item: unknown) => {
|
|
214
196
|
if (typeof item !== "string") {
|
|
215
197
|
throw new Error("Invalid subagent skills option: every skill name must be a string.");
|
|
216
198
|
}
|
|
@@ -218,11 +200,9 @@ function normalizeExplicitSkillSelectors(value: unknown): string[] {
|
|
|
218
200
|
if (!normalized || isGenericSkillWord(normalized)) {
|
|
219
201
|
throw new Error(`Invalid subagent skills option: \"${item}\" is not a valid skill name.`);
|
|
220
202
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
return result;
|
|
203
|
+
return normalized;
|
|
204
|
+
});
|
|
205
|
+
return [...new Set(selectors)];
|
|
226
206
|
}
|
|
227
207
|
|
|
228
208
|
function logSkillDiagnostics(log: ((message: string) => void) | undefined, diagnostics: readonly ResourceDiagnostic[], requested: boolean): void {
|
|
@@ -233,18 +213,6 @@ function logSkillDiagnostics(log: ((message: string) => void) | undefined, diagn
|
|
|
233
213
|
}
|
|
234
214
|
}
|
|
235
215
|
|
|
236
|
-
function uniqueSelectors(values: Iterable<string>): string[] {
|
|
237
|
-
const seen = new Set<string>();
|
|
238
|
-
const result: string[] = [];
|
|
239
|
-
for (const value of values) {
|
|
240
|
-
const normalized = normalizeSkillSelector(value);
|
|
241
|
-
if (!normalized || isGenericSkillWord(normalized) || seen.has(normalized)) continue;
|
|
242
|
-
seen.add(normalized);
|
|
243
|
-
result.push(normalized);
|
|
244
|
-
}
|
|
245
|
-
return result;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
216
|
function isGenericSkillWord(value: string): boolean {
|
|
249
217
|
return value === "skill" || value === "skills" || value === "the" || value === "this" || value === "these" || value === "that";
|
|
250
218
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { AgentExecutionOptions, AgentProgress } from "./agent-runner-types.ts";
|
|
2
|
-
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
3
2
|
import type { WorktreeRegistry } from "./worktree.ts";
|
|
4
3
|
|
|
5
4
|
interface AgentWorkspaceBase {
|
|
@@ -21,7 +20,7 @@ export type AgentWorkspace = SharedAgentWorkspace | IsolatedAgentWorkspace;
|
|
|
21
20
|
|
|
22
21
|
export interface AgentWorkspaceContext {
|
|
23
22
|
readonly cwd: string;
|
|
24
|
-
readonly worktrees: Pick<WorktreeRegistry, "probe" | "add" | "capturePatch" | "remove">;
|
|
23
|
+
readonly worktrees: Pick<WorktreeRegistry, "probe" | "add" | "capturePatch" | "applyPatch" | "remove">;
|
|
25
24
|
readonly signal: AbortSignal | undefined;
|
|
26
25
|
readonly progress: Pick<AgentProgress, "log">;
|
|
27
26
|
}
|
|
@@ -31,6 +30,7 @@ export async function createAgentWorkspace(
|
|
|
31
30
|
opts: AgentExecutionOptions,
|
|
32
31
|
label: string,
|
|
33
32
|
): Promise<AgentWorkspace> {
|
|
33
|
+
if (opts.candidatePatch && opts.isolation !== "worktree") throw new Error("Candidate evaluation requires worktree isolation");
|
|
34
34
|
if (opts.isolation !== "worktree") {
|
|
35
35
|
return {
|
|
36
36
|
kind: "shared",
|
|
@@ -44,7 +44,7 @@ export async function createAgentWorkspace(
|
|
|
44
44
|
|
|
45
45
|
const probe = await rc.worktrees.probe(rc.signal);
|
|
46
46
|
if (!probe.ok) {
|
|
47
|
-
throw new Error(`Failed to check git worktree availability for isolated agent: ${probe.error
|
|
47
|
+
throw new Error(`Failed to check git worktree availability for isolated agent: ${probe.error}`);
|
|
48
48
|
}
|
|
49
49
|
if (!probe.inside) {
|
|
50
50
|
throw new Error("Agent requested worktree isolation, but the workflow cwd is not inside a git work tree.");
|
|
@@ -53,6 +53,19 @@ export async function createAgentWorkspace(
|
|
|
53
53
|
const added = await rc.worktrees.add(rc.signal, opts.worktreeBaseline);
|
|
54
54
|
if ("error" in added) throw new Error(`Failed to create isolated worktree: ${added.error}`);
|
|
55
55
|
const worktreePath = added.path;
|
|
56
|
+
if (opts.candidatePatch) {
|
|
57
|
+
try {
|
|
58
|
+
if (opts.candidatePatch.baselineOid !== added.baselineOid) throw new Error("Candidate baseline differs from evaluator baseline");
|
|
59
|
+
if (opts.candidatePatch.patch.trim()) {
|
|
60
|
+
const applied = await rc.worktrees.applyPatch(worktreePath, opts.candidatePatch.patch, rc.signal);
|
|
61
|
+
if (!applied.ok) throw new Error(`Candidate patch could not be applied: ${applied.error}`);
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const removed = await rc.worktrees.remove(worktreePath);
|
|
65
|
+
if (!removed.ok) rc.progress.log(`${label}: evaluator setup cleanup failed: ${removed.error}`);
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
56
69
|
rc.progress.log(`${label}: using isolated worktree ${worktreePath}`);
|
|
57
70
|
|
|
58
71
|
return {
|
|
@@ -62,28 +75,15 @@ export async function createAgentWorkspace(
|
|
|
62
75
|
async wrapResult(result) {
|
|
63
76
|
const patch = await rc.worktrees.capturePatch(worktreePath, added.baselineOid, rc.signal);
|
|
64
77
|
if ("error" in patch) throw new Error(`Failed to capture isolated worktree patch: ${patch.error}`);
|
|
65
|
-
return { result, patch: patch.patch, changed: patch.changed };
|
|
78
|
+
return { result, patch: patch.patch, changed: patch.changed, baselineOid: added.baselineOid };
|
|
66
79
|
},
|
|
67
80
|
async dispose() {
|
|
68
81
|
const removed = await rc.worktrees.remove(worktreePath);
|
|
69
82
|
if (!removed.ok) {
|
|
70
83
|
rc.progress.log(
|
|
71
|
-
`${label}: failed to remove isolated worktree (${removed.error
|
|
84
|
+
`${label}: failed to remove isolated worktree (${removed.error})`,
|
|
72
85
|
);
|
|
73
86
|
}
|
|
74
87
|
},
|
|
75
88
|
};
|
|
76
89
|
}
|
|
77
|
-
|
|
78
|
-
export async function disposeAgentWorkspace(
|
|
79
|
-
rc: Pick<AgentWorkspaceContext, "progress">,
|
|
80
|
-
label: string,
|
|
81
|
-
workspace: AgentWorkspace | undefined,
|
|
82
|
-
): Promise<void> {
|
|
83
|
-
if (!workspace) return;
|
|
84
|
-
try {
|
|
85
|
-
await workspace.dispose();
|
|
86
|
-
} catch (error) {
|
|
87
|
-
rc.progress.log(`${label}: failed to dispose isolated workspace (${unknownErrorMessage(error)})`);
|
|
88
|
-
}
|
|
89
|
-
}
|