pi-workflow-engine 0.11.0 → 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 +255 -97
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +0 -4
- package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +3 -1
- package/.pi/extensions/pi-workflow-engine/src/agent-limits.ts +35 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +27 -4
- package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +116 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +49 -45
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +95 -26
- package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +158 -203
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +48 -42
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +11 -3
- 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 +3 -3
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +38 -0
- package/.pi/extensions/pi-workflow-engine/src/command-completions.ts +36 -0
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +70 -4
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +16 -130
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +9 -5
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +109 -60
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +166 -18
- package/.pi/extensions/pi-workflow-engine/src/filesystem-error.ts +4 -0
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +2 -2
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +12 -6
- 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 +88 -1
- package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +32 -0
- package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +72 -117
- package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +189 -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 +74 -38
- package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +49 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +2 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +11 -30
- package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +5 -5
- package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +3 -4
- 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 +1 -4
- package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +1 -4
- package/.pi/extensions/pi-workflow-engine/src/types.ts +49 -15
- 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 +2 -1
- 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/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 +1 -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 +2 -0
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +40 -3
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +18 -42
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +10 -17
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +8 -15
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +8 -15
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +170 -0
- package/README.md +72 -141
- package/USAGE.md +211 -17
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
createAgentSessionFromServices,
|
|
4
|
+
createAgentSessionServices,
|
|
4
5
|
defineTool,
|
|
5
6
|
SessionManager,
|
|
6
7
|
type CreateAgentSessionOptions,
|
|
@@ -8,32 +9,28 @@ import {
|
|
|
8
9
|
type Skill,
|
|
9
10
|
type ToolDefinition,
|
|
10
11
|
} from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import {
|
|
12
|
-
appendSkillReminder,
|
|
13
|
-
createAgentSkillResourceLoader,
|
|
14
|
-
extractSkillSelectorsFromText,
|
|
15
|
-
} from "./agent-skills.ts";
|
|
12
|
+
import { prepareAgentSkillResources } from "./agent-skills.ts";
|
|
16
13
|
import type {
|
|
17
14
|
AgentExecutionOptions,
|
|
18
15
|
AgentRunTags,
|
|
19
16
|
AgentRunnerSession,
|
|
20
|
-
AgentRunnerToolInfo,
|
|
21
17
|
RunContext,
|
|
22
18
|
} from "./agent-runner-types.ts";
|
|
19
|
+
import { raceWithAbort, throwIfAborted } from "./cancellation.ts";
|
|
23
20
|
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
} from "./agent-
|
|
28
|
-
import {
|
|
21
|
+
MAX_SCHEMA_REPAIR_ATTEMPTS,
|
|
22
|
+
WorkflowStructuredOutputError,
|
|
23
|
+
} from "./structured-output.ts";
|
|
24
|
+
import { providerErrorFromMessages } from "./agent-retry.ts";
|
|
25
|
+
import { synchronizeWorkflowModelRuntime } from "./agent-session-providers.ts";
|
|
26
|
+
import { matchesAgentToolHint, WorkflowToolHintUnavailableError } from "./tool-capabilities.ts";
|
|
27
|
+
import type { AgentToolHint } from "./types.ts";
|
|
29
28
|
|
|
30
29
|
export const FINAL_TOOL = "final_answer";
|
|
31
30
|
|
|
32
|
-
const SCHEMA_REPROMPT_ATTEMPTS = 1;
|
|
33
31
|
const SCHEMA_REPROMPT =
|
|
34
32
|
`You ended your turn without calling the ${FINAL_TOOL} tool, so no result was recorded. ` +
|
|
35
33
|
`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
34
|
const DEFAULT_SEARCH_BASE_TOOLS = ["read", "bash", "grep", "find", "ls"];
|
|
38
35
|
|
|
39
36
|
export interface ResolvedAgentModelRequest {
|
|
@@ -50,13 +47,10 @@ export interface ResolvedAgentModel {
|
|
|
50
47
|
export interface AgentSessionHandle {
|
|
51
48
|
readonly session: AgentRunnerSession;
|
|
52
49
|
readonly selectedSkills: readonly Skill[];
|
|
50
|
+
hasStructuredResult(): boolean;
|
|
53
51
|
structuredResult(): unknown;
|
|
54
52
|
}
|
|
55
53
|
|
|
56
|
-
export type EffectiveSessionCapture =
|
|
57
|
-
| { readonly kind: "verified"; readonly identity: EffectiveAgentSessionIdentity }
|
|
58
|
-
| { readonly kind: "unverifiable"; readonly reason: string };
|
|
59
|
-
|
|
60
54
|
export function resolveAgentModel(
|
|
61
55
|
modelRef: string | undefined,
|
|
62
56
|
modelRegistry: Pick<ModelRegistry, "find">,
|
|
@@ -82,22 +76,8 @@ export async function openAgentSession(input: {
|
|
|
82
76
|
readonly tags: AgentRunTags;
|
|
83
77
|
}): Promise<AgentSessionHandle> {
|
|
84
78
|
const { rc, prompt, opts, cwd, model, label, tags } = input;
|
|
85
|
-
|
|
86
|
-
|
|
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;
|
|
79
|
+
let captured = false;
|
|
80
|
+
let structuredResult: unknown;
|
|
101
81
|
const customTools: ToolDefinition[] = opts.schema
|
|
102
82
|
? [
|
|
103
83
|
defineTool({
|
|
@@ -107,47 +87,47 @@ export async function openAgentSession(input: {
|
|
|
107
87
|
"Return your final structured answer. This MUST be your last action — do not write a normal reply after calling it.",
|
|
108
88
|
parameters: opts.schema,
|
|
109
89
|
async execute(_toolCallId, params) {
|
|
110
|
-
captured =
|
|
90
|
+
captured = true;
|
|
91
|
+
structuredResult = params;
|
|
111
92
|
return { content: [{ type: "text", text: "Recorded." }], details: params, terminate: true };
|
|
112
93
|
},
|
|
113
94
|
}),
|
|
114
95
|
]
|
|
115
96
|
: [];
|
|
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
97
|
let session: AgentRunnerSession | undefined;
|
|
136
98
|
try {
|
|
137
99
|
throwIfAborted(rc.signal);
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
rc.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
100
|
+
const resourceInput = { rc, prompt, opts, cwd, model, customTools, label };
|
|
101
|
+
const resources = rc.createSession
|
|
102
|
+
? await prepareAgentSessionResources(resourceInput)
|
|
103
|
+
: await rc.perf.time(
|
|
104
|
+
"agent.session_resources_ms",
|
|
105
|
+
() => prepareAgentSessionResources(resourceInput),
|
|
106
|
+
tags,
|
|
107
|
+
);
|
|
108
|
+
const toolSelection = buildToolSelection(opts, resources.selectedSkills.length > 0);
|
|
109
|
+
session = (
|
|
110
|
+
await rc.perf.time(
|
|
111
|
+
"agent.create_session_ms",
|
|
112
|
+
() => resources.createSession(toolSelection.sessionOptions),
|
|
113
|
+
tags,
|
|
114
|
+
)
|
|
115
|
+
).session;
|
|
116
|
+
session.setAutoRetryEnabled(false);
|
|
117
|
+
const matchedToolHints = toolSelection.toolHints.length === 0
|
|
118
|
+
? new Set<AgentToolHint>()
|
|
119
|
+
: applyDynamicToolHints(session, toolSelection);
|
|
120
|
+
if (opts.requireToolHints) {
|
|
121
|
+
const missing = toolSelection.toolHints.filter((hint) => !matchedToolHints.has(hint));
|
|
122
|
+
if (missing.length > 0) throw new WorkflowToolHintUnavailableError(missing);
|
|
148
123
|
}
|
|
149
124
|
throwIfAborted(rc.signal);
|
|
150
|
-
return {
|
|
125
|
+
return {
|
|
126
|
+
session,
|
|
127
|
+
selectedSkills: resources.selectedSkills,
|
|
128
|
+
hasStructuredResult: () => captured,
|
|
129
|
+
structuredResult: () => structuredResult,
|
|
130
|
+
};
|
|
151
131
|
} catch (error) {
|
|
152
132
|
if (session) {
|
|
153
133
|
const activeSession = session;
|
|
@@ -168,7 +148,7 @@ export async function promptAgentSession(input: {
|
|
|
168
148
|
readonly tags: AgentRunTags;
|
|
169
149
|
}): Promise<unknown> {
|
|
170
150
|
const { rc, handle, prompt, opts, label, rowId, tags } = input;
|
|
171
|
-
const { session
|
|
151
|
+
const { session } = handle;
|
|
172
152
|
const unsubscribe = session.subscribe((event) => {
|
|
173
153
|
if (event.type === "tool_execution_start" && event.toolName !== undefined && event.toolName !== FINAL_TOOL) {
|
|
174
154
|
rc.progress.agentTool(label, event.toolName, rowId);
|
|
@@ -176,21 +156,28 @@ export async function promptAgentSession(input: {
|
|
|
176
156
|
});
|
|
177
157
|
|
|
178
158
|
try {
|
|
179
|
-
const promptedWithSkills = appendSkillReminder(prompt, selectedSkills);
|
|
180
159
|
const finalPrompt = opts.schema
|
|
181
|
-
? `${
|
|
182
|
-
:
|
|
160
|
+
? `${prompt}\n\nWhen finished, return your result by calling the ${FINAL_TOOL} tool.`
|
|
161
|
+
: prompt;
|
|
183
162
|
throwIfAborted(rc.signal);
|
|
184
163
|
const unlinkPromptAbort = linkSessionAbort(rc.signal, session);
|
|
185
164
|
try {
|
|
186
165
|
try {
|
|
187
|
-
|
|
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);
|
|
188
174
|
if (opts.schema) {
|
|
189
|
-
for (let attempt = 0; handle.
|
|
175
|
+
for (let attempt = 0; !handle.hasStructuredResult() && attempt < MAX_SCHEMA_REPAIR_ATTEMPTS; attempt++) {
|
|
190
176
|
throwIfAborted(rc.signal);
|
|
191
|
-
|
|
177
|
+
session.setActiveToolsByName([FINAL_TOOL]);
|
|
178
|
+
rc.progress.log(`${label}: no final answer; re-prompting (${attempt + 1}/${MAX_SCHEMA_REPAIR_ATTEMPTS})`);
|
|
192
179
|
rc.perf.counter("agent.structured_reprompt", 1, tags);
|
|
193
|
-
await
|
|
180
|
+
await promptSession(SCHEMA_REPROMPT);
|
|
194
181
|
}
|
|
195
182
|
}
|
|
196
183
|
|
|
@@ -198,18 +185,20 @@ export async function promptAgentSession(input: {
|
|
|
198
185
|
return rc.perf.timeSync(
|
|
199
186
|
"agent.extract_result_ms",
|
|
200
187
|
() => {
|
|
201
|
-
if (!opts.schema)
|
|
202
|
-
|
|
203
|
-
|
|
188
|
+
if (!opts.schema) {
|
|
189
|
+
return session.getLastAssistantText() ?? "";
|
|
190
|
+
}
|
|
191
|
+
if (!handle.hasStructuredResult()) {
|
|
204
192
|
rc.progress.log(`${label}: no structured answer returned`);
|
|
205
193
|
rc.perf.counter("agent.structured_missing", 1, tags);
|
|
194
|
+
throw new WorkflowStructuredOutputError(label, MAX_SCHEMA_REPAIR_ATTEMPTS);
|
|
206
195
|
}
|
|
207
|
-
return
|
|
196
|
+
return handle.structuredResult();
|
|
208
197
|
},
|
|
209
198
|
tags,
|
|
210
199
|
);
|
|
211
200
|
} finally {
|
|
212
|
-
rc.usage.recordAgentSession({ label, phase: tags.phase, messages: session.
|
|
201
|
+
rc.usage.recordAgentSession({ label, phase: tags.phase, messages: session.messages });
|
|
213
202
|
}
|
|
214
203
|
} finally {
|
|
215
204
|
unlinkPromptAbort();
|
|
@@ -219,38 +208,6 @@ export async function promptAgentSession(input: {
|
|
|
219
208
|
}
|
|
220
209
|
}
|
|
221
210
|
|
|
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
211
|
function parseAgentModelRef(modelRef: string): ResolvedAgentModelRequest {
|
|
255
212
|
const normalized = modelRef.trim();
|
|
256
213
|
if (normalized.length === 0) {
|
|
@@ -270,51 +227,111 @@ function parseAgentModelRef(modelRef: string): ResolvedAgentModelRequest {
|
|
|
270
227
|
return { ref: modelRef, provider, id };
|
|
271
228
|
}
|
|
272
229
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
230
|
+
interface AgentSessionResources {
|
|
231
|
+
readonly selectedSkills: readonly Skill[];
|
|
232
|
+
createSession(sessionOptions: ToolSessionOptions): Promise<{ session: AgentRunnerSession }>;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function prepareAgentSessionResources(input: {
|
|
236
|
+
readonly rc: RunContext;
|
|
237
|
+
readonly prompt: string;
|
|
238
|
+
readonly opts: AgentExecutionOptions;
|
|
239
|
+
readonly cwd: string;
|
|
240
|
+
readonly model: Model<Api> | undefined;
|
|
241
|
+
readonly customTools: ToolDefinition[];
|
|
242
|
+
readonly label: string;
|
|
243
|
+
}): Promise<AgentSessionResources> {
|
|
244
|
+
const { rc, prompt, opts, cwd, model, customTools, label } = input;
|
|
245
|
+
const commonSessionOptions = (sessionOptions: ToolSessionOptions) => ({
|
|
246
|
+
model,
|
|
247
|
+
thinkingLevel: opts.thinkingLevel,
|
|
248
|
+
noTools: sessionOptions.noTools,
|
|
249
|
+
tools: sessionOptions.tools,
|
|
250
|
+
customTools,
|
|
251
|
+
sessionManager: SessionManager.inMemory(cwd),
|
|
252
|
+
});
|
|
253
|
+
const createSession = rc.createSession;
|
|
254
|
+
if (createSession) {
|
|
255
|
+
return {
|
|
256
|
+
selectedSkills: [],
|
|
257
|
+
createSession: (sessionOptions) =>
|
|
258
|
+
createSession({
|
|
259
|
+
cwd,
|
|
260
|
+
...commonSessionOptions(sessionOptions),
|
|
261
|
+
}),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const skillOptions = {
|
|
266
|
+
prompt,
|
|
267
|
+
skills: opts.skills,
|
|
268
|
+
log: (message: string) => rc.progress.log(`${label}: ${message}`),
|
|
269
|
+
};
|
|
270
|
+
const preparedSkills = prepareAgentSkillResources(skillOptions);
|
|
271
|
+
const services = await createAgentSessionServices({
|
|
272
|
+
cwd,
|
|
273
|
+
resourceLoaderOptions: preparedSkills.resourceLoaderOptions,
|
|
274
|
+
});
|
|
275
|
+
await synchronizeWorkflowModelRuntime({
|
|
276
|
+
host: rc.modelRegistry,
|
|
277
|
+
child: services.modelRuntime,
|
|
278
|
+
selectedModel: model,
|
|
279
|
+
// Shared-cwd sessions mirror live removals; isolated cwd sessions retain target-only providers.
|
|
280
|
+
removeChildOnlyProviders: cwd === rc.cwd,
|
|
281
|
+
});
|
|
282
|
+
for (const diagnostic of services.diagnostics) {
|
|
283
|
+
rc.progress.log(`${label}: session ${diagnostic.type}: ${diagnostic.message}`);
|
|
284
|
+
}
|
|
285
|
+
const selectedSkills = preparedSkills.resolve(services.resourceLoader);
|
|
286
|
+
return {
|
|
287
|
+
selectedSkills,
|
|
288
|
+
createSession: (sessionOptions) =>
|
|
289
|
+
createAgentSessionFromServices({
|
|
290
|
+
services,
|
|
291
|
+
...commonSessionOptions(sessionOptions),
|
|
292
|
+
}),
|
|
293
|
+
};
|
|
276
294
|
}
|
|
277
295
|
|
|
278
296
|
function linkSessionAbort(signal: AbortSignal | undefined, session: AgentRunnerSession): () => void {
|
|
279
297
|
if (!signal) return () => {};
|
|
298
|
+
const onAbort = () => {
|
|
299
|
+
void session.abort().catch(() => undefined);
|
|
300
|
+
};
|
|
280
301
|
if (signal.aborted) {
|
|
281
|
-
|
|
302
|
+
onAbort();
|
|
282
303
|
return () => {};
|
|
283
304
|
}
|
|
284
|
-
const onAbort = () => {
|
|
285
|
-
void session.abort();
|
|
286
|
-
};
|
|
287
305
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
288
306
|
return () => signal.removeEventListener("abort", onAbort);
|
|
289
307
|
}
|
|
290
308
|
|
|
291
|
-
type ToolSessionOptions = Pick<CreateAgentSessionOptions, "tools" | "
|
|
309
|
+
type ToolSessionOptions = Pick<CreateAgentSessionOptions, "tools" | "noTools">;
|
|
292
310
|
|
|
293
311
|
interface ToolSelection {
|
|
294
312
|
readonly sessionOptions: ToolSessionOptions;
|
|
295
|
-
readonly fallbackSessionOptions: ToolSessionOptions;
|
|
296
313
|
readonly activeTools?: readonly string[];
|
|
297
314
|
readonly toolHints: NonNullable<AgentExecutionOptions["toolHints"]>;
|
|
298
315
|
}
|
|
299
316
|
|
|
300
|
-
interface DynamicToolSession extends AgentRunnerSession {
|
|
301
|
-
getAllTools(): readonly AgentRunnerToolInfo[];
|
|
302
|
-
setActiveToolsByName(toolNames: readonly string[]): void;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
317
|
function buildToolSelection(opts: AgentExecutionOptions, skillsEnabled: boolean): ToolSelection {
|
|
306
318
|
const toolHints = opts.toolHints ?? [];
|
|
307
|
-
const fallback = toolHints.includes("search")
|
|
319
|
+
const fallback = toolHints.includes("search")
|
|
320
|
+
? DEFAULT_SEARCH_BASE_TOOLS
|
|
321
|
+
: toolHints.includes("external-search")
|
|
322
|
+
? ["read"]
|
|
323
|
+
: undefined;
|
|
308
324
|
const activeTools = buildToolList(opts, skillsEnabled, fallback);
|
|
309
|
-
const strictSessionOptions = { tools: activeTools ? [...activeTools] : undefined };
|
|
310
325
|
if (toolHints.length === 0) {
|
|
311
|
-
return {
|
|
326
|
+
return {
|
|
327
|
+
sessionOptions: { tools: activeTools ? [...activeTools] : undefined },
|
|
328
|
+
activeTools,
|
|
329
|
+
toolHints,
|
|
330
|
+
};
|
|
312
331
|
}
|
|
313
332
|
|
|
314
|
-
const explicitlyActive = new Set(activeTools ?? []);
|
|
315
333
|
return {
|
|
316
|
-
sessionOptions: {
|
|
317
|
-
fallbackSessionOptions: { tools: activeTools ? [...activeTools] : [] },
|
|
334
|
+
sessionOptions: { noTools: "builtin" },
|
|
318
335
|
activeTools,
|
|
319
336
|
toolHints,
|
|
320
337
|
};
|
|
@@ -333,81 +350,19 @@ function buildToolList(
|
|
|
333
350
|
return allow;
|
|
334
351
|
}
|
|
335
352
|
|
|
336
|
-
function applyDynamicToolHints(
|
|
337
|
-
|
|
338
|
-
|
|
353
|
+
function applyDynamicToolHints(
|
|
354
|
+
session: AgentRunnerSession,
|
|
355
|
+
selection: ToolSelection,
|
|
356
|
+
): ReadonlySet<AgentToolHint> {
|
|
339
357
|
const active = new Set(selection.activeTools ?? []);
|
|
358
|
+
const matched = new Set<AgentToolHint>();
|
|
340
359
|
for (const tool of session.getAllTools()) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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}` };
|
|
360
|
+
for (const hint of selection.toolHints) {
|
|
361
|
+
if (!matchesAgentToolHint(tool, hint)) continue;
|
|
362
|
+
matched.add(hint);
|
|
363
|
+
active.add(tool.name);
|
|
371
364
|
}
|
|
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
365
|
}
|
|
380
|
-
|
|
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
|
-
);
|
|
366
|
+
session.setActiveToolsByName([...active]);
|
|
367
|
+
return matched;
|
|
413
368
|
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type {
|
|
2
|
+
CreateAgentSessionServicesOptions,
|
|
3
|
+
ResourceDiagnostic,
|
|
4
|
+
ResourceLoader,
|
|
5
|
+
Skill,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
2
7
|
import { dirname, resolve } from "node:path";
|
|
3
8
|
import { logicalWorkspacePath, type ReplayWorkspaceRoots } from "./replay-path-identity.ts";
|
|
4
9
|
import { captureTreeFingerprint } from "./tree-fingerprint.ts";
|
|
5
10
|
import type { ResolvedSkillIdentity } from "./resume-context.ts";
|
|
6
11
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export interface AgentSkillRequest {
|
|
12
|
+
interface AgentSkillRequest {
|
|
10
13
|
readonly selectors: readonly string[];
|
|
11
|
-
readonly source: AgentSkillRequestSource;
|
|
12
14
|
readonly strict: boolean;
|
|
13
15
|
}
|
|
14
16
|
|
|
@@ -17,11 +19,14 @@ export interface AgentSkillResolution {
|
|
|
17
19
|
readonly unmatched: readonly string[];
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
type AgentSkillResourceLoaderOptions = Pick<
|
|
23
|
+
NonNullable<CreateAgentSessionServicesOptions["resourceLoaderOptions"]>,
|
|
24
|
+
"noSkills" | "skillsOverride"
|
|
25
|
+
>;
|
|
26
|
+
|
|
27
|
+
interface PreparedAgentSkillResources {
|
|
28
|
+
readonly resourceLoaderOptions: AgentSkillResourceLoaderOptions;
|
|
29
|
+
resolve(resourceLoader: Pick<ResourceLoader, "getSkills">): readonly Skill[];
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
export type AgentSkillIdentityCapture =
|
|
@@ -51,9 +56,9 @@ const PURPOSE_BOUNDARY_PATTERN = /\s+\b(?:for|to|when|while|so|because|in\s+orde
|
|
|
51
56
|
*/
|
|
52
57
|
export function resolveAgentSkillRequest(prompt: string, explicitSkills: unknown): AgentSkillRequest {
|
|
53
58
|
if (explicitSkills !== undefined) {
|
|
54
|
-
return { selectors: normalizeExplicitSkillSelectors(explicitSkills),
|
|
59
|
+
return { selectors: normalizeExplicitSkillSelectors(explicitSkills), strict: true };
|
|
55
60
|
}
|
|
56
|
-
return { selectors: extractSkillSelectorsFromText(prompt),
|
|
61
|
+
return { selectors: extractSkillSelectorsFromText(prompt), strict: false };
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
export function extractSkillSelectorsFromText(text: string): string[] {
|
|
@@ -98,19 +103,21 @@ export function selectAgentSkills(skills: readonly Skill[], selectors: readonly
|
|
|
98
103
|
return { selected: [...selected.values()], unmatched };
|
|
99
104
|
}
|
|
100
105
|
|
|
101
|
-
|
|
102
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Prepare the skill-selection hooks consumed by pi's session-service factory.
|
|
108
|
+
* The returned resolver is scoped to one resource loader and must not be reused
|
|
109
|
+
* across concurrently-created session services.
|
|
110
|
+
*/
|
|
111
|
+
export function prepareAgentSkillResources(options: {
|
|
103
112
|
readonly prompt: string;
|
|
104
113
|
readonly skills: unknown;
|
|
105
114
|
readonly log?: (message: string) => void;
|
|
106
|
-
}):
|
|
115
|
+
}): PreparedAgentSkillResources {
|
|
107
116
|
const request = resolveAgentSkillRequest(options.prompt, options.skills);
|
|
108
117
|
let resolution: AgentSkillResolution = { selected: [], unmatched: [] };
|
|
109
118
|
let availableSkillNames: readonly string[] = [];
|
|
110
119
|
|
|
111
|
-
const
|
|
112
|
-
cwd: options.cwd,
|
|
113
|
-
agentDir: getAgentDir(),
|
|
120
|
+
const resourceLoaderOptions: AgentSkillResourceLoaderOptions = {
|
|
114
121
|
noSkills: request.selectors.length === 0,
|
|
115
122
|
skillsOverride:
|
|
116
123
|
request.selectors.length === 0
|
|
@@ -120,30 +127,29 @@ export async function createAgentSkillResourceLoader(options: {
|
|
|
120
127
|
resolution = selectAgentSkills(current.skills, request.selectors);
|
|
121
128
|
return { skills: [...resolution.selected], diagnostics: current.diagnostics };
|
|
122
129
|
},
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return `${prompt}\n\nWorkflow subagent skills enabled: ${skillList}. If you rely on an enabled skill, first read its SKILL.md with the read tool and follow its instructions. No other skills are available in this subagent.`;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
resourceLoaderOptions,
|
|
134
|
+
resolve(resourceLoader) {
|
|
135
|
+
const { diagnostics } = resourceLoader.getSkills();
|
|
136
|
+
logSkillDiagnostics(options.log, diagnostics, request.selectors.length > 0);
|
|
137
|
+
|
|
138
|
+
if (request.strict && resolution.unmatched.length > 0) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Unknown subagent skill${resolution.unmatched.length === 1 ? "" : "s"}: ${resolution.unmatched.join(", ")}. Available: ${
|
|
141
|
+
availableSkillNames.join(", ") || "(none)"
|
|
142
|
+
}.`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!request.strict && resolution.unmatched.length > 0) {
|
|
147
|
+
options.log?.(`Ignoring unrecognized subagent skill mention${resolution.unmatched.length === 1 ? "" : "s"}: ${resolution.unmatched.join(", ")}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return resolution.selected;
|
|
151
|
+
},
|
|
152
|
+
};
|
|
147
153
|
}
|
|
148
154
|
|
|
149
155
|
/** Capture the exact selected skill directories before journal lookup. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { AgentExecutionOptions,
|
|
1
|
+
import type { AgentExecutionOptions, AgentProgress } from "./agent-runner-types.ts";
|
|
2
2
|
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
3
|
+
import type { WorktreeRegistry } from "./worktree.ts";
|
|
3
4
|
|
|
4
5
|
interface AgentWorkspaceBase {
|
|
5
6
|
readonly cwd: string;
|
|
@@ -18,8 +19,15 @@ export interface IsolatedAgentWorkspace extends AgentWorkspaceBase {
|
|
|
18
19
|
|
|
19
20
|
export type AgentWorkspace = SharedAgentWorkspace | IsolatedAgentWorkspace;
|
|
20
21
|
|
|
22
|
+
export interface AgentWorkspaceContext {
|
|
23
|
+
readonly cwd: string;
|
|
24
|
+
readonly worktrees: Pick<WorktreeRegistry, "probe" | "add" | "capturePatch" | "remove">;
|
|
25
|
+
readonly signal: AbortSignal | undefined;
|
|
26
|
+
readonly progress: Pick<AgentProgress, "log">;
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
export async function createAgentWorkspace(
|
|
22
|
-
rc:
|
|
30
|
+
rc: AgentWorkspaceContext,
|
|
23
31
|
opts: AgentExecutionOptions,
|
|
24
32
|
label: string,
|
|
25
33
|
): Promise<AgentWorkspace> {
|
|
@@ -68,7 +76,7 @@ export async function createAgentWorkspace(
|
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
export async function disposeAgentWorkspace(
|
|
71
|
-
rc:
|
|
79
|
+
rc: Pick<AgentWorkspaceContext, "progress">,
|
|
72
80
|
label: string,
|
|
73
81
|
workspace: AgentWorkspace | undefined,
|
|
74
82
|
): Promise<void> {
|