@marcoscale98/pi-extensible-workflows 5.19.1-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 +77 -0
- package/README.md +2 -2
- package/dist/src/agent-execution.d.ts +43 -10
- package/dist/src/agent-execution.js +365 -127
- package/dist/src/background-widget.d.ts +0 -1
- package/dist/src/background-widget.js +4 -9
- package/dist/src/decoders.d.ts +2 -3
- package/dist/src/decoders.js +33 -37
- package/dist/src/execution.d.ts +2 -0
- package/dist/src/execution.js +5 -5
- package/dist/src/host-delivery.js +4 -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 +6 -6
- package/dist/src/host-recovery.js +14 -22
- package/dist/src/host-runtime.js +56 -5
- package/dist/src/host-view.d.ts +1 -1
- package/dist/src/host-view.js +17 -15
- package/dist/src/host.js +62 -87
- package/dist/src/index.d.ts +1 -2
- package/dist/src/index.js +1523 -1630
- package/dist/src/index.js.map +3 -3
- package/dist/src/paths.d.ts +7 -6
- package/dist/src/paths.js +24 -15
- 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 +28 -70
- package/dist/src/runtime/agent-runner.d.ts +2 -0
- package/dist/src/session-lease.d.ts +1 -1
- package/dist/src/session-lease.js +45 -11
- package/dist/src/settings.d.ts +6 -3
- package/dist/src/settings.js +36 -47
- package/dist/src/store.d.ts +3 -1
- package/dist/src/store.js +17 -1
- package/dist/src/trajectory.d.ts +0 -1
- package/dist/src/trajectory.js +2 -3
- package/dist/src/types.d.ts +100 -44
- package/dist/src/types.js +4 -7
- package/dist/src/utils.d.ts +3 -2
- package/dist/src/utils.js +20 -10
- package/dist/src/validation.d.ts +8 -2
- package/dist/src/validation.js +122 -86
- package/dist/starter/index.js +8 -9
- package/dist/starter/index.js.map +1 -1
- package/dist/starter/review-loop.js +8 -7
- package/dist/subagents/index.js +654 -814
- package/dist/subagents/index.js.map +3 -3
- package/dist/subagents/src/contracts.d.ts +4 -3
- package/dist/subagents/src/contracts.js +2 -2
- package/dist/subagents/src/index.d.ts +1 -1
- package/dist/subagents/src/index.js +1 -2
- package/dist/subagents/src/manager.js +49 -70
- package/dist/subagents/src/navigator.js +3 -14
- package/dist/subagents/src/view.d.ts +0 -1
- package/dist/subagents/src/view.js +2 -16
- package/dist/subagents/src/worktree.js +1 -1
- package/dist/trajectory/assets/index.html +8 -8
- package/dist/trajectory/index.js +487 -891
- package/dist/trajectory/index.js.map +3 -3
- package/dist/trajectory/src/assets/index.html +8 -8
- package/dist/trajectory/src/index.js +126 -86
- package/dist/trajectory/src/server.js +264 -25
- 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 +5 -5
- package/skills/pi-extensible-workflows/SKILL.md +10 -8
- package/src/agent-execution.ts +339 -127
- package/src/background-widget.ts +4 -10
- package/src/decoders.ts +31 -37
- package/src/execution.ts +5 -5
- package/src/host-delivery.ts +4 -8
- package/src/host-navigator.ts +4 -2
- package/src/host-phases.ts +1 -1
- package/src/host-recovery.ts +20 -26
- package/src/host-runtime.ts +44 -6
- package/src/host-view.ts +18 -11
- package/src/host.ts +62 -113
- package/src/index.ts +1 -2
- package/src/paths.ts +18 -12
- package/src/persistence.ts +1 -0
- package/src/pi-runtime-runner.ts +3 -1
- package/src/registry.ts +27 -63
- package/src/runtime/agent-runner.ts +2 -0
- package/src/session-lease.ts +34 -6
- package/src/settings.ts +35 -47
- package/src/store.ts +18 -2
- package/src/trajectory.ts +2 -4
- package/src/types.ts +71 -30
- package/src/utils.ts +18 -9
- package/src/validation.ts +102 -79
- package/starter/index.ts +0 -1
- package/starter/review-loop.ts +8 -7
- 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 +8 -14
- package/subagents/src/contracts.ts +3 -4
- package/subagents/src/index.ts +2 -3
- package/subagents/src/manager.ts +45 -125
- package/subagents/src/navigator.ts +4 -13
- package/subagents/src/view.ts +4 -14
- package/subagents/src/worktree.ts +1 -1
- package/trajectory/src/assets/index.html +8 -8
- package/trajectory/src/index.ts +106 -82
- package/trajectory/src/server.ts +44 -8
- package/trajectory/src/startup-mutex.ts +127 -0
- package/dist/src/roles.d.ts +0 -70
- package/dist/src/roles.js +0 -326
- 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 -327
|
@@ -1,23 +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, createCodemodeExtension, createMcpExtension, createToolSearchExtension, 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, extensionIdentity } from "./paths.js";
|
|
18
|
+
import { isCoreAgentOption, validateAgentOptions, validateSchema } from "./validation.js";
|
|
19
|
+
import { canonicalPath, extensionIdentity, piResourceContains, piResourcePath, sameFilesystemPath } from "./paths.js";
|
|
19
20
|
import { isCorePackageName } from "./package-identity.js";
|
|
20
|
-
import { canonicalExtensionSelector,
|
|
21
|
+
import { canonicalExtensionSelector, validateContextFileScopes, validateSelectorList, validateWorkflowExtensionSettings } from "./settings.js";
|
|
22
|
+
import { agentIdentityPath } from "./execution.js";
|
|
21
23
|
const localToolTimingExtension = createToolTimingExtension();
|
|
22
24
|
function parseModel(value, fallback, aliases = {}, knownModels, settingsPath) {
|
|
23
25
|
if (!value)
|
|
@@ -25,6 +27,59 @@ function parseModel(value, fallback, aliases = {}, knownModels, settingsPath) {
|
|
|
25
27
|
assertModelThinking(value);
|
|
26
28
|
return resolveModelReference(value, aliases, knownModels, settingsPath);
|
|
27
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
|
+
}
|
|
28
83
|
function accounting(stats) {
|
|
29
84
|
return { input: stats.tokens.input, output: stats.tokens.output, cacheRead: stats.tokens.cacheRead, cacheWrite: stats.tokens.cacheWrite, cost: stats.cost };
|
|
30
85
|
}
|
|
@@ -212,6 +267,48 @@ function builtinExtensions() {
|
|
|
212
267
|
}
|
|
213
268
|
const BUILTIN_EXTENSION_PREFIX = "builtin:";
|
|
214
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
|
+
}
|
|
215
312
|
async function createLocalPiSessionHandle(input, sessionStartEvent) {
|
|
216
313
|
const agentDir = input.agentDir ?? getAgentDir();
|
|
217
314
|
const systemPromptSource = workflowSystemPromptPath(input.cwd, agentDir, input.resourcePolicy?.projectTrusted ?? true);
|
|
@@ -236,25 +333,23 @@ async function createLocalPiSessionHandle(input, sessionStartEvent) {
|
|
|
236
333
|
const resolved = await packageManager.resolve();
|
|
237
334
|
const discoveredExtensions = [...new Set(resolved.extensions.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => extensionIdentity(path)))];
|
|
238
335
|
const selectorSources = policy.selectorSources;
|
|
239
|
-
const
|
|
240
|
-
cwd: input.cwd,
|
|
241
|
-
selectorSources,
|
|
242
|
-
resources: { extensions: discoveredExtensions },
|
|
243
|
-
});
|
|
244
|
-
const selectedExtensions = selection.selectedExtensions ?? [];
|
|
336
|
+
const selectedExtensions = selectResourcesByLayers(resourceSelectorLayers(selectorSources, "extensions"), discoveredExtensions).filter((path) => policy.capabilities === undefined || policy.capabilities.extensions.includes(path));
|
|
245
337
|
const extensionPaths = selectedExtensions.filter((path) => path.startsWith(BUILTIN_EXTENSION_PREFIX) || !isWorkflowHostEntry(path));
|
|
246
338
|
policy.selectedExtensions = selectedExtensions;
|
|
247
|
-
policy.unmatchedExtensions =
|
|
339
|
+
policy.unmatchedExtensions = unmatchedResourcePatterns(policy.effective.extensions, discoveredExtensions);
|
|
248
340
|
const skillPaths = [...new Set(resolved.skills.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => path))];
|
|
249
|
-
const updateSkillMatches = (skills) => {
|
|
341
|
+
const updateSkillMatches = (skills, extensionSkillPaths) => {
|
|
250
342
|
const names = [...new Set(skills.map(({ name }) => name))];
|
|
251
|
-
const
|
|
252
|
-
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];
|
|
253
348
|
policy.selectedSkills = selectedSkills;
|
|
254
|
-
policy.unmatchedSkills =
|
|
349
|
+
policy.unmatchedSkills = unmatchedResourcePatterns(policy.effective.skills, names);
|
|
255
350
|
return new Set(names.filter((name) => !selectedSkills.includes(name)));
|
|
256
351
|
};
|
|
257
|
-
|
|
352
|
+
const workflowLoader = new WorkflowResourceLoader(input.cwd, {
|
|
258
353
|
cwd: input.cwd,
|
|
259
354
|
agentDir,
|
|
260
355
|
settingsManager,
|
|
@@ -265,12 +360,13 @@ async function createLocalPiSessionHandle(input, sessionStartEvent) {
|
|
|
265
360
|
extensionFactories,
|
|
266
361
|
...(contextFilesOverride ? { agentsFilesOverride: contextFilesOverride } : {}),
|
|
267
362
|
skillsOverride: (base) => {
|
|
268
|
-
const disabledSkills = updateSkillMatches(base.skills);
|
|
363
|
+
const disabledSkills = updateSkillMatches(base.skills, workflowLoader.extensionSkillPaths);
|
|
269
364
|
return { ...base, skills: base.skills.filter(({ name }) => !disabledSkills.has(name)) };
|
|
270
365
|
},
|
|
271
366
|
...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}),
|
|
272
367
|
...systemPromptOptions,
|
|
273
368
|
});
|
|
369
|
+
resourceLoader = workflowLoader;
|
|
274
370
|
await resourceLoader.reload();
|
|
275
371
|
}
|
|
276
372
|
else {
|
|
@@ -293,7 +389,7 @@ async function createLocalPiSessionHandle(input, sessionStartEvent) {
|
|
|
293
389
|
throw new WorkflowError("UNKNOWN_MODEL", `Unknown model: ${input.model.provider}/${input.model.model}${because}`);
|
|
294
390
|
}
|
|
295
391
|
const effectiveSessionStartEvent = input.settings === undefined && sessionStartEvent === undefined ? undefined : { ...(sessionStartEvent ?? { type: "session_start", reason: "startup" }), settings: input.settings ?? Object.freeze({}) };
|
|
296
|
-
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 });
|
|
297
393
|
const nativeDispose = session.dispose.bind(session);
|
|
298
394
|
let disposal;
|
|
299
395
|
const shutdown = (reason, targetSessionFile) => {
|
|
@@ -663,6 +759,11 @@ export async function createLocalWorkflowAgentSession(prepared, context) {
|
|
|
663
759
|
return session;
|
|
664
760
|
}
|
|
665
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
|
+
}
|
|
666
767
|
function changedOption(options, baseline, key) { return JSON.stringify(options[key]) !== JSON.stringify(baseline[key]); }
|
|
667
768
|
function isChildAgentToolParams(value) {
|
|
668
769
|
if (!jsonObject(value) || typeof value.prompt !== "string" || typeof value.label !== "string")
|
|
@@ -675,8 +776,6 @@ function isChildAgentToolParams(value) {
|
|
|
675
776
|
return false;
|
|
676
777
|
if (value.model !== undefined && typeof value.model !== "string")
|
|
677
778
|
return false;
|
|
678
|
-
if (value.role !== undefined && (typeof value.role !== "string" || !value.role.trim()))
|
|
679
|
-
return false;
|
|
680
779
|
if (value.contextFiles !== undefined && (!Array.isArray(value.contextFiles) || !value.contextFiles.every(isContextFileScope)))
|
|
681
780
|
return false;
|
|
682
781
|
if (value.outputSchema !== undefined && !jsonObject(value.outputSchema))
|
|
@@ -702,7 +801,7 @@ function resourcePolicySummary(policy, tools = policy.selectedTools ?? []) {
|
|
|
702
801
|
}
|
|
703
802
|
const RESOURCE_SELECTOR_KEYS = ["skills", "extensions", "tools"];
|
|
704
803
|
function resourceSelectorLayers(sources, key) {
|
|
705
|
-
return [sources.global[key], sources.project[key], sources.
|
|
804
|
+
return [sources.global[key], sources.project[key], sources.call?.[key]];
|
|
706
805
|
}
|
|
707
806
|
function selectorListWidened(ceiling, candidate) {
|
|
708
807
|
let candidateIndex = 0;
|
|
@@ -775,7 +874,7 @@ function appendResourcePolicyNarrowing(ceiling, candidate) {
|
|
|
775
874
|
};
|
|
776
875
|
}
|
|
777
876
|
function packageRoot(start) {
|
|
778
|
-
let current = dirname(
|
|
877
|
+
let current = dirname(canonicalPath(start));
|
|
779
878
|
for (;;) {
|
|
780
879
|
const candidates = [current, join(current, "..", "@earendil-works", "pi-coding-agent"), join(current, "..", "pi-coding-agent")];
|
|
781
880
|
for (const candidate of candidates) {
|
|
@@ -835,10 +934,9 @@ async function resolvePiRuntime() {
|
|
|
835
934
|
}
|
|
836
935
|
}
|
|
837
936
|
const piRuntimeResolution = resolvePiRuntime();
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
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");
|
|
842
940
|
}
|
|
843
941
|
async function preparedAgentSession(input, initialPrompt) {
|
|
844
942
|
const { runtime: piRuntime, error: piRuntimeError } = await piRuntimeResolution;
|
|
@@ -861,47 +959,27 @@ async function attemptSessionInput(source, attempt) {
|
|
|
861
959
|
await copyFile(source, target);
|
|
862
960
|
return target;
|
|
863
961
|
}
|
|
864
|
-
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) {
|
|
865
963
|
const setupSignal = signal ?? root.runContext?.signal ?? new AbortController().signal;
|
|
866
964
|
const baselineOptions = structuredClone(options.agentOptions ?? {});
|
|
867
965
|
const baseResourcePolicy = await root.agentResourcePolicy?.();
|
|
868
|
-
const
|
|
869
|
-
const
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
if (Array.isArray(value) && value.every((entry) => typeof entry === "string"))
|
|
878
|
-
return value;
|
|
879
|
-
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,
|
|
880
975
|
};
|
|
881
|
-
const
|
|
882
|
-
const rawCallExtensions = selectorValue("extensions", options.extensions)?.map((selector) => canonicalExtensionSelector(selector, cwd));
|
|
883
|
-
const rawCallTools = selectorValue("tools", options.tools);
|
|
884
|
-
const callSelectors = { ...(rawCallSkills === undefined ? {} : { skills: rawCallSkills }), ...(rawCallExtensions === undefined ? {} : { extensions: rawCallExtensions }), ...(rawCallTools === undefined ? {} : { tools: rawCallTools }) };
|
|
885
|
-
const resourcePolicy = baseResourcePolicy ? {
|
|
886
|
-
...baseResourcePolicy,
|
|
887
|
-
effective: {
|
|
888
|
-
skills: [...baseResourcePolicy.effective.skills, ...(roleSelectors?.skills ?? []), ...(rawCallSkills ?? [])],
|
|
889
|
-
extensions: [...baseResourcePolicy.effective.extensions, ...(roleSelectors?.extensions ?? []), ...(rawCallExtensions ?? [])],
|
|
890
|
-
...(baseResourcePolicy.effective.tools === undefined && roleSelectors?.tools === undefined && rawCallTools === undefined ? {} : { tools: [...(baseResourcePolicy.effective.tools ?? []), ...(roleSelectors?.tools ?? []), ...(rawCallTools ?? [])] }),
|
|
891
|
-
},
|
|
892
|
-
selectorSources: { global: baseResourcePolicy.selectorSources.global, project: baseResourcePolicy.selectorSources.project, ...(roleSelectors ? { role: roleSelectors } : {}), ...(Object.keys(callSelectors).length ? { call: callSelectors } : {}) },
|
|
893
|
-
} : undefined;
|
|
894
|
-
if (resourcePolicy) {
|
|
895
|
-
resourcePolicy.selectedTools = resolved.tools;
|
|
896
|
-
resourcePolicy.unmatchedTools = unmatchedResourcePatterns(resourceSelectorLayers(resourcePolicy.selectorSources, "tools").flatMap((layer) => layer ?? []), [...root.tools]);
|
|
897
|
-
}
|
|
898
|
-
const resourcePolicyCeiling = resourcePolicy ? structuredClone(resourcePolicy) : undefined;
|
|
976
|
+
const resourcePolicyCeiling = structuredClone(resourcePolicy);
|
|
899
977
|
const sessionPath = options.sessionPath === undefined || inspection ? options.sessionPath : await attemptSessionInput(options.sessionPath, attempt);
|
|
900
|
-
const extensionSettings =
|
|
901
|
-
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 } : {}) });
|
|
902
980
|
if (options.agentNodeId !== undefined)
|
|
903
981
|
root.onAgentSettings?.(options.agentNodeId, extensionSettings);
|
|
904
|
-
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) };
|
|
905
983
|
const setup = { prompt: task, options: sessionInput.options ?? {}, sessionInput, prepared: await preparedAgentSession(sessionInput, task), transport };
|
|
906
984
|
const base = fallbackSetupContext(root, options, setupSignal);
|
|
907
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" } : {}) });
|
|
@@ -925,30 +1003,69 @@ async function prepareAgentSetup(root, transport, task, options, resolved, cwd,
|
|
|
925
1003
|
if (resourcePolicyWidened(resourcePolicyCeiling, setup.sessionInput.resourcePolicy))
|
|
926
1004
|
throw new WorkflowError("INVALID_METADATA", "Agent setup widened the prepared resource policy");
|
|
927
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
|
+
}
|
|
928
1017
|
setup.sessionInput.options = setup.options;
|
|
929
1018
|
if (changedOption(setup.options, baselineOptions, "model") && typeof setup.options.model === "string")
|
|
930
1019
|
setup.sessionInput.model = parseModel(setup.options.model, setup.sessionInput.model, root.modelAliases, root.knownModels ?? root.availableModels, root.settingsPath);
|
|
931
1020
|
if (changedOption(setup.options, baselineOptions, "tools") && Array.isArray(setup.options.tools) && setup.options.tools.every((tool) => typeof tool === "string"))
|
|
932
1021
|
setup.sessionInput.tools = [...setup.options.tools];
|
|
933
|
-
|
|
934
|
-
|
|
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}`);
|
|
935
1044
|
const customToolNames = new Set([...(setup.sessionInput.customTools ?? []).map(({ name }) => name), ...(setup.sessionInput.resultTool ? [setup.sessionInput.resultTool.name] : [])]);
|
|
936
1045
|
const widened = setup.sessionInput.tools.find((tool) => !resolved.tools.includes(tool) && !customToolNames.has(tool));
|
|
937
1046
|
const outsideTool = widened ?? setup.sessionInput.tools.find((tool) => !root.tools.has(tool) && !customToolNames.has(tool));
|
|
938
1047
|
if (outsideTool)
|
|
939
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") ?? [];
|
|
940
1057
|
}
|
|
941
1058
|
catch (error) {
|
|
942
1059
|
setup.prepared = await preparedAgentSession(setup.sessionInput, task);
|
|
943
1060
|
return { setup, summary: agentSetupSummary(setup, hookNames), failure: { error } };
|
|
944
1061
|
}
|
|
945
|
-
setup.prepared = await preparedAgentSession(setup.sessionInput, inspection ? task :
|
|
1062
|
+
setup.prepared = await preparedAgentSession(setup.sessionInput, inspection ? task : setup.prompt);
|
|
946
1063
|
return { setup, summary: agentSetupSummary(setup, hookNames) };
|
|
947
1064
|
}
|
|
948
1065
|
export async function prepareAgentSetupForInspection(root, task, options, transport) {
|
|
949
1066
|
const executor = new WorkflowAgentExecutor(root);
|
|
950
|
-
const
|
|
951
|
-
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);
|
|
952
1069
|
}
|
|
953
1070
|
function attemptRecord(transport, attempt, session, setup, stats, result, error) {
|
|
954
1071
|
return { attempt, transport, session: session.reference, ...(result === undefined ? {} : { result }), ...(error ? { error } : {}), accounting: stats, setup };
|
|
@@ -966,52 +1083,105 @@ export function getAgentAttempts(error) {
|
|
|
966
1083
|
export class WorkflowAgentExecutor {
|
|
967
1084
|
root;
|
|
968
1085
|
transport;
|
|
969
|
-
warnedRoleTools = new Set();
|
|
970
1086
|
constructor(root, transport = localAgentTransport) {
|
|
971
1087
|
this.root = root;
|
|
972
1088
|
this.transport = transport;
|
|
973
1089
|
}
|
|
974
1090
|
setRunContext(runContext) { this.root.runContext = runContext; }
|
|
975
1091
|
resolve(options, inheritedTools) {
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
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)];
|
|
990
1134
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
}
|
|
1014
|
-
|
|
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;
|
|
1015
1185
|
}
|
|
1016
1186
|
async execute(task, options, signal, customTools = [], setSteer, beforeRetry) {
|
|
1017
1187
|
const executionSignal = signal ?? this.root.runContext?.signal;
|
|
@@ -1019,7 +1189,7 @@ export class WorkflowAgentExecutor {
|
|
|
1019
1189
|
throw new WorkflowError("INVALID_METADATA", "retries must be a non-negative integer");
|
|
1020
1190
|
if (options.timeoutMs !== undefined && options.timeoutMs !== null && (!Number.isInteger(options.timeoutMs) || options.timeoutMs <= 0))
|
|
1021
1191
|
throw new WorkflowError("INVALID_METADATA", "timeoutMs must be null or a positive integer");
|
|
1022
|
-
let resolved
|
|
1192
|
+
let resolved;
|
|
1023
1193
|
let recoveryModel;
|
|
1024
1194
|
let cwd;
|
|
1025
1195
|
if (options.parent) {
|
|
@@ -1049,6 +1219,15 @@ export class WorkflowAgentExecutor {
|
|
|
1049
1219
|
throw new WorkflowError("INVALID_METADATA", "Only child agents or worktree scopes may provide a cwd");
|
|
1050
1220
|
cwd = this.root.cwd;
|
|
1051
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); } });
|
|
1052
1231
|
const attempts = [];
|
|
1053
1232
|
let maxAttempts = (options.retries ?? 0) + 1;
|
|
1054
1233
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
@@ -1076,7 +1255,7 @@ export class WorkflowAgentExecutor {
|
|
|
1076
1255
|
const resultSchema = Compile(options.schema ?? defaultWorkflowResultSchema);
|
|
1077
1256
|
let resultAccepted = false;
|
|
1078
1257
|
const resultTool = defineTool({
|
|
1079
|
-
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(),
|
|
1080
1259
|
async execute(_id, value) {
|
|
1081
1260
|
if (!resultSchema.Check(value) || !jsonValue(value))
|
|
1082
1261
|
return { content: [{ type: "text", text: "Result does not match the required schema." }], details: {}, isError: true };
|
|
@@ -1112,12 +1291,24 @@ export class WorkflowAgentExecutor {
|
|
|
1112
1291
|
} : undefined;
|
|
1113
1292
|
try {
|
|
1114
1293
|
setupFailed = true;
|
|
1115
|
-
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);
|
|
1116
1295
|
setup = prepared.setup;
|
|
1117
1296
|
const attemptSetup = prepared.setup;
|
|
1118
1297
|
setupSummary = prepared.summary;
|
|
1119
1298
|
if (prepared.failure)
|
|
1120
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
|
+
}
|
|
1121
1312
|
setupFailed = false;
|
|
1122
1313
|
if (attemptSignal.aborted)
|
|
1123
1314
|
throw new WorkflowError("CANCELLED", "Agent cancelled");
|
|
@@ -1153,6 +1344,14 @@ export class WorkflowAgentExecutor {
|
|
|
1153
1344
|
}
|
|
1154
1345
|
if (policy)
|
|
1155
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?.();
|
|
1156
1355
|
},
|
|
1157
1356
|
onBeforeComplete: async () => {
|
|
1158
1357
|
if (options.worktreeOwner)
|
|
@@ -1192,6 +1391,7 @@ export class WorkflowAgentExecutor {
|
|
|
1192
1391
|
const runtimeRun = { id: transportBase.run.runId, namespaceId: transportBase.run.sessionId, workflowName: transportBase.run.workflow.name };
|
|
1193
1392
|
const providerRecovery = options.providerErrorRecovery;
|
|
1194
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);
|
|
1195
1395
|
const runtimeRequest = {
|
|
1196
1396
|
task: setup.prepared.initialPrompt ?? setup.prompt,
|
|
1197
1397
|
cwd: setup.prepared.cwd,
|
|
@@ -1200,6 +1400,7 @@ export class WorkflowAgentExecutor {
|
|
|
1200
1400
|
// Pi sessions execute the prepared native definitions; the executor does not duplicate them as neutral request tools.
|
|
1201
1401
|
customTools: [],
|
|
1202
1402
|
...(options.schema ? { resultSchema: options.schema } : {}),
|
|
1403
|
+
...(attemptControl ? { turnControl: attemptControl } : {}),
|
|
1203
1404
|
run: runtimeRun,
|
|
1204
1405
|
agent: runtimeAgent,
|
|
1205
1406
|
signal: attemptSignal,
|
|
@@ -1272,6 +1473,30 @@ export class WorkflowAgentExecutor {
|
|
|
1272
1473
|
throw new WorkflowError("AGENT_FAILED", "Agent execution failed");
|
|
1273
1474
|
}
|
|
1274
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
|
+
}
|
|
1275
1500
|
export class FairAgentScheduler {
|
|
1276
1501
|
runner;
|
|
1277
1502
|
sessionLimit;
|
|
@@ -1314,6 +1539,17 @@ export class FairAgentScheduler {
|
|
|
1314
1539
|
throw new WorkflowError("INTERNAL_ERROR", `Unknown scheduler run: ${runId}`);
|
|
1315
1540
|
run.extensionSettings = settings === undefined ? undefined : deepFreeze(structuredClone(settings));
|
|
1316
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
|
+
}
|
|
1317
1553
|
setExtensionSettings(agentId, settings) {
|
|
1318
1554
|
const node = this.#node(agentId);
|
|
1319
1555
|
node.options = Object.freeze({ ...node.options, extensionSettings: deepFreeze(structuredClone(settings)) });
|
|
@@ -1325,7 +1561,7 @@ export class FairAgentScheduler {
|
|
|
1325
1561
|
throw new WorkflowError("INTERNAL_ERROR", `Unknown scheduler run: ${runId}`);
|
|
1326
1562
|
const parent = parentId ? this.#nodes.get(parentId) : undefined;
|
|
1327
1563
|
if (parentId && (!parent || parent.runId !== runId))
|
|
1328
|
-
throw new WorkflowError("
|
|
1564
|
+
throw new WorkflowError("UNKNOWN_AGENT", "Parent agent is not owned by this run");
|
|
1329
1565
|
const effective = this.#inherit(run, parent, options);
|
|
1330
1566
|
const id = `${runId}:${String(++this.#nextId)}`;
|
|
1331
1567
|
const tuiIndex = ++run.nextIndex;
|
|
@@ -1342,12 +1578,11 @@ export class FairAgentScheduler {
|
|
|
1342
1578
|
node.state = "running";
|
|
1343
1579
|
this.#persist(runId);
|
|
1344
1580
|
try {
|
|
1345
|
-
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; } });
|
|
1346
1582
|
this.#settle(node, { id, ok: true, value });
|
|
1347
1583
|
}
|
|
1348
1584
|
catch (error) {
|
|
1349
|
-
|
|
1350
|
-
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) } });
|
|
1351
1586
|
}
|
|
1352
1587
|
};
|
|
1353
1588
|
this.#nodes.set(id, node);
|
|
@@ -1360,7 +1595,7 @@ export class FairAgentScheduler {
|
|
|
1360
1595
|
const parent = this.#node(parentId);
|
|
1361
1596
|
const child = this.#node(childId);
|
|
1362
1597
|
if (child.parentId !== parentId)
|
|
1363
|
-
throw new WorkflowError("
|
|
1598
|
+
throw new WorkflowError("UNKNOWN_AGENT", "Results are scoped to direct children");
|
|
1364
1599
|
if (child.collected || !child.promise)
|
|
1365
1600
|
throw new WorkflowError("AGENT_RESULT_COLLECTED", "Child result has already been collected; nested results are one-shot");
|
|
1366
1601
|
if (child.collecting)
|
|
@@ -1402,7 +1637,7 @@ export class FairAgentScheduler {
|
|
|
1402
1637
|
async steer(parentId, childId, message) {
|
|
1403
1638
|
const child = this.#node(childId);
|
|
1404
1639
|
if (child.parentId !== parentId)
|
|
1405
|
-
throw new WorkflowError("
|
|
1640
|
+
throw new WorkflowError("UNKNOWN_AGENT", "Steering is scoped to direct children");
|
|
1406
1641
|
if (child.state !== "running" && child.state !== "waiting_for_child")
|
|
1407
1642
|
throw new WorkflowError("AGENT_FAILED", "Child is not running");
|
|
1408
1643
|
if (!child.steer)
|
|
@@ -1472,13 +1707,13 @@ export class FairAgentScheduler {
|
|
|
1472
1707
|
}
|
|
1473
1708
|
this.#dispatch();
|
|
1474
1709
|
}
|
|
1475
|
-
toolsFor(parentId
|
|
1710
|
+
toolsFor(parentId) {
|
|
1476
1711
|
const parent = this.#node(parentId);
|
|
1477
|
-
if (!parent.options.tools.includes("agent"))
|
|
1712
|
+
if (!(parent.attemptCapabilities?.tools ?? parent.options.tools).includes("agent"))
|
|
1478
1713
|
return [];
|
|
1479
1714
|
const agentTool = defineTool({
|
|
1480
|
-
name: "agent", label: "Child Agent", description: "Start a direct child agent",
|
|
1481
|
-
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 }),
|
|
1482
1717
|
outputSchema: Type.Object({ id: Type.String() }),
|
|
1483
1718
|
execute: async (_id, params) => {
|
|
1484
1719
|
if (!isChildAgentToolParams(params))
|
|
@@ -1487,10 +1722,10 @@ export class FairAgentScheduler {
|
|
|
1487
1722
|
const outputSchema = params.outputSchema;
|
|
1488
1723
|
if (outputSchema !== undefined)
|
|
1489
1724
|
validateSchema(outputSchema, "agent outputSchema");
|
|
1490
|
-
const tools =
|
|
1725
|
+
const tools = parent.attemptCapabilities?.tools ?? parent.options.tools;
|
|
1491
1726
|
const agentOptions = { ...params };
|
|
1492
1727
|
Reflect.deleteProperty(agentOptions, "prompt");
|
|
1493
|
-
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 }) };
|
|
1494
1729
|
const child = this.spawn(parent.runId, params.prompt, options, parentId);
|
|
1495
1730
|
return { content: [{ type: "text", text: JSON.stringify({ id: child.id }) }], details: { id: child.id }, structuredContent: { id: child.id } };
|
|
1496
1731
|
},
|
|
@@ -1561,11 +1796,15 @@ export class FairAgentScheduler {
|
|
|
1561
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]) }) } : {}) });
|
|
1562
1797
|
if (options.cwd !== parent.options.cwd)
|
|
1563
1798
|
throw new WorkflowError("UNKNOWN_TOOL", "Child cwd cannot differ from its parent");
|
|
1564
|
-
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));
|
|
1565
1801
|
if (forbidden)
|
|
1566
1802
|
throw new WorkflowError("UNKNOWN_TOOL", `Child tool escalates parent boundary: ${forbidden}`);
|
|
1567
|
-
const identity = options.agentIdentity ?? parent.options.agentIdentity;
|
|
1568
|
-
|
|
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 } : {}) });
|
|
1569
1808
|
}
|
|
1570
1809
|
#enqueue(runId, node, start) { this.#runs.get(runId)?.queue.push({ ...(node ? { node } : {}), start }); this.#dispatch(); }
|
|
1571
1810
|
#dispatch() {
|
|
@@ -1590,8 +1829,7 @@ export class FairAgentScheduler {
|
|
|
1590
1829
|
run.beforeLaunch?.();
|
|
1591
1830
|
}
|
|
1592
1831
|
catch (error) {
|
|
1593
|
-
|
|
1594
|
-
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) } });
|
|
1595
1833
|
continue;
|
|
1596
1834
|
}
|
|
1597
1835
|
}
|
|
@@ -1643,7 +1881,7 @@ export class FairAgentScheduler {
|
|
|
1643
1881
|
#node(id) {
|
|
1644
1882
|
const node = this.#nodes.get(id);
|
|
1645
1883
|
if (!node)
|
|
1646
|
-
throw new WorkflowError("
|
|
1884
|
+
throw new WorkflowError("UNKNOWN_AGENT", `Unknown owned agent: ${id}`);
|
|
1647
1885
|
return node;
|
|
1648
1886
|
}
|
|
1649
1887
|
#persist(runId) {
|