@marcoscale98/pi-extensible-workflows 5.14.1-fork.1 → 5.19.1-fork.1

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 (94) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/dist/src/agent-execution.js +114 -87
  3. package/dist/src/background-widget.d.ts +61 -0
  4. package/dist/src/background-widget.js +23 -16
  5. package/dist/src/decoders.js +28 -1
  6. package/dist/src/execution.d.ts +4 -2
  7. package/dist/src/execution.js +43 -2
  8. package/dist/src/host-delivery.js +1 -0
  9. package/dist/src/host-navigator.d.ts +18 -0
  10. package/dist/src/host-navigator.js +71 -13
  11. package/dist/src/host-recovery.d.ts +5 -2
  12. package/dist/src/host-recovery.js +3 -3
  13. package/dist/src/host-view.d.ts +21 -1
  14. package/dist/src/host-view.js +138 -47
  15. package/dist/src/host.d.ts +2 -2
  16. package/dist/src/host.js +246 -60
  17. package/dist/src/index.d.ts +3 -1
  18. package/dist/src/index.js +1346 -622
  19. package/dist/src/index.js.map +3 -3
  20. package/dist/src/paths.d.ts +2 -0
  21. package/dist/src/paths.js +2 -0
  22. package/dist/src/registry.js +3 -2
  23. package/dist/src/roles.d.ts +70 -0
  24. package/dist/src/roles.js +326 -0
  25. package/dist/src/script-tools.d.ts +27 -0
  26. package/dist/src/script-tools.js +87 -0
  27. package/dist/src/settings.d.ts +26 -0
  28. package/dist/src/settings.js +277 -0
  29. package/dist/src/trajectory.d.ts +39 -4
  30. package/dist/src/trajectory.js +82 -5
  31. package/dist/src/types.d.ts +30 -3
  32. package/dist/src/types.js +1 -1
  33. package/dist/src/utils.d.ts +21 -1
  34. package/dist/src/utils.js +40 -0
  35. package/dist/src/validation.d.ts +10 -17
  36. package/dist/src/validation.js +29 -458
  37. package/dist/starter/index.js +0 -2
  38. package/dist/starter/index.js.map +1 -1
  39. package/dist/starter/review-loop.d.ts +0 -13
  40. package/dist/starter/review-loop.js +0 -2
  41. package/dist/subagents/index.js +1344 -798
  42. package/dist/subagents/index.js.map +3 -3
  43. package/dist/subagents/src/contracts.d.ts +49 -29
  44. package/dist/subagents/src/contracts.js +18 -7
  45. package/dist/subagents/src/index.d.ts +1 -1
  46. package/dist/subagents/src/index.js +15 -7
  47. package/dist/subagents/src/manager.js +32 -13
  48. package/dist/subagents/src/navigator.js +396 -247
  49. package/dist/subagents/src/view.d.ts +24 -2
  50. package/dist/subagents/src/view.js +107 -80
  51. package/dist/trajectory/assets/index.html +370 -232
  52. package/dist/trajectory/assets/prism.min.js +13 -0
  53. package/dist/trajectory/index.js +445 -277
  54. package/dist/trajectory/index.js.map +3 -3
  55. package/dist/trajectory/src/assets/index.html +370 -232
  56. package/dist/trajectory/src/assets/prism.min.js +13 -0
  57. package/dist/trajectory/src/export.js +4 -2
  58. package/dist/trajectory/src/index.d.ts +2 -0
  59. package/dist/trajectory/src/index.js +151 -60
  60. package/dist/trajectory/src/server.js +138 -11
  61. package/dist/trajectory/src/server.js.map +2 -2
  62. package/package.json +7 -6
  63. package/skills/pi-extensible-workflows/SKILL.md +2 -0
  64. package/src/agent-execution.ts +110 -80
  65. package/src/background-widget.ts +26 -19
  66. package/src/decoders.ts +22 -1
  67. package/src/execution.ts +41 -4
  68. package/src/host-delivery.ts +1 -0
  69. package/src/host-navigator.ts +58 -14
  70. package/src/host-recovery.ts +8 -7
  71. package/src/host-view.ts +116 -42
  72. package/src/host.ts +223 -55
  73. package/src/index.ts +3 -1
  74. package/src/paths.ts +3 -0
  75. package/src/registry.ts +3 -2
  76. package/src/roles.ts +327 -0
  77. package/src/script-tools.ts +76 -0
  78. package/src/settings.ts +224 -0
  79. package/src/trajectory.ts +97 -9
  80. package/src/types.ts +12 -5
  81. package/src/utils.ts +36 -1
  82. package/src/validation.ts +30 -356
  83. package/starter/review-loop.ts +0 -2
  84. package/subagents/README.md +3 -3
  85. package/subagents/src/contracts.ts +22 -7
  86. package/subagents/src/index.ts +18 -7
  87. package/subagents/src/manager.ts +31 -13
  88. package/subagents/src/navigator.ts +336 -180
  89. package/subagents/src/view.ts +108 -76
  90. package/trajectory/src/assets/index.html +370 -232
  91. package/trajectory/src/assets/prism.min.js +13 -0
  92. package/trajectory/src/export.ts +4 -2
  93. package/trajectory/src/index.ts +124 -47
  94. package/trajectory/src/server.ts +114 -14
@@ -5,7 +5,7 @@ import { basename, dirname, join, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { Type } from "@earendil-works/pi-ai";
7
7
  import { Compile } from "typebox/compile";
8
- import { createAgentSession, DefaultPackageManager, DefaultResourceLoader, defineTool, getAgentDir, ModelRuntime, SessionManager, SettingsManager } from "@earendil-works/pi-coding-agent";
8
+ import { createAgentSession, createCodemodeExtension, createMcpExtension, createToolSearchExtension, DefaultPackageManager, DefaultResourceLoader, defineTool, getAgentDir, ModelRuntime, SessionManager, SettingsManager, type InlineExtension } from "@earendil-works/pi-coding-agent";
9
9
  import type { ExtensionContext, ModelRegistry, ToolDefinition } from "@earendil-works/pi-coding-agent";
10
10
  type HerdrModelContext = { readonly model: ExtensionContext["model"]; readonly modelRegistry: ModelRegistry | undefined };
11
11
  type AgentMessage = { role: string; content?: unknown; stopReason?: string; errorMessage?: string; usage?: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: { total: number } } };
@@ -46,7 +46,7 @@ export interface PiResourceInspection {
46
46
  readonly systemPromptSource?: string;
47
47
  }
48
48
  import type { AgentAccounting, AgentActivity, AgentContinuity, AgentIdentity, AgentResourceInspection, AgentResourcePolicy, AgentResourceSelectors, AgentResourceSelectorSources, AgentSetup, AgentSetupSummary, AgentTransport, AgentTransportContext, ContextFileScope, JsonSchema, JsonValue, LiveSessionHandoff, ModelSpec, PiRuntimeLaunchInfo, PreparedAgentSession, RegisteredAgentSetupHook, SessionInput, WorkflowAgentMessage, WorkflowAgentSession, WorkflowAgentSessionEvent, WorkflowAgentSessionReference, WorkflowAgentSessionState, WorkflowAgentSessionStats, WorkflowAgentTurnResult, WorkflowExtensionSettings, WorkflowExtensionSettingsValidatorContext, WorkflowRunContext, WorkflowSessionStartEvent } from "./types.js";
49
- import { SerialLane, assertModelThinking, byPriorityThenName, deepFreeze, errorText, jsonObject, jsonValue, mergeWorkflowExtensionSettings, object, modelAliasName, modelCapability, resolveModelReference, resourcePatternHasMagic, selectResourcesByLayers, unmatchedResourcePatterns } from "./utils.js";
49
+ import { SerialLane, assertModelThinking, byPriorityThenName, deepFreeze, errorText, jsonObject, jsonValue, mergeWorkflowExtensionSettings, object, physicalModel, resolveModelReference, resourcePatternHasMagic, unmatchedResourcePatterns } from "./utils.js";
50
50
  import { SETTLED_AGENT_STATES, WorkflowError, isContextFileScope, zeroAccounting, type AgentDefinition, type AgentState } from "./types.js";
51
51
  import { createLiveSessionHandoff } from "./session-handoff.js";
52
52
  import { createToolTimingExtension } from "./tool-timing.js";
@@ -55,7 +55,9 @@ import { createPiRuntimeAgentRunner, isRuntimeAgentProviderError, normalizePiRun
55
55
  import type { RuntimeAgentProgress, RuntimeUsage } from "./runtime/agent-runner.js";
56
56
  import { defaultWorkflowResultSchema } from "./runtime/workflow-result.js";
57
57
  import { validateAgentOptions, validateSchema } from "./validation.js";
58
- import { canonicalPath } from "./paths.js";
58
+ import { canonicalPath, extensionIdentity } from "./paths.js";
59
+ import { isCorePackageName } from "./package-identity.js";
60
+ import { canonicalExtensionSelector, resolveRole } from "./roles.js";
59
61
  import type { RunStore } from "./persistence.js";
60
62
  type AgentExecutionRunStore = Pick<RunStore, "recordSystemPrompt" | "validateWorktree" | "worktree" | "snapshotWorktree">;
61
63
  const localToolTimingExtension = createToolTimingExtension();
@@ -140,23 +142,22 @@ function accounting(stats: WorkflowAgentSessionStats): AgentAccounting {
140
142
  return { input: stats.tokens.input, output: stats.tokens.output, cacheRead: stats.tokens.cacheRead, cacheWrite: stats.tokens.cacheWrite, cost: stats.cost };
141
143
  }
142
144
 
143
- const extensionDirectory = dirname(fileURLToPath(import.meta.url));
144
- const workflowPackageRoot = basename(dirname(extensionDirectory)) === "dist" ? resolve(extensionDirectory, "../..") : resolve(extensionDirectory, "..");
145
- const WORKFLOW_HOST_ENTRIES = new Set([
146
- canonicalPath(resolve(workflowPackageRoot, "src/index.ts")),
147
- canonicalPath(resolve(workflowPackageRoot, "dist/src/index.js")),
148
- canonicalPath(resolve(workflowPackageRoot, "starter/index.ts")),
149
- canonicalPath(resolve(workflowPackageRoot, "dist/starter/index.js")),
150
- canonicalPath(resolve(workflowPackageRoot, "subagents/index.ts")),
151
- canonicalPath(resolve(workflowPackageRoot, "dist/subagents/index.js")),
152
- ]);
153
- function canonicalExtensionSelector(selector: string, base = process.cwd()): string {
154
- const negated = selector.startsWith("!");
155
- const body = negated ? selector.slice(1) : selector;
156
- if (body === "*" || body === "**" || body.startsWith("**/")) return selector;
157
- const resolved = resolve(base, body);
158
- if (resourcePatternHasMagic(body)) return `${negated ? "!" : ""}${resolved}`;
159
- return `${negated ? "!" : ""}${canonicalPath(resolved)}`;
145
+ const WORKFLOW_HOST_ENTRY_PATHS = ["src/index.ts", "dist/src/index.js", "starter/index.ts", "dist/starter/index.js", "subagents/index.ts", "dist/subagents/index.js"];
146
+ function isWorkflowHostEntry(path: string): boolean {
147
+ const entryPath = canonicalPath(path);
148
+ let packageRoot = dirname(entryPath);
149
+ while (packageRoot !== dirname(packageRoot)) {
150
+ const packageMetadataPath = join(packageRoot, "package.json");
151
+ if (existsSync(packageMetadataPath)) {
152
+ try {
153
+ const metadata: unknown = JSON.parse(readFileSync(packageMetadataPath, "utf8"));
154
+ if (typeof metadata !== "object" || metadata === null || !("name" in metadata) || !isCorePackageName(metadata.name)) return false;
155
+ return WORKFLOW_HOST_ENTRY_PATHS.some((entry) => canonicalPath(join(packageRoot, entry)) === entryPath);
156
+ } catch { return false; }
157
+ }
158
+ packageRoot = dirname(packageRoot);
159
+ }
160
+ return false;
160
161
  }
161
162
  const WORKFLOW_DIRECTORY = "pi-extensible-workflows";
162
163
  function workflowSystemPromptPath(cwd: string, agentDir: string, projectTrusted: boolean): string | undefined {
@@ -191,24 +192,29 @@ function loadPiPackageDirectory(): Promise<PiPackageDirectoryResolution> {
191
192
  }
192
193
  }).catch((error: unknown) => ({ error: `could not load @earendil-works/pi-coding-agent package metadata: ${errorText(error)}` }));
193
194
  }
194
- let nativePromptTemplateModule: Promise<NativePromptTemplateModule> | undefined;
195
- async function loadNativePromptTemplateModule(): Promise<NativePromptTemplateModule> {
195
+ const nativeCoreModules = new Map<string, Promise<unknown>>();
196
+ /** Pi does not export these core modules, so inspection loads them from the installed package. */
197
+ async function loadNativeCoreModule(file: string, label: string): Promise<unknown> {
196
198
  const packageDirectory = await loadPiPackageDirectory();
197
- let promptTemplatePath: string | undefined;
199
+ let modulePath: string | undefined;
198
200
  if (packageDirectory.directory) {
199
- const candidate = resolve(packageDirectory.directory, "dist/core/prompt-templates.js");
200
- if (existsSync(candidate)) promptTemplatePath = candidate;
201
+ const candidate = resolve(packageDirectory.directory, "dist/core", file);
202
+ if (existsSync(candidate)) modulePath = candidate;
201
203
  }
202
- promptTemplatePath ??= resolve(dirname(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"))), "core/prompt-templates.js");
204
+ modulePath ??= resolve(dirname(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"))), "core", file);
203
205
  const packageDirectoryError = packageDirectory.error ? ` Package directory lookup failed: ${packageDirectory.error}` : "";
204
- return nativePromptTemplateModule ??= import(promptTemplatePath).then((module) => module as NativePromptTemplateModule).catch((error: unknown) => { throw new Error(`Could not load Pi prompt templates.${packageDirectoryError}`, { cause: error }); });
206
+ let loaded = nativeCoreModules.get(file);
207
+ if (!loaded) { loaded = import(modulePath).catch((error: unknown) => { throw new Error(`Could not load Pi ${label}.${packageDirectoryError}`, { cause: error }); }); nativeCoreModules.set(file, loaded); }
208
+ return loaded;
205
209
  }
206
210
  async function expandPromptTemplateForInspection(text: string, templates: readonly { name: string; content: string }[]): Promise<string> {
207
- return (await loadNativePromptTemplateModule()).expandPromptTemplate(text, templates);
211
+ return (await loadNativeCoreModule("prompt-templates.js", "prompt templates") as NativePromptTemplateModule).expandPromptTemplate(text, templates);
212
+ }
213
+ async function buildSystemPromptForInspection(options: unknown): Promise<string> {
214
+ return (await loadNativeCoreModule("system-prompt.js", "system prompt builder") as { buildSystemPrompt(options: unknown): string }).buildSystemPrompt(options);
208
215
  }
209
216
  type NativePromptSession = PiSession & {
210
- readonly _extensionRunner?: { hasHandlers(event: string): boolean; emitInput(text: string, images: undefined, source: "interactive"): Promise<unknown>; emitBeforeAgentStart(prompt: string, images: undefined, systemPrompt: string, options: unknown): Promise<unknown>; onError(listener: (error: unknown) => void): () => void };
211
- readonly _baseSystemPrompt?: string;
217
+ readonly _extensionRunner?: { hasHandlers(event: string): boolean; emitInput(text: string, images: undefined, source: "interactive"): Promise<unknown>; emitBeforeAgentStart(prompt: string, images: undefined, options: unknown): Promise<unknown>; onError(listener: (error: unknown) => void): () => void };
212
218
  readonly _baseSystemPromptOptions?: unknown;
213
219
  readonly _expandSkillCommand?: (text: string) => string;
214
220
  readonly promptTemplates?: readonly { name: string; content: string }[];
@@ -217,12 +223,11 @@ async function preparePiPrompt(native: PiSession, text: string): Promise<PiPromp
217
223
  const session = native as NativePromptSession;
218
224
  const diagnostics: Array<{ type: "error"; message: string; source?: string }> = [];
219
225
  const runner = session._extensionRunner;
220
- const baseSystemPrompt = typeof session._baseSystemPrompt === "string" ? session._baseSystemPrompt : session.systemPrompt ?? "";
226
+ const baseSystemPrompt = session.systemPrompt ?? "";
221
227
  const baseOptions = session._baseSystemPromptOptions;
222
228
  const expandSkillCommand = session._expandSkillCommand;
223
229
  const templates = session.promptTemplates;
224
230
  if (!runner) diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: extension runner is missing", source: "Pi session" });
225
- if (typeof session._baseSystemPrompt !== "string") diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: base system prompt is missing", source: "Pi session" });
226
231
  if (baseOptions === undefined) diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: base system prompt options are missing", source: "Pi session" });
227
232
  if (typeof expandSkillCommand !== "function") diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: skill expansion is missing", source: "Pi session" });
228
233
  if (!Array.isArray(templates)) diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: prompt templates are missing", source: "Pi session" });
@@ -244,9 +249,11 @@ async function preparePiPrompt(native: PiSession, text: string): Promise<PiPromp
244
249
  try { expandedPrompt = await expandPromptTemplateForInspection(skillExpanded, Array.isArray(templates) ? templates : []); }
245
250
  catch (error) { diagnostics.push({ type: "error", message: `Pi prompt template expansion is unavailable: ${errorText(error)}`, source: "Pi session" }); }
246
251
  }
247
- const result = !inputHandled && runner && baseOptions !== undefined ? await runner.emitBeforeAgentStart(expandedPrompt, undefined, baseSystemPrompt, baseOptions) : undefined;
248
- const prepared = result as { messages?: readonly unknown[]; systemPrompt?: unknown } | undefined;
249
- return { prompt: text, expandedPrompt, systemPrompt: typeof prepared?.systemPrompt === "string" ? prepared.systemPrompt : baseSystemPrompt, systemPromptOptions: baseOptions, messages: prepared?.messages ?? [], inputHandled, diagnostics };
252
+ const result = !inputHandled && runner && baseOptions !== undefined ? await runner.emitBeforeAgentStart(expandedPrompt, undefined, baseOptions) : undefined;
253
+ const prepared = result as { messages?: readonly unknown[]; systemPromptOptions?: unknown } | undefined;
254
+ const systemPromptOptions = prepared?.systemPromptOptions ?? baseOptions;
255
+ const systemPrompt = prepared?.systemPromptOptions === undefined ? baseSystemPrompt : await buildSystemPromptForInspection(prepared.systemPromptOptions);
256
+ return { prompt: text, expandedPrompt, systemPrompt, systemPromptOptions, messages: prepared?.messages ?? [], inputHandled, diagnostics };
250
257
  } finally { unsubscribe?.(); }
251
258
  }
252
259
 
@@ -288,6 +295,23 @@ export function flushExtensionProviders(resourceLoader: DefaultResourceLoader, m
288
295
  return failures;
289
296
  }
290
297
 
298
+ /**
299
+ * The CLI adds Pi's built-in extensions itself; SDK sessions only get them from these public
300
+ * factories (docs/sdk.md). As `builtin` entries they obey `-builtin:<name>` settings, and like the
301
+ * CLI's they are replaceable by an extension that registers the same tools or commands.
302
+ * NOTE: Pi does not export the built-in llama.cpp extension, so workflow agents cannot use models it
303
+ * provides. Declare the server as a provider in models.json instead.
304
+ */
305
+ function builtinExtensions(): InlineExtension[] {
306
+ return [
307
+ { name: "codemode", factory: createCodemodeExtension(), replaceable: true, builtin: true },
308
+ { name: "tool-search", factory: createToolSearchExtension(), replaceable: true, builtin: true },
309
+ { name: "mcp", factory: createMcpExtension(), replaceable: true, builtin: true },
310
+ ];
311
+ }
312
+ const BUILTIN_EXTENSION_PREFIX = "builtin:";
313
+ function isLlamaModel(model: ModelSpec): boolean { return model.provider === "llama.cpp"; }
314
+
291
315
  async function createLocalPiSessionHandle(input: SessionInput, sessionStartEvent?: WorkflowSessionStartEvent): Promise<LocalPiSessionHandle> {
292
316
  const agentDir = input.agentDir ?? getAgentDir();
293
317
  const systemPromptSource = workflowSystemPromptPath(input.cwd, agentDir, input.resourcePolicy?.projectTrusted ?? true);
@@ -300,28 +324,33 @@ async function createLocalPiSessionHandle(input: SessionInput, sessionStartEvent
300
324
  const tools = [...new Set([...input.tools, ...customTools.map(({ name }) => name)])];
301
325
  let settingsManager: SettingsManager;
302
326
  let resourceLoader: DefaultResourceLoader;
327
+ const builtins = builtinExtensions();
328
+ const builtinNames = builtins.flatMap((extension) => typeof extension === "function" ? [] : [extension.name]);
329
+ const extensionFactories = [...builtins, ...(input.extensionFactories ?? [])];
303
330
  const policy = input.resourcePolicy;
304
331
  if (policy) {
305
332
  settingsManager = SettingsManager.create(input.cwd, agentDir, { projectTrusted: false });
306
333
  settingsManager.setProjectTrusted(policy.projectTrusted);
307
- const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager });
334
+ const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager, builtinExtensions: builtinNames });
308
335
  const resolved = await packageManager.resolve();
309
- const discoveredExtensions = [...new Set(resolved.extensions.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => canonicalPath(path)))];
336
+ const discoveredExtensions = [...new Set(resolved.extensions.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => extensionIdentity(path)))];
310
337
  const selectorSources = policy.selectorSources;
311
- const extensionLayers = [selectorSources.global.extensions, selectorSources.project.extensions, selectorSources.role?.extensions, selectorSources.call?.extensions];
312
- const extensionSelectors = extensionLayers.flatMap((layer) => (layer ?? []).map((original) => ({ original, matching: canonicalExtensionSelector(original, input.cwd) })));
313
- const selectedExtensions = selectResourcesByLayers(extensionLayers.map((layer) => layer?.map((selector) => canonicalExtensionSelector(selector, input.cwd))), discoveredExtensions);
314
- const extensionPaths = selectedExtensions.filter((path) => !WORKFLOW_HOST_ENTRIES.has(path));
315
- const unmatchedExtensions = extensionSelectors.filter(({ matching }) => unmatchedResourcePatterns([matching], discoveredExtensions).length > 0).map(({ original }) => original);
338
+ const selection = resolveRole(undefined, {
339
+ cwd: input.cwd,
340
+ selectorSources,
341
+ resources: { extensions: discoveredExtensions },
342
+ });
343
+ const selectedExtensions = selection.selectedExtensions ?? [];
344
+ const extensionPaths = selectedExtensions.filter((path) => path.startsWith(BUILTIN_EXTENSION_PREFIX) || !isWorkflowHostEntry(path));
316
345
  policy.selectedExtensions = selectedExtensions;
317
- policy.unmatchedExtensions = unmatchedExtensions;
346
+ policy.unmatchedExtensions = selection.unmatchedExtensions ?? [];
318
347
  const skillPaths = [...new Set(resolved.skills.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => path))];
319
348
  const updateSkillMatches = (skills: readonly { name: string }[]): Set<string> => {
320
349
  const names = [...new Set(skills.map(({ name }) => name))];
321
- const skillLayers = [selectorSources.global.skills, selectorSources.project.skills, selectorSources.role?.skills, selectorSources.call?.skills];
322
- const selectedSkills = selectResourcesByLayers(skillLayers, names);
350
+ const selected = resolveRole(undefined, { cwd: input.cwd, selectorSources, resources: { skills: names } });
351
+ const selectedSkills = selected.selectedSkills ?? [];
323
352
  policy.selectedSkills = selectedSkills;
324
- policy.unmatchedSkills = unmatchedResourcePatterns(skillLayers.flatMap((layer) => layer ?? []), names);
353
+ policy.unmatchedSkills = selected.unmatchedSkills ?? [];
325
354
  return new Set(names.filter((name) => !selectedSkills.includes(name)));
326
355
  };
327
356
  resourceLoader = new DefaultResourceLoader({
@@ -332,7 +361,7 @@ async function createLocalPiSessionHandle(input: SessionInput, sessionStartEvent
332
361
  additionalExtensionPaths: extensionPaths,
333
362
  noSkills: true,
334
363
  additionalSkillPaths: [...new Set([...skillPaths, ...(input.additionalSkillPaths ?? [])])],
335
- ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}),
364
+ extensionFactories,
336
365
  ...(contextFilesOverride ? { agentsFilesOverride: contextFilesOverride } : {}),
337
366
  skillsOverride: (base) => {
338
367
  const disabledSkills = updateSkillMatches(base.skills);
@@ -344,10 +373,10 @@ async function createLocalPiSessionHandle(input: SessionInput, sessionStartEvent
344
373
  await resourceLoader.reload();
345
374
  } else {
346
375
  settingsManager = SettingsManager.create(input.cwd, agentDir, { projectTrusted: true });
347
- const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager });
376
+ const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager, builtinExtensions: builtinNames });
348
377
  const resolved = await packageManager.resolve();
349
- const extensionPaths = [...new Set(resolved.extensions.filter(({ enabled }) => enabled).map(({ path }) => canonicalPath(path)).filter((path) => !WORKFLOW_HOST_ENTRIES.has(path)))];
350
- resourceLoader = new DefaultResourceLoader({ cwd: input.cwd, agentDir, settingsManager, noExtensions: true, additionalExtensionPaths: extensionPaths, ...(input.additionalSkillPaths?.length ? { additionalSkillPaths: [...input.additionalSkillPaths] } : {}), ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}), ...(contextFilesOverride ? { agentsFilesOverride: contextFilesOverride } : {}), ...systemPromptOptions, ...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}) });
378
+ const extensionPaths = [...new Set(resolved.extensions.filter(({ enabled }) => enabled).map(({ path }) => extensionIdentity(path)).filter((path) => path.startsWith(BUILTIN_EXTENSION_PREFIX) || !isWorkflowHostEntry(path)))];
379
+ resourceLoader = new DefaultResourceLoader({ cwd: input.cwd, agentDir, settingsManager, noExtensions: true, additionalExtensionPaths: extensionPaths, ...(input.additionalSkillPaths?.length ? { additionalSkillPaths: [...input.additionalSkillPaths] } : {}), extensionFactories, ...(contextFilesOverride ? { agentsFilesOverride: contextFilesOverride } : {}), ...systemPromptOptions, ...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}) });
351
380
  await resourceLoader.reload();
352
381
  }
353
382
  const providerFailures = flushExtensionProviders(resourceLoader, modelRuntime);
@@ -358,7 +387,7 @@ async function createLocalPiSessionHandle(input: SessionInput, sessionStartEvent
358
387
  // missing, so the failure travels with the complaint rather than being
359
388
  // swallowed — otherwise a misconfigured gateway reads as a typo in a model
360
389
  // name.
361
- const because = providerFailures.length > 0 ? ` (provider registration failed — ${providerFailures.join("; ")})` : "";
390
+ const because = providerFailures.length > 0 ? ` (provider registration failed — ${providerFailures.join("; ")})` : isLlamaModel(input.model) ? " (Pi does not expose its built-in llama.cpp provider to extension sessions; declare the llama.cpp server as a provider in models.json and use that model instead)" : "";
362
391
  throw new WorkflowError("UNKNOWN_MODEL", `Unknown model: ${input.model.provider}/${input.model.model}${because}`);
363
392
  }
364
393
  const effectiveSessionStartEvent = input.settings === undefined && sessionStartEvent === undefined ? undefined : { ...(sessionStartEvent ?? { type: "session_start" as const, reason: "startup" as const }), settings: input.settings ?? Object.freeze({}) };
@@ -383,13 +412,13 @@ async function createLocalPiSessionHandle(input: SessionInput, sessionStartEvent
383
412
  await shutdown("quit").catch(() => undefined);
384
413
  throw error;
385
414
  }
386
- const resourcePaths = { extensions: resourceLoader.getExtensions().extensions.filter(({ path }) => !path.startsWith("<")).map(({ resolvedPath }) => canonicalPath(resolvedPath)), skills: resourceLoader.getSkills().skills.map(({ filePath }) => canonicalPath(filePath)) };
415
+ const resourcePaths = { extensions: resourceLoader.getExtensions().extensions.filter(({ path }) => !path.startsWith("<")).map(({ resolvedPath }) => extensionIdentity(resolvedPath)), skills: resourceLoader.getSkills().skills.map(({ filePath }) => canonicalPath(filePath)) };
387
416
  const resourceInspection = (): PiResourceInspection => {
388
417
  const extensions = resourceLoader.getExtensions();
389
418
  const skills = resourceLoader.getSkills();
390
419
  const diagnostics = [...extensions.errors.map(({ path, error }) => ({ type: "error" as const, message: error, source: path })), ...skills.diagnostics.map(({ type, path, message }) => ({ type, message, ...(path ? { source: path } : {}) }))];
391
420
  const systemSource = systemPromptSource;
392
- return { extensions: resourceLoader.getExtensions().extensions.filter(({ path }) => !path.startsWith("<")).map(({ resolvedPath }) => canonicalPath(resolvedPath)), skills: skills.skills.map(({ name }) => name), diagnostics, ...(systemSource ? { systemPromptSource: systemSource } : resourceLoader.getSystemPrompt() !== undefined ? { systemPromptSource: "Pi resource loader" } : {}) };
421
+ return { extensions: resourceLoader.getExtensions().extensions.filter(({ path }) => !path.startsWith("<")).map(({ resolvedPath }) => extensionIdentity(resolvedPath)), skills: skills.skills.map(({ name }) => name), diagnostics, ...(systemSource ? { systemPromptSource: systemSource } : resourceLoader.getSystemPrompt() !== undefined ? { systemPromptSource: "Pi resource loader" } : {}) };
393
422
  };
394
423
  const managedSession = Object.assign(session, {
395
424
  getLeafId: () => manager.getLeafId(),
@@ -916,7 +945,6 @@ export class WorkflowAgentExecutor {
916
945
  const roleName = typeof options.role === "string" ? options.role : undefined;
917
946
  const roleDefinition = roleName ? this.root.agentDefinitions?.[roleName] : undefined;
918
947
  if (roleName && !roleDefinition) throw new WorkflowError("UNKNOWN_AGENT_TYPE", `Unknown agent role: ${roleName}`);
919
- const candidateTools = inheritedTools !== undefined ? [...inheritedTools] : [...this.root.tools];
920
948
  if (inheritedTools === undefined) for (const pattern of roleDefinition?.tools ?? []) {
921
949
  const body = pattern.startsWith("!") ? pattern.slice(1) : pattern;
922
950
  if (!pattern.startsWith("!") && !resourcePatternHasMagic(pattern) && !this.root.tools.has(body)) {
@@ -924,30 +952,30 @@ export class WorkflowAgentExecutor {
924
952
  if (!this.warnedRoleTools.has(key)) { this.warnedRoleTools.add(key); this.root.onResourceWarning?.(`Tool not available in this session for role ${roleName ?? "agent"}: ${body}. The agent runs without it.`); }
925
953
  }
926
954
  }
927
- const selectorLayers = [
928
- this.root.resourceSelectors?.tools,
929
- roleDefinition?.tools,
930
- options.tools,
931
- ];
932
- for (const pattern of options.tools ?? []) {
933
- const body = pattern.startsWith("!") ? pattern.slice(1) : pattern;
934
- if (!pattern.startsWith("!") && !resourcePatternHasMagic(pattern) && !candidateTools.includes(body)) throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the launching session boundary: ${body}`);
935
- }
936
- const outsideEffectiveTool = options.effectiveTools?.find((tool) => !candidateTools.includes(tool));
937
- if (outsideEffectiveTool) throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the launching session boundary: ${outsideEffectiveTool}`);
938
- const requested = options.effectiveTools ?? selectResourcesByLayers(selectorLayers, candidateTools);
939
- const forbidden = requested.find((tool) => !this.root.tools.has(tool));
940
- if (forbidden) throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the launching session boundary: ${forbidden}`);
941
- const requestedModel = options.model ?? (roleDefinition?.model && roleDefinition.thinking && !roleDefinition.model.includes(":") ? `${roleDefinition.model}:${roleDefinition.thinking}` : roleDefinition?.model);
942
- const alias = requestedModel === undefined ? undefined : modelAliasName(requestedModel, this.root.modelAliases ?? {});
943
- const blockedAlias = requestedModel?.split(":", 1)[0];
944
- if (requestedModel !== undefined && blockedAlias && this.root.blockedAliases?.has(blockedAlias) && !alias) { const target = this.root.blockedAliasTargets?.[blockedAlias]; throw new WorkflowError("UNKNOWN_MODEL", `Unknown model alias ${requestedModel}${target ? ` resolved to ${target}` : ""}${this.root.settingsPath ? ` (settings: ${this.root.settingsPath})` : ""}`); }
945
- const model = options.modelOverride ?? parseModel(requestedModel, this.root.model, this.root.modelAliases, this.root.knownModels ?? this.root.availableModels, this.root.settingsPath);
946
- const availableModels = this.root.knownModels ?? this.root.availableModels ?? new Set([modelCapability(this.root.model)]);
947
- if (!availableModels.has(modelCapability(model))) throw new WorkflowError("UNKNOWN_MODEL", `Unknown model${requestedModel ? ` ${requestedModel} resolved to ${modelCapability(model)}` : ""}${this.root.settingsPath ? ` (settings: ${this.root.settingsPath})` : ""}`);
948
- const overrideSystemPrompt = roleDefinition?.overrideSystemPrompt === true;
949
- const contextFiles = options.contextFiles ?? roleDefinition?.contextFiles;
950
- return { model, ...(alias && requestedModel ? { requestedModel } : {}), tools: requested, ...(overrideSystemPrompt ? { systemPrompt: roleDefinition.prompt ?? "" } : {}), systemPromptAppend: overrideSystemPrompt ? "" : roleDefinition?.prompt ?? "", ...(contextFiles === undefined ? {} : { contextFiles: [...contextFiles] }) };
955
+ const resolved = resolveRole(roleName, {
956
+ cwd: this.root.cwd,
957
+ ...(roleDefinition === undefined ? {} : { definition: roleDefinition }),
958
+ ...(this.root.agentDefinitions === undefined ? {} : { definitions: this.root.agentDefinitions }),
959
+ ...(this.root.agentDir === undefined ? {} : { agentDir: this.root.agentDir }),
960
+ selectorSources: { global: this.root.resourceSelectors ?? {}, project: {} },
961
+ ...(this.root.modelAliases === undefined ? {} : { modelAliases: this.root.modelAliases }),
962
+ ...(this.root.knownModels === undefined ? {} : { knownModels: this.root.knownModels }),
963
+ ...(this.root.availableModels === undefined ? {} : { availableModels: this.root.availableModels }),
964
+ ...(this.root.blockedAliases === undefined ? {} : { blockedAliases: this.root.blockedAliases }),
965
+ ...(this.root.blockedAliasTargets === undefined ? {} : { blockedAliasTargets: this.root.blockedAliasTargets }),
966
+ ...(this.root.settingsPath === undefined ? {} : { settingsPath: this.root.settingsPath }),
967
+ rootModel: this.root.model,
968
+ rootTools: this.root.tools,
969
+ ...(inheritedTools === undefined ? {} : { inheritedTools }),
970
+ ...(options.model === undefined ? {} : { model: options.model }),
971
+ ...(options.modelOverride === undefined ? {} : { modelOverride: options.modelOverride }),
972
+ ...(options.tools === undefined ? {} : { tools: options.tools }),
973
+ ...(options.skills === undefined ? {} : { skills: options.skills }),
974
+ ...(options.extensions === undefined ? {} : { extensions: options.extensions }),
975
+ ...(options.contextFiles === undefined ? {} : { contextFiles: options.contextFiles }),
976
+ ...(options.effectiveTools === undefined ? {} : { effectiveTools: options.effectiveTools }),
977
+ });
978
+ return { model: physicalModel(resolved.model ?? this.root.model, this.root.modelAliases, this.root.knownModels, this.root.settingsPath), ...(resolved.requestedModel === undefined ? {} : { requestedModel: resolved.requestedModel }), tools: resolved.tools ?? [], ...(resolved.overrideSystemPrompt ? { systemPrompt: resolved.prompt } : {}), systemPromptAppend: resolved.overrideSystemPrompt ? "" : resolved.prompt, ...(resolved.contextFiles === undefined ? {} : { contextFiles: resolved.contextFiles }) };
951
979
  }
952
980
 
953
981
  async execute(task: string, options: AgentExecutionOptions, signal?: AbortSignal, customTools: readonly ToolDefinition[] = [], setSteer?: (handler: (message: string) => void | Promise<void>) => void, beforeRetry?: () => void): Promise<AgentExecutionResult> {
@@ -1392,6 +1420,7 @@ export class FairAgentScheduler {
1392
1420
  const agentTool = defineTool({
1393
1421
  name: "agent", label: "Child Agent", description: "Start a direct child agent",
1394
1422
  parameters: Type.Object({ prompt: Type.String(), label: Type.String(), tools: Type.Optional(Type.Array(Type.String())), skills: Type.Optional(Type.Array(Type.String())), extensions: Type.Optional(Type.Array(Type.String())), model: Type.Optional(Type.String()), role: Type.Optional(Type.String()), contextFiles: Type.Optional(Type.Array(Type.String())), outputSchema: Type.Optional(Type.Record(Type.String(), Type.Unknown())), retries: Type.Optional(Type.Integer({ minimum: 0 })), timeoutMs: Type.Optional(Type.Union([Type.Integer({ minimum: 1 }), Type.Null()])) }, { additionalProperties: true }),
1423
+ outputSchema: Type.Object({ id: Type.String() }),
1395
1424
  execute: async (_id, params) => {
1396
1425
  if (!isChildAgentToolParams(params)) throw new WorkflowError("INVALID_METADATA", "Invalid child agent parameters");
1397
1426
  validateAgentOptions(params);
@@ -1402,13 +1431,14 @@ export class FairAgentScheduler {
1402
1431
  Reflect.deleteProperty(agentOptions, "prompt");
1403
1432
  const options: ScheduledAgentOptions = { label: params.label, requestedLabel: params.label, cwd: parent.options.cwd, tools, agentOptions, ...(params.skills ? { skills: params.skills } : {}), ...(params.extensions ? { extensions: params.extensions } : {}), ...(params.model ? { model: params.model } : {}), ...(params.role ? { role: params.role } : {}), ...(params.contextFiles ? { contextFiles: params.contextFiles } : {}), ...(outputSchema === undefined ? {} : { schema: outputSchema }), ...(params.retries === undefined ? {} : { retries: params.retries }), ...(params.timeoutMs === undefined ? {} : { timeoutMs: params.timeoutMs }) };
1404
1433
  const child = this.spawn(parent.runId, params.prompt, options, parentId);
1405
- return { content: [{ type: "text" as const, text: JSON.stringify({ id: child.id }) }], details: { id: child.id } };
1434
+ return { content: [{ type: "text" as const, text: JSON.stringify({ id: child.id }) }], details: { id: child.id }, structuredContent: { id: child.id } };
1406
1435
  },
1407
1436
  });
1408
1437
  const resultTool = defineTool({
1409
1438
  name: "get_subagent_result", label: "Child Result", description: "Wait for a direct child and return its result once; repeated retrieval fails with AGENT_RESULT_COLLECTED",
1410
1439
  parameters: Type.Object({ id: Type.String() }),
1411
- execute: async (_id, params) => { const value = await this.result(parentId, params.id); if (!value.ok && value.error.code === "BUDGET_EXHAUSTED") throw new WorkflowError("BUDGET_EXHAUSTED", value.error.message); return { content: [{ type: "text" as const, text: JSON.stringify(value) }], details: value }; }
1440
+ outputSchema: Type.Object({ id: Type.String(), ok: Type.Boolean(), value: Type.Optional(Type.Unknown()), error: Type.Optional(Type.Object({ code: Type.String(), message: Type.String() })) }),
1441
+ execute: async (_id, params) => { const value = await this.result(parentId, params.id); if (!value.ok && value.error.code === "BUDGET_EXHAUSTED") throw new WorkflowError("BUDGET_EXHAUSTED", value.error.message); return { content: [{ type: "text" as const, text: JSON.stringify(value) }], details: value, structuredContent: value }; }
1412
1442
  });
1413
1443
  const steerTool = defineTool({
1414
1444
  name: "steer_subagent", label: "Steer Child", description: "Steer a running direct child",
@@ -49,7 +49,7 @@ const ENTRY_TYPE = "piewf-run-receipt";
49
49
  * Repaint interval. Fast enough that the spinner reads as motion rather than a
50
50
  * stutter; the underlying data is only re-read when an event says it changed.
51
51
  */
52
- const REPAINT_MS = 125;
52
+ export const REPAINT_MS = 125;
53
53
 
54
54
  /**
55
55
  * Filesystem rescan interval. Shell-only activity has no event, but it does not
@@ -78,7 +78,7 @@ const MAX_ROWS = 10;
78
78
  * of thinking and far shorter than the ten minutes it takes to be declared
79
79
  * stalled.
80
80
  */
81
- const QUIET_MS = 3 * 60 * 1000;
81
+ export const QUIET_MS = 3 * 60 * 1000;
82
82
 
83
83
  /** The core's own threshold for calling an agent stalled outright. */
84
84
  const STALL_MS = WORKFLOW_AGENT_STALL_THRESHOLD_MS;
@@ -144,7 +144,7 @@ const RESET = "\x1b[0m";
144
144
  * Paints text in a themed colour role. Falls back to the bare text when no
145
145
  * theme is available, so a frame rendered outside the TUI is still legible.
146
146
  */
147
- type Paint = (role: Parameters<Theme["fg"]>[0], text: string) => string;
147
+ export type Paint = (role: Parameters<Theme["fg"]>[0], text: string) => string;
148
148
 
149
149
  /**
150
150
  * States that mean work is still happening, derived from the core's own lists
@@ -223,14 +223,14 @@ function spinner(now: number): string {
223
223
  }
224
224
 
225
225
  /** Status mark for a finished-or-running unit of work. */
226
- function mark(state: string | undefined, now: number, paint: Paint, runState?: string): string {
226
+ export function mark(state: string | undefined, now: number, paint: Paint, runState?: string): string {
227
227
  if (state === "running" && (runState === undefined || runState === "running")) return paint(ROLE.live, spinner(now));
228
228
  if (state === "completed") return paint(ROLE.done, "✓");
229
229
  if (state === "failed" || state === "budget_exhausted") return paint(ROLE.failed, "✗");
230
230
  return paint(ROLE.quiet, "·");
231
231
  }
232
232
 
233
- function formatElapsed(ms: number): string {
233
+ export function formatElapsed(ms: number): string {
234
234
  const total = Math.max(0, Math.floor(ms / 1000));
235
235
  const minutes = Math.floor(total / 60);
236
236
  const seconds = total % 60;
@@ -727,15 +727,6 @@ function renderFrame(runs: readonly Run[], now: number, width: number, offset =
727
727
  sizeOut.rows = rows.length;
728
728
  const body = collapse(rows, MAX_ROWS - 2, paint, offset, scrolling);
729
729
 
730
- // Width budget: the rule takes a column and a space on each side, so the
731
- // text has four fewer than the widget is given. Every piece is measured
732
- // against `inner` so the right-hand rule lands in one column on every row.
733
- const inner = Math.max(20, width - 4);
734
-
735
-
736
- // Side rules below the lid, and no floor: the box opens downward into the
737
- // editor rather than closing on itself, which is one more line kept for the
738
- // run.
739
730
  // A scrollbar on the right rule, drawn only when there is more tree than
740
731
  // frame. Without it the window's position is a guess: three rows of a
741
732
  // twenty-row tree look the same near the top as near the bottom.
@@ -763,6 +754,27 @@ function renderFrame(runs: readonly Run[], now: number, width: number, offset =
763
754
  return index >= top && index < top + size ? "█" : "░";
764
755
  };
765
756
 
757
+ // Only advertise navigation when it can reveal hidden rows: a hint for keys
758
+ // that do nothing is worse than no hint.
759
+ const hint = !__navigationForTests.enabled || maxOffset(total, MAX_ROWS - 2) === 0
760
+ ? ""
761
+ : scrolling ? "↑/↓ to scroll · Esc to exit" : "Alt+O to scroll";
762
+ return drawFrame(title, hint, body, width, paint, accent, scrollbar);
763
+ }
764
+
765
+ /**
766
+ * The box around the live rows, shared with the subagent widget so both sit
767
+ * under the editor in one style.
768
+ */
769
+ export function drawFrame(title: string, hint: string, body: readonly Pick<Row, "text" | "right">[], width: number, paint: Paint, accent: (text: string) => string, scrollbar: (index: number) => string | undefined = () => undefined): string[] {
770
+ // Width budget: the rule takes a column and a space on each side, so the
771
+ // text has four fewer than the widget is given. Every piece is measured
772
+ // against `inner` so the right-hand rule lands in one column on every row.
773
+ const inner = Math.max(20, width - 4);
774
+
775
+ // Side rules below the lid, and no floor: the box opens downward into the
776
+ // editor rather than closing on itself, which is one more line kept for the
777
+ // run.
766
778
  const lines = body.map((row, index) => {
767
779
  // The numbers hold the right edge; the name gives way when the two would
768
780
  // meet. A truncated workflow name is still recognisable, where a truncated
@@ -784,11 +796,6 @@ function renderFrame(runs: readonly Run[], now: number, width: number, offset =
784
796
  // rather than sitting under a separate run of dashes. In ten rows a line
785
797
  // spent on decoration is a line not spent on the run, and the box still
786
798
  // reads as closed at the top.
787
- // Only advertise navigation when it can reveal hidden rows: a hint for keys
788
- // that do nothing is worse than no hint.
789
- const hint = !__navigationForTests.enabled || maxOffset(total, MAX_ROWS - 2) === 0
790
- ? ""
791
- : scrolling ? "↑/↓ to scroll · Esc to exit" : "Alt+O to scroll";
792
799
  const label = ` ${title} `;
793
800
  const tail = hint === "" ? "" : ` ${hint} `;
794
801
  const spare = inner + 2 - visibleLength(label) - visibleLength(tail);
package/src/decoders.ts CHANGED
@@ -17,6 +17,7 @@ type PersistedAgent = RunRecord["agents"][number];
17
17
  type PersistedPhaseRecord = NonNullable<RunRecord["phaseHistory"]>[number];
18
18
  type PersistedShellActivity = NonNullable<RunRecord["activeShellsByPhase"]>[number];
19
19
  type PersistedDelivery = NonNullable<RunRecord["delivery"]>;
20
+ type PersistedScriptCall = NonNullable<RunRecord["scriptCalls"]>[number];
20
21
  type PersistedIdentity = NonNullable<ScheduledAgentOptions["agentIdentity"]>;
21
22
  type PersistedOptions = ScheduledAgentOptions;
22
23
 
@@ -115,8 +116,9 @@ export function decodeWorkflowExtensions(value: unknown): WorkflowExtensionSetti
115
116
  continue;
116
117
  }
117
118
  if (namespace === "trajectory") {
119
+ // Snapshots persisted before the themes setting was removed still carry a boolean `themes`; it is accepted and dropped.
118
120
  if (!object(raw) || Object.keys(raw).some((key) => key !== "port" && key !== "themes") || raw.port !== undefined && (typeof raw.port !== "number" || !Number.isSafeInteger(raw.port) || raw.port < 1 || raw.port > 65535) || raw.themes !== undefined && typeof raw.themes !== "boolean") return undefined;
119
- normalized.trajectory = { ...(raw.port === undefined ? {} : { port: raw.port }), ...(raw.themes === undefined ? {} : { themes: raw.themes }) };
121
+ normalized.trajectory = { ...(raw.port === undefined ? {} : { port: raw.port }) };
120
122
  continue;
121
123
  }
122
124
  if (!jsonValue(raw)) return undefined;
@@ -393,6 +395,22 @@ function decodeRunEvent(value: unknown): WorkflowRunEvent | undefined {
393
395
  if (timestamp === INVALID_PERSISTED_VALUE) return undefined;
394
396
  return { type: value.type, message: value.message, ...(timestamp === undefined ? {} : { timestamp }) };
395
397
  }
398
+ function decodeScriptCall(value: unknown): PersistedScriptCall | undefined {
399
+ if (!object(value) || value.kind !== "shell" && value.kind !== "tool" || typeof value.name !== "string" || typeof value.path !== "string" || !finiteNumber(value.startedAt)) return undefined;
400
+ const shell = value.kind === "shell";
401
+ if (shell && !jsonValue(value.input)) return undefined;
402
+ const inputBytes = shell ? optionalNumber(value.inputBytes) : undefined;
403
+ const phase = optionalString(value.phase);
404
+ const finishedAt = optionalNumber(value.finishedAt);
405
+ let error: PersistedScriptCall["error"];
406
+ if (value.error !== undefined) {
407
+ if (shell) error = decodeWorkflowError(value.error);
408
+ else if (object(value.error) && typeof value.error.code === "string") error = { code: isWorkflowErrorCode(value.error.code) ? value.error.code : "INTERNAL_ERROR" };
409
+ if (!error) return undefined;
410
+ }
411
+ if (inputBytes === INVALID_PERSISTED_VALUE || phase === INVALID_PERSISTED_VALUE || finishedAt === INVALID_PERSISTED_VALUE) return undefined;
412
+ return { kind: value.kind, name: value.name, path: value.path, ...(shell && jsonValue(value.input) ? { input: value.input } : {}), ...(inputBytes === undefined ? {} : { inputBytes }), ...(phase === undefined ? {} : { phase }), startedAt: value.startedAt, ...(finishedAt === undefined ? {} : { finishedAt }), ...(error === undefined ? {} : { error }) };
413
+ }
396
414
  function decodeDelivery(value: unknown): PersistedDelivery | undefined {
397
415
  if (!object(value)) return undefined;
398
416
  const mode = value.mode;
@@ -422,6 +440,8 @@ export function decodePersistedRun(value: unknown, allowLegacyAgentSessions = fa
422
440
  const budgetEvents = value.budgetEvents === undefined ? undefined : decodeArray(value.budgetEvents, decodeBudgetEvent);
423
441
  const events = value.events === undefined ? undefined : decodeArray(value.events, decodeRunEvent);
424
442
  const delivery = value.delivery === undefined ? undefined : decodeDelivery(value.delivery);
443
+ const scriptCalls = value.scriptCalls === undefined ? undefined : decodeArray(value.scriptCalls, decodeScriptCall);
444
+ if (value.scriptCalls !== undefined && !scriptCalls) return undefined;
425
445
  if (!agentSessions || !agents || parentRunId === INVALID_PERSISTED_VALUE || value.retry !== undefined && !retry || phase === INVALID_PERSISTED_VALUE || value.phaseHistory !== undefined && !phaseHistory || phaseHistoryIndex === INVALID_PERSISTED_VALUE || activeShells === INVALID_PERSISTED_VALUE || activeShellStartedAt === INVALID_PERSISTED_VALUE || value.activeShellsByPhase !== undefined && !activeShellsByPhase || value.error !== undefined && !error || failedAt === INVALID_PERSISTED_VALUE || value.budget !== undefined && !budget || budgetVersion === INVALID_PERSISTED_VALUE || value.budgetVersion !== undefined && !integer(budgetVersion) || value.usage !== undefined && !usage || value.budgetEvents !== undefined && !budgetEvents || value.events !== undefined && !events || value.delivery !== undefined && !delivery) return undefined;
426
446
  if (value.agentSessions === undefined && !allowLegacyAgentSessions) return undefined;
427
447
  return {
@@ -440,6 +460,7 @@ export function decodePersistedRun(value: unknown, allowLegacyAgentSessions = fa
440
460
  ...(activeShells === undefined ? {} : { activeShells }),
441
461
  ...(activeShellStartedAt === undefined ? {} : { activeShellStartedAt }),
442
462
  ...(activeShellsByPhase === undefined ? {} : { activeShellsByPhase }),
463
+ ...(scriptCalls === undefined ? {} : { scriptCalls }),
443
464
  ...(error === undefined ? {} : { error }),
444
465
  ...(failedAt === undefined ? {} : { failedAt }),
445
466
  ...(budget === undefined ? {} : { budget }),
package/src/execution.ts CHANGED
@@ -5,9 +5,9 @@ import { join } from "node:path";
5
5
  import { StringDecoder } from "node:string_decoder";
6
6
  import { RunStore, structuralPath as operationPath } from "./persistence.js";
7
7
  import type { AgentAttempt } from "./agent-execution.js";
8
- import type { AgentIdentity, AgentAttemptSummary, FunctionIdentity, JsonValue, ShellIdentity, ShellOptions, ShellResult, WorkflowAgentSessionReference, WorkflowBridge, WorkflowErrorCode, WorkflowExecution } from "./types.js";
8
+ import type { AgentIdentity, AgentAttemptSummary, FunctionIdentity, JsonValue, ShellIdentity, ShellOptions, ShellResult, ToolIdentity, WorkflowAgentSessionReference, WorkflowBridge, WorkflowErrorCode, WorkflowExecution } from "./types.js";
9
9
  import { WorkflowError, roleNameOf, sumAccounting, zeroAccounting } from "./types.js";
10
- import { asWorkflowError, errorText, fail, isWorkflowAuthored, isWorkflowErrorCode, jsonValue, markWorkflowAuthored, object, positiveInteger } from "./utils.js";
10
+ import { asWorkflowError, errorText, fail, isWorkflowAuthored, isWorkflowErrorCode, jsonDigest, jsonValue, markWorkflowAuthored, object, positiveInteger } from "./utils.js";
11
11
  import { instrumentWorkflow, validateAgentOptions, validateShellCommand, validateShellOptions } from "./validation.js";
12
12
 
13
13
  export const RPC_LIMIT_BYTES = 10 * 1024 * 1024;
@@ -275,6 +275,30 @@ const functions = Object.freeze(Object.fromEntries(Object.entries(config.functio
275
275
  Object.defineProperty(result, "toJSON", { value() { throw workError("INVALID_METADATA", "Workflow function result is a Promise; await it before serialization"); } });
276
276
  return result;
277
277
  }])));
278
+ // tools.<id>(args) runs a Pi session tool through the host. Any identifier resolves, so a resumed run
279
+ // replays journaled calls even when the tool is gone; the host rejects identifiers it cannot call.
280
+ // A call is the n-th call with the same arguments in its scope, so replay does not depend on the
281
+ // order in which concurrent calls start.
282
+ const NOT_TOOLS = new Set(["then", "toJSON", "toString", "valueOf", "constructor"]);
283
+ const sortedJson = value => Array.isArray(value) ? value.map(sortedJson) : value && typeof value === "object" ? Object.fromEntries(Object.keys(value).sort().map(key => [key, sortedJson(value[key])])) : value;
284
+ const toolOccurrences = new Map();
285
+ const toolCall = id => (...values) => {
286
+ if (worktreeOwners.getStore()) throw workError("INVALID_METADATA", "tools." + id + " is unavailable inside withWorktree: session tools resolve paths against the session working directory");
287
+ if (values.length > 1 || values[0] !== undefined && (!values[0] || typeof values[0] !== "object" || Array.isArray(values[0]))) throw workError("INVALID_METADATA", "tools." + id + " takes one arguments object");
288
+ const args = values[0] === undefined ? {} : values[0];
289
+ const structuralPath = inheritedAgentPath.getStore() || [];
290
+ const key = JSON.stringify([structuralPath, id, sortedJson(args)]);
291
+ const occurrence = (toolOccurrences.get(key) || 0) + 1;
292
+ toolOccurrences.set(key, occurrence);
293
+ return guardedResult(rpc("tool", [id, args, { structuralPath: [...structuralPath], occurrence }]), "tool");
294
+ };
295
+ const tools = new Proxy(Object.create(null), {
296
+ get: (_target, key) => typeof key === "string" && !NOT_TOOLS.has(key) ? toolCall(key) : undefined,
297
+ // Probing would branch on the session that resumed the run; the launch already checks named tools.
298
+ has: () => { throw workError("INVALID_METADATA", "Workflow scripts cannot probe tools with 'in'; call tools.<name>(...) directly, and the launch checks that the tool exists"); },
299
+ set: () => false,
300
+ defineProperty: () => false,
301
+ });
278
302
  const recordEntries = (value, kind) => {
279
303
  if (!value || typeof value !== "object" || Array.isArray(value)) throw workError("INVALID_METADATA", kind + " must be a record");
280
304
  return Object.entries(value);
@@ -335,7 +359,7 @@ const pipeline = async (operationName, items, stages) => {
335
359
  return Object.fromEntries(results.map(result => [result.name, result.value]));
336
360
  };
337
361
  const safeMath = Object.fromEntries(Object.getOwnPropertyNames(Math).filter(name => name !== "random").map(name => [name, Math[name]]));
338
- const sandbox = { agent, shell, withWorktree: rejectWorktree, prompt, checkpoint, parallel, pipeline, phase, log, args: config.args, Promise, JSON, Math: Object.freeze(safeMath) };
362
+ const sandbox = { agent, shell, withWorktree: rejectWorktree, prompt, checkpoint, parallel, pipeline, phase, log, tools, args: config.args, Promise, JSON, Math: Object.freeze(safeMath) };
339
363
  for (const [name, fn] of Object.entries(functions)) Object.defineProperty(sandbox, name, { value: fn, writable: false, configurable: false });
340
364
  for (const name of ["Date","eval","Function","WebAssembly","process","require","module","exports","console","fetch","XMLHttpRequest","WebSocket","performance","crypto","setTimeout","setInterval","setImmediate","queueMicrotask","Intl","SharedArrayBuffer","Atomics"]) sandbox[name] = undefined;
341
365
  const context = vm.createContext(sandbox, { codeGeneration: { strings: false, wasm: false } });
@@ -384,6 +408,14 @@ function readShellIdentity(value: unknown): ShellIdentity {
384
408
  const identity = readAgentIdentity(value);
385
409
  return { structuralPath: identity.structuralPath, callSite: identity.callSite, occurrence: identity.occurrence, ...(identity.worktreeOwner ? { worktreeOwner: identity.worktreeOwner } : {}) };
386
410
  }
411
+ function readToolIdentity(value: unknown): ToolIdentity {
412
+ if (!object(value) || !Array.isArray(value.structuralPath) || !value.structuralPath.every((part): part is string => typeof part === "string" && Boolean(part.trim())) || !positiveInteger(value.occurrence)) fail("INTERNAL_ERROR", "Invalid workflow tool identity");
413
+ return { structuralPath: [...value.structuralPath], occurrence: value.occurrence };
414
+ }
415
+ /** The journal path of a script tool call; the arguments' digest keeps calls with different arguments apart whatever order they start in. */
416
+ export function toolIdentityPath(identifier: string, args: JsonValue, identity: ToolIdentity): string {
417
+ return operationPath("tool", ...identity.structuralPath, identifier, `args:${jsonDigest(args)}`, String(identity.occurrence));
418
+ }
387
419
  export function agentHandleTurnPath(handle: string, turn: number): string {
388
420
  return operationPath("agent", "handle", handle, `turn:${String(turn)}`);
389
421
  }
@@ -595,6 +627,11 @@ export function runWorkflow(script: string, args: JsonValue = null, bridge: Work
595
627
  if (!OUTCOME_ERRORS.has(typed.code)) throw typed;
596
628
  value = branded({ name, ok: false, failedAt: name, error: workerErrorShape(typed) });
597
629
  }
630
+ } else if (method === "tool") {
631
+ if (typeof values[0] !== "string" || !object(values[1])) fail("INTERNAL_ERROR", "tool requires an identifier and object arguments");
632
+ const identity = readToolIdentity(values[2]);
633
+ if (!bridge.tool) fail("UNKNOWN_TOOL", `tools.${values[0]} is unavailable: this run has no session tools`);
634
+ value = await bridge.tool(values[0], values[1], controller.signal, identity);
598
635
  } else if (method === "worktree") {
599
636
  if (!bridge.worktree || typeof values[0] !== "string" || !values[0]) fail("INTERNAL_ERROR", "worktree requires an active host bridge and scope");
600
637
  value = await bridge.worktree(values[0], controller.signal);
@@ -649,4 +686,4 @@ export async function persistAgentAttempts(store: RunStore, id: string, attempts
649
686
  return { ...run, agents: run.agents.map((candidate) => candidate.id === id ? { ...candidate, attempts: attempts.length, attemptDetails, accounting: total } : candidate), agentSessions: [...run.agentSessions.filter(({ transport, sessionId }) => !sessionKeys.has(`${transport}:${sessionId}`)), ...sessions] };
650
687
  });
651
688
  }
652
- export type { AgentIdentity, FunctionIdentity, ShellIdentity, WorkflowBridge, WorkflowExecution } from "./types.js";
689
+ export type { AgentIdentity, FunctionIdentity, ShellIdentity, ToolIdentity, WorkflowBridge, WorkflowExecution } from "./types.js";