@marcoscale98/pi-extensible-workflows 5.17.0-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 +68 -0
- package/dist/src/agent-execution.js +81 -39
- package/dist/src/background-widget.d.ts +5 -0
- package/dist/src/background-widget.js +1 -1
- package/dist/src/decoders.js +26 -0
- 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-recovery.d.ts +5 -2
- package/dist/src/host-recovery.js +3 -3
- package/dist/src/host-view.d.ts +5 -0
- package/dist/src/host-view.js +29 -5
- package/dist/src/host.d.ts +2 -2
- package/dist/src/host.js +190 -37
- package/dist/src/index.js +495 -93
- 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 +1 -1
- package/dist/src/roles.js +4 -3
- package/dist/src/script-tools.d.ts +27 -0
- package/dist/src/script-tools.js +87 -0
- package/dist/src/settings.d.ts +13 -0
- package/dist/src/settings.js +27 -3
- package/dist/src/trajectory.d.ts +12 -2
- package/dist/src/trajectory.js +30 -8
- package/dist/src/types.d.ts +30 -2
- package/dist/src/types.js +1 -1
- package/dist/src/utils.d.ts +19 -0
- package/dist/src/utils.js +30 -0
- package/dist/src/validation.d.ts +6 -1
- package/dist/src/validation.js +21 -1
- 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 +267 -159
- package/dist/subagents/index.js.map +2 -2
- package/dist/subagents/src/contracts.d.ts +45 -29
- package/dist/subagents/src/contracts.js +18 -7
- package/dist/subagents/src/index.d.ts +1 -1
- package/dist/subagents/src/index.js +13 -6
- package/dist/subagents/src/manager.js +3 -3
- package/dist/subagents/src/view.d.ts +1 -0
- package/dist/subagents/src/view.js +13 -52
- package/dist/trajectory/assets/index.html +27 -5
- package/dist/trajectory/index.js +76 -28
- package/dist/trajectory/index.js.map +2 -2
- package/dist/trajectory/src/assets/index.html +27 -5
- package/dist/trajectory/src/index.js +8 -18
- package/dist/trajectory/src/server.js +4 -2
- package/dist/trajectory/src/server.js.map +2 -2
- package/package.json +6 -6
- package/skills/pi-extensible-workflows/SKILL.md +2 -0
- package/src/agent-execution.ts +77 -41
- package/src/background-widget.ts +1 -1
- package/src/decoders.ts +20 -0
- package/src/execution.ts +41 -4
- package/src/host-delivery.ts +1 -0
- package/src/host-recovery.ts +8 -7
- package/src/host-view.ts +23 -5
- package/src/host.ts +173 -35
- package/src/paths.ts +3 -0
- package/src/registry.ts +1 -1
- package/src/roles.ts +4 -3
- package/src/script-tools.ts +76 -0
- package/src/settings.ts +22 -4
- package/src/trajectory.ts +30 -11
- package/src/types.ts +11 -4
- package/src/utils.ts +28 -0
- package/src/validation.ts +19 -1
- package/starter/review-loop.ts +0 -2
- package/subagents/README.md +2 -2
- package/subagents/src/contracts.ts +18 -7
- package/subagents/src/index.ts +16 -6
- package/subagents/src/manager.ts +3 -3
- package/subagents/src/view.ts +13 -50
- package/trajectory/src/assets/index.html +27 -5
- package/trajectory/src/index.ts +6 -9
- package/trajectory/src/server.ts +2 -2
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, resolveModelReference, resourcePatternHasMagic, 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,8 @@ 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";
|
|
59
60
|
import { canonicalExtensionSelector, resolveRole } from "./roles.js";
|
|
60
61
|
import type { RunStore } from "./persistence.js";
|
|
61
62
|
type AgentExecutionRunStore = Pick<RunStore, "recordSystemPrompt" | "validateWorktree" | "worktree" | "snapshotWorktree">;
|
|
@@ -141,16 +142,23 @@ function accounting(stats: WorkflowAgentSessionStats): AgentAccounting {
|
|
|
141
142
|
return { input: stats.tokens.input, output: stats.tokens.output, cacheRead: stats.tokens.cacheRead, cacheWrite: stats.tokens.cacheWrite, cost: stats.cost };
|
|
142
143
|
}
|
|
143
144
|
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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;
|
|
161
|
+
}
|
|
154
162
|
const WORKFLOW_DIRECTORY = "pi-extensible-workflows";
|
|
155
163
|
function workflowSystemPromptPath(cwd: string, agentDir: string, projectTrusted: boolean): string | undefined {
|
|
156
164
|
const projectPath = join(cwd, ".pi", WORKFLOW_DIRECTORY, "SYSTEM.md");
|
|
@@ -184,24 +192,29 @@ function loadPiPackageDirectory(): Promise<PiPackageDirectoryResolution> {
|
|
|
184
192
|
}
|
|
185
193
|
}).catch((error: unknown) => ({ error: `could not load @earendil-works/pi-coding-agent package metadata: ${errorText(error)}` }));
|
|
186
194
|
}
|
|
187
|
-
|
|
188
|
-
|
|
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> {
|
|
189
198
|
const packageDirectory = await loadPiPackageDirectory();
|
|
190
|
-
let
|
|
199
|
+
let modulePath: string | undefined;
|
|
191
200
|
if (packageDirectory.directory) {
|
|
192
|
-
const candidate = resolve(packageDirectory.directory, "dist/core
|
|
193
|
-
if (existsSync(candidate))
|
|
201
|
+
const candidate = resolve(packageDirectory.directory, "dist/core", file);
|
|
202
|
+
if (existsSync(candidate)) modulePath = candidate;
|
|
194
203
|
}
|
|
195
|
-
|
|
204
|
+
modulePath ??= resolve(dirname(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"))), "core", file);
|
|
196
205
|
const packageDirectoryError = packageDirectory.error ? ` Package directory lookup failed: ${packageDirectory.error}` : "";
|
|
197
|
-
|
|
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;
|
|
198
209
|
}
|
|
199
210
|
async function expandPromptTemplateForInspection(text: string, templates: readonly { name: string; content: string }[]): Promise<string> {
|
|
200
|
-
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);
|
|
201
215
|
}
|
|
202
216
|
type NativePromptSession = PiSession & {
|
|
203
|
-
readonly _extensionRunner?: { hasHandlers(event: string): boolean; emitInput(text: string, images: undefined, source: "interactive"): Promise<unknown>; emitBeforeAgentStart(prompt: string, images: undefined,
|
|
204
|
-
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 };
|
|
205
218
|
readonly _baseSystemPromptOptions?: unknown;
|
|
206
219
|
readonly _expandSkillCommand?: (text: string) => string;
|
|
207
220
|
readonly promptTemplates?: readonly { name: string; content: string }[];
|
|
@@ -210,12 +223,11 @@ async function preparePiPrompt(native: PiSession, text: string): Promise<PiPromp
|
|
|
210
223
|
const session = native as NativePromptSession;
|
|
211
224
|
const diagnostics: Array<{ type: "error"; message: string; source?: string }> = [];
|
|
212
225
|
const runner = session._extensionRunner;
|
|
213
|
-
const baseSystemPrompt =
|
|
226
|
+
const baseSystemPrompt = session.systemPrompt ?? "";
|
|
214
227
|
const baseOptions = session._baseSystemPromptOptions;
|
|
215
228
|
const expandSkillCommand = session._expandSkillCommand;
|
|
216
229
|
const templates = session.promptTemplates;
|
|
217
230
|
if (!runner) diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: extension runner is missing", source: "Pi session" });
|
|
218
|
-
if (typeof session._baseSystemPrompt !== "string") diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: base system prompt is missing", source: "Pi session" });
|
|
219
231
|
if (baseOptions === undefined) diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: base system prompt options are missing", source: "Pi session" });
|
|
220
232
|
if (typeof expandSkillCommand !== "function") diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: skill expansion is missing", source: "Pi session" });
|
|
221
233
|
if (!Array.isArray(templates)) diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: prompt templates are missing", source: "Pi session" });
|
|
@@ -237,9 +249,11 @@ async function preparePiPrompt(native: PiSession, text: string): Promise<PiPromp
|
|
|
237
249
|
try { expandedPrompt = await expandPromptTemplateForInspection(skillExpanded, Array.isArray(templates) ? templates : []); }
|
|
238
250
|
catch (error) { diagnostics.push({ type: "error", message: `Pi prompt template expansion is unavailable: ${errorText(error)}`, source: "Pi session" }); }
|
|
239
251
|
}
|
|
240
|
-
const result = !inputHandled && runner && baseOptions !== undefined ? await runner.emitBeforeAgentStart(expandedPrompt, undefined,
|
|
241
|
-
const prepared = result as { messages?: readonly unknown[];
|
|
242
|
-
|
|
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 };
|
|
243
257
|
} finally { unsubscribe?.(); }
|
|
244
258
|
}
|
|
245
259
|
|
|
@@ -281,6 +295,23 @@ export function flushExtensionProviders(resourceLoader: DefaultResourceLoader, m
|
|
|
281
295
|
return failures;
|
|
282
296
|
}
|
|
283
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
|
+
|
|
284
315
|
async function createLocalPiSessionHandle(input: SessionInput, sessionStartEvent?: WorkflowSessionStartEvent): Promise<LocalPiSessionHandle> {
|
|
285
316
|
const agentDir = input.agentDir ?? getAgentDir();
|
|
286
317
|
const systemPromptSource = workflowSystemPromptPath(input.cwd, agentDir, input.resourcePolicy?.projectTrusted ?? true);
|
|
@@ -293,13 +324,16 @@ async function createLocalPiSessionHandle(input: SessionInput, sessionStartEvent
|
|
|
293
324
|
const tools = [...new Set([...input.tools, ...customTools.map(({ name }) => name)])];
|
|
294
325
|
let settingsManager: SettingsManager;
|
|
295
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 ?? [])];
|
|
296
330
|
const policy = input.resourcePolicy;
|
|
297
331
|
if (policy) {
|
|
298
332
|
settingsManager = SettingsManager.create(input.cwd, agentDir, { projectTrusted: false });
|
|
299
333
|
settingsManager.setProjectTrusted(policy.projectTrusted);
|
|
300
|
-
const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager });
|
|
334
|
+
const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager, builtinExtensions: builtinNames });
|
|
301
335
|
const resolved = await packageManager.resolve();
|
|
302
|
-
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)))];
|
|
303
337
|
const selectorSources = policy.selectorSources;
|
|
304
338
|
const selection = resolveRole(undefined, {
|
|
305
339
|
cwd: input.cwd,
|
|
@@ -307,7 +341,7 @@ async function createLocalPiSessionHandle(input: SessionInput, sessionStartEvent
|
|
|
307
341
|
resources: { extensions: discoveredExtensions },
|
|
308
342
|
});
|
|
309
343
|
const selectedExtensions = selection.selectedExtensions ?? [];
|
|
310
|
-
const extensionPaths = selectedExtensions.filter((path) =>
|
|
344
|
+
const extensionPaths = selectedExtensions.filter((path) => path.startsWith(BUILTIN_EXTENSION_PREFIX) || !isWorkflowHostEntry(path));
|
|
311
345
|
policy.selectedExtensions = selectedExtensions;
|
|
312
346
|
policy.unmatchedExtensions = selection.unmatchedExtensions ?? [];
|
|
313
347
|
const skillPaths = [...new Set(resolved.skills.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => path))];
|
|
@@ -327,7 +361,7 @@ async function createLocalPiSessionHandle(input: SessionInput, sessionStartEvent
|
|
|
327
361
|
additionalExtensionPaths: extensionPaths,
|
|
328
362
|
noSkills: true,
|
|
329
363
|
additionalSkillPaths: [...new Set([...skillPaths, ...(input.additionalSkillPaths ?? [])])],
|
|
330
|
-
|
|
364
|
+
extensionFactories,
|
|
331
365
|
...(contextFilesOverride ? { agentsFilesOverride: contextFilesOverride } : {}),
|
|
332
366
|
skillsOverride: (base) => {
|
|
333
367
|
const disabledSkills = updateSkillMatches(base.skills);
|
|
@@ -339,10 +373,10 @@ async function createLocalPiSessionHandle(input: SessionInput, sessionStartEvent
|
|
|
339
373
|
await resourceLoader.reload();
|
|
340
374
|
} else {
|
|
341
375
|
settingsManager = SettingsManager.create(input.cwd, agentDir, { projectTrusted: true });
|
|
342
|
-
const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager });
|
|
376
|
+
const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager, builtinExtensions: builtinNames });
|
|
343
377
|
const resolved = await packageManager.resolve();
|
|
344
|
-
const extensionPaths = [...new Set(resolved.extensions.filter(({ enabled }) => enabled).map(({ path }) =>
|
|
345
|
-
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 ?? ""] } : {}) });
|
|
346
380
|
await resourceLoader.reload();
|
|
347
381
|
}
|
|
348
382
|
const providerFailures = flushExtensionProviders(resourceLoader, modelRuntime);
|
|
@@ -353,7 +387,7 @@ async function createLocalPiSessionHandle(input: SessionInput, sessionStartEvent
|
|
|
353
387
|
// missing, so the failure travels with the complaint rather than being
|
|
354
388
|
// swallowed — otherwise a misconfigured gateway reads as a typo in a model
|
|
355
389
|
// name.
|
|
356
|
-
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)" : "";
|
|
357
391
|
throw new WorkflowError("UNKNOWN_MODEL", `Unknown model: ${input.model.provider}/${input.model.model}${because}`);
|
|
358
392
|
}
|
|
359
393
|
const effectiveSessionStartEvent = input.settings === undefined && sessionStartEvent === undefined ? undefined : { ...(sessionStartEvent ?? { type: "session_start" as const, reason: "startup" as const }), settings: input.settings ?? Object.freeze({}) };
|
|
@@ -378,13 +412,13 @@ async function createLocalPiSessionHandle(input: SessionInput, sessionStartEvent
|
|
|
378
412
|
await shutdown("quit").catch(() => undefined);
|
|
379
413
|
throw error;
|
|
380
414
|
}
|
|
381
|
-
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)) };
|
|
382
416
|
const resourceInspection = (): PiResourceInspection => {
|
|
383
417
|
const extensions = resourceLoader.getExtensions();
|
|
384
418
|
const skills = resourceLoader.getSkills();
|
|
385
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 } : {}) }))];
|
|
386
420
|
const systemSource = systemPromptSource;
|
|
387
|
-
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" } : {}) };
|
|
388
422
|
};
|
|
389
423
|
const managedSession = Object.assign(session, {
|
|
390
424
|
getLeafId: () => manager.getLeafId(),
|
|
@@ -941,7 +975,7 @@ export class WorkflowAgentExecutor {
|
|
|
941
975
|
...(options.contextFiles === undefined ? {} : { contextFiles: options.contextFiles }),
|
|
942
976
|
...(options.effectiveTools === undefined ? {} : { effectiveTools: options.effectiveTools }),
|
|
943
977
|
});
|
|
944
|
-
return { model: resolved.model ?? this.root.model, ...(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 }) };
|
|
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 }) };
|
|
945
979
|
}
|
|
946
980
|
|
|
947
981
|
async execute(task: string, options: AgentExecutionOptions, signal?: AbortSignal, customTools: readonly ToolDefinition[] = [], setSteer?: (handler: (message: string) => void | Promise<void>) => void, beforeRetry?: () => void): Promise<AgentExecutionResult> {
|
|
@@ -1386,6 +1420,7 @@ export class FairAgentScheduler {
|
|
|
1386
1420
|
const agentTool = defineTool({
|
|
1387
1421
|
name: "agent", label: "Child Agent", description: "Start a direct child agent",
|
|
1388
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() }),
|
|
1389
1424
|
execute: async (_id, params) => {
|
|
1390
1425
|
if (!isChildAgentToolParams(params)) throw new WorkflowError("INVALID_METADATA", "Invalid child agent parameters");
|
|
1391
1426
|
validateAgentOptions(params);
|
|
@@ -1396,13 +1431,14 @@ export class FairAgentScheduler {
|
|
|
1396
1431
|
Reflect.deleteProperty(agentOptions, "prompt");
|
|
1397
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 }) };
|
|
1398
1433
|
const child = this.spawn(parent.runId, params.prompt, options, parentId);
|
|
1399
|
-
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 } };
|
|
1400
1435
|
},
|
|
1401
1436
|
});
|
|
1402
1437
|
const resultTool = defineTool({
|
|
1403
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",
|
|
1404
1439
|
parameters: Type.Object({ id: Type.String() }),
|
|
1405
|
-
|
|
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 }; }
|
|
1406
1442
|
});
|
|
1407
1443
|
const steerTool = defineTool({
|
|
1408
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
|
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
|
|
|
@@ -394,6 +395,22 @@ function decodeRunEvent(value: unknown): WorkflowRunEvent | undefined {
|
|
|
394
395
|
if (timestamp === INVALID_PERSISTED_VALUE) return undefined;
|
|
395
396
|
return { type: value.type, message: value.message, ...(timestamp === undefined ? {} : { timestamp }) };
|
|
396
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
|
+
}
|
|
397
414
|
function decodeDelivery(value: unknown): PersistedDelivery | undefined {
|
|
398
415
|
if (!object(value)) return undefined;
|
|
399
416
|
const mode = value.mode;
|
|
@@ -423,6 +440,8 @@ export function decodePersistedRun(value: unknown, allowLegacyAgentSessions = fa
|
|
|
423
440
|
const budgetEvents = value.budgetEvents === undefined ? undefined : decodeArray(value.budgetEvents, decodeBudgetEvent);
|
|
424
441
|
const events = value.events === undefined ? undefined : decodeArray(value.events, decodeRunEvent);
|
|
425
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;
|
|
426
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;
|
|
427
446
|
if (value.agentSessions === undefined && !allowLegacyAgentSessions) return undefined;
|
|
428
447
|
return {
|
|
@@ -441,6 +460,7 @@ export function decodePersistedRun(value: unknown, allowLegacyAgentSessions = fa
|
|
|
441
460
|
...(activeShells === undefined ? {} : { activeShells }),
|
|
442
461
|
...(activeShellStartedAt === undefined ? {} : { activeShellStartedAt }),
|
|
443
462
|
...(activeShellsByPhase === undefined ? {} : { activeShellsByPhase }),
|
|
463
|
+
...(scriptCalls === undefined ? {} : { scriptCalls }),
|
|
444
464
|
...(error === undefined ? {} : { error }),
|
|
445
465
|
...(failedAt === undefined ? {} : { failedAt }),
|
|
446
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";
|
package/src/host-delivery.ts
CHANGED
|
@@ -102,6 +102,7 @@ const WORKFLOW_ERROR_PROSE: Record<WorkflowErrorCode, (detail: string) => string
|
|
|
102
102
|
RUN_NOT_FOUND: (detail) => /^Unknown workflow run\b/.test(detail) ? "The workflow run was not found." : `The workflow run was not found: ${detail}.`,
|
|
103
103
|
RPC_LIMIT_EXCEEDED: (detail) => `The workflow communication data exceeded its size limit: ${detail}.`,
|
|
104
104
|
SHELL_FAILED: (detail) => `The workflow shell command failed: ${detail}.`,
|
|
105
|
+
TOOL_FAILED: (detail) => `The workflow tool call failed: ${detail}.`,
|
|
105
106
|
AGENT_TIMEOUT: (detail) => `The workflow agent timed out: ${detail}.`,
|
|
106
107
|
AGENT_FAILED: (detail) => `The workflow agent failed: ${detail}.`,
|
|
107
108
|
AGENT_RESULT_COLLECTED: (detail) => `The nested agent result was already collected: ${detail}.`,
|
package/src/host-recovery.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
3
|
-
import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { type ExtensionAPI, type ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { FairAgentScheduler, WorkflowAgentExecutor, type AgentDefinition, type AgentExecutionRoot, type AgentProviderFailure, type AgentProviderRecovery } from "./agent-execution.js";
|
|
5
5
|
import { listRunIds, RunStore, structuralPath as operationPath, type PersistedRun } from "./persistence.js";
|
|
6
6
|
import { budgetUsage, budgetRelaxed, mergeBudget, resumeBudgetAllowed, validateBudget, validateBudgetPatch, WorkflowBudgetRuntime } from "./budget.js";
|
|
7
7
|
import { aliasDrift, createLaunchSnapshot, errorCode, errorText, jsonValue, object } from "./utils.js";
|
|
8
|
-
import { LAUNCH_SNAPSHOT_IDENTITY_VERSION, WorkflowError, isExternallyEndedRunState, type AgentIdentity, type AgentResourcePolicy, type BudgetApprovalRequest, type JsonValue, type LaunchSnapshot, type ModelSpec, type RunState, type ShellIdentity, type ShellOptions, type ShellResult, type WorkflowExtensionSettings, type WorkflowMetadata, type WorkflowRetryProvenance, type WorkflowWorktreeReference } from "./types.js";
|
|
8
|
+
import { LAUNCH_SNAPSHOT_IDENTITY_VERSION, WorkflowError, isExternallyEndedRunState, type AgentIdentity, type AgentResourcePolicy, type BudgetApprovalRequest, type JsonValue, type LaunchSnapshot, type ModelSpec, type RunState, type ShellIdentity, type ShellOptions, type ShellResult, type ToolIdentity, type WorkflowExtensionSettings, type WorkflowMetadata, type WorkflowRetryProvenance, type WorkflowWorktreeReference } from "./types.js";
|
|
9
9
|
import { type WorkflowRegistryApi } from "./registry.js";
|
|
10
10
|
import { RunLifecycle, WorkflowEventPublisher, hostSessionContext, withWorkflowFunctions, withoutActiveShells, workflowRunContext, type WorkflowRunRecord, type WorkflowToolUpdate } from "./host-runtime.js";
|
|
11
11
|
import { runWorkflow } from "./execution.js";
|
|
@@ -13,7 +13,7 @@ import { createWorkflowFailureDiagnostics, formatWorkflowFailureDelivery, format
|
|
|
13
13
|
|
|
14
14
|
/** The subset of Pi's model registry the host relies on; every member is optional because older or headless hosts expose fewer of them. */
|
|
15
15
|
export type ModelRegistryCapability = { getAll?: () => readonly Model<Api>[]; getAvailable?: () => readonly Model<Api>[]; find?: (provider: string, model: string) => Model<Api> | undefined; refresh?: () => Promise<void>; getError?: () => string | undefined };
|
|
16
|
-
export type WorkflowRecoveryContext = { model: { provider: string; id: string } | undefined; modelRegistry: ModelRegistryCapability | undefined; deliveryContext: CompletionDeliveryContext; signal?: AbortSignal; resolvedAliases?: Readonly<Record<string, string>>; blockedAliases?: ReadonlySet<string>; blockedAliasTargets?: Readonly<Record<string, string>> };
|
|
16
|
+
export type WorkflowRecoveryContext = { model: { provider: string; id: string } | undefined; modelRegistry: ModelRegistryCapability | undefined; deliveryContext: CompletionDeliveryContext; toolContext?: ExtensionToolContext | undefined; signal?: AbortSignal; resolvedAliases?: Readonly<Record<string, string>>; blockedAliases?: ReadonlySet<string>; blockedAliasTargets?: Readonly<Record<string, string>> };
|
|
17
17
|
export type WorkflowRecoveryDependencies = {
|
|
18
18
|
pi: Pick<ExtensionAPI, "getThinkingLevel">;
|
|
19
19
|
home: string | undefined;
|
|
@@ -32,6 +32,7 @@ export type WorkflowRecoveryDependencies = {
|
|
|
32
32
|
resumeRoles: (store: RunStore, snapshot: Readonly<LaunchSnapshot>, cwd: string, trustedProject: boolean) => { definitions: Readonly<Record<string, AgentDefinition>>; capture: (role: string, model: ModelSpec) => Promise<void> };
|
|
33
33
|
workflowAgentHandler: (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle, executor: WorkflowAgentExecutor, cwd: string, runId: string, captureRole?: (role: string, model: ModelSpec) => Promise<void>) => (prompt: string, options: Readonly<Record<string, JsonValue>>, signal: AbortSignal, identity: AgentIdentity) => Promise<JsonValue>;
|
|
34
34
|
shellForRun: (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle, command: string, options: ShellOptions, signal: AbortSignal, identity: ShellIdentity) => Promise<ShellResult>;
|
|
35
|
+
toolForRun: (store: RunStore, lifecycle: RunLifecycle, context: ExtensionToolContext | undefined, identifier: string, args: Readonly<Record<string, JsonValue>>, signal: AbortSignal, identity: ToolIdentity) => Promise<JsonValue>;
|
|
35
36
|
resolveWorktree: (store: RunStore, metadata: WorkflowMetadata, owner: string) => Promise<Readonly<WorkflowWorktreeReference>>;
|
|
36
37
|
checkpointBridge: (runId: string, store: RunStore, metadata: WorkflowMetadata, foreground: boolean, ui?: { select?: (prompt: string, options: string[]) => Promise<string | undefined> }, headless?: boolean) => (raw: Readonly<Record<string, JsonValue>>, signal: AbortSignal) => Promise<boolean>;
|
|
37
38
|
phaseBridge: (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle) => (phase: string) => Promise<void>;
|
|
@@ -74,7 +75,7 @@ function assertExpectedWorkflowState(expectedState: string | undefined, actualSt
|
|
|
74
75
|
export function persistedFailure(run: PersistedRun, error: WorkflowError): PersistedRun { const failedAt = workflowFailedAt(error); return { ...run, error: { code: error.code, message: error.message, ...(failedAt ? { failedAt } : {}) }, ...(failedAt ? { failedAt } : {}) }; }
|
|
75
76
|
|
|
76
77
|
export function createWorkflowRecovery(deps: WorkflowRecoveryDependencies) {
|
|
77
|
-
const { pi, home, runs, scheduler, eventPublisher, persistRunState, projectTrusted, resumeHostContext, ensureSessionLease, createAgentExecutor, activeSnapshotTools, frozenResourcePolicy, resolveLaunchPrologue, resumeRoles, workflowAgentHandler, shellForRun, resolveWorktree, checkpointBridge, phaseBridge, logBridge, lifecycleFor, createProviderErrorRecovery, cleanupTerminalRun, deliver, deliverTerminal, workflowToolUpdate, registry, modelSpec } = deps;
|
|
78
|
+
const { pi, home, runs, scheduler, eventPublisher, persistRunState, projectTrusted, resumeHostContext, ensureSessionLease, createAgentExecutor, activeSnapshotTools, frozenResourcePolicy, resolveLaunchPrologue, resumeRoles, workflowAgentHandler, shellForRun, toolForRun, resolveWorktree, checkpointBridge, phaseBridge, logBridge, lifecycleFor, createProviderErrorRecovery, cleanupTerminalRun, deliver, deliverTerminal, workflowToolUpdate, registry, modelSpec } = deps;
|
|
78
79
|
const coordinateRunMutation = deps.coordinateRunMutation ?? (<T>(task: () => Promise<T>): Promise<T> => task());
|
|
79
80
|
type BudgetDecisionResult = { state: "running" | "completed" | "budget_exhausted"; approved: boolean; value?: JsonValue; run?: PersistedRun; completion?: CompletionDeliveryResult };
|
|
80
81
|
const budgetDecisionDelivery = (metadata: WorkflowMetadata, request: BudgetApprovalRequest) => `Workflow ${metadata.name} budget adjustment ${request.proposalId} for run ${request.runId} requires approval. Consumed usage: ${JSON.stringify(request.consumed)}. Previous limits: ${JSON.stringify(request.previous)}. Proposed limits: ${JSON.stringify(request.proposed)}. Respond with workflow_respond using proposalId ${request.proposalId}.`;
|
|
@@ -111,7 +112,7 @@ export function createWorkflowRecovery(deps: WorkflowRecoveryDependencies) {
|
|
|
111
112
|
return { hasUI: host?.hasUI === true, ui };
|
|
112
113
|
};
|
|
113
114
|
type ColdResumeResult = { value: JsonValue; resultPath: string; resultBytes: number; completion: CompletionDeliveryResult };
|
|
114
|
-
const coldResumeRun = async (run: WorkflowRunRecord, hasUI: boolean, ui: { select?: (prompt: string, options: string[]) => Promise<string | undefined> }, trustedProject: boolean, context?: { model: { provider: string; id: string } | undefined; modelRegistry: WorkflowRecoveryContext["modelRegistry"]; deliveryContext: CompletionDeliveryContext; signal?: AbortSignal | undefined; resolvedAliases?: Readonly<Record<string, string>>; blockedAliases?: ReadonlySet<string>; blockedAliasTargets?: Readonly<Record<string, string>> }, modeOverride?: boolean, waitForCompletion = true): Promise<ColdResumeResult | undefined> => {
|
|
115
|
+
const coldResumeRun = async (run: WorkflowRunRecord, hasUI: boolean, ui: { select?: (prompt: string, options: string[]) => Promise<string | undefined> }, trustedProject: boolean, context?: { model: { provider: string; id: string } | undefined; modelRegistry: WorkflowRecoveryContext["modelRegistry"]; deliveryContext: CompletionDeliveryContext; toolContext?: ExtensionToolContext | undefined; signal?: AbortSignal | undefined; resolvedAliases?: Readonly<Record<string, string>>; blockedAliases?: ReadonlySet<string>; blockedAliasTargets?: Readonly<Record<string, string>> }, modeOverride?: boolean, waitForCompletion = true): Promise<ColdResumeResult | undefined> => {
|
|
115
116
|
const loaded = await run.store.load();
|
|
116
117
|
const foreground = modeOverride ?? (loaded.run.delivery?.mode === "foreground" || (loaded.run.delivery?.mode === "background" && loaded.run.delivery.toolCallId !== undefined) || (loaded.run.delivery === undefined && loaded.snapshot.launchMode === "foreground"));
|
|
117
118
|
if (loaded.run.activeShells !== undefined || loaded.run.activeShellStartedAt !== undefined || loaded.run.activeShellsByPhase !== undefined) await persistRunState(run.store, run.metadata, withoutActiveShells);
|
|
@@ -137,7 +138,7 @@ export function createWorkflowRecovery(deps: WorkflowRecoveryDependencies) {
|
|
|
137
138
|
run.executor.setRunContext(runContext);
|
|
138
139
|
await scheduler.cancelRun(run.store.runId);
|
|
139
140
|
await run.lifecycle.resume();
|
|
140
|
-
const execution = runWorkflow(script, loaded.snapshot.args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(run.store, run.metadata, run.lifecycle, command, options, signal, identity), agent: workflowAgentHandler(run.store, run.metadata, run.lifecycle, run.executor, run.store.cwd, run.store.runId, roles.capture), worktree: async (owner) => resolveWorktree(run.store, run.metadata, owner), checkpoint: checkpointBridge(run.store.runId, run.store, run.metadata, foreground, hasUI ? ui : undefined), phase: phaseBridge(run.store, run.metadata, run.lifecycle), log: logBridge(run.store, run.lifecycle, run.metadata.name) }, run.store, runContext, registry, snapshot.settings.extensionSettings), controller.signal);
|
|
141
|
+
const execution = runWorkflow(script, loaded.snapshot.args, withWorkflowFunctions({ tool: (identifier, args, signal, identity) => toolForRun(run.store, run.lifecycle, context?.toolContext, identifier, args, signal, identity), shell: (command, options, signal, identity) => shellForRun(run.store, run.metadata, run.lifecycle, command, options, signal, identity), agent: workflowAgentHandler(run.store, run.metadata, run.lifecycle, run.executor, run.store.cwd, run.store.runId, roles.capture), worktree: async (owner) => resolveWorktree(run.store, run.metadata, owner), checkpoint: checkpointBridge(run.store.runId, run.store, run.metadata, foreground, hasUI ? ui : undefined), phase: phaseBridge(run.store, run.metadata, run.lifecycle), log: logBridge(run.store, run.lifecycle, run.metadata.name) }, run.store, runContext, registry, snapshot.settings.extensionSettings), controller.signal);
|
|
141
142
|
run.execution = execution;
|
|
142
143
|
const completion = execution.result.then(async (value) => {
|
|
143
144
|
await scheduler.flush(run.store.runId);
|
|
@@ -314,7 +315,7 @@ export function createWorkflowRecovery(deps: WorkflowRecoveryDependencies) {
|
|
|
314
315
|
await eventPublisher.runStarted(prepared.childRun.store, prepared.childRun.metadata);
|
|
315
316
|
const { hasUI, ui } = recoveryUi(context);
|
|
316
317
|
const recoveryContext = resumeHostContext(context);
|
|
317
|
-
const completed = await coldResumeRun(prepared.childRun, hasUI, ui, projectTrusted(context), { model: prepared.hostModel, modelRegistry: prepared.modelRegistry, deliveryContext: recoveryContext.deliveryContext, resolvedAliases: prepared.currentAliases, blockedAliases: prepared.blockedAliases, blockedAliasTargets: prepared.blockedAliasTargets, ...(signal ? { signal } : {}) }, modeOverride);
|
|
318
|
+
const completed = await coldResumeRun(prepared.childRun, hasUI, ui, projectTrusted(context), { model: prepared.hostModel, modelRegistry: prepared.modelRegistry, deliveryContext: recoveryContext.deliveryContext, toolContext: recoveryContext.toolContext, resolvedAliases: prepared.currentAliases, blockedAliases: prepared.blockedAliases, blockedAliasTargets: prepared.blockedAliasTargets, ...(signal ? { signal } : {}) }, modeOverride);
|
|
318
319
|
const completion = runs.get(prepared.childRun.store.runId)?.completion;
|
|
319
320
|
if (completion) {
|
|
320
321
|
childStarted = true;
|
package/src/host-view.ts
CHANGED
|
@@ -90,6 +90,15 @@ function formatShellActivity(activeShells: number | undefined, startedAt: number
|
|
|
90
90
|
const timing = started && startedAt !== undefined && !Number.isNaN(started.getTime()) ? ` ${styles.dim(`started=${started.toISOString()} elapsed=${formatWorkflowRuntime(Math.max(0, now - startedAt))}`)}` : "";
|
|
91
91
|
return `${styles.accent(spinner)} shell ${styles.accent("[running]")} ${styles.dim(`(${String(count)} active)`)}${timing}`;
|
|
92
92
|
}
|
|
93
|
+
function formatToolActivity(run: Pick<PersistedRun, "activeTools">, spinner: string, styles: WorkflowProgressStyles, now: number): string | undefined {
|
|
94
|
+
const calls = run.activeTools ?? [];
|
|
95
|
+
if (!calls.length) return undefined;
|
|
96
|
+
const counts = new Map<string, number>();
|
|
97
|
+
for (const { name } of calls) counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
98
|
+
const names = [...counts].map(([name, count]) => count > 1 ? `${name} ×${String(count)}` : name).join(", ");
|
|
99
|
+
const elapsed = formatWorkflowRuntime(Math.max(0, now - Math.min(...calls.map(({ startedAt }) => startedAt))));
|
|
100
|
+
return `${styles.accent(spinner)} tools ${styles.accent("[running]")} ${sanitizeDisplayText(names)} ${styles.dim(`elapsed=${elapsed}`)}`;
|
|
101
|
+
}
|
|
93
102
|
function formatLogTimestamp(timestamp: number | undefined): string {
|
|
94
103
|
if (timestamp === undefined || !Number.isFinite(timestamp)) return "--:--:--";
|
|
95
104
|
const date = new Date(timestamp);
|
|
@@ -161,12 +170,15 @@ function workflowProgressLines(run: PersistedRun, spinner: string, styles: Workf
|
|
|
161
170
|
const scopedShells = (run.activeShellsByPhase?.length ?? 0) > 0;
|
|
162
171
|
const shellActivity = scopedShells ? undefined : formatShellActivity(run.activeShells, run.activeShellStartedAt, spinner, styles, now);
|
|
163
172
|
if (shellActivity) lines.push(` ${shellActivity}`);
|
|
173
|
+
const toolActivity = formatToolActivity(run, spinner, styles, now);
|
|
174
|
+
if (toolActivity) lines.push(` ${toolActivity}`);
|
|
164
175
|
const byId = new Map(run.agents.map((agent) => [agent.id, agent]));
|
|
165
176
|
const position = new Map(run.agents.map((agent, index) => [agent, index]));
|
|
166
177
|
const renderAgents = (agents: readonly AgentRecord[], nested: boolean) => renderGroupedAgents(agents, ({ agent, depth }, grouped) => {
|
|
167
|
-
const icon = agentStateGlyph(agent.state,
|
|
178
|
+
const icon = agentStateGlyph(agent.state, "●");
|
|
168
179
|
const indent = " ".repeat((grouped ? 2 : 1) + depth);
|
|
169
|
-
const
|
|
180
|
+
const idleSpinner = agent.state === "running" && !agentActivityLabel(agent) ? styles.accent(spinner) : "";
|
|
181
|
+
const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : [idleSpinner, formatAgentActivity(agent, spinner, styles, now)].filter(Boolean).join(" ");
|
|
170
182
|
const name = grouped ? agent.label ?? agent.name : styledAgentBreadcrumb(agent, byId, styles);
|
|
171
183
|
const state = progressStyleForState(agent.state, styles);
|
|
172
184
|
const detail = expanded ? formatWorkflowAgentDetail(agent, now) : "";
|
|
@@ -469,7 +481,7 @@ export function workflowProgressBlock(run: PersistedRun, theme: Theme, progress?
|
|
|
469
481
|
invalidate() {
|
|
470
482
|
const displayed = currentRun();
|
|
471
483
|
const now = Date.now();
|
|
472
|
-
if (!progress || !refresh || displayed.state !== "running" || (!displayed.agents.some((agent) => agent.state === "running") && (displayed.activeShells ?? 0) <= 0)) return;
|
|
484
|
+
if (!progress || !refresh || displayed.state !== "running" || (!displayed.agents.some((agent) => agent.state === "running") && (displayed.activeShells ?? 0) <= 0 && !displayed.activeTools?.length)) return;
|
|
473
485
|
if (progress.refresh || now - progress.lastRefreshAt < WORKFLOW_PROGRESS_REFRESH_MS) return;
|
|
474
486
|
progress.lastRefreshAt = now;
|
|
475
487
|
const inputRun = progress.inputRun;
|
|
@@ -573,7 +585,7 @@ export function formatStalledDuration(durationMs: number): string {
|
|
|
573
585
|
const remainingMinutes = minutes % 60;
|
|
574
586
|
return `${String(hours)}h${remainingMinutes ? ` ${String(remainingMinutes)}m` : ""}`;
|
|
575
587
|
}
|
|
576
|
-
function stalledDuration(agent: Pick<AgentDetailPresentation, "state" | "lastEventAt">, now: number): number | undefined {
|
|
588
|
+
export function stalledDuration(agent: Pick<AgentDetailPresentation, "state" | "lastEventAt">, now: number): number | undefined {
|
|
577
589
|
if (agent.state !== "running" || agent.lastEventAt === undefined || !Number.isFinite(agent.lastEventAt)) return undefined;
|
|
578
590
|
const duration = now - agent.lastEventAt;
|
|
579
591
|
return duration >= WORKFLOW_AGENT_STALL_THRESHOLD_MS ? duration : undefined;
|
|
@@ -583,7 +595,7 @@ function elapsedDurationMs(agent: Pick<AgentDetailPresentation, "durationMs" | "
|
|
|
583
595
|
if (agent.durationMs !== undefined && Number.isFinite(agent.durationMs)) return Math.max(0, agent.durationMs);
|
|
584
596
|
return agent.startedAt === undefined || !Number.isFinite(agent.startedAt) ? undefined : Math.max(0, end - agent.startedAt);
|
|
585
597
|
}
|
|
586
|
-
function agentActivityLabel(agent: { readonly activity?: AgentRecord["activity"]; readonly toolCalls?: AgentRecord["toolCalls"] }): string {
|
|
598
|
+
export function agentActivityLabel(agent: { readonly activity?: AgentRecord["activity"]; readonly toolCalls?: AgentRecord["toolCalls"] }): string {
|
|
587
599
|
const activity = agent.activity;
|
|
588
600
|
if (activity?.kind === "reasoning" || activity?.kind === "text") {
|
|
589
601
|
const name = activity.kind === "reasoning" ? "reasoning" : "responding";
|
|
@@ -715,6 +727,8 @@ export function formatNavigatorDashboard(run: PersistedRun, checkpoints: readonl
|
|
|
715
727
|
const lines = [header, meta, ...formatCompactBudgetStatus(run)];
|
|
716
728
|
const shellActivity = formatShellActivity(run.activeShells, run.activeShellStartedAt, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now);
|
|
717
729
|
if (shellActivity) lines.push(` ${shellActivity}`);
|
|
730
|
+
const toolActivity = formatToolActivity(run, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now);
|
|
731
|
+
if (toolActivity) lines.push(` ${toolActivity}`);
|
|
718
732
|
if (run.error) lines.push(`Error: ${run.error.code}: ${run.error.message}`);
|
|
719
733
|
if (run.events?.length) lines.push(...run.events.filter((event) => event.type === "warning").map((event) => `Warning: ${event.message}`));
|
|
720
734
|
lines.push("");
|
|
@@ -752,6 +766,8 @@ export function formatNavigatorRun(loaded: { run: PersistedRun; snapshot: Readon
|
|
|
752
766
|
];
|
|
753
767
|
const shellActivity = formatShellActivity(run.activeShells, run.activeShellStartedAt, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now);
|
|
754
768
|
if (shellActivity) lines.push(` ${shellActivity}`);
|
|
769
|
+
const toolActivity = formatToolActivity(run, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now);
|
|
770
|
+
if (toolActivity) lines.push(` ${toolActivity}`);
|
|
755
771
|
if (run.error) lines.push(`Run error: ${run.error.code}: ${run.error.message}`);
|
|
756
772
|
if (run.events?.length) lines.push(...run.events.filter((event) => event.type === "warning").map((event) => `Warning: ${event.message}`));
|
|
757
773
|
const aliases = snapshot.modelAliases ?? snapshot.settings.modelAliases;
|
|
@@ -849,6 +865,8 @@ export function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readon
|
|
|
849
865
|
const scopedShells = (run.activeShellsByPhase?.length ?? 0) > 0;
|
|
850
866
|
const shellActivity = scopedShells ? undefined : formatShellActivity(run.activeShells, run.activeShellStartedAt, "⠦", styles, now);
|
|
851
867
|
if (shellActivity) lines.push(` ${shellActivity}`);
|
|
868
|
+
const toolActivity = formatToolActivity(run, "⠦", styles, now);
|
|
869
|
+
if (toolActivity) lines.push(` ${toolActivity}`);
|
|
852
870
|
const actionRows = (): string[] => {
|
|
853
871
|
const actions = selection.actions;
|
|
854
872
|
if (!actions) return [];
|