@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.
- package/CHANGELOG.md +135 -0
- package/dist/src/agent-execution.js +114 -87
- package/dist/src/background-widget.d.ts +61 -0
- package/dist/src/background-widget.js +23 -16
- package/dist/src/decoders.js +28 -1
- package/dist/src/execution.d.ts +4 -2
- package/dist/src/execution.js +43 -2
- package/dist/src/host-delivery.js +1 -0
- package/dist/src/host-navigator.d.ts +18 -0
- package/dist/src/host-navigator.js +71 -13
- package/dist/src/host-recovery.d.ts +5 -2
- package/dist/src/host-recovery.js +3 -3
- package/dist/src/host-view.d.ts +21 -1
- package/dist/src/host-view.js +138 -47
- package/dist/src/host.d.ts +2 -2
- package/dist/src/host.js +246 -60
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +1346 -622
- package/dist/src/index.js.map +3 -3
- package/dist/src/paths.d.ts +2 -0
- package/dist/src/paths.js +2 -0
- package/dist/src/registry.js +3 -2
- package/dist/src/roles.d.ts +70 -0
- package/dist/src/roles.js +326 -0
- package/dist/src/script-tools.d.ts +27 -0
- package/dist/src/script-tools.js +87 -0
- package/dist/src/settings.d.ts +26 -0
- package/dist/src/settings.js +277 -0
- package/dist/src/trajectory.d.ts +39 -4
- package/dist/src/trajectory.js +82 -5
- package/dist/src/types.d.ts +30 -3
- package/dist/src/types.js +1 -1
- package/dist/src/utils.d.ts +21 -1
- package/dist/src/utils.js +40 -0
- package/dist/src/validation.d.ts +10 -17
- package/dist/src/validation.js +29 -458
- package/dist/starter/index.js +0 -2
- package/dist/starter/index.js.map +1 -1
- package/dist/starter/review-loop.d.ts +0 -13
- package/dist/starter/review-loop.js +0 -2
- package/dist/subagents/index.js +1344 -798
- package/dist/subagents/index.js.map +3 -3
- package/dist/subagents/src/contracts.d.ts +49 -29
- package/dist/subagents/src/contracts.js +18 -7
- package/dist/subagents/src/index.d.ts +1 -1
- package/dist/subagents/src/index.js +15 -7
- package/dist/subagents/src/manager.js +32 -13
- package/dist/subagents/src/navigator.js +396 -247
- package/dist/subagents/src/view.d.ts +24 -2
- package/dist/subagents/src/view.js +107 -80
- package/dist/trajectory/assets/index.html +370 -232
- package/dist/trajectory/assets/prism.min.js +13 -0
- package/dist/trajectory/index.js +445 -277
- package/dist/trajectory/index.js.map +3 -3
- package/dist/trajectory/src/assets/index.html +370 -232
- package/dist/trajectory/src/assets/prism.min.js +13 -0
- package/dist/trajectory/src/export.js +4 -2
- package/dist/trajectory/src/index.d.ts +2 -0
- package/dist/trajectory/src/index.js +151 -60
- package/dist/trajectory/src/server.js +138 -11
- package/dist/trajectory/src/server.js.map +2 -2
- package/package.json +7 -6
- package/skills/pi-extensible-workflows/SKILL.md +2 -0
- package/src/agent-execution.ts +110 -80
- package/src/background-widget.ts +26 -19
- package/src/decoders.ts +22 -1
- package/src/execution.ts +41 -4
- package/src/host-delivery.ts +1 -0
- package/src/host-navigator.ts +58 -14
- package/src/host-recovery.ts +8 -7
- package/src/host-view.ts +116 -42
- package/src/host.ts +223 -55
- package/src/index.ts +3 -1
- package/src/paths.ts +3 -0
- package/src/registry.ts +3 -2
- package/src/roles.ts +327 -0
- package/src/script-tools.ts +76 -0
- package/src/settings.ts +224 -0
- package/src/trajectory.ts +97 -9
- package/src/types.ts +12 -5
- package/src/utils.ts +36 -1
- package/src/validation.ts +30 -356
- package/starter/review-loop.ts +0 -2
- package/subagents/README.md +3 -3
- package/subagents/src/contracts.ts +22 -7
- package/subagents/src/index.ts +18 -7
- package/subagents/src/manager.ts +31 -13
- package/subagents/src/navigator.ts +336 -180
- package/subagents/src/view.ts +108 -76
- package/trajectory/src/assets/index.html +370 -232
- package/trajectory/src/assets/prism.min.js +13 -0
- package/trajectory/src/export.ts +4 -2
- package/trajectory/src/index.ts +124 -47
- package/trajectory/src/server.ts +114 -14
package/src/agent-execution.ts
CHANGED
|
@@ -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,
|
|
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
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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
|
|
199
|
+
let modulePath: string | undefined;
|
|
198
200
|
if (packageDirectory.directory) {
|
|
199
|
-
const candidate = resolve(packageDirectory.directory, "dist/core
|
|
200
|
-
if (existsSync(candidate))
|
|
201
|
+
const candidate = resolve(packageDirectory.directory, "dist/core", file);
|
|
202
|
+
if (existsSync(candidate)) modulePath = candidate;
|
|
201
203
|
}
|
|
202
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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 =
|
|
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,
|
|
248
|
-
const prepared = result as { messages?: readonly unknown[];
|
|
249
|
-
|
|
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 }) =>
|
|
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
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
|
322
|
-
const selectedSkills =
|
|
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 =
|
|
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
|
-
|
|
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 }) =>
|
|
350
|
-
resourceLoader = new DefaultResourceLoader({ cwd: input.cwd, agentDir, settingsManager, noExtensions: true, additionalExtensionPaths: extensionPaths, ...(input.additionalSkillPaths?.length ? { additionalSkillPaths: [...input.additionalSkillPaths] } : {}),
|
|
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 }) =>
|
|
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 }) =>
|
|
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
|
|
928
|
-
this.root.
|
|
929
|
-
roleDefinition
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
return { model, ...(
|
|
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
|
-
|
|
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",
|
package/src/background-widget.ts
CHANGED
|
@@ -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 })
|
|
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";
|