pi-workflow-engine 0.11.0 → 0.13.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.
Files changed (79) hide show
  1. package/.pi/extensions/pi-workflow-engine/index.ts +255 -97
  2. package/.pi/extensions/pi-workflow-engine/src/advisory-challenge.ts +75 -0
  3. package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +90 -0
  4. package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +13 -5
  5. package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +3 -1
  6. package/.pi/extensions/pi-workflow-engine/src/agent-limits.ts +35 -0
  7. package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +27 -4
  8. package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +116 -0
  9. package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +49 -45
  10. package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +95 -26
  11. package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +50 -0
  12. package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +158 -203
  13. package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +48 -42
  14. package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +26 -4
  15. package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +75 -0
  16. package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +492 -0
  17. package/.pi/extensions/pi-workflow-engine/src/budget.ts +3 -3
  18. package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +38 -0
  19. package/.pi/extensions/pi-workflow-engine/src/command-completions.ts +36 -0
  20. package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +82 -10
  21. package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +16 -130
  22. package/.pi/extensions/pi-workflow-engine/src/discovery.ts +9 -5
  23. package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +109 -60
  24. package/.pi/extensions/pi-workflow-engine/src/engine.ts +166 -18
  25. package/.pi/extensions/pi-workflow-engine/src/filesystem-error.ts +4 -0
  26. package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +2 -2
  27. package/.pi/extensions/pi-workflow-engine/src/journal.ts +13 -6
  28. package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +167 -0
  29. package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +310 -0
  30. package/.pi/extensions/pi-workflow-engine/src/options.ts +88 -1
  31. package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +32 -0
  32. package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +50 -0
  33. package/.pi/extensions/pi-workflow-engine/src/progress.ts +72 -117
  34. package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +189 -0
  35. package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +89 -0
  36. package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +289 -0
  37. package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +127 -41
  38. package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +24 -0
  39. package/.pi/extensions/pi-workflow-engine/src/review/patch-validation.ts +120 -0
  40. package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +43 -7
  41. package/.pi/extensions/pi-workflow-engine/src/review/review-format.ts +2 -1
  42. package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +1 -1
  43. package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +1 -1
  44. package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +2 -1
  45. package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +11 -30
  46. package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +5 -5
  47. package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +3 -4
  48. package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +130 -0
  49. package/.pi/extensions/pi-workflow-engine/src/session-identity.ts +6 -0
  50. package/.pi/extensions/pi-workflow-engine/src/structured-output.ts +32 -0
  51. package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +44 -0
  52. package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +1 -4
  53. package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +1 -4
  54. package/.pi/extensions/pi-workflow-engine/src/types.ts +52 -15
  55. package/.pi/extensions/pi-workflow-engine/src/ui/dynamax-editor-decoration.ts +220 -0
  56. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +26 -6
  57. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +88 -46
  58. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +7 -3
  59. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-viewer-layout.ts +51 -0
  60. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +12 -67
  61. package/.pi/extensions/pi-workflow-engine/src/usage.ts +124 -16
  62. package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +151 -178
  63. package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +1 -1
  64. package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +72 -0
  65. package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +354 -0
  66. package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +162 -0
  67. package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +632 -0
  68. package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +172 -0
  69. package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +80 -0
  70. package/.pi/extensions/pi-workflow-engine/src/workflows.ts +2 -0
  71. package/.pi/extensions/pi-workflow-engine/src/worktree.ts +47 -3
  72. package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +65 -162
  73. package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +48 -149
  74. package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +28 -77
  75. package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +28 -77
  76. package/.pi/extensions/pi-workflow-engine/workflows/research.ts +170 -0
  77. package/README.md +72 -141
  78. package/USAGE.md +270 -17
  79. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import type { Api, Model } from "@earendil-works/pi-ai";
2
2
  import {
3
- createAgentSession,
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
- captureEffectiveAgentSessionIdentity,
25
- type EffectiveAgentSessionIdentity,
26
- type EffectiveToolInfoLike,
27
- } from "./agent-session-identity.ts";
28
- import { throwIfAborted } from "./cancellation.ts";
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
- const skillSetup = shouldCreateSkillResourceLoader(rc, prompt, opts)
86
- ? await rc.perf.time(
87
- "agent.skills_ms",
88
- () =>
89
- createAgentSkillResourceLoader({
90
- cwd,
91
- prompt,
92
- skills: opts.skills,
93
- log: (message) => rc.progress.log(`${label}: ${message}`),
94
- }),
95
- tags,
96
- )
97
- : undefined;
98
- const selectedSkills = skillSetup?.selectedSkills ?? [];
99
- const toolSelection = buildToolSelection(opts, selectedSkills.length > 0);
100
- let captured: unknown = null;
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 = params;
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
- session = (await createSubagentSession(toolSelection.sessionOptions)).session;
139
- const dynamicToolHintsApplied = toolSelection.toolHints.length === 0 || applyDynamicToolHints(session, toolSelection);
140
- if (!dynamicToolHintsApplied) {
141
- rc.progress.log(`${label}: dynamic tool hints unavailable; falling back to concrete tools only`);
142
- rc.perf.counter("agent.tool_hint_fallback", 1, tags);
143
- const dynamicSession = session;
144
- session = undefined;
145
- rc.perf.timeSync("agent.dispose_ms", () => dynamicSession.dispose(), tags);
146
- throwIfAborted(rc.signal);
147
- session = (await createSubagentSession(toolSelection.fallbackSessionOptions)).session;
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 { session, selectedSkills, structuredResult: () => captured };
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, selectedSkills } = handle;
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
- ? `${promptedWithSkills}\n\nWhen finished, return your result by calling the ${FINAL_TOOL} tool.`
182
- : promptedWithSkills;
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
- await rc.perf.time("agent.prompt_ms", () => session.prompt(finalPrompt), tags);
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.structuredResult() === null && attempt < SCHEMA_REPROMPT_ATTEMPTS; attempt++) {
175
+ for (let attempt = 0; !handle.hasStructuredResult() && attempt < MAX_SCHEMA_REPAIR_ATTEMPTS; attempt++) {
190
176
  throwIfAborted(rc.signal);
191
- rc.progress.log(`${label}: no final answer; re-prompting (${attempt + 1}/${SCHEMA_REPROMPT_ATTEMPTS})`);
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 rc.perf.time("agent.prompt_ms", () => session.prompt(SCHEMA_REPROMPT), tags);
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) return lastAssistantText(session.state);
202
- const result = handle.structuredResult();
203
- if (result === null) {
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 result;
196
+ return handle.structuredResult();
208
197
  },
209
198
  tags,
210
199
  );
211
200
  } finally {
212
- rc.usage.recordAgentSession({ label, phase: tags.phase, messages: session.state.messages });
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
- async function defaultCreateSession(options: CreateAgentSessionOptions): Promise<{ session: AgentRunnerSession }> {
274
- const { session } = await createAgentSession(options);
275
- return { session };
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
- void session.abort();
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" | "excludeTools">;
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") ? DEFAULT_SEARCH_BASE_TOOLS : undefined;
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 { sessionOptions: strictSessionOptions, fallbackSessionOptions: strictSessionOptions, activeTools, toolHints };
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: { excludeTools: BUILTIN_TOOL_NAMES.filter((name) => !explicitlyActive.has(name)) },
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(session: AgentRunnerSession, selection: ToolSelection): boolean {
337
- if (!hasDynamicToolApis(session)) return false;
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
- if (selection.toolHints.some((hint) => hint === "search" && isSearchLikeTool(tool))) active.add(tool.name);
342
- }
343
- session.setActiveToolsByName([...active]);
344
- return true;
345
- }
346
-
347
- function hasDynamicToolApis(session: AgentRunnerSession): session is DynamicToolSession {
348
- return typeof session.getAllTools === "function" && typeof session.setActiveToolsByName === "function";
349
- }
350
-
351
- function isSearchLikeTool(tool: AgentRunnerToolInfo): boolean {
352
- if (/(?:^|[-_])(?:edit|write|replace|patch|apply|delete|remove|move|rename|create|commit|push)(?:$|[-_])/.test(tool.name.toLowerCase())) {
353
- return false;
354
- }
355
- const name = tool.name.toLowerCase();
356
- if (name.includes("grep") || name.includes("find") || name.includes("search") || name === "rg" || name.includes("ripgrep")) return true;
357
- return /\b(?:grep|find|search|ripgrep|rg|structural search|code search)\b/.test(tool.description?.toLowerCase() ?? "");
358
- }
359
-
360
- function shouldCreateSkillResourceLoader(rc: RunContext, prompt: string, opts: AgentExecutionOptions): boolean {
361
- return !rc.createSession || opts.skills !== undefined || extractSkillSelectorsFromText(prompt).length > 0;
362
- }
363
-
364
- function effectiveToolInfos(
365
- tools: readonly AgentRunnerToolInfo[],
366
- ): { readonly kind: "verified"; readonly tools: readonly EffectiveToolInfoLike[] } | { readonly kind: "unverifiable"; readonly reason: string } {
367
- const normalized: EffectiveToolInfoLike[] = [];
368
- for (const tool of tools) {
369
- if (typeof tool.description !== "string" || tool.parameters === undefined || !tool.sourceInfo) {
370
- return { kind: "unverifiable", reason: `active tool metadata is incomplete for ${tool.name}` };
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
- return { kind: "verified", tools: normalized };
381
- }
382
-
383
- function lastAssistantText(state: { readonly messages: readonly unknown[] }): string {
384
- for (let index = state.messages.length - 1; index >= 0; index--) {
385
- const message = state.messages[index];
386
- if (hasRole(message, "assistant")) return messageTextContent(message).trim();
387
- }
388
- return "";
389
- }
390
-
391
- function hasRole(value: unknown, role: string): value is { role: string } {
392
- return typeof value === "object" && value !== null && "role" in value && value.role === role;
393
- }
394
-
395
- function messageTextContent(message: unknown): string {
396
- if (!hasContentArray(message)) return "";
397
- return message.content.filter(isTextPart).map((part) => part.text).join("");
398
- }
399
-
400
- function hasContentArray(value: unknown): value is { content: unknown[] } {
401
- return typeof value === "object" && value !== null && "content" in value && Array.isArray(value.content);
402
- }
403
-
404
- function isTextPart(value: unknown): value is { type: "text"; text: string } {
405
- return (
406
- typeof value === "object" &&
407
- value !== null &&
408
- "type" in value &&
409
- value.type === "text" &&
410
- "text" in value &&
411
- typeof value.text === "string"
412
- );
366
+ session.setActiveToolsByName([...active]);
367
+ return matched;
413
368
  }
@@ -1,14 +1,16 @@
1
- import { DefaultResourceLoader, getAgentDir, type ResourceDiagnostic, type Skill } from "@earendil-works/pi-coding-agent";
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
- export type AgentSkillRequestSource = "explicit" | "prompt";
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
- export interface AgentSkillResourceSetup {
21
- readonly resourceLoader: DefaultResourceLoader;
22
- readonly selectedSkills: readonly Skill[];
23
- readonly unmatchedSelectors: readonly string[];
24
- readonly diagnostics: readonly ResourceDiagnostic[];
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), source: "explicit", strict: true };
59
+ return { selectors: normalizeExplicitSkillSelectors(explicitSkills), strict: true };
55
60
  }
56
- return { selectors: extractSkillSelectorsFromText(prompt), source: "prompt", strict: false };
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
- export async function createAgentSkillResourceLoader(options: {
102
- readonly cwd: string;
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
- }): Promise<AgentSkillResourceSetup> {
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 loader = new DefaultResourceLoader({
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
- await loader.reload();
125
- const { diagnostics } = loader.getSkills();
126
- logSkillDiagnostics(options.log, diagnostics, request.selectors.length > 0);
127
-
128
- if (request.strict && resolution.unmatched.length > 0) {
129
- throw new Error(
130
- `Unknown subagent skill${resolution.unmatched.length === 1 ? "" : "s"}: ${resolution.unmatched.join(", ")}. Available: ${
131
- availableSkillNames.join(", ") || "(none)"
132
- }.`,
133
- );
134
- }
135
-
136
- if (!request.strict && resolution.unmatched.length > 0) {
137
- options.log?.(`Ignoring unrecognized subagent skill mention${resolution.unmatched.length === 1 ? "" : "s"}: ${resolution.unmatched.join(", ")}`);
138
- }
139
-
140
- return { resourceLoader: loader, selectedSkills: resolution.selected, unmatchedSelectors: resolution.unmatched, diagnostics };
141
- }
142
-
143
- export function appendSkillReminder(prompt: string, skills: readonly Pick<Skill, "name" | "filePath">[]): string {
144
- if (skills.length === 0) return prompt;
145
- const skillList = skills.map((skill) => `${skill.name} (${skill.filePath})`).join(", ");
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. */