@marcoscale98/pi-extensible-workflows 5.17.0-fork.1 → 6.2.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 +145 -0
- package/README.md +2 -2
- package/dist/src/agent-execution.d.ts +43 -10
- package/dist/src/agent-execution.js +442 -162
- package/dist/src/background-widget.d.ts +5 -1
- package/dist/src/background-widget.js +5 -10
- package/dist/src/decoders.d.ts +2 -3
- package/dist/src/decoders.js +59 -37
- package/dist/src/execution.d.ts +6 -2
- package/dist/src/execution.js +48 -7
- package/dist/src/host-delivery.js +5 -8
- package/dist/src/host-navigator.js +5 -2
- package/dist/src/host-phases.d.ts +1 -0
- package/dist/src/host-recovery.d.ts +11 -8
- package/dist/src/host-recovery.js +14 -22
- package/dist/src/host-runtime.js +56 -5
- package/dist/src/host-view.d.ts +6 -1
- package/dist/src/host-view.js +46 -20
- package/dist/src/host.d.ts +2 -2
- package/dist/src/host.js +246 -118
- package/dist/src/index.d.ts +1 -2
- package/dist/src/index.js +1985 -1690
- package/dist/src/index.js.map +3 -3
- package/dist/src/paths.d.ts +8 -5
- package/dist/src/paths.js +24 -13
- package/dist/src/persistence.d.ts +1 -0
- package/dist/src/persistence.js +1 -0
- package/dist/src/pi-runtime-runner.js +4 -1
- package/dist/src/registry.d.ts +9 -7
- package/dist/src/registry.js +29 -71
- package/dist/src/runtime/agent-runner.d.ts +2 -0
- package/dist/src/script-tools.d.ts +27 -0
- package/dist/src/script-tools.js +87 -0
- package/dist/src/session-lease.d.ts +1 -1
- package/dist/src/session-lease.js +45 -11
- package/dist/src/settings.d.ts +19 -3
- package/dist/src/settings.js +63 -50
- package/dist/src/store.d.ts +3 -1
- package/dist/src/store.js +17 -1
- package/dist/src/trajectory.d.ts +12 -3
- package/dist/src/trajectory.js +32 -11
- package/dist/src/types.d.ts +129 -45
- package/dist/src/types.js +5 -8
- package/dist/src/utils.d.ts +21 -1
- package/dist/src/utils.js +46 -6
- package/dist/src/validation.d.ts +14 -3
- package/dist/src/validation.js +143 -87
- package/dist/starter/index.js +8 -11
- package/dist/starter/index.js.map +1 -1
- package/dist/starter/review-loop.d.ts +0 -13
- package/dist/starter/review-loop.js +8 -9
- package/dist/subagents/index.js +904 -956
- package/dist/subagents/index.js.map +3 -3
- package/dist/subagents/src/contracts.d.ts +48 -31
- package/dist/subagents/src/contracts.js +20 -9
- package/dist/subagents/src/index.d.ts +1 -1
- package/dist/subagents/src/index.js +14 -8
- package/dist/subagents/src/manager.js +52 -73
- package/dist/subagents/src/navigator.js +3 -14
- package/dist/subagents/src/view.d.ts +1 -1
- package/dist/subagents/src/view.js +15 -68
- package/dist/subagents/src/worktree.js +1 -1
- package/dist/trajectory/assets/index.html +35 -13
- package/dist/trajectory/index.js +576 -932
- package/dist/trajectory/index.js.map +3 -3
- package/dist/trajectory/src/assets/index.html +35 -13
- package/dist/trajectory/src/index.js +134 -104
- package/dist/trajectory/src/server.js +268 -27
- package/dist/trajectory/src/server.js.map +3 -3
- package/dist/trajectory/src/startup-mutex.d.ts +17 -0
- package/dist/trajectory/src/startup-mutex.js +172 -0
- package/examples/workflow-extension-template/README.md +4 -7
- package/examples/workflow-extension-template/extension.test.mjs +4 -8
- package/examples/workflow-extension-template/index.js +0 -3
- package/package.json +10 -10
- package/skills/pi-extensible-workflows/SKILL.md +12 -8
- package/src/agent-execution.ts +412 -164
- package/src/background-widget.ts +5 -11
- package/src/decoders.ts +51 -37
- package/src/execution.ts +46 -9
- package/src/host-delivery.ts +5 -8
- package/src/host-navigator.ts +4 -2
- package/src/host-phases.ts +1 -1
- package/src/host-recovery.ts +23 -28
- package/src/host-runtime.ts +44 -6
- package/src/host-view.ts +41 -16
- package/src/host.ts +229 -142
- package/src/index.ts +1 -2
- package/src/paths.ts +18 -9
- package/src/persistence.ts +1 -0
- package/src/pi-runtime-runner.ts +3 -1
- package/src/registry.ts +28 -64
- package/src/runtime/agent-runner.ts +2 -0
- package/src/script-tools.ts +76 -0
- package/src/session-lease.ts +34 -6
- package/src/settings.ts +56 -50
- package/src/store.ts +18 -2
- package/src/trajectory.ts +32 -15
- package/src/types.ts +82 -34
- package/src/utils.ts +42 -5
- package/src/validation.ts +121 -80
- package/starter/index.ts +0 -1
- package/starter/review-loop.ts +8 -9
- package/starter/roles/developer.md +0 -1
- package/starter/roles/oracle.md +0 -1
- package/starter/roles/researcher.md +0 -1
- package/starter/roles/reviewer.md +0 -1
- package/starter/roles/scout.md +0 -1
- package/subagents/README.md +10 -16
- package/subagents/src/contracts.ts +21 -11
- package/subagents/src/index.ts +17 -8
- package/subagents/src/manager.ts +48 -128
- package/subagents/src/navigator.ts +4 -13
- package/subagents/src/view.ts +16 -63
- package/subagents/src/worktree.ts +1 -1
- package/trajectory/src/assets/index.html +35 -13
- package/trajectory/src/index.ts +112 -91
- package/trajectory/src/server.ts +46 -10
- package/trajectory/src/startup-mutex.ts +127 -0
- package/dist/src/roles.d.ts +0 -70
- package/dist/src/roles.js +0 -325
- package/dist/starter/roles/developer.md +0 -6
- package/dist/starter/roles/oracle.md +0 -18
- package/dist/starter/roles/researcher.md +0 -20
- package/dist/starter/roles/reviewer.md +0 -19
- package/dist/starter/roles/scout.md +0 -18
- package/examples/workflow-extension-template/roles/reviewer.md +0 -4
- package/src/roles.ts +0 -326
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { copyFile } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
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
|
+
import { Value } from "typebox/value";
|
|
7
8
|
import { Compile } from "typebox/compile";
|
|
8
|
-
import { createAgentSession, DefaultPackageManager, DefaultResourceLoader, defineTool, getAgentDir, ModelRuntime, SessionManager, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { createAgentSession, createCodemodeExtension, createMcpExtension, createToolSearchExtension, DefaultPackageManager, DefaultResourceLoader, defineTool, getAgentDir, ModelRuntime, SessionManager, SettingsManager, loadSkills } from "@earendil-works/pi-coding-agent";
|
|
9
10
|
;
|
|
10
|
-
import { SerialLane, assertModelThinking, byPriorityThenName, deepFreeze, errorText, jsonObject, jsonValue,
|
|
11
|
+
import { SerialLane, assertModelThinking, byPriorityThenName, deepFreeze, errorCode, errorText, jsonObject, jsonValue, object, physicalModel, resolveModelReference, resourcePatternHasMagic, selectResourcesByLayers, modelCapability, unmatchedResourcePatterns } from "./utils.js";
|
|
11
12
|
import { SETTLED_AGENT_STATES, WorkflowError, isContextFileScope, zeroAccounting } from "./types.js";
|
|
12
13
|
import { createLiveSessionHandoff } from "./session-handoff.js";
|
|
13
14
|
import { createToolTimingExtension } from "./tool-timing.js";
|
|
14
15
|
import { isEmptyAbortedAssistant, normalizePiMessage, normalizePiSessionEvent, runtimeProgressToAgentProgress } from "./pi-runtime-adapter.js";
|
|
15
16
|
import { createPiRuntimeAgentRunner, isRuntimeAgentProviderError, normalizePiRuntimeError } from "./pi-runtime-runner.js";
|
|
16
17
|
import { defaultWorkflowResultSchema } from "./runtime/workflow-result.js";
|
|
17
|
-
import { validateAgentOptions, validateSchema } from "./validation.js";
|
|
18
|
-
import { canonicalPath } from "./paths.js";
|
|
19
|
-
import {
|
|
18
|
+
import { isCoreAgentOption, validateAgentOptions, validateSchema } from "./validation.js";
|
|
19
|
+
import { canonicalPath, extensionIdentity, piResourceContains, piResourcePath, sameFilesystemPath } from "./paths.js";
|
|
20
|
+
import { isCorePackageName } from "./package-identity.js";
|
|
21
|
+
import { canonicalExtensionSelector, validateContextFileScopes, validateSelectorList, validateWorkflowExtensionSettings } from "./settings.js";
|
|
22
|
+
import { agentIdentityPath } from "./execution.js";
|
|
20
23
|
const localToolTimingExtension = createToolTimingExtension();
|
|
21
24
|
function parseModel(value, fallback, aliases = {}, knownModels, settingsPath) {
|
|
22
25
|
if (!value)
|
|
@@ -24,19 +27,83 @@ function parseModel(value, fallback, aliases = {}, knownModels, settingsPath) {
|
|
|
24
27
|
assertModelThinking(value);
|
|
25
28
|
return resolveModelReference(value, aliases, knownModels, settingsPath);
|
|
26
29
|
}
|
|
30
|
+
async function authorizedResources(root, cwd, options) {
|
|
31
|
+
const agentDir = root.agentDir ?? getAgentDir();
|
|
32
|
+
const trusted = root.projectTrusted ?? false;
|
|
33
|
+
const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: false });
|
|
34
|
+
settingsManager.setProjectTrusted(trusted);
|
|
35
|
+
const resolved = await new DefaultPackageManager({ cwd, agentDir, settingsManager, builtinExtensions: ["codemode", "tool-search", "mcp"] }).resolve();
|
|
36
|
+
const extensions = resolved.extensions.filter(({ enabled, metadata }) => enabled && (trusted || metadata.scope !== "project")).map(({ path }) => extensionIdentity(path));
|
|
37
|
+
const skillPaths = resolved.skills.filter(({ enabled, metadata }) => enabled && (trusted || metadata.scope !== "project")).map(({ path }) => path);
|
|
38
|
+
const skills = loadSkills({ cwd, agentDir, skillPaths: [...skillPaths, ...(root.additionalSkillPaths ?? [])], includeDefaults: false }).skills.map(({ name }) => name);
|
|
39
|
+
const parent = options.capabilities;
|
|
40
|
+
return { tools: [...root.tools].filter((tool) => options.effectiveTools === undefined || options.effectiveTools.includes(tool)).filter((tool) => parent === undefined || parent.tools.includes(tool)), skills: [...new Set(skills)].filter((name) => parent === undefined || parent.skills.includes(name)), extensions: [...new Set(extensions)].filter((path) => parent === undefined || parent.extensions.includes(path)) };
|
|
41
|
+
}
|
|
42
|
+
function validatePreparedConfiguration(root, configuration, capabilities, cwd, resumed) {
|
|
43
|
+
const code = resumed ? "RESUME_INCOMPATIBLE" : "INVALID_METADATA";
|
|
44
|
+
if (!jsonObject(configuration) || typeof configuration.projectTrusted !== "boolean" || typeof configuration.systemPromptAppend !== "string" || configuration.systemPrompt !== undefined && typeof configuration.systemPrompt !== "string")
|
|
45
|
+
throw new WorkflowError(code, "Invalid prepared agent configuration");
|
|
46
|
+
if (configuration.projectTrusted && !root.projectTrusted)
|
|
47
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", "Prepared agent configuration lost project trust");
|
|
48
|
+
if (!object(configuration.model) || typeof configuration.model.provider !== "string" || typeof configuration.model.model !== "string")
|
|
49
|
+
throw new WorkflowError(code, "Invalid prepared model");
|
|
50
|
+
const model = resolveModelReference(`${configuration.model.provider}/${configuration.model.model}${configuration.model.thinking === undefined ? "" : `:${configuration.model.thinking}`}`);
|
|
51
|
+
if (root.availableModels !== undefined && !root.availableModels.has(modelCapability(model)))
|
|
52
|
+
throw new WorkflowError(resumed ? "RESUME_INCOMPATIBLE" : "UNKNOWN_MODEL", `Required model is unavailable: ${modelCapability(model)}`);
|
|
53
|
+
const tools = validateSelectorList(configuration.tools, "configuration", "tools", "INVALID_METADATA", false);
|
|
54
|
+
if (tools === undefined)
|
|
55
|
+
throw new WorkflowError(code, "Prepared tools are missing");
|
|
56
|
+
const missing = tools.find((tool) => !capabilities.tools.includes(tool));
|
|
57
|
+
if (missing)
|
|
58
|
+
throw new WorkflowError(resumed ? "RESUME_INCOMPATIBLE" : "UNKNOWN_TOOL", `Required tool is unavailable: ${missing}`);
|
|
59
|
+
const excludeTools = configuration.excludeTools;
|
|
60
|
+
try {
|
|
61
|
+
validateAgentOptions({ excludeTools });
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new WorkflowError(code, "Invalid prepared excludeTools");
|
|
65
|
+
}
|
|
66
|
+
if (!Array.isArray(excludeTools) || tools.some((tool) => excludeTools.includes(tool)))
|
|
67
|
+
throw new WorkflowError(code, "Prepared excludeTools contradict the prepared tools");
|
|
68
|
+
validateSelectorList(configuration.skills, "configuration", "skills", "INVALID_METADATA", false);
|
|
69
|
+
validateSelectorList(configuration.extensions, "configuration", "extensions", "INVALID_METADATA", false);
|
|
70
|
+
if (resumed)
|
|
71
|
+
for (const key of ["skills", "extensions"]) {
|
|
72
|
+
const selectors = configuration[key];
|
|
73
|
+
const selected = selectors.slice(selectors.lastIndexOf("!*") + 1);
|
|
74
|
+
const missing = selected.find((name) => !capabilities[key].includes(name));
|
|
75
|
+
if (missing)
|
|
76
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", `Required ${key} capability is unavailable: ${missing}`);
|
|
77
|
+
}
|
|
78
|
+
validateContextFileScopes(configuration.contextFiles, "configuration");
|
|
79
|
+
validateWorkflowExtensionSettings(configuration.settings, "configuration", "INVALID_METADATA");
|
|
80
|
+
root.validateExtensionSettings?.(configuration.settings, { source: "effective", cwd, projectTrusted: root.projectTrusted ?? false });
|
|
81
|
+
return deepFreeze(structuredClone(configuration));
|
|
82
|
+
}
|
|
27
83
|
function accounting(stats) {
|
|
28
84
|
return { input: stats.tokens.input, output: stats.tokens.output, cacheRead: stats.tokens.cacheRead, cacheWrite: stats.tokens.cacheWrite, cost: stats.cost };
|
|
29
85
|
}
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
86
|
+
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"];
|
|
87
|
+
function isWorkflowHostEntry(path) {
|
|
88
|
+
const entryPath = canonicalPath(path);
|
|
89
|
+
let packageRoot = dirname(entryPath);
|
|
90
|
+
while (packageRoot !== dirname(packageRoot)) {
|
|
91
|
+
const packageMetadataPath = join(packageRoot, "package.json");
|
|
92
|
+
if (existsSync(packageMetadataPath)) {
|
|
93
|
+
try {
|
|
94
|
+
const metadata = JSON.parse(readFileSync(packageMetadataPath, "utf8"));
|
|
95
|
+
if (typeof metadata !== "object" || metadata === null || !("name" in metadata) || !isCorePackageName(metadata.name))
|
|
96
|
+
return false;
|
|
97
|
+
return WORKFLOW_HOST_ENTRY_PATHS.some((entry) => canonicalPath(join(packageRoot, entry)) === entryPath);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
packageRoot = dirname(packageRoot);
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
40
107
|
const WORKFLOW_DIRECTORY = "pi-extensible-workflows";
|
|
41
108
|
function workflowSystemPromptPath(cwd, agentDir, projectTrusted) {
|
|
42
109
|
const projectPath = join(cwd, ".pi", WORKFLOW_DIRECTORY, "SYSTEM.md");
|
|
@@ -71,34 +138,41 @@ function loadPiPackageDirectory() {
|
|
|
71
138
|
}
|
|
72
139
|
}).catch((error) => ({ error: `could not load @earendil-works/pi-coding-agent package metadata: ${errorText(error)}` }));
|
|
73
140
|
}
|
|
74
|
-
|
|
75
|
-
|
|
141
|
+
const nativeCoreModules = new Map();
|
|
142
|
+
/** Pi does not export these core modules, so inspection loads them from the installed package. */
|
|
143
|
+
async function loadNativeCoreModule(file, label) {
|
|
76
144
|
const packageDirectory = await loadPiPackageDirectory();
|
|
77
|
-
let
|
|
145
|
+
let modulePath;
|
|
78
146
|
if (packageDirectory.directory) {
|
|
79
|
-
const candidate = resolve(packageDirectory.directory, "dist/core
|
|
147
|
+
const candidate = resolve(packageDirectory.directory, "dist/core", file);
|
|
80
148
|
if (existsSync(candidate))
|
|
81
|
-
|
|
149
|
+
modulePath = candidate;
|
|
82
150
|
}
|
|
83
|
-
|
|
151
|
+
modulePath ??= resolve(dirname(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"))), "core", file);
|
|
84
152
|
const packageDirectoryError = packageDirectory.error ? ` Package directory lookup failed: ${packageDirectory.error}` : "";
|
|
85
|
-
|
|
153
|
+
let loaded = nativeCoreModules.get(file);
|
|
154
|
+
if (!loaded) {
|
|
155
|
+
loaded = import(modulePath).catch((error) => { throw new Error(`Could not load Pi ${label}.${packageDirectoryError}`, { cause: error }); });
|
|
156
|
+
nativeCoreModules.set(file, loaded);
|
|
157
|
+
}
|
|
158
|
+
return loaded;
|
|
86
159
|
}
|
|
87
160
|
async function expandPromptTemplateForInspection(text, templates) {
|
|
88
|
-
return (await
|
|
161
|
+
return (await loadNativeCoreModule("prompt-templates.js", "prompt templates")).expandPromptTemplate(text, templates);
|
|
162
|
+
}
|
|
163
|
+
async function buildSystemPromptForInspection(options) {
|
|
164
|
+
return (await loadNativeCoreModule("system-prompt.js", "system prompt builder")).buildSystemPrompt(options);
|
|
89
165
|
}
|
|
90
166
|
async function preparePiPrompt(native, text) {
|
|
91
167
|
const session = native;
|
|
92
168
|
const diagnostics = [];
|
|
93
169
|
const runner = session._extensionRunner;
|
|
94
|
-
const baseSystemPrompt =
|
|
170
|
+
const baseSystemPrompt = session.systemPrompt ?? "";
|
|
95
171
|
const baseOptions = session._baseSystemPromptOptions;
|
|
96
172
|
const expandSkillCommand = session._expandSkillCommand;
|
|
97
173
|
const templates = session.promptTemplates;
|
|
98
174
|
if (!runner)
|
|
99
175
|
diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: extension runner is missing", source: "Pi session" });
|
|
100
|
-
if (typeof session._baseSystemPrompt !== "string")
|
|
101
|
-
diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: base system prompt is missing", source: "Pi session" });
|
|
102
176
|
if (baseOptions === undefined)
|
|
103
177
|
diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: base system prompt options are missing", source: "Pi session" });
|
|
104
178
|
if (typeof expandSkillCommand !== "function")
|
|
@@ -129,9 +203,11 @@ async function preparePiPrompt(native, text) {
|
|
|
129
203
|
diagnostics.push({ type: "error", message: `Pi prompt template expansion is unavailable: ${errorText(error)}`, source: "Pi session" });
|
|
130
204
|
}
|
|
131
205
|
}
|
|
132
|
-
const result = !inputHandled && runner && baseOptions !== undefined ? await runner.emitBeforeAgentStart(expandedPrompt, undefined,
|
|
206
|
+
const result = !inputHandled && runner && baseOptions !== undefined ? await runner.emitBeforeAgentStart(expandedPrompt, undefined, baseOptions) : undefined;
|
|
133
207
|
const prepared = result;
|
|
134
|
-
|
|
208
|
+
const systemPromptOptions = prepared?.systemPromptOptions ?? baseOptions;
|
|
209
|
+
const systemPrompt = prepared?.systemPromptOptions === undefined ? baseSystemPrompt : await buildSystemPromptForInspection(prepared.systemPromptOptions);
|
|
210
|
+
return { prompt: text, expandedPrompt, systemPrompt, systemPromptOptions, messages: prepared?.messages ?? [], inputHandled, diagnostics };
|
|
135
211
|
}
|
|
136
212
|
finally {
|
|
137
213
|
unsubscribe?.();
|
|
@@ -175,6 +251,64 @@ export function flushExtensionProviders(resourceLoader, modelRuntime) {
|
|
|
175
251
|
runtime.pendingNativeProviderRegistrations = [];
|
|
176
252
|
return failures;
|
|
177
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* The CLI adds Pi's built-in extensions itself; SDK sessions only get them from these public
|
|
256
|
+
* factories (docs/sdk.md). As `builtin` entries they obey `-builtin:<name>` settings, and like the
|
|
257
|
+
* CLI's they are replaceable by an extension that registers the same tools or commands.
|
|
258
|
+
* NOTE: Pi does not export the built-in llama.cpp extension, so workflow agents cannot use models it
|
|
259
|
+
* provides. Declare the server as a provider in models.json instead.
|
|
260
|
+
*/
|
|
261
|
+
function builtinExtensions() {
|
|
262
|
+
return [
|
|
263
|
+
{ name: "codemode", factory: createCodemodeExtension(), replaceable: true, builtin: true },
|
|
264
|
+
{ name: "tool-search", factory: createToolSearchExtension(), replaceable: true, builtin: true },
|
|
265
|
+
{ name: "mcp", factory: createMcpExtension(), replaceable: true, builtin: true },
|
|
266
|
+
];
|
|
267
|
+
}
|
|
268
|
+
const BUILTIN_EXTENSION_PREFIX = "builtin:";
|
|
269
|
+
function isLlamaModel(model) { return model.provider === "llama.cpp"; }
|
|
270
|
+
/**
|
|
271
|
+
* Pi only extends resources with paths that loaded extensions return from resources_discover. Recording them, normalized as
|
|
272
|
+
* Pi does, lets selection tell those skills from the static inventory. Like Pi's own source attribution, the record belongs
|
|
273
|
+
* to one resource generation: reload() starts a new one, so withdrawn contributions stop counting as discovered.
|
|
274
|
+
*/
|
|
275
|
+
class WorkflowResourceLoader extends DefaultResourceLoader {
|
|
276
|
+
loaderCwd;
|
|
277
|
+
extensionSkillPaths = [];
|
|
278
|
+
constructor(loaderCwd, options) {
|
|
279
|
+
super(options);
|
|
280
|
+
this.loaderCwd = loaderCwd;
|
|
281
|
+
}
|
|
282
|
+
async reload(...args) {
|
|
283
|
+
this.extensionSkillPaths.length = 0;
|
|
284
|
+
await super.reload(...args);
|
|
285
|
+
}
|
|
286
|
+
extendResources(paths) {
|
|
287
|
+
this.extensionSkillPaths.push(...(paths.skillPaths ?? []).map(({ path }) => piResourcePath(path, this.loaderCwd)));
|
|
288
|
+
super.extendResources(paths);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Prepared selectors end with a frozen segment: `!*` followed by the static inventory. Extension-discovered skills keep every
|
|
293
|
+
* other selector, including negations setup hooks inserted anywhere (a terminal `!*` too), but not that frozen restriction.
|
|
294
|
+
* Setup can only insert negations, so the inventory names are matched from the right and the boundary is the nearest `!*`
|
|
295
|
+
* before them; any `!*` with only negations up to the inventory selects the same skills.
|
|
296
|
+
*/
|
|
297
|
+
function discoveredSkillSelectors(selectors, inventory) {
|
|
298
|
+
if (selectors === undefined || inventory === undefined)
|
|
299
|
+
return selectors;
|
|
300
|
+
const frozen = new Set();
|
|
301
|
+
let index = selectors.length;
|
|
302
|
+
for (const name of [...inventory].reverse().concat("!*")) {
|
|
303
|
+
do
|
|
304
|
+
index -= 1;
|
|
305
|
+
while (index >= 0 && selectors[index] !== name);
|
|
306
|
+
if (index < 0)
|
|
307
|
+
return selectors;
|
|
308
|
+
frozen.add(index);
|
|
309
|
+
}
|
|
310
|
+
return selectors.filter((_selector, position) => !frozen.has(position));
|
|
311
|
+
}
|
|
178
312
|
async function createLocalPiSessionHandle(input, sessionStartEvent) {
|
|
179
313
|
const agentDir = input.agentDir ?? getAgentDir();
|
|
180
314
|
const systemPromptSource = workflowSystemPromptPath(input.cwd, agentDir, input.resourcePolicy?.projectTrusted ?? true);
|
|
@@ -188,33 +322,34 @@ async function createLocalPiSessionHandle(input, sessionStartEvent) {
|
|
|
188
322
|
const tools = [...new Set([...input.tools, ...customTools.map(({ name }) => name)])];
|
|
189
323
|
let settingsManager;
|
|
190
324
|
let resourceLoader;
|
|
325
|
+
const builtins = builtinExtensions();
|
|
326
|
+
const builtinNames = builtins.flatMap((extension) => typeof extension === "function" ? [] : [extension.name]);
|
|
327
|
+
const extensionFactories = [...builtins, ...(input.extensionFactories ?? [])];
|
|
191
328
|
const policy = input.resourcePolicy;
|
|
192
329
|
if (policy) {
|
|
193
330
|
settingsManager = SettingsManager.create(input.cwd, agentDir, { projectTrusted: false });
|
|
194
331
|
settingsManager.setProjectTrusted(policy.projectTrusted);
|
|
195
|
-
const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager });
|
|
332
|
+
const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager, builtinExtensions: builtinNames });
|
|
196
333
|
const resolved = await packageManager.resolve();
|
|
197
|
-
const discoveredExtensions = [...new Set(resolved.extensions.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) =>
|
|
334
|
+
const discoveredExtensions = [...new Set(resolved.extensions.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => extensionIdentity(path)))];
|
|
198
335
|
const selectorSources = policy.selectorSources;
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
selectorSources,
|
|
202
|
-
resources: { extensions: discoveredExtensions },
|
|
203
|
-
});
|
|
204
|
-
const selectedExtensions = selection.selectedExtensions ?? [];
|
|
205
|
-
const extensionPaths = selectedExtensions.filter((path) => !WORKFLOW_HOST_ENTRIES.has(path));
|
|
336
|
+
const selectedExtensions = selectResourcesByLayers(resourceSelectorLayers(selectorSources, "extensions"), discoveredExtensions).filter((path) => policy.capabilities === undefined || policy.capabilities.extensions.includes(path));
|
|
337
|
+
const extensionPaths = selectedExtensions.filter((path) => path.startsWith(BUILTIN_EXTENSION_PREFIX) || !isWorkflowHostEntry(path));
|
|
206
338
|
policy.selectedExtensions = selectedExtensions;
|
|
207
|
-
policy.unmatchedExtensions =
|
|
339
|
+
policy.unmatchedExtensions = unmatchedResourcePatterns(policy.effective.extensions, discoveredExtensions);
|
|
208
340
|
const skillPaths = [...new Set(resolved.skills.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => path))];
|
|
209
|
-
const updateSkillMatches = (skills) => {
|
|
341
|
+
const updateSkillMatches = (skills, extensionSkillPaths) => {
|
|
210
342
|
const names = [...new Set(skills.map(({ name }) => name))];
|
|
211
|
-
const
|
|
212
|
-
const
|
|
343
|
+
const discovered = new Set(skills.filter(({ filePath }) => extensionSkillPaths.some((path) => piResourceContains(path, filePath))).map(({ name }) => name));
|
|
344
|
+
const [global, project, call] = resourceSelectorLayers(selectorSources, "skills");
|
|
345
|
+
const selectedStatic = selectResourcesByLayers([global, project, call], names.filter((name) => !discovered.has(name))).filter((name) => policy.capabilities === undefined || policy.capabilities.skills.includes(name));
|
|
346
|
+
const selectedDiscovered = selectResourcesByLayers([discoveredSkillSelectors(global, policy.capabilities?.skills), project, call], [...discovered]).filter((name) => policy.parentSkills === undefined || policy.parentSkills.includes(name));
|
|
347
|
+
const selectedSkills = [...selectedStatic, ...selectedDiscovered];
|
|
213
348
|
policy.selectedSkills = selectedSkills;
|
|
214
|
-
policy.unmatchedSkills =
|
|
349
|
+
policy.unmatchedSkills = unmatchedResourcePatterns(policy.effective.skills, names);
|
|
215
350
|
return new Set(names.filter((name) => !selectedSkills.includes(name)));
|
|
216
351
|
};
|
|
217
|
-
|
|
352
|
+
const workflowLoader = new WorkflowResourceLoader(input.cwd, {
|
|
218
353
|
cwd: input.cwd,
|
|
219
354
|
agentDir,
|
|
220
355
|
settingsManager,
|
|
@@ -222,23 +357,24 @@ async function createLocalPiSessionHandle(input, sessionStartEvent) {
|
|
|
222
357
|
additionalExtensionPaths: extensionPaths,
|
|
223
358
|
noSkills: true,
|
|
224
359
|
additionalSkillPaths: [...new Set([...skillPaths, ...(input.additionalSkillPaths ?? [])])],
|
|
225
|
-
|
|
360
|
+
extensionFactories,
|
|
226
361
|
...(contextFilesOverride ? { agentsFilesOverride: contextFilesOverride } : {}),
|
|
227
362
|
skillsOverride: (base) => {
|
|
228
|
-
const disabledSkills = updateSkillMatches(base.skills);
|
|
363
|
+
const disabledSkills = updateSkillMatches(base.skills, workflowLoader.extensionSkillPaths);
|
|
229
364
|
return { ...base, skills: base.skills.filter(({ name }) => !disabledSkills.has(name)) };
|
|
230
365
|
},
|
|
231
366
|
...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}),
|
|
232
367
|
...systemPromptOptions,
|
|
233
368
|
});
|
|
369
|
+
resourceLoader = workflowLoader;
|
|
234
370
|
await resourceLoader.reload();
|
|
235
371
|
}
|
|
236
372
|
else {
|
|
237
373
|
settingsManager = SettingsManager.create(input.cwd, agentDir, { projectTrusted: true });
|
|
238
|
-
const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager });
|
|
374
|
+
const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager, builtinExtensions: builtinNames });
|
|
239
375
|
const resolved = await packageManager.resolve();
|
|
240
|
-
const extensionPaths = [...new Set(resolved.extensions.filter(({ enabled }) => enabled).map(({ path }) =>
|
|
241
|
-
resourceLoader = new DefaultResourceLoader({ cwd: input.cwd, agentDir, settingsManager, noExtensions: true, additionalExtensionPaths: extensionPaths, ...(input.additionalSkillPaths?.length ? { additionalSkillPaths: [...input.additionalSkillPaths] } : {}),
|
|
376
|
+
const extensionPaths = [...new Set(resolved.extensions.filter(({ enabled }) => enabled).map(({ path }) => extensionIdentity(path)).filter((path) => path.startsWith(BUILTIN_EXTENSION_PREFIX) || !isWorkflowHostEntry(path)))];
|
|
377
|
+
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 ?? ""] } : {}) });
|
|
242
378
|
await resourceLoader.reload();
|
|
243
379
|
}
|
|
244
380
|
const providerFailures = flushExtensionProviders(resourceLoader, modelRuntime);
|
|
@@ -249,11 +385,11 @@ async function createLocalPiSessionHandle(input, sessionStartEvent) {
|
|
|
249
385
|
// missing, so the failure travels with the complaint rather than being
|
|
250
386
|
// swallowed — otherwise a misconfigured gateway reads as a typo in a model
|
|
251
387
|
// name.
|
|
252
|
-
const because = providerFailures.length > 0 ? ` (provider registration failed — ${providerFailures.join("; ")})` : "";
|
|
388
|
+
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)" : "";
|
|
253
389
|
throw new WorkflowError("UNKNOWN_MODEL", `Unknown model: ${input.model.provider}/${input.model.model}${because}`);
|
|
254
390
|
}
|
|
255
391
|
const effectiveSessionStartEvent = input.settings === undefined && sessionStartEvent === undefined ? undefined : { ...(sessionStartEvent ?? { type: "session_start", reason: "startup" }), settings: input.settings ?? Object.freeze({}) };
|
|
256
|
-
const { session } = await createAgentSession({
|
|
392
|
+
const { session } = await createAgentSession({ cwd: input.cwd, agentDir, modelRuntime, model, settingsManager, ...(input.model.thinking ? { thinkingLevel: input.model.thinking } : {}), tools, ...(customTools.length ? { customTools } : {}), ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}), resourceLoader, ...(effectiveSessionStartEvent ? { sessionStartEvent: effectiveSessionStartEvent } : {}), sessionManager: manager });
|
|
257
393
|
const nativeDispose = session.dispose.bind(session);
|
|
258
394
|
let disposal;
|
|
259
395
|
const shutdown = (reason, targetSessionFile) => {
|
|
@@ -278,13 +414,13 @@ async function createLocalPiSessionHandle(input, sessionStartEvent) {
|
|
|
278
414
|
await shutdown("quit").catch(() => undefined);
|
|
279
415
|
throw error;
|
|
280
416
|
}
|
|
281
|
-
const resourcePaths = { extensions: resourceLoader.getExtensions().extensions.filter(({ path }) => !path.startsWith("<")).map(({ resolvedPath }) =>
|
|
417
|
+
const resourcePaths = { extensions: resourceLoader.getExtensions().extensions.filter(({ path }) => !path.startsWith("<")).map(({ resolvedPath }) => extensionIdentity(resolvedPath)), skills: resourceLoader.getSkills().skills.map(({ filePath }) => canonicalPath(filePath)) };
|
|
282
418
|
const resourceInspection = () => {
|
|
283
419
|
const extensions = resourceLoader.getExtensions();
|
|
284
420
|
const skills = resourceLoader.getSkills();
|
|
285
421
|
const diagnostics = [...extensions.errors.map(({ path, error }) => ({ type: "error", message: error, source: path })), ...skills.diagnostics.map(({ type, path, message }) => ({ type, message, ...(path ? { source: path } : {}) }))];
|
|
286
422
|
const systemSource = systemPromptSource;
|
|
287
|
-
return { extensions: resourceLoader.getExtensions().extensions.filter(({ path }) => !path.startsWith("<")).map(({ resolvedPath }) =>
|
|
423
|
+
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" } : {}) };
|
|
288
424
|
};
|
|
289
425
|
const managedSession = Object.assign(session, {
|
|
290
426
|
getLeafId: () => manager.getLeafId(),
|
|
@@ -623,6 +759,11 @@ export async function createLocalWorkflowAgentSession(prepared, context) {
|
|
|
623
759
|
return session;
|
|
624
760
|
}
|
|
625
761
|
export const localAgentTransport = Object.freeze({ id: "local", createSession: createLocalWorkflowAgentSession });
|
|
762
|
+
/** Like Pi's excludeTools, exclusions name tools exactly and apply to every tool the session would get, custom tools included. */
|
|
763
|
+
function excludedToolNames(options) {
|
|
764
|
+
const own = options.agentOptions?.excludeTools;
|
|
765
|
+
return new Set([...(options.inheritedExcludeTools ?? []), ...(Array.isArray(own) ? own.filter((tool) => typeof tool === "string") : [])]);
|
|
766
|
+
}
|
|
626
767
|
function changedOption(options, baseline, key) { return JSON.stringify(options[key]) !== JSON.stringify(baseline[key]); }
|
|
627
768
|
function isChildAgentToolParams(value) {
|
|
628
769
|
if (!jsonObject(value) || typeof value.prompt !== "string" || typeof value.label !== "string")
|
|
@@ -635,8 +776,6 @@ function isChildAgentToolParams(value) {
|
|
|
635
776
|
return false;
|
|
636
777
|
if (value.model !== undefined && typeof value.model !== "string")
|
|
637
778
|
return false;
|
|
638
|
-
if (value.role !== undefined && (typeof value.role !== "string" || !value.role.trim()))
|
|
639
|
-
return false;
|
|
640
779
|
if (value.contextFiles !== undefined && (!Array.isArray(value.contextFiles) || !value.contextFiles.every(isContextFileScope)))
|
|
641
780
|
return false;
|
|
642
781
|
if (value.outputSchema !== undefined && !jsonObject(value.outputSchema))
|
|
@@ -662,7 +801,7 @@ function resourcePolicySummary(policy, tools = policy.selectedTools ?? []) {
|
|
|
662
801
|
}
|
|
663
802
|
const RESOURCE_SELECTOR_KEYS = ["skills", "extensions", "tools"];
|
|
664
803
|
function resourceSelectorLayers(sources, key) {
|
|
665
|
-
return [sources.global[key], sources.project[key], sources.
|
|
804
|
+
return [sources.global[key], sources.project[key], sources.call?.[key]];
|
|
666
805
|
}
|
|
667
806
|
function selectorListWidened(ceiling, candidate) {
|
|
668
807
|
let candidateIndex = 0;
|
|
@@ -735,7 +874,7 @@ function appendResourcePolicyNarrowing(ceiling, candidate) {
|
|
|
735
874
|
};
|
|
736
875
|
}
|
|
737
876
|
function packageRoot(start) {
|
|
738
|
-
let current = dirname(
|
|
877
|
+
let current = dirname(canonicalPath(start));
|
|
739
878
|
for (;;) {
|
|
740
879
|
const candidates = [current, join(current, "..", "@earendil-works", "pi-coding-agent"), join(current, "..", "pi-coding-agent")];
|
|
741
880
|
for (const candidate of candidates) {
|
|
@@ -795,10 +934,9 @@ async function resolvePiRuntime() {
|
|
|
795
934
|
}
|
|
796
935
|
}
|
|
797
936
|
const piRuntimeResolution = resolvePiRuntime();
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
return `${basePrompt}\n\nTask:\n${task}${instruction ? `\n\n${instruction}` : ""}`;
|
|
937
|
+
/** Transient per-attempt control text. It is queued as a separate steered message after the task, so the system prompt stays stable and the task message stays exact. */
|
|
938
|
+
function attemptControlText(options, attempt, previousError) {
|
|
939
|
+
return [attempt > 1 ? `Retry attempt ${String(attempt)}. Previous state: ${options.retryState ?? previousError ?? "failed attempt"}` : "", options.budget?.instruction() ?? ""].filter(Boolean).join("\n");
|
|
802
940
|
}
|
|
803
941
|
async function preparedAgentSession(input, initialPrompt) {
|
|
804
942
|
const { runtime: piRuntime, error: piRuntimeError } = await piRuntimeResolution;
|
|
@@ -821,47 +959,27 @@ async function attemptSessionInput(source, attempt) {
|
|
|
821
959
|
await copyFile(source, target);
|
|
822
960
|
return target;
|
|
823
961
|
}
|
|
824
|
-
async function prepareAgentSetup(root, transport, task, options, resolved, cwd, attempt, signal, customTools, resultTool, inspection = false
|
|
962
|
+
async function prepareAgentSetup(root, transport, task, options, resolved, cwd, attempt, signal, customTools, resultTool, inspection = false) {
|
|
825
963
|
const setupSignal = signal ?? root.runContext?.signal ?? new AbortController().signal;
|
|
826
964
|
const baselineOptions = structuredClone(options.agentOptions ?? {});
|
|
827
965
|
const baseResourcePolicy = await root.agentResourcePolicy?.();
|
|
828
|
-
const
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
if (Array.isArray(value) && value.every((entry) => typeof entry === "string"))
|
|
838
|
-
return value;
|
|
839
|
-
return options.agentOptions === undefined ? fallback : undefined;
|
|
966
|
+
const configuration = options.configuration;
|
|
967
|
+
const resourcePolicy = {
|
|
968
|
+
...(baseResourcePolicy ?? { globalSettingsPath: "", projectSettingsPath: "", projectTrusted: root.projectTrusted ?? false, global: { skills: [], extensions: [] }, project: { skills: [], extensions: [] }, unmatchedSkills: [], unmatchedExtensions: [] }),
|
|
969
|
+
projectTrusted: configuration?.projectTrusted ?? root.projectTrusted ?? baseResourcePolicy?.projectTrusted ?? false,
|
|
970
|
+
effective: { skills: configuration?.skills ?? [], extensions: configuration?.extensions ?? [], tools: resolved.tools },
|
|
971
|
+
selectorSources: { global: { skills: configuration?.skills ?? [], extensions: configuration?.extensions ?? [], tools: resolved.tools }, project: {} },
|
|
972
|
+
...(options.capabilities ? { capabilities: options.capabilities } : {}),
|
|
973
|
+
...(options.parentSkills ? { parentSkills: [...options.parentSkills] } : {}),
|
|
974
|
+
selectedTools: resolved.tools,
|
|
840
975
|
};
|
|
841
|
-
const
|
|
842
|
-
const rawCallExtensions = selectorValue("extensions", options.extensions)?.map((selector) => canonicalExtensionSelector(selector, cwd));
|
|
843
|
-
const rawCallTools = selectorValue("tools", options.tools);
|
|
844
|
-
const callSelectors = { ...(rawCallSkills === undefined ? {} : { skills: rawCallSkills }), ...(rawCallExtensions === undefined ? {} : { extensions: rawCallExtensions }), ...(rawCallTools === undefined ? {} : { tools: rawCallTools }) };
|
|
845
|
-
const resourcePolicy = baseResourcePolicy ? {
|
|
846
|
-
...baseResourcePolicy,
|
|
847
|
-
effective: {
|
|
848
|
-
skills: [...baseResourcePolicy.effective.skills, ...(roleSelectors?.skills ?? []), ...(rawCallSkills ?? [])],
|
|
849
|
-
extensions: [...baseResourcePolicy.effective.extensions, ...(roleSelectors?.extensions ?? []), ...(rawCallExtensions ?? [])],
|
|
850
|
-
...(baseResourcePolicy.effective.tools === undefined && roleSelectors?.tools === undefined && rawCallTools === undefined ? {} : { tools: [...(baseResourcePolicy.effective.tools ?? []), ...(roleSelectors?.tools ?? []), ...(rawCallTools ?? [])] }),
|
|
851
|
-
},
|
|
852
|
-
selectorSources: { global: baseResourcePolicy.selectorSources.global, project: baseResourcePolicy.selectorSources.project, ...(roleSelectors ? { role: roleSelectors } : {}), ...(Object.keys(callSelectors).length ? { call: callSelectors } : {}) },
|
|
853
|
-
} : undefined;
|
|
854
|
-
if (resourcePolicy) {
|
|
855
|
-
resourcePolicy.selectedTools = resolved.tools;
|
|
856
|
-
resourcePolicy.unmatchedTools = unmatchedResourcePatterns(resourceSelectorLayers(resourcePolicy.selectorSources, "tools").flatMap((layer) => layer ?? []), [...root.tools]);
|
|
857
|
-
}
|
|
858
|
-
const resourcePolicyCeiling = resourcePolicy ? structuredClone(resourcePolicy) : undefined;
|
|
976
|
+
const resourcePolicyCeiling = structuredClone(resourcePolicy);
|
|
859
977
|
const sessionPath = options.sessionPath === undefined || inspection ? options.sessionPath : await attemptSessionInput(options.sessionPath, attempt);
|
|
860
|
-
const extensionSettings =
|
|
861
|
-
root.validateExtensionSettings?.(extensionSettings, { source:
|
|
978
|
+
const extensionSettings = configuration?.settings ?? options.inheritedExtensionSettings ?? root.extensionSettings ?? Object.freeze({});
|
|
979
|
+
root.validateExtensionSettings?.(extensionSettings, { source: "effective", cwd, projectTrusted: resourcePolicy.projectTrusted, ...((root.extensionSettingsPath ?? root.settingsPath) ? { settingsPath: root.extensionSettingsPath ?? root.settingsPath } : {}) });
|
|
862
980
|
if (options.agentNodeId !== undefined)
|
|
863
981
|
root.onAgentSettings?.(options.agentNodeId, extensionSettings);
|
|
864
|
-
const sessionInput = { cwd, model: { ...resolved.model }, tools: [...resolved.tools], sessionLabel: `${options.workflowName}:${options.label}:attempt-${String(attempt)}`, ...(sessionPath ? { sessionPath } : {}), ...(root.agentDir ? { agentDir: root.agentDir } : {}), ...(root.additionalSkillPaths?.length ? { additionalSkillPaths: [...root.additionalSkillPaths] } : {}), ...(resolved.contextFiles === undefined ? {} : { contextFiles: [...resolved.contextFiles] }), ...(customTools.length ? { customTools: [...customTools] } : {}), ...(resultTool ? { resultTool } : {}), ...(resolved.systemPrompt !== undefined ? { systemPrompt: resolved.systemPrompt } : {}), systemPromptAppend: resolved.systemPromptAppend,
|
|
982
|
+
const sessionInput = { cwd, model: { ...resolved.model }, tools: [...resolved.tools], sessionLabel: `${options.workflowName}:${options.label}:attempt-${String(attempt)}`, ...(sessionPath ? { sessionPath } : {}), ...(root.agentDir ? { agentDir: root.agentDir } : {}), ...(root.additionalSkillPaths?.length ? { additionalSkillPaths: [...root.additionalSkillPaths] } : {}), ...(resolved.contextFiles === undefined ? {} : { contextFiles: [...resolved.contextFiles] }), ...(customTools.length ? { customTools: [...customTools] } : {}), ...(resultTool ? { resultTool } : {}), ...(resolved.systemPrompt !== undefined ? { systemPrompt: resolved.systemPrompt } : {}), systemPromptAppend: resolved.systemPromptAppend, resourcePolicy, settings: extensionSettings, options: structuredClone(baselineOptions) };
|
|
865
983
|
const setup = { prompt: task, options: sessionInput.options ?? {}, sessionInput, prepared: await preparedAgentSession(sessionInput, task), transport };
|
|
866
984
|
const base = fallbackSetupContext(root, options, setupSignal);
|
|
867
985
|
const context = Object.freeze({ run: base.run, identity: base.identity, attempt, signal: setupSignal, settings: extensionSettings, ...(base.tuiIndex === undefined ? {} : { tuiIndex: base.tuiIndex }), ...(base.tuiLabel === undefined ? {} : { tuiLabel: base.tuiLabel }), ...(inspection ? { mode: "inspection" } : {}) });
|
|
@@ -885,30 +1003,69 @@ async function prepareAgentSetup(root, transport, task, options, resolved, cwd,
|
|
|
885
1003
|
if (resourcePolicyWidened(resourcePolicyCeiling, setup.sessionInput.resourcePolicy))
|
|
886
1004
|
throw new WorkflowError("INVALID_METADATA", "Agent setup widened the prepared resource policy");
|
|
887
1005
|
appendResourcePolicyNarrowing(resourcePolicyCeiling, setup.sessionInput.resourcePolicy);
|
|
1006
|
+
if (setup.sessionInput.resourcePolicy) {
|
|
1007
|
+
const policy = setup.sessionInput.resourcePolicy;
|
|
1008
|
+
policy.effective.extensions = policy.effective.extensions.map((selector) => canonicalExtensionSelector(selector, cwd));
|
|
1009
|
+
for (const layer of [policy.selectorSources.global, policy.selectorSources.project, policy.selectorSources.call])
|
|
1010
|
+
if (layer?.extensions !== undefined)
|
|
1011
|
+
layer.extensions = layer.extensions.map((selector) => canonicalExtensionSelector(selector, cwd));
|
|
1012
|
+
if (resourcePolicyCeiling.parentSkills === undefined)
|
|
1013
|
+
Reflect.deleteProperty(policy, "parentSkills");
|
|
1014
|
+
else
|
|
1015
|
+
policy.parentSkills = [...resourcePolicyCeiling.parentSkills];
|
|
1016
|
+
}
|
|
888
1017
|
setup.sessionInput.options = setup.options;
|
|
889
1018
|
if (changedOption(setup.options, baselineOptions, "model") && typeof setup.options.model === "string")
|
|
890
1019
|
setup.sessionInput.model = parseModel(setup.options.model, setup.sessionInput.model, root.modelAliases, root.knownModels ?? root.availableModels, root.settingsPath);
|
|
891
1020
|
if (changedOption(setup.options, baselineOptions, "tools") && Array.isArray(setup.options.tools) && setup.options.tools.every((tool) => typeof tool === "string"))
|
|
892
1021
|
setup.sessionInput.tools = [...setup.options.tools];
|
|
893
|
-
|
|
894
|
-
|
|
1022
|
+
// Pi derives trust, settings and .pi resources from cwd, so neither the session input nor a cwd option may move it.
|
|
1023
|
+
if (changedOption(setup.options, baselineOptions, "cwd") || !sameFilesystemPath(cwd, setup.sessionInput.cwd))
|
|
1024
|
+
throw new WorkflowError("INVALID_METADATA", "Agent setup cannot change cwd");
|
|
1025
|
+
if (!root.availableModels?.has(modelCapability(setup.sessionInput.model)) && root.availableModels !== undefined)
|
|
1026
|
+
throw new WorkflowError("UNKNOWN_MODEL", "Agent setup selected an unavailable model");
|
|
1027
|
+
// A custom tool must not reintroduce, by name, a root tool the prepared policy left out (excludeTools, parent ceilings, selectors).
|
|
1028
|
+
const shadowed = (setup.sessionInput.customTools ?? []).map(({ name }) => name).find((name) => root.tools.has(name) && !resolved.tools.includes(name));
|
|
1029
|
+
if (shadowed)
|
|
1030
|
+
throw new WorkflowError("UNKNOWN_TOOL", `Custom tool reintroduces a tool outside the prepared agent policy: ${shadowed}`);
|
|
1031
|
+
// Like Pi's excludeTools, own and inherited exclusions also remove custom tools of those names, including ones setup hooks
|
|
1032
|
+
// add, before materialization. A name that matches no root or custom tool excludes nothing and only warns. The names are
|
|
1033
|
+
// the frozen configuration's, so re-evaluated options of a later turn, retry or resume cannot change them.
|
|
1034
|
+
const excludedTools = new Set(configuration?.excludeTools ?? excludedToolNames(options));
|
|
1035
|
+
const excludedCustomTools = (setup.sessionInput.customTools ?? []).filter(({ name }) => excludedTools.has(name)).map(({ name }) => name);
|
|
1036
|
+
if (excludedCustomTools.length) {
|
|
1037
|
+
setup.sessionInput.customTools = setup.sessionInput.customTools?.filter(({ name }) => !excludedTools.has(name)) ?? [];
|
|
1038
|
+
// Hooks may also list their custom tool by name; root names stay subject to the policy checks below.
|
|
1039
|
+
setup.sessionInput.tools = setup.sessionInput.tools.filter((tool) => root.tools.has(tool) || !excludedCustomTools.includes(tool));
|
|
1040
|
+
}
|
|
1041
|
+
for (const tool of excludedTools)
|
|
1042
|
+
if (!options.inheritedExcludeTools?.includes(tool) && !root.tools.has(tool) && !excludedCustomTools.includes(tool))
|
|
1043
|
+
root.onResourceWarning?.(`excludeTools names no root or custom tool of this agent: ${tool}`);
|
|
895
1044
|
const customToolNames = new Set([...(setup.sessionInput.customTools ?? []).map(({ name }) => name), ...(setup.sessionInput.resultTool ? [setup.sessionInput.resultTool.name] : [])]);
|
|
896
1045
|
const widened = setup.sessionInput.tools.find((tool) => !resolved.tools.includes(tool) && !customToolNames.has(tool));
|
|
897
1046
|
const outsideTool = widened ?? setup.sessionInput.tools.find((tool) => !root.tools.has(tool) && !customToolNames.has(tool));
|
|
898
1047
|
if (outsideTool)
|
|
899
1048
|
throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the prepared agent policy: ${outsideTool}`);
|
|
1049
|
+
if (setup.sessionInput.resourcePolicy) {
|
|
1050
|
+
const policy = setup.sessionInput.resourcePolicy;
|
|
1051
|
+
setup.sessionInput.tools = selectResourcesByLayers(resourceSelectorLayers(policy.selectorSources, "tools"), setup.sessionInput.tools);
|
|
1052
|
+
if (policy.projectTrusted !== resourcePolicyCeiling.projectTrusted)
|
|
1053
|
+
policy.capabilities = await authorizedResources({ ...root, projectTrusted: policy.projectTrusted }, cwd, options);
|
|
1054
|
+
}
|
|
1055
|
+
if (!setup.sessionInput.tools.includes("agent"))
|
|
1056
|
+
setup.sessionInput.customTools = setup.sessionInput.customTools?.filter(({ name }) => name !== "agent" && name !== "get_subagent_result" && name !== "steer_subagent") ?? [];
|
|
900
1057
|
}
|
|
901
1058
|
catch (error) {
|
|
902
1059
|
setup.prepared = await preparedAgentSession(setup.sessionInput, task);
|
|
903
1060
|
return { setup, summary: agentSetupSummary(setup, hookNames), failure: { error } };
|
|
904
1061
|
}
|
|
905
|
-
setup.prepared = await preparedAgentSession(setup.sessionInput, inspection ? task :
|
|
1062
|
+
setup.prepared = await preparedAgentSession(setup.sessionInput, inspection ? task : setup.prompt);
|
|
906
1063
|
return { setup, summary: agentSetupSummary(setup, hookNames) };
|
|
907
1064
|
}
|
|
908
1065
|
export async function prepareAgentSetupForInspection(root, task, options, transport) {
|
|
909
1066
|
const executor = new WorkflowAgentExecutor(root);
|
|
910
|
-
const
|
|
911
|
-
return prepareAgentSetup(root, transport, task, options,
|
|
1067
|
+
const configuration = await executor.prepare(options, root.cwd, root.runContext?.signal, "inspection");
|
|
1068
|
+
return prepareAgentSetup(root, transport, task, { ...options, label: configuration.label ?? options.label, configuration, capabilities: { tools: configuration.tools, skills: configuration.skills.slice(configuration.skills.lastIndexOf("!*") + 1), extensions: configuration.extensions.slice(configuration.extensions.lastIndexOf("!*") + 1) } }, configuration, root.cwd, 1, root.runContext?.signal, [], undefined, true);
|
|
912
1069
|
}
|
|
913
1070
|
function attemptRecord(transport, attempt, session, setup, stats, result, error) {
|
|
914
1071
|
return { attempt, transport, session: session.reference, ...(result === undefined ? {} : { result }), ...(error ? { error } : {}), accounting: stats, setup };
|
|
@@ -926,52 +1083,105 @@ export function getAgentAttempts(error) {
|
|
|
926
1083
|
export class WorkflowAgentExecutor {
|
|
927
1084
|
root;
|
|
928
1085
|
transport;
|
|
929
|
-
warnedRoleTools = new Set();
|
|
930
1086
|
constructor(root, transport = localAgentTransport) {
|
|
931
1087
|
this.root = root;
|
|
932
1088
|
this.transport = transport;
|
|
933
1089
|
}
|
|
934
1090
|
setRunContext(runContext) { this.root.runContext = runContext; }
|
|
935
1091
|
resolve(options, inheritedTools) {
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1092
|
+
if (options.configuration)
|
|
1093
|
+
return { ...options.configuration, ...(options.modelOverride ? { model: options.modelOverride } : {}) };
|
|
1094
|
+
const ceiling = inheritedTools ?? [...this.root.tools];
|
|
1095
|
+
for (const pattern of options.tools ?? [])
|
|
1096
|
+
if (!pattern.startsWith("!") && !resourcePatternHasMagic(pattern) && !ceiling.includes(pattern))
|
|
1097
|
+
throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the prepared agent policy: ${pattern}`);
|
|
1098
|
+
const tools = options.effectiveTools ?? selectResourcesByLayers([this.root.resourceSelectors?.tools, options.tools], [...(inheritedTools ?? this.root.tools)]);
|
|
1099
|
+
const missing = tools.find((tool) => !ceiling.includes(tool));
|
|
1100
|
+
if (missing)
|
|
1101
|
+
throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the prepared agent policy: ${missing}`);
|
|
1102
|
+
const model = options.modelOverride ?? parseModel(options.model, this.root.model, this.root.modelAliases, this.root.knownModels, this.root.settingsPath);
|
|
1103
|
+
return { model: physicalModel(model, this.root.modelAliases, this.root.knownModels, this.root.settingsPath), tools, systemPromptAppend: "", ...(options.contextFiles === undefined ? {} : { contextFiles: options.contextFiles }) };
|
|
1104
|
+
}
|
|
1105
|
+
async prepare(options, cwd = this.root.cwd, signal = this.root.runContext?.signal ?? new AbortController().signal, mode = "execution") {
|
|
1106
|
+
const root = { ...this.root, projectTrusted: Boolean(this.root.projectTrusted) && options.projectTrusted !== false };
|
|
1107
|
+
const policy = await this.root.agentResourcePolicy?.();
|
|
1108
|
+
const capabilities = await authorizedResources(root, cwd, options);
|
|
1109
|
+
// Children are new spawns of their parent's attempt; their scheduler node keys the configuration their own retries reuse.
|
|
1110
|
+
const persistedKey = options.parent !== undefined ? options.agentNodeId : options.agentIdentity?.handle !== undefined ? `handle:${options.agentIdentity.handle}` : options.agentIdentity ? agentIdentityPath(options.agentIdentity) : options.agentNodeId;
|
|
1111
|
+
const stored = options.configuration ?? (mode === "execution" && persistedKey && this.root.runStore?.load ? (await this.root.runStore.load()).snapshot.agentConfigurations[persistedKey] : undefined);
|
|
1112
|
+
if (stored) {
|
|
1113
|
+
// Without the field, the configuration was prepared before exclusions were frozen. Re-evaluated options cannot prove
|
|
1114
|
+
// which exclusions it had, so it never resumes.
|
|
1115
|
+
if (stored.excludeTools === undefined)
|
|
1116
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", "Prepared agent configuration predates frozen excludeTools; relaunch the workflow");
|
|
1117
|
+
return validatePreparedConfiguration(root, stored, capabilities, cwd, true);
|
|
1118
|
+
}
|
|
1119
|
+
const original = deepFreeze(structuredClone(validateAgentOptions(options.agentOptions ?? {
|
|
1120
|
+
...(options.model === undefined ? {} : { model: options.model }), ...(options.tools === undefined ? {} : { tools: [...options.tools] }), ...(options.skills === undefined ? {} : { skills: [...options.skills] }), ...(options.extensions === undefined ? {} : { extensions: [...options.extensions] }), ...(options.contextFiles === undefined ? {} : { contextFiles: [...options.contextFiles] }),
|
|
1121
|
+
})));
|
|
1122
|
+
const dynamicModelAliasNames = this.root.dynamicModelAliasNames;
|
|
1123
|
+
const defaults = deepFreeze({ model: { ...this.root.model }, modelAliases: { ...this.root.modelAliases }, ...(dynamicModelAliasNames === undefined ? {} : { dynamicModelAliasNames: [...dynamicModelAliasNames] }), selectorSources: structuredClone(policy?.selectorSources ?? { global: this.root.resourceSelectors ?? {}, project: {} }), settings: structuredClone(options.inheritedExtensionSettings ?? this.root.extensionSettings ?? {}) });
|
|
1124
|
+
const context = Object.freeze({ options: original, cwd, projectCwd: this.root.cwd, agentDir: this.root.agentDir ?? getAgentDir(), projectTrusted: root.projectTrusted, defaults, capabilities: deepFreeze(structuredClone(capabilities)), knownModels: new Set(this.root.knownModels), availableModels: new Set(this.root.availableModels), signal, mode });
|
|
1125
|
+
const configuration = {
|
|
1126
|
+
...(typeof original.label === "string" ? { label: original.label } : {}), ...(typeof original.model === "string" ? { model: original.model } : {}),
|
|
1127
|
+
tools: [...(policy?.effective.tools ?? this.root.resourceSelectors?.tools ?? []), ...(options.tools ?? [])], skills: [...(policy?.effective.skills ?? this.root.resourceSelectors?.skills ?? []), ...(options.skills ?? [])], extensions: [...(policy?.effective.extensions ?? this.root.resourceSelectors?.extensions ?? []), ...(options.extensions ?? []).map((selector) => canonicalExtensionSelector(selector, cwd))],
|
|
1128
|
+
...(options.contextFiles === undefined ? {} : { contextFiles: [...options.contextFiles] }), systemPromptAppend: "", settings: structuredClone(defaults.settings),
|
|
1129
|
+
};
|
|
1130
|
+
for (const key of ["tools", "skills", "extensions"])
|
|
1131
|
+
if (Array.isArray(original[key])) {
|
|
1132
|
+
const selectors = validateSelectorList(original[key], "agent options", key, "INVALID_METADATA", false) ?? [];
|
|
1133
|
+
configuration[key] = [...(policy?.effective[key] ?? this.root.resourceSelectors?.[key] ?? []), ...selectors.map((selector) => key === "extensions" ? canonicalExtensionSelector(selector, cwd) : selector)];
|
|
950
1134
|
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
}
|
|
974
|
-
|
|
1135
|
+
if (original.contextFiles !== undefined)
|
|
1136
|
+
configuration.contextFiles = [...(validateContextFileScopes(original.contextFiles, "agent options") ?? [])];
|
|
1137
|
+
const hooks = [...(this.root.agentPreparationHooks ?? [])].sort(byPriorityThenName);
|
|
1138
|
+
const load = this.root.extensionLoad;
|
|
1139
|
+
if (load?.errors.length) {
|
|
1140
|
+
// A failed extension may have owned an option, and its factory may have registered hooks before it failed. Only a
|
|
1141
|
+
// schema whose registration names a loaded extension as `source` proves ownership.
|
|
1142
|
+
const loaded = new Set(load.loaded.map(extensionIdentity));
|
|
1143
|
+
const declared = new Set(hooks.flatMap(({ optionsSchema, source }) => source !== undefined && loaded.has(extensionIdentity(source)) && object(optionsSchema?.properties) ? Object.keys(optionsSchema.properties) : []));
|
|
1144
|
+
const unowned = Object.keys(original).find((key) => !isCoreAgentOption(key) && !declared.has(key));
|
|
1145
|
+
if (unowned !== undefined)
|
|
1146
|
+
throw new WorkflowError("INVALID_METADATA", `Agent option ${unowned} has no owner proven to have loaded while extensions failed to load: ${load.errors.join("; ")}`);
|
|
1147
|
+
}
|
|
1148
|
+
for (const hook of hooks)
|
|
1149
|
+
if (hook.optionsSchema !== undefined && !Value.Check(hook.optionsSchema, original))
|
|
1150
|
+
throw new WorkflowError("INVALID_METADATA", `Agent options do not match preparation schema: ${hook.name}`);
|
|
1151
|
+
if (typeof original.model === "string" && this.root.blockedAliases?.has(original.model.split(":")[0] ?? ""))
|
|
1152
|
+
throw new WorkflowError("UNKNOWN_MODEL", `Unknown model alias ${original.model} resolved to ${this.root.blockedAliasTargets?.[original.model.split(":")[0] ?? ""] ?? "an unavailable target"}`);
|
|
1153
|
+
for (const hook of hooks) {
|
|
1154
|
+
if (signal.aborted)
|
|
1155
|
+
throw new WorkflowError("CANCELLED", "Agent preparation cancelled");
|
|
1156
|
+
await hook.prepare(configuration, context);
|
|
1157
|
+
}
|
|
1158
|
+
if (signal.aborted)
|
|
1159
|
+
throw new WorkflowError("CANCELLED", "Agent preparation cancelled");
|
|
1160
|
+
const { model: preparedModel, ...preparedOptions } = configuration;
|
|
1161
|
+
validateAgentOptions(preparedOptions);
|
|
1162
|
+
if (preparedModel !== undefined && (typeof preparedModel !== "string" || !preparedModel.trim()))
|
|
1163
|
+
throw new WorkflowError("INVALID_METADATA", "Prepared agent model must be a non-empty string");
|
|
1164
|
+
if (preparedModel && this.root.blockedAliases?.has(preparedModel.split(":")[0] ?? ""))
|
|
1165
|
+
throw new WorkflowError("UNKNOWN_MODEL", `Unknown model alias ${preparedModel} resolved to ${this.root.blockedAliasTargets?.[preparedModel.split(":")[0] ?? ""] ?? "an unavailable target"}`);
|
|
1166
|
+
configuration.extensions = configuration.extensions.map((selector) => canonicalExtensionSelector(selector, cwd));
|
|
1167
|
+
// A hook that passes on an inherited virtual `workflow/<alias>` root model gets the same physical target as no hook.
|
|
1168
|
+
const model = physicalModel(preparedModel === undefined ? this.root.model : resolveModelReference(preparedModel, this.root.modelAliases, this.root.knownModels, this.root.settingsPath), this.root.modelAliases, this.root.knownModels, this.root.settingsPath);
|
|
1169
|
+
const excludedTools = excludedToolNames({ agentOptions: original, ...(options.inheritedExcludeTools ? { inheritedExcludeTools: options.inheritedExcludeTools } : {}) });
|
|
1170
|
+
const tools = selectResourcesByLayers([configuration.tools], capabilities.tools).filter((tool) => !excludedTools.has(tool));
|
|
1171
|
+
for (const pattern of validateSelectorList(original.tools, "agent options", "tools", "INVALID_METADATA", false) ?? [])
|
|
1172
|
+
if (!pattern.startsWith("!") && !resourcePatternHasMagic(pattern) && !capabilities.tools.includes(pattern))
|
|
1173
|
+
throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the prepared agent policy: ${pattern}`);
|
|
1174
|
+
for (const pattern of unmatchedResourcePatterns(configuration.tools, capabilities.tools))
|
|
1175
|
+
this.root.onResourceWarning?.(`Tool selector currently matches no authorized tool: ${pattern}`);
|
|
1176
|
+
const skills = selectResourcesByLayers([configuration.skills], capabilities.skills);
|
|
1177
|
+
const extensions = selectResourcesByLayers([configuration.extensions], capabilities.extensions);
|
|
1178
|
+
const concrete = validatePreparedConfiguration(root, { ...configuration, model, tools, excludeTools: [...excludedTools], skills: [...configuration.skills, "!*", ...skills], extensions: [...configuration.extensions, "!*", ...extensions], projectTrusted: context.projectTrusted }, capabilities, cwd, false);
|
|
1179
|
+
if (mode === "execution") {
|
|
1180
|
+
if (persistedKey && this.root.runStore?.saveAgentConfiguration)
|
|
1181
|
+
await this.root.runStore.saveAgentConfiguration(persistedKey, concrete);
|
|
1182
|
+
await options.onConfiguration?.(concrete);
|
|
1183
|
+
}
|
|
1184
|
+
return concrete;
|
|
975
1185
|
}
|
|
976
1186
|
async execute(task, options, signal, customTools = [], setSteer, beforeRetry) {
|
|
977
1187
|
const executionSignal = signal ?? this.root.runContext?.signal;
|
|
@@ -979,7 +1189,7 @@ export class WorkflowAgentExecutor {
|
|
|
979
1189
|
throw new WorkflowError("INVALID_METADATA", "retries must be a non-negative integer");
|
|
980
1190
|
if (options.timeoutMs !== undefined && options.timeoutMs !== null && (!Number.isInteger(options.timeoutMs) || options.timeoutMs <= 0))
|
|
981
1191
|
throw new WorkflowError("INVALID_METADATA", "timeoutMs must be null or a positive integer");
|
|
982
|
-
let resolved
|
|
1192
|
+
let resolved;
|
|
983
1193
|
let recoveryModel;
|
|
984
1194
|
let cwd;
|
|
985
1195
|
if (options.parent) {
|
|
@@ -1009,6 +1219,15 @@ export class WorkflowAgentExecutor {
|
|
|
1009
1219
|
throw new WorkflowError("INVALID_METADATA", "Only child agents or worktree scopes may provide a cwd");
|
|
1010
1220
|
cwd = this.root.cwd;
|
|
1011
1221
|
}
|
|
1222
|
+
const configuration = await this.prepare(options, cwd, executionSignal);
|
|
1223
|
+
const parentSkills = options.parent === undefined ? undefined : options.capabilities?.skills;
|
|
1224
|
+
options = { ...options, label: configuration.label ?? options.label, configuration, ...(parentSkills ? { parentSkills } : {}), capabilities: { tools: configuration.tools, skills: configuration.skills.slice(configuration.skills.lastIndexOf("!*") + 1), extensions: configuration.extensions.slice(configuration.extensions.lastIndexOf("!*") + 1) } };
|
|
1225
|
+
resolved = this.resolve(options);
|
|
1226
|
+
if (!resolved.tools.includes("agent"))
|
|
1227
|
+
customTools = customTools.filter(({ name }) => name !== "agent" && name !== "get_subagent_result" && name !== "steer_subagent");
|
|
1228
|
+
// A child's skill ceiling is what its parent session has loaded when it spawns, so withdrawn skills never stay in it.
|
|
1229
|
+
let publishLoadedSkills;
|
|
1230
|
+
customTools = customTools.map((tool) => tool.name !== "agent" ? tool : { ...tool, execute: (...args) => { publishLoadedSkills?.(); return tool.execute(...args); } });
|
|
1012
1231
|
const attempts = [];
|
|
1013
1232
|
let maxAttempts = (options.retries ?? 0) + 1;
|
|
1014
1233
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
@@ -1036,7 +1255,7 @@ export class WorkflowAgentExecutor {
|
|
|
1036
1255
|
const resultSchema = Compile(options.schema ?? defaultWorkflowResultSchema);
|
|
1037
1256
|
let resultAccepted = false;
|
|
1038
1257
|
const resultTool = defineTool({
|
|
1039
|
-
name: "workflow_result", label: "Workflow Result", description: "Submit the terminal workflow result", parameters: resultSchema.Type(),
|
|
1258
|
+
name: "workflow_result", label: "Workflow Result", description: options.schema ? "Submit the terminal workflow result. Call exactly once to finish the current task or message, with a value matching the required schema. Do not return prose as the final result." : "Submit the terminal workflow result. Call exactly once to finish the current task or message, with { result: \"final response\" }. Do not return prose as the final result.", parameters: resultSchema.Type(),
|
|
1040
1259
|
async execute(_id, value) {
|
|
1041
1260
|
if (!resultSchema.Check(value) || !jsonValue(value))
|
|
1042
1261
|
return { content: [{ type: "text", text: "Result does not match the required schema." }], details: {}, isError: true };
|
|
@@ -1072,12 +1291,24 @@ export class WorkflowAgentExecutor {
|
|
|
1072
1291
|
} : undefined;
|
|
1073
1292
|
try {
|
|
1074
1293
|
setupFailed = true;
|
|
1075
|
-
const prepared = await prepareAgentSetup(this.root, this.transport, task, options, resolved, cwd, attempt, attemptSignal, customTools, resultTool, false
|
|
1294
|
+
const prepared = await prepareAgentSetup(this.root, this.transport, task, options, resolved, cwd, attempt, attemptSignal, customTools, resultTool, false);
|
|
1076
1295
|
setup = prepared.setup;
|
|
1077
1296
|
const attemptSetup = prepared.setup;
|
|
1078
1297
|
setupSummary = prepared.summary;
|
|
1079
1298
|
if (prepared.failure)
|
|
1080
1299
|
throw prepared.failure.error;
|
|
1300
|
+
let attemptCapabilities;
|
|
1301
|
+
if (options.agentNodeId !== undefined) {
|
|
1302
|
+
const policy = setup.prepared.resourcePolicy;
|
|
1303
|
+
const sources = policy?.selectorSources;
|
|
1304
|
+
const capabilities = policy?.capabilities ?? options.capabilities;
|
|
1305
|
+
attemptCapabilities = {
|
|
1306
|
+
tools: setup.prepared.tools,
|
|
1307
|
+
skills: selectResourcesByLayers(sources ? resourceSelectorLayers(sources, "skills") : [], capabilities?.skills ?? []),
|
|
1308
|
+
extensions: selectResourcesByLayers(sources ? resourceSelectorLayers(sources, "extensions") : [], capabilities?.extensions ?? []),
|
|
1309
|
+
};
|
|
1310
|
+
this.root.onAgentCapabilities?.(options.agentNodeId, attemptCapabilities, policy?.projectTrusted ?? false);
|
|
1311
|
+
}
|
|
1081
1312
|
setupFailed = false;
|
|
1082
1313
|
if (attemptSignal.aborted)
|
|
1083
1314
|
throw new WorkflowError("CANCELLED", "Agent cancelled");
|
|
@@ -1113,6 +1344,14 @@ export class WorkflowAgentExecutor {
|
|
|
1113
1344
|
}
|
|
1114
1345
|
if (policy)
|
|
1115
1346
|
setupSummary = { ...setupSummary, resourceSelectors: { ...resourcePolicySummary(policy, attemptSetup.sessionInput.tools), ...(inspection ? { skills: [...inspection.skills], extensions: [...inspection.extensions] } : {}) } };
|
|
1347
|
+
// The session's currently loaded skills, within its authorization, are the skill ceiling its children inherit.
|
|
1348
|
+
const nodeId = options.agentNodeId, selected = attemptCapabilities, live = session;
|
|
1349
|
+
publishLoadedSkills = nodeId === undefined || selected === undefined || live?.getResourceInspection === undefined ? undefined : () => {
|
|
1350
|
+
const loaded = live.getResourceInspection?.();
|
|
1351
|
+
if (loaded)
|
|
1352
|
+
this.root.onAgentCapabilities?.(nodeId, { ...selected, skills: [...loaded.skills] }, attemptSetup.prepared.resourcePolicy?.projectTrusted ?? false);
|
|
1353
|
+
};
|
|
1354
|
+
publishLoadedSkills?.();
|
|
1116
1355
|
},
|
|
1117
1356
|
onBeforeComplete: async () => {
|
|
1118
1357
|
if (options.worktreeOwner)
|
|
@@ -1152,6 +1391,7 @@ export class WorkflowAgentExecutor {
|
|
|
1152
1391
|
const runtimeRun = { id: transportBase.run.runId, namespaceId: transportBase.run.sessionId, workflowName: transportBase.run.workflow.name };
|
|
1153
1392
|
const providerRecovery = options.providerErrorRecovery;
|
|
1154
1393
|
const runtimeAgent = { id: transportBase.identity.callSite, structuralPath: [...transportBase.identity.structuralPath], ...(options.parent ? { parentId: options.parent } : {}) };
|
|
1394
|
+
const attemptControl = attemptControlText(options, attempt, attempts.at(-1)?.error?.message);
|
|
1155
1395
|
const runtimeRequest = {
|
|
1156
1396
|
task: setup.prepared.initialPrompt ?? setup.prompt,
|
|
1157
1397
|
cwd: setup.prepared.cwd,
|
|
@@ -1160,6 +1400,7 @@ export class WorkflowAgentExecutor {
|
|
|
1160
1400
|
// Pi sessions execute the prepared native definitions; the executor does not duplicate them as neutral request tools.
|
|
1161
1401
|
customTools: [],
|
|
1162
1402
|
...(options.schema ? { resultSchema: options.schema } : {}),
|
|
1403
|
+
...(attemptControl ? { turnControl: attemptControl } : {}),
|
|
1163
1404
|
run: runtimeRun,
|
|
1164
1405
|
agent: runtimeAgent,
|
|
1165
1406
|
signal: attemptSignal,
|
|
@@ -1232,6 +1473,30 @@ export class WorkflowAgentExecutor {
|
|
|
1232
1473
|
throw new WorkflowError("AGENT_FAILED", "Agent execution failed");
|
|
1233
1474
|
}
|
|
1234
1475
|
}
|
|
1476
|
+
/**
|
|
1477
|
+
* A child spawned by its parent's `agent` tool shares the parent's structural scope, breadcrumb and worktree, but owns its
|
|
1478
|
+
* call site under the parent's identity and spawn generation, with a per-label occurrence. It never carries the parent's
|
|
1479
|
+
* handle or turn. Children are new spawns: a retried or resumed parent spawns them again under new identities.
|
|
1480
|
+
*/
|
|
1481
|
+
function childIdentity(parent, scope, label) {
|
|
1482
|
+
const occurrences = parent.childOccurrences ?? new Map();
|
|
1483
|
+
parent.childOccurrences = occurrences;
|
|
1484
|
+
const occurrence = (occurrences.get(label) ?? 0) + 1;
|
|
1485
|
+
occurrences.set(label, occurrence);
|
|
1486
|
+
// The parent's scheduler node number marks the spawn generation: a cold-resumed or retried parent runs as a new node.
|
|
1487
|
+
const generation = parent.id.slice(parent.id.lastIndexOf(":") + 1);
|
|
1488
|
+
return { structuralPath: [...scope.structuralPath], callSite: `child:${agentIdentityPath(scope)}/spawn:${generation}/${label}`, occurrence, ...(scope.parentBreadcrumb ? { parentBreadcrumb: scope.parentBreadcrumb } : {}), ...(scope.worktreeOwner ? { worktreeOwner: scope.worktreeOwner } : {}) };
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Foreign errors keep descriptive core codes such as UNKNOWN_MODEL. Cancellation and budget exhaustion drive node and run
|
|
1492
|
+
* state, so a foreign error may claim CANCELLED only for an aborted node and never BUDGET_EXHAUSTED.
|
|
1493
|
+
*/
|
|
1494
|
+
function scheduledErrorCode(error, aborted) {
|
|
1495
|
+
if (error instanceof WorkflowError)
|
|
1496
|
+
return error.code;
|
|
1497
|
+
const code = errorCode(error);
|
|
1498
|
+
return code === undefined || code === "BUDGET_EXHAUSTED" || (code === "CANCELLED" && !aborted) ? "AGENT_FAILED" : code;
|
|
1499
|
+
}
|
|
1235
1500
|
export class FairAgentScheduler {
|
|
1236
1501
|
runner;
|
|
1237
1502
|
sessionLimit;
|
|
@@ -1274,6 +1539,17 @@ export class FairAgentScheduler {
|
|
|
1274
1539
|
throw new WorkflowError("INTERNAL_ERROR", `Unknown scheduler run: ${runId}`);
|
|
1275
1540
|
run.extensionSettings = settings === undefined ? undefined : deepFreeze(structuredClone(settings));
|
|
1276
1541
|
}
|
|
1542
|
+
async setConfiguration(agentId, configuration) {
|
|
1543
|
+
const node = this.#node(agentId);
|
|
1544
|
+
node.options = Object.freeze({ ...node.options, configuration: deepFreeze(structuredClone(configuration)), tools: Object.freeze([...configuration.tools]) });
|
|
1545
|
+
this.#persist(node.runId);
|
|
1546
|
+
await this.flush(node.runId);
|
|
1547
|
+
}
|
|
1548
|
+
setAttemptCapabilities(agentId, capabilities, projectTrusted) {
|
|
1549
|
+
const node = this.#node(agentId);
|
|
1550
|
+
node.attemptCapabilities = deepFreeze(structuredClone(capabilities));
|
|
1551
|
+
node.attemptProjectTrusted = projectTrusted;
|
|
1552
|
+
}
|
|
1277
1553
|
setExtensionSettings(agentId, settings) {
|
|
1278
1554
|
const node = this.#node(agentId);
|
|
1279
1555
|
node.options = Object.freeze({ ...node.options, extensionSettings: deepFreeze(structuredClone(settings)) });
|
|
@@ -1285,7 +1561,7 @@ export class FairAgentScheduler {
|
|
|
1285
1561
|
throw new WorkflowError("INTERNAL_ERROR", `Unknown scheduler run: ${runId}`);
|
|
1286
1562
|
const parent = parentId ? this.#nodes.get(parentId) : undefined;
|
|
1287
1563
|
if (parentId && (!parent || parent.runId !== runId))
|
|
1288
|
-
throw new WorkflowError("
|
|
1564
|
+
throw new WorkflowError("UNKNOWN_AGENT", "Parent agent is not owned by this run");
|
|
1289
1565
|
const effective = this.#inherit(run, parent, options);
|
|
1290
1566
|
const id = `${runId}:${String(++this.#nextId)}`;
|
|
1291
1567
|
const tuiIndex = ++run.nextIndex;
|
|
@@ -1302,12 +1578,11 @@ export class FairAgentScheduler {
|
|
|
1302
1578
|
node.state = "running";
|
|
1303
1579
|
this.#persist(runId);
|
|
1304
1580
|
try {
|
|
1305
|
-
const value = await this.runner({ id, runId, tuiIndex, ...(parentId ? { parentId } : {}), prompt, options:
|
|
1581
|
+
const value = await this.runner({ id, runId, tuiIndex, ...(parentId ? { parentId } : {}), prompt, options: node.options, signal: node.controller.signal, setSteer: (handler) => { node.steer = handler; } });
|
|
1306
1582
|
this.#settle(node, { id, ok: true, value });
|
|
1307
1583
|
}
|
|
1308
1584
|
catch (error) {
|
|
1309
|
-
|
|
1310
|
-
this.#settle(node, { id, ok: false, error: { code: typed.code, message: typed.message } });
|
|
1585
|
+
this.#settle(node, { id, ok: false, error: { code: scheduledErrorCode(error, node.controller.signal.aborted), message: errorText(error) } });
|
|
1311
1586
|
}
|
|
1312
1587
|
};
|
|
1313
1588
|
this.#nodes.set(id, node);
|
|
@@ -1320,7 +1595,7 @@ export class FairAgentScheduler {
|
|
|
1320
1595
|
const parent = this.#node(parentId);
|
|
1321
1596
|
const child = this.#node(childId);
|
|
1322
1597
|
if (child.parentId !== parentId)
|
|
1323
|
-
throw new WorkflowError("
|
|
1598
|
+
throw new WorkflowError("UNKNOWN_AGENT", "Results are scoped to direct children");
|
|
1324
1599
|
if (child.collected || !child.promise)
|
|
1325
1600
|
throw new WorkflowError("AGENT_RESULT_COLLECTED", "Child result has already been collected; nested results are one-shot");
|
|
1326
1601
|
if (child.collecting)
|
|
@@ -1362,7 +1637,7 @@ export class FairAgentScheduler {
|
|
|
1362
1637
|
async steer(parentId, childId, message) {
|
|
1363
1638
|
const child = this.#node(childId);
|
|
1364
1639
|
if (child.parentId !== parentId)
|
|
1365
|
-
throw new WorkflowError("
|
|
1640
|
+
throw new WorkflowError("UNKNOWN_AGENT", "Steering is scoped to direct children");
|
|
1366
1641
|
if (child.state !== "running" && child.state !== "waiting_for_child")
|
|
1367
1642
|
throw new WorkflowError("AGENT_FAILED", "Child is not running");
|
|
1368
1643
|
if (!child.steer)
|
|
@@ -1432,13 +1707,14 @@ export class FairAgentScheduler {
|
|
|
1432
1707
|
}
|
|
1433
1708
|
this.#dispatch();
|
|
1434
1709
|
}
|
|
1435
|
-
toolsFor(parentId
|
|
1710
|
+
toolsFor(parentId) {
|
|
1436
1711
|
const parent = this.#node(parentId);
|
|
1437
|
-
if (!parent.options.tools.includes("agent"))
|
|
1712
|
+
if (!(parent.attemptCapabilities?.tools ?? parent.options.tools).includes("agent"))
|
|
1438
1713
|
return [];
|
|
1439
1714
|
const agentTool = defineTool({
|
|
1440
|
-
name: "agent", label: "Child Agent", description: "Start a direct child agent",
|
|
1441
|
-
parameters: Type.Object({ prompt: Type.String(), label: Type.String(), tools: Type.Optional(Type.Array(Type.String())),
|
|
1715
|
+
name: "agent", label: "Child Agent", description: "Start a direct child agent. You own the children you create: collect each result with get_subagent_result and do not leave descendants running.",
|
|
1716
|
+
parameters: Type.Object({ prompt: Type.String(), label: Type.String(), tools: Type.Optional(Type.Array(Type.String())), excludeTools: Type.Optional(Type.Array(Type.String(), { description: "Exact tool names removed after preparation; cannot remove workflow_result" })), skills: Type.Optional(Type.Array(Type.String())), extensions: Type.Optional(Type.Array(Type.String())), model: 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 }),
|
|
1717
|
+
outputSchema: Type.Object({ id: Type.String() }),
|
|
1442
1718
|
execute: async (_id, params) => {
|
|
1443
1719
|
if (!isChildAgentToolParams(params))
|
|
1444
1720
|
throw new WorkflowError("INVALID_METADATA", "Invalid child agent parameters");
|
|
@@ -1446,19 +1722,20 @@ export class FairAgentScheduler {
|
|
|
1446
1722
|
const outputSchema = params.outputSchema;
|
|
1447
1723
|
if (outputSchema !== undefined)
|
|
1448
1724
|
validateSchema(outputSchema, "agent outputSchema");
|
|
1449
|
-
const tools =
|
|
1725
|
+
const tools = parent.attemptCapabilities?.tools ?? parent.options.tools;
|
|
1450
1726
|
const agentOptions = { ...params };
|
|
1451
1727
|
Reflect.deleteProperty(agentOptions, "prompt");
|
|
1452
|
-
const options = { 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.
|
|
1728
|
+
const options = { 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.contextFiles ? { contextFiles: params.contextFiles } : {}), ...(outputSchema === undefined ? {} : { schema: outputSchema }), ...(params.retries === undefined ? {} : { retries: params.retries }), ...(params.timeoutMs === undefined ? {} : { timeoutMs: params.timeoutMs }) };
|
|
1453
1729
|
const child = this.spawn(parent.runId, params.prompt, options, parentId);
|
|
1454
|
-
return { content: [{ type: "text", text: JSON.stringify({ id: child.id }) }], details: { id: child.id } };
|
|
1730
|
+
return { content: [{ type: "text", text: JSON.stringify({ id: child.id }) }], details: { id: child.id }, structuredContent: { id: child.id } };
|
|
1455
1731
|
},
|
|
1456
1732
|
});
|
|
1457
1733
|
const resultTool = defineTool({
|
|
1458
1734
|
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",
|
|
1459
1735
|
parameters: Type.Object({ id: Type.String() }),
|
|
1736
|
+
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() })) }),
|
|
1460
1737
|
execute: async (_id, params) => { const value = await this.result(parentId, params.id); if (!value.ok && value.error.code === "BUDGET_EXHAUSTED")
|
|
1461
|
-
throw new WorkflowError("BUDGET_EXHAUSTED", value.error.message); return { content: [{ type: "text", text: JSON.stringify(value) }], details: value }; }
|
|
1738
|
+
throw new WorkflowError("BUDGET_EXHAUSTED", value.error.message); return { content: [{ type: "text", text: JSON.stringify(value) }], details: value, structuredContent: value }; }
|
|
1462
1739
|
});
|
|
1463
1740
|
const steerTool = defineTool({
|
|
1464
1741
|
name: "steer_subagent", label: "Steer Child", description: "Steer a running direct child",
|
|
@@ -1519,11 +1796,15 @@ export class FairAgentScheduler {
|
|
|
1519
1796
|
return Object.freeze({ ...options, tools: Object.freeze([...inheritedTools]), ...(extensionSettings === undefined ? {} : { extensionSettings }), ...(options.agentOptions ? { agentOptions: structuredClone(options.agentOptions) } : {}), ...(options.agentIdentity ? { agentIdentity: Object.freeze({ ...options.agentIdentity, structuralPath: Object.freeze([...options.agentIdentity.structuralPath]) }) } : {}) });
|
|
1520
1797
|
if (options.cwd !== parent.options.cwd)
|
|
1521
1798
|
throw new WorkflowError("UNKNOWN_TOOL", "Child cwd cannot differ from its parent");
|
|
1522
|
-
const
|
|
1799
|
+
const parentCapabilities = parent.attemptCapabilities ?? (parent.options.configuration ? { tools: parent.options.configuration.tools, skills: parent.options.configuration.skills.slice(parent.options.configuration.skills.lastIndexOf("!*") + 1), extensions: parent.options.configuration.extensions.slice(parent.options.configuration.extensions.lastIndexOf("!*") + 1) } : parent.options.capabilities);
|
|
1800
|
+
const forbidden = inheritedTools.find((tool) => !(parentCapabilities?.tools ?? parent.options.tools).includes(tool));
|
|
1523
1801
|
if (forbidden)
|
|
1524
1802
|
throw new WorkflowError("UNKNOWN_TOOL", `Child tool escalates parent boundary: ${forbidden}`);
|
|
1525
|
-
const identity = options.agentIdentity ?? parent.options.agentIdentity;
|
|
1526
|
-
|
|
1803
|
+
const identity = options.agentIdentity ?? (parent.options.agentIdentity ? childIdentity(parent, parent.options.agentIdentity, options.label) : undefined);
|
|
1804
|
+
// Children inherit the exclusions frozen in their parent's configuration, not its re-evaluated options.
|
|
1805
|
+
const inheritedExcludeTools = [...(parent.options.configuration?.excludeTools ?? excludedToolNames(parent.options))];
|
|
1806
|
+
const projectTrusted = (parent.attemptProjectTrusted ?? parent.options.configuration?.projectTrusted ?? parent.options.projectTrusted) !== false && options.projectTrusted !== false;
|
|
1807
|
+
return Object.freeze({ ...options, projectTrusted, ...(inheritedExcludeTools.length ? { inheritedExcludeTools: Object.freeze(inheritedExcludeTools) } : {}), ...(parentCapabilities ? { capabilities: parentCapabilities } : {}), cwd: parent.options.cwd, tools: Object.freeze([...inheritedTools]), ...(extensionSettings === undefined ? {} : { extensionSettings }), ...(options.agentOptions ? { agentOptions: structuredClone(options.agentOptions) } : {}), ...(parent.options.parentBreadcrumb && !options.parentBreadcrumb ? { parentBreadcrumb: parent.options.parentBreadcrumb } : {}), ...(identity ? { agentIdentity: Object.freeze({ ...identity, structuralPath: Object.freeze([...identity.structuralPath]) }) } : {}), ...(parent.options.worktreeOwner ? { worktreeOwner: parent.options.worktreeOwner } : {}) });
|
|
1527
1808
|
}
|
|
1528
1809
|
#enqueue(runId, node, start) { this.#runs.get(runId)?.queue.push({ ...(node ? { node } : {}), start }); this.#dispatch(); }
|
|
1529
1810
|
#dispatch() {
|
|
@@ -1548,8 +1829,7 @@ export class FairAgentScheduler {
|
|
|
1548
1829
|
run.beforeLaunch?.();
|
|
1549
1830
|
}
|
|
1550
1831
|
catch (error) {
|
|
1551
|
-
|
|
1552
|
-
this.#settle(item.node, { id: item.node.id, ok: false, error: { code: typed.code, message: typed.message } });
|
|
1832
|
+
this.#settle(item.node, { id: item.node.id, ok: false, error: { code: scheduledErrorCode(error, item.node.controller.signal.aborted), message: errorText(error) } });
|
|
1553
1833
|
continue;
|
|
1554
1834
|
}
|
|
1555
1835
|
}
|
|
@@ -1601,7 +1881,7 @@ export class FairAgentScheduler {
|
|
|
1601
1881
|
#node(id) {
|
|
1602
1882
|
const node = this.#nodes.get(id);
|
|
1603
1883
|
if (!node)
|
|
1604
|
-
throw new WorkflowError("
|
|
1884
|
+
throw new WorkflowError("UNKNOWN_AGENT", `Unknown owned agent: ${id}`);
|
|
1605
1885
|
return node;
|
|
1606
1886
|
}
|
|
1607
1887
|
#persist(runId) {
|