@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
package/src/agent-execution.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
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, type InlineExtension } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { createAgentSession, createCodemodeExtension, createMcpExtension, createToolSearchExtension, DefaultPackageManager, DefaultResourceLoader, defineTool, getAgentDir, ModelRuntime, SessionManager, SettingsManager, loadSkills, type InlineExtension } from "@earendil-works/pi-coding-agent";
|
|
9
10
|
import type { ExtensionContext, ModelRegistry, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
10
11
|
type HerdrModelContext = { readonly model: ExtensionContext["model"]; readonly modelRegistry: ModelRegistry | undefined };
|
|
11
12
|
type AgentMessage = { role: string; content?: unknown; stopReason?: string; errorMessage?: string; usage?: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: { total: number } } };
|
|
@@ -45,30 +46,30 @@ export interface PiResourceInspection {
|
|
|
45
46
|
readonly diagnostics: readonly { type: "warning" | "error" | "collision"; message: string; source?: string }[];
|
|
46
47
|
readonly systemPromptSource?: string;
|
|
47
48
|
}
|
|
48
|
-
import type { AgentAccounting, AgentActivity, AgentContinuity, AgentIdentity, AgentResourceInspection, AgentResourcePolicy, AgentResourceSelectors, AgentResourceSelectorSources, AgentSetup, AgentSetupSummary, AgentTransport, AgentTransportContext, ContextFileScope, JsonSchema, JsonValue, LiveSessionHandoff, ModelSpec, PiRuntimeLaunchInfo, PreparedAgentSession, RegisteredAgentSetupHook, SessionInput, WorkflowAgentMessage, WorkflowAgentSession, WorkflowAgentSessionEvent, WorkflowAgentSessionReference, WorkflowAgentSessionState, WorkflowAgentSessionStats, WorkflowAgentTurnResult, WorkflowExtensionSettings, WorkflowExtensionSettingsValidatorContext, WorkflowRunContext, WorkflowSessionStartEvent } from "./types.js";
|
|
49
|
-
import { SerialLane, assertModelThinking, byPriorityThenName, deepFreeze, errorText, jsonObject, jsonValue,
|
|
50
|
-
import { SETTLED_AGENT_STATES, WorkflowError, isContextFileScope, zeroAccounting, type
|
|
49
|
+
import type { AgentPreparation, AgentPreparationContext, PreparedAgentConfiguration, RegisteredAgentPreparationHook, AgentAccounting, AgentActivity, AgentContinuity, AgentIdentity, AgentResourceInspection, AgentResourcePolicy, AgentResourceSelectors, AgentResourceSelectorSources, AgentSetup, AgentSetupSummary, AgentTransport, AgentTransportContext, ContextFileScope, JsonSchema, JsonValue, LiveSessionHandoff, ModelSpec, PiRuntimeLaunchInfo, PreparedAgentSession, RegisteredAgentSetupHook, SessionInput, WorkflowAgentMessage, WorkflowAgentSession, WorkflowAgentSessionEvent, WorkflowAgentSessionReference, WorkflowAgentSessionState, WorkflowAgentSessionStats, WorkflowAgentTurnResult, WorkflowExtensionSettings, WorkflowExtensionSettingsValidatorContext, WorkflowRunContext, WorkflowSessionStartEvent } from "./types.js";
|
|
50
|
+
import { SerialLane, assertModelThinking, byPriorityThenName, deepFreeze, errorCode, errorText, jsonObject, jsonValue, object, physicalModel, resolveModelReference, resourcePatternHasMagic, selectResourcesByLayers, modelCapability, unmatchedResourcePatterns } from "./utils.js";
|
|
51
|
+
import { SETTLED_AGENT_STATES, WorkflowError, isContextFileScope, zeroAccounting, type AgentState, type WorkflowErrorCode } from "./types.js";
|
|
51
52
|
import { createLiveSessionHandoff } from "./session-handoff.js";
|
|
52
53
|
import { createToolTimingExtension } from "./tool-timing.js";
|
|
53
54
|
import { isEmptyAbortedAssistant, normalizePiMessage, normalizePiSessionEvent, runtimeProgressToAgentProgress } from "./pi-runtime-adapter.js";
|
|
54
55
|
import { createPiRuntimeAgentRunner, isRuntimeAgentProviderError, normalizePiRuntimeError } from "./pi-runtime-runner.js";
|
|
55
56
|
import type { RuntimeAgentProgress, RuntimeUsage } from "./runtime/agent-runner.js";
|
|
56
57
|
import { defaultWorkflowResultSchema } from "./runtime/workflow-result.js";
|
|
57
|
-
import { validateAgentOptions, validateSchema } from "./validation.js";
|
|
58
|
-
import { canonicalPath, extensionIdentity } from "./paths.js";
|
|
58
|
+
import { isCoreAgentOption, validateAgentOptions, validateSchema } from "./validation.js";
|
|
59
|
+
import { canonicalPath, extensionIdentity, piResourceContains, piResourcePath, sameFilesystemPath } from "./paths.js";
|
|
59
60
|
import { isCorePackageName } from "./package-identity.js";
|
|
60
|
-
import { canonicalExtensionSelector,
|
|
61
|
+
import { canonicalExtensionSelector, validateContextFileScopes, validateSelectorList, validateWorkflowExtensionSettings } from "./settings.js";
|
|
62
|
+
import { agentIdentityPath } from "./execution.js";
|
|
61
63
|
import type { RunStore } from "./persistence.js";
|
|
62
|
-
type AgentExecutionRunStore = Pick<RunStore, "recordSystemPrompt" | "validateWorktree" | "worktree" | "snapshotWorktree"
|
|
64
|
+
type AgentExecutionRunStore = Pick<RunStore, "recordSystemPrompt" | "validateWorktree" | "worktree" | "snapshotWorktree"> & Partial<Pick<RunStore, "load" | "saveAgentConfiguration">>;
|
|
63
65
|
const localToolTimingExtension = createToolTimingExtension();
|
|
64
|
-
export type { AgentAccounting, AgentActivity, AgentInspectionMode, AgentSetup, AgentSetupContext, AgentSetupHook, AgentTransport, AgentTransportContext, PiRuntimeLaunchInfo, PreparedAgentSession, RegisteredAgentSetupHook, SessionInput, WorkflowAgentMessage, WorkflowAgentSession, WorkflowAgentSessionEvent, WorkflowAgentSessionReference, WorkflowAgentSessionState, WorkflowAgentSessionStats, WorkflowAgentTurnResult } from "./types.js";
|
|
66
|
+
export type { AgentPreparation, AgentPreparationContext, PreparedAgentConfiguration, RegisteredAgentPreparationHook, AgentAccounting, AgentActivity, AgentInspectionMode, AgentSetup, AgentSetupContext, AgentSetupHook, AgentTransport, AgentTransportContext, PiRuntimeLaunchInfo, PreparedAgentSession, RegisteredAgentSetupHook, SessionInput, WorkflowAgentMessage, WorkflowAgentSession, WorkflowAgentSessionEvent, WorkflowAgentSessionReference, WorkflowAgentSessionState, WorkflowAgentSessionStats, WorkflowAgentTurnResult } from "./types.js";
|
|
65
67
|
export interface AgentBudgetHooks {
|
|
66
68
|
beforeAttempt(): void;
|
|
67
69
|
beforeTurn(): void;
|
|
68
70
|
afterTurn(accounting: AgentAccounting, final: boolean): void;
|
|
69
71
|
instruction(): string | undefined;
|
|
70
72
|
}
|
|
71
|
-
export type { AgentDefinition } from "./types.js";
|
|
72
73
|
export interface AgentProviderFailure { label: string; provider: string; model: string; error: string }
|
|
73
74
|
export type AgentProviderRecovery = "retry" | "abort" | { model: string };
|
|
74
75
|
export interface AgentExecutionOptions {
|
|
@@ -87,7 +88,6 @@ export interface AgentExecutionOptions {
|
|
|
87
88
|
skills?: readonly string[];
|
|
88
89
|
extensions?: readonly string[];
|
|
89
90
|
effectiveTools?: readonly string[];
|
|
90
|
-
role?: string;
|
|
91
91
|
contextFiles?: readonly ContextFileScope[];
|
|
92
92
|
schema?: JsonSchema;
|
|
93
93
|
retries?: number;
|
|
@@ -101,31 +101,46 @@ export interface AgentExecutionOptions {
|
|
|
101
101
|
agentIdentity?: AgentIdentity;
|
|
102
102
|
inheritedExtensionSettings?: Readonly<WorkflowExtensionSettings>;
|
|
103
103
|
agentNodeId?: string;
|
|
104
|
+
capabilities?: Readonly<{ tools: readonly string[]; skills: readonly string[]; extensions: readonly string[] }>;
|
|
105
|
+
parentSkills?: readonly string[];
|
|
106
|
+
/** Tool names that ancestors excluded; with this agent's own `excludeTools`, they are removed from root and custom tools alike. */
|
|
107
|
+
inheritedExcludeTools?: readonly string[];
|
|
108
|
+
projectTrusted?: boolean;
|
|
109
|
+
configuration?: PreparedAgentConfiguration;
|
|
110
|
+
onConfiguration?: (configuration: PreparedAgentConfiguration) => void | Promise<void>;
|
|
104
111
|
}
|
|
105
112
|
export interface AgentExecutionRoot {
|
|
113
|
+
projectTrusted?: boolean;
|
|
106
114
|
cwd: string;
|
|
107
115
|
model: ModelSpec;
|
|
108
116
|
tools: ReadonlySet<string>;
|
|
109
117
|
extensionSettings?: Readonly<WorkflowExtensionSettings> | undefined;
|
|
110
118
|
validateExtensionSettings?: (settings: Readonly<WorkflowExtensionSettings> | undefined, context: WorkflowExtensionSettingsValidatorContext) => void;
|
|
111
119
|
onAgentSettings?: (agentNodeId: string, settings: Readonly<WorkflowExtensionSettings>) => void;
|
|
120
|
+
onAgentCapabilities?: (agentNodeId: string, capabilities: AgentPreparationContext["capabilities"], projectTrusted: boolean) => void;
|
|
112
121
|
resourceSelectors?: AgentResourceSelectors;
|
|
113
|
-
agentDefinitions?: Readonly<Record<string, AgentDefinition>>;
|
|
114
122
|
agentDir?: string;
|
|
115
123
|
additionalSkillPaths?: readonly string[];
|
|
116
124
|
availableModels?: ReadonlySet<string>;
|
|
117
125
|
knownModels?: ReadonlySet<string>;
|
|
118
126
|
modelAliases?: Readonly<Record<string, string>>;
|
|
127
|
+
dynamicModelAliasNames?: readonly string[];
|
|
119
128
|
blockedAliases?: ReadonlySet<string>;
|
|
120
129
|
blockedAliasTargets?: Readonly<Record<string, string>>;
|
|
121
130
|
settingsPath?: string;
|
|
122
131
|
extensionSettingsPath?: string;
|
|
123
132
|
runStore?: AgentExecutionRunStore;
|
|
124
133
|
providerPause?: () => Promise<void>;
|
|
134
|
+
agentPreparationHooks?: readonly RegisteredAgentPreparationHook[];
|
|
125
135
|
agentSetupHooks?: readonly RegisteredAgentSetupHook[];
|
|
126
136
|
agentResourcePolicy?: () => AgentResourcePolicy | Promise<AgentResourcePolicy>;
|
|
127
137
|
runContext?: Readonly<WorkflowRunContext>;
|
|
128
138
|
onResourceWarning?: (message: string) => void;
|
|
139
|
+
/**
|
|
140
|
+
* Load failures this host tolerated and the identities of the extensions it did load. With failures, a non-core option
|
|
141
|
+
* counts as owned only by the schema of a preparation hook whose `source` is a loaded extension; others fail closed.
|
|
142
|
+
*/
|
|
143
|
+
extensionLoad?: Readonly<{ errors: readonly string[]; loaded: readonly string[] }>;
|
|
129
144
|
}
|
|
130
145
|
export interface AgentToolCallProgress { id: string; name: string; state: "running" | "completed" | "failed" }
|
|
131
146
|
export interface AgentProgress { accounting: AgentAccounting; toolCalls: readonly AgentToolCallProgress[]; state?: WorkflowAgentSessionState; activity?: AgentActivity; lastEventAt?: number; persist: boolean }
|
|
@@ -138,6 +153,46 @@ function parseModel(value: string | undefined, fallback: ModelSpec, aliases: Rea
|
|
|
138
153
|
return resolveModelReference(value, aliases, knownModels, settingsPath);
|
|
139
154
|
}
|
|
140
155
|
|
|
156
|
+
async function authorizedResources(root: AgentExecutionRoot, cwd: string, options: AgentExecutionOptions): Promise<{ tools: string[]; skills: string[]; extensions: string[] }> {
|
|
157
|
+
const agentDir = root.agentDir ?? getAgentDir();
|
|
158
|
+
const trusted = root.projectTrusted ?? false;
|
|
159
|
+
const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: false });
|
|
160
|
+
settingsManager.setProjectTrusted(trusted);
|
|
161
|
+
const resolved = await new DefaultPackageManager({ cwd, agentDir, settingsManager, builtinExtensions: ["codemode", "tool-search", "mcp"] }).resolve();
|
|
162
|
+
const extensions = resolved.extensions.filter(({ enabled, metadata }) => enabled && (trusted || metadata.scope !== "project")).map(({ path }) => extensionIdentity(path));
|
|
163
|
+
const skillPaths = resolved.skills.filter(({ enabled, metadata }) => enabled && (trusted || metadata.scope !== "project")).map(({ path }) => path);
|
|
164
|
+
const skills = loadSkills({ cwd, agentDir, skillPaths: [...skillPaths, ...(root.additionalSkillPaths ?? [])], includeDefaults: false }).skills.map(({ name }) => name);
|
|
165
|
+
const parent = options.capabilities;
|
|
166
|
+
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)) };
|
|
167
|
+
}
|
|
168
|
+
function validatePreparedConfiguration(root: AgentExecutionRoot, configuration: PreparedAgentConfiguration, capabilities: { tools: readonly string[]; skills: readonly string[]; extensions: readonly string[] }, cwd: string, resumed: boolean): PreparedAgentConfiguration {
|
|
169
|
+
const code = resumed ? "RESUME_INCOMPATIBLE" : "INVALID_METADATA";
|
|
170
|
+
if (!jsonObject(configuration) || typeof configuration.projectTrusted !== "boolean" || typeof configuration.systemPromptAppend !== "string" || configuration.systemPrompt !== undefined && typeof configuration.systemPrompt !== "string") throw new WorkflowError(code, "Invalid prepared agent configuration");
|
|
171
|
+
if (configuration.projectTrusted && !root.projectTrusted) throw new WorkflowError("RESUME_INCOMPATIBLE", "Prepared agent configuration lost project trust");
|
|
172
|
+
if (!object(configuration.model) || typeof configuration.model.provider !== "string" || typeof configuration.model.model !== "string") throw new WorkflowError(code, "Invalid prepared model");
|
|
173
|
+
const model = resolveModelReference(`${configuration.model.provider}/${configuration.model.model}${configuration.model.thinking === undefined ? "" : `:${configuration.model.thinking}`}`);
|
|
174
|
+
if (root.availableModels !== undefined && !root.availableModels.has(modelCapability(model))) throw new WorkflowError(resumed ? "RESUME_INCOMPATIBLE" : "UNKNOWN_MODEL", `Required model is unavailable: ${modelCapability(model)}`);
|
|
175
|
+
const tools = validateSelectorList(configuration.tools, "configuration", "tools", "INVALID_METADATA", false);
|
|
176
|
+
if (tools === undefined) throw new WorkflowError(code, "Prepared tools are missing");
|
|
177
|
+
const missing = tools.find((tool) => !capabilities.tools.includes(tool));
|
|
178
|
+
if (missing) throw new WorkflowError(resumed ? "RESUME_INCOMPATIBLE" : "UNKNOWN_TOOL", `Required tool is unavailable: ${missing}`);
|
|
179
|
+
const excludeTools = configuration.excludeTools;
|
|
180
|
+
try { validateAgentOptions({ excludeTools }); } catch { throw new WorkflowError(code, "Invalid prepared excludeTools"); }
|
|
181
|
+
if (!Array.isArray(excludeTools) || tools.some((tool) => excludeTools.includes(tool))) throw new WorkflowError(code, "Prepared excludeTools contradict the prepared tools");
|
|
182
|
+
validateSelectorList(configuration.skills, "configuration", "skills", "INVALID_METADATA", false);
|
|
183
|
+
validateSelectorList(configuration.extensions, "configuration", "extensions", "INVALID_METADATA", false);
|
|
184
|
+
if (resumed) for (const key of ["skills", "extensions"] as const) {
|
|
185
|
+
const selectors = configuration[key];
|
|
186
|
+
const selected = selectors.slice(selectors.lastIndexOf("!*") + 1);
|
|
187
|
+
const missing = selected.find((name) => !capabilities[key].includes(name));
|
|
188
|
+
if (missing) throw new WorkflowError("RESUME_INCOMPATIBLE", `Required ${key} capability is unavailable: ${missing}`);
|
|
189
|
+
}
|
|
190
|
+
validateContextFileScopes(configuration.contextFiles, "configuration");
|
|
191
|
+
validateWorkflowExtensionSettings(configuration.settings, "configuration", "INVALID_METADATA");
|
|
192
|
+
root.validateExtensionSettings?.(configuration.settings, { source: "effective", cwd, projectTrusted: root.projectTrusted ?? false });
|
|
193
|
+
return deepFreeze(structuredClone(configuration));
|
|
194
|
+
}
|
|
195
|
+
|
|
141
196
|
function accounting(stats: WorkflowAgentSessionStats): AgentAccounting {
|
|
142
197
|
return { input: stats.tokens.input, output: stats.tokens.output, cacheRead: stats.tokens.cacheRead, cacheWrite: stats.tokens.cacheWrite, cost: stats.cost };
|
|
143
198
|
}
|
|
@@ -311,6 +366,40 @@ function builtinExtensions(): InlineExtension[] {
|
|
|
311
366
|
}
|
|
312
367
|
const BUILTIN_EXTENSION_PREFIX = "builtin:";
|
|
313
368
|
function isLlamaModel(model: ModelSpec): boolean { return model.provider === "llama.cpp"; }
|
|
369
|
+
/**
|
|
370
|
+
* Pi only extends resources with paths that loaded extensions return from resources_discover. Recording them, normalized as
|
|
371
|
+
* Pi does, lets selection tell those skills from the static inventory. Like Pi's own source attribution, the record belongs
|
|
372
|
+
* to one resource generation: reload() starts a new one, so withdrawn contributions stop counting as discovered.
|
|
373
|
+
*/
|
|
374
|
+
class WorkflowResourceLoader extends DefaultResourceLoader {
|
|
375
|
+
readonly extensionSkillPaths: string[] = [];
|
|
376
|
+
constructor(private readonly loaderCwd: string, options: ConstructorParameters<typeof DefaultResourceLoader>[0]) { super(options); }
|
|
377
|
+
override async reload(...args: Parameters<DefaultResourceLoader["reload"]>): Promise<void> {
|
|
378
|
+
this.extensionSkillPaths.length = 0;
|
|
379
|
+
await super.reload(...args);
|
|
380
|
+
}
|
|
381
|
+
override extendResources(paths: Parameters<DefaultResourceLoader["extendResources"]>[0]): void {
|
|
382
|
+
this.extensionSkillPaths.push(...(paths.skillPaths ?? []).map(({ path }) => piResourcePath(path, this.loaderCwd)));
|
|
383
|
+
super.extendResources(paths);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Prepared selectors end with a frozen segment: `!*` followed by the static inventory. Extension-discovered skills keep every
|
|
388
|
+
* other selector, including negations setup hooks inserted anywhere (a terminal `!*` too), but not that frozen restriction.
|
|
389
|
+
* Setup can only insert negations, so the inventory names are matched from the right and the boundary is the nearest `!*`
|
|
390
|
+
* before them; any `!*` with only negations up to the inventory selects the same skills.
|
|
391
|
+
*/
|
|
392
|
+
function discoveredSkillSelectors(selectors: readonly string[] | undefined, inventory: readonly string[] | undefined): readonly string[] | undefined {
|
|
393
|
+
if (selectors === undefined || inventory === undefined) return selectors;
|
|
394
|
+
const frozen = new Set<number>();
|
|
395
|
+
let index = selectors.length;
|
|
396
|
+
for (const name of [...inventory].reverse().concat("!*")) {
|
|
397
|
+
do index -= 1; while (index >= 0 && selectors[index] !== name);
|
|
398
|
+
if (index < 0) return selectors;
|
|
399
|
+
frozen.add(index);
|
|
400
|
+
}
|
|
401
|
+
return selectors.filter((_selector, position) => !frozen.has(position));
|
|
402
|
+
}
|
|
314
403
|
|
|
315
404
|
async function createLocalPiSessionHandle(input: SessionInput, sessionStartEvent?: WorkflowSessionStartEvent): Promise<LocalPiSessionHandle> {
|
|
316
405
|
const agentDir = input.agentDir ?? getAgentDir();
|
|
@@ -335,25 +424,23 @@ async function createLocalPiSessionHandle(input: SessionInput, sessionStartEvent
|
|
|
335
424
|
const resolved = await packageManager.resolve();
|
|
336
425
|
const discoveredExtensions = [...new Set(resolved.extensions.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => extensionIdentity(path)))];
|
|
337
426
|
const selectorSources = policy.selectorSources;
|
|
338
|
-
const
|
|
339
|
-
cwd: input.cwd,
|
|
340
|
-
selectorSources,
|
|
341
|
-
resources: { extensions: discoveredExtensions },
|
|
342
|
-
});
|
|
343
|
-
const selectedExtensions = selection.selectedExtensions ?? [];
|
|
427
|
+
const selectedExtensions = selectResourcesByLayers(resourceSelectorLayers(selectorSources, "extensions"), discoveredExtensions).filter((path) => policy.capabilities === undefined || policy.capabilities.extensions.includes(path));
|
|
344
428
|
const extensionPaths = selectedExtensions.filter((path) => path.startsWith(BUILTIN_EXTENSION_PREFIX) || !isWorkflowHostEntry(path));
|
|
345
429
|
policy.selectedExtensions = selectedExtensions;
|
|
346
|
-
policy.unmatchedExtensions =
|
|
430
|
+
policy.unmatchedExtensions = unmatchedResourcePatterns(policy.effective.extensions, discoveredExtensions);
|
|
347
431
|
const skillPaths = [...new Set(resolved.skills.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => path))];
|
|
348
|
-
const updateSkillMatches = (skills: readonly { name: string }[]): Set<string> => {
|
|
432
|
+
const updateSkillMatches = (skills: readonly { name: string; filePath: string }[], extensionSkillPaths: readonly string[]): Set<string> => {
|
|
349
433
|
const names = [...new Set(skills.map(({ name }) => name))];
|
|
350
|
-
const
|
|
351
|
-
const
|
|
434
|
+
const discovered = new Set(skills.filter(({ filePath }) => extensionSkillPaths.some((path) => piResourceContains(path, filePath))).map(({ name }) => name));
|
|
435
|
+
const [global, project, call] = resourceSelectorLayers(selectorSources, "skills");
|
|
436
|
+
const selectedStatic = selectResourcesByLayers([global, project, call], names.filter((name) => !discovered.has(name))).filter((name) => policy.capabilities === undefined || policy.capabilities.skills.includes(name));
|
|
437
|
+
const selectedDiscovered = selectResourcesByLayers([discoveredSkillSelectors(global, policy.capabilities?.skills), project, call], [...discovered]).filter((name) => policy.parentSkills === undefined || policy.parentSkills.includes(name));
|
|
438
|
+
const selectedSkills = [...selectedStatic, ...selectedDiscovered];
|
|
352
439
|
policy.selectedSkills = selectedSkills;
|
|
353
|
-
policy.unmatchedSkills =
|
|
440
|
+
policy.unmatchedSkills = unmatchedResourcePatterns(policy.effective.skills, names);
|
|
354
441
|
return new Set(names.filter((name) => !selectedSkills.includes(name)));
|
|
355
442
|
};
|
|
356
|
-
|
|
443
|
+
const workflowLoader: WorkflowResourceLoader = new WorkflowResourceLoader(input.cwd, {
|
|
357
444
|
cwd: input.cwd,
|
|
358
445
|
agentDir,
|
|
359
446
|
settingsManager,
|
|
@@ -364,12 +451,13 @@ async function createLocalPiSessionHandle(input: SessionInput, sessionStartEvent
|
|
|
364
451
|
extensionFactories,
|
|
365
452
|
...(contextFilesOverride ? { agentsFilesOverride: contextFilesOverride } : {}),
|
|
366
453
|
skillsOverride: (base) => {
|
|
367
|
-
const disabledSkills = updateSkillMatches(base.skills);
|
|
454
|
+
const disabledSkills = updateSkillMatches(base.skills, workflowLoader.extensionSkillPaths);
|
|
368
455
|
return { ...base, skills: base.skills.filter(({ name }) => !disabledSkills.has(name)) };
|
|
369
456
|
},
|
|
370
457
|
...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}),
|
|
371
458
|
...systemPromptOptions,
|
|
372
459
|
});
|
|
460
|
+
resourceLoader = workflowLoader;
|
|
373
461
|
await resourceLoader.reload();
|
|
374
462
|
} else {
|
|
375
463
|
settingsManager = SettingsManager.create(input.cwd, agentDir, { projectTrusted: true });
|
|
@@ -391,7 +479,7 @@ async function createLocalPiSessionHandle(input: SessionInput, sessionStartEvent
|
|
|
391
479
|
throw new WorkflowError("UNKNOWN_MODEL", `Unknown model: ${input.model.provider}/${input.model.model}${because}`);
|
|
392
480
|
}
|
|
393
481
|
const effectiveSessionStartEvent = input.settings === undefined && sessionStartEvent === undefined ? undefined : { ...(sessionStartEvent ?? { type: "session_start" as const, reason: "startup" as const }), settings: input.settings ?? Object.freeze({}) };
|
|
394
|
-
const { session } = await createAgentSession({
|
|
482
|
+
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 });
|
|
395
483
|
const nativeDispose = session.dispose.bind(session);
|
|
396
484
|
let disposal: Promise<void> | undefined;
|
|
397
485
|
const shutdown = (reason: LocalSessionShutdownReason, targetSessionFile?: string): Promise<void> => {
|
|
@@ -671,6 +759,11 @@ export async function createLocalWorkflowAgentSession(prepared: Readonly<Prepare
|
|
|
671
759
|
return session;
|
|
672
760
|
}
|
|
673
761
|
export const localAgentTransport: AgentTransport = 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: Pick<AgentExecutionOptions, "agentOptions" | "inheritedExcludeTools">): Set<string> {
|
|
764
|
+
const own = options.agentOptions?.excludeTools;
|
|
765
|
+
return new Set([...(options.inheritedExcludeTools ?? []), ...(Array.isArray(own) ? own.filter((tool): tool is string => typeof tool === "string") : [])]);
|
|
766
|
+
}
|
|
674
767
|
function changedOption(options: Readonly<Record<string, JsonValue>>, baseline: Readonly<Record<string, JsonValue>>, key: string): boolean { return JSON.stringify(options[key]) !== JSON.stringify(baseline[key]); }
|
|
675
768
|
interface ChildAgentToolParams {
|
|
676
769
|
prompt: string;
|
|
@@ -679,7 +772,6 @@ interface ChildAgentToolParams {
|
|
|
679
772
|
skills?: string[];
|
|
680
773
|
extensions?: string[];
|
|
681
774
|
model?: string;
|
|
682
|
-
role?: string;
|
|
683
775
|
contextFiles?: ContextFileScope[];
|
|
684
776
|
outputSchema?: unknown;
|
|
685
777
|
retries?: number;
|
|
@@ -692,7 +784,6 @@ function isChildAgentToolParams(value: unknown): value is ChildAgentToolParams &
|
|
|
692
784
|
if (value.skills !== undefined && (!Array.isArray(value.skills) || value.skills.some((skill) => typeof skill !== "string"))) return false;
|
|
693
785
|
if (value.extensions !== undefined && (!Array.isArray(value.extensions) || value.extensions.some((extension) => typeof extension !== "string"))) return false;
|
|
694
786
|
if (value.model !== undefined && typeof value.model !== "string") return false;
|
|
695
|
-
if (value.role !== undefined && (typeof value.role !== "string" || !value.role.trim())) return false;
|
|
696
787
|
if (value.contextFiles !== undefined && (!Array.isArray(value.contextFiles) || !value.contextFiles.every(isContextFileScope))) return false;
|
|
697
788
|
if (value.outputSchema !== undefined && !jsonObject(value.outputSchema)) return false;
|
|
698
789
|
if (value.retries !== undefined && (typeof value.retries !== "number" || !Number.isInteger(value.retries) || value.retries < 0)) return false;
|
|
@@ -715,7 +806,7 @@ function resourcePolicySummary(policy: AgentResourcePolicy, tools: readonly stri
|
|
|
715
806
|
const RESOURCE_SELECTOR_KEYS = ["skills", "extensions", "tools"] as const;
|
|
716
807
|
type ResourceSelectorKey = (typeof RESOURCE_SELECTOR_KEYS)[number];
|
|
717
808
|
function resourceSelectorLayers(sources: AgentResourceSelectorSources, key: ResourceSelectorKey): readonly (readonly string[] | undefined)[] {
|
|
718
|
-
return [sources.global[key], sources.project[key], sources.
|
|
809
|
+
return [sources.global[key], sources.project[key], sources.call?.[key]];
|
|
719
810
|
}
|
|
720
811
|
function selectorListWidened(ceiling: readonly string[], candidate: readonly string[]): boolean {
|
|
721
812
|
let candidateIndex = 0;
|
|
@@ -772,7 +863,7 @@ function appendResourcePolicyNarrowing(ceiling: AgentResourcePolicy | undefined,
|
|
|
772
863
|
};
|
|
773
864
|
}
|
|
774
865
|
function packageRoot(start: string): string | undefined {
|
|
775
|
-
let current = dirname(
|
|
866
|
+
let current = dirname(canonicalPath(start));
|
|
776
867
|
for (;;) {
|
|
777
868
|
const candidates = [current, join(current, "..", "@earendil-works", "pi-coding-agent"), join(current, "..", "pi-coding-agent")];
|
|
778
869
|
for (const candidate of candidates) {
|
|
@@ -819,10 +910,9 @@ async function resolvePiRuntime(): Promise<PiRuntimeResolution> {
|
|
|
819
910
|
}
|
|
820
911
|
}
|
|
821
912
|
const piRuntimeResolution = resolvePiRuntime();
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
return `${basePrompt}\n\nTask:\n${task}${instruction ? `\n\n${instruction}` : ""}`;
|
|
913
|
+
/** 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. */
|
|
914
|
+
function attemptControlText(options: AgentExecutionOptions, attempt: number, previousError?: string): string {
|
|
915
|
+
return [attempt > 1 ? `Retry attempt ${String(attempt)}. Previous state: ${options.retryState ?? previousError ?? "failed attempt"}` : "", options.budget?.instruction() ?? ""].filter(Boolean).join("\n");
|
|
826
916
|
}
|
|
827
917
|
async function preparedAgentSession(input: SessionInput, initialPrompt?: string): Promise<Readonly<PreparedAgentSession>> {
|
|
828
918
|
const { runtime: piRuntime, error: piRuntimeError } = await piRuntimeResolution;
|
|
@@ -846,45 +936,26 @@ async function attemptSessionInput(source: string, attempt: number): Promise<str
|
|
|
846
936
|
await copyFile(source, target);
|
|
847
937
|
return target;
|
|
848
938
|
}
|
|
849
|
-
async function prepareAgentSetup(root: AgentExecutionRoot, transport: AgentTransport, task: string, options: AgentExecutionOptions, resolved: { model: ModelSpec; tools: readonly string[]; systemPrompt?: string; systemPromptAppend: string; contextFiles?: readonly ContextFileScope[] }, cwd: string, attempt: number, signal: AbortSignal | undefined, customTools: readonly ToolDefinition[], resultTool: ToolDefinition | undefined, inspection = false
|
|
939
|
+
async function prepareAgentSetup(root: AgentExecutionRoot, transport: AgentTransport, task: string, options: AgentExecutionOptions, resolved: { model: ModelSpec; tools: readonly string[]; systemPrompt?: string; systemPromptAppend: string; contextFiles?: readonly ContextFileScope[] }, cwd: string, attempt: number, signal: AbortSignal | undefined, customTools: readonly ToolDefinition[], resultTool: ToolDefinition | undefined, inspection = false): Promise<PreparedAgentSetup> {
|
|
850
940
|
const setupSignal = signal ?? root.runContext?.signal ?? new AbortController().signal;
|
|
851
941
|
const baselineOptions = structuredClone(options.agentOptions ?? {});
|
|
852
942
|
const baseResourcePolicy = await root.agentResourcePolicy?.();
|
|
853
|
-
const
|
|
854
|
-
const
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
if (Array.isArray(value) && value.every((entry) => typeof entry === "string")) return value;
|
|
863
|
-
return options.agentOptions === undefined ? fallback : undefined;
|
|
943
|
+
const configuration = options.configuration;
|
|
944
|
+
const resourcePolicy: AgentResourcePolicy = {
|
|
945
|
+
...(baseResourcePolicy ?? { globalSettingsPath: "", projectSettingsPath: "", projectTrusted: root.projectTrusted ?? false, global: { skills: [], extensions: [] }, project: { skills: [], extensions: [] }, unmatchedSkills: [], unmatchedExtensions: [] }),
|
|
946
|
+
projectTrusted: configuration?.projectTrusted ?? root.projectTrusted ?? baseResourcePolicy?.projectTrusted ?? false,
|
|
947
|
+
effective: { skills: configuration?.skills ?? [], extensions: configuration?.extensions ?? [], tools: resolved.tools },
|
|
948
|
+
selectorSources: { global: { skills: configuration?.skills ?? [], extensions: configuration?.extensions ?? [], tools: resolved.tools }, project: {} },
|
|
949
|
+
...(options.capabilities ? { capabilities: options.capabilities } : {}),
|
|
950
|
+
...(options.parentSkills ? { parentSkills: [...options.parentSkills] } : {}),
|
|
951
|
+
selectedTools: resolved.tools,
|
|
864
952
|
};
|
|
865
|
-
const
|
|
866
|
-
const rawCallExtensions = selectorValue("extensions", options.extensions)?.map((selector) => canonicalExtensionSelector(selector, cwd));
|
|
867
|
-
const rawCallTools = selectorValue("tools", options.tools);
|
|
868
|
-
const callSelectors: AgentResourceSelectors = { ...(rawCallSkills === undefined ? {} : { skills: rawCallSkills }), ...(rawCallExtensions === undefined ? {} : { extensions: rawCallExtensions }), ...(rawCallTools === undefined ? {} : { tools: rawCallTools }) };
|
|
869
|
-
const resourcePolicy: AgentResourcePolicy | undefined = baseResourcePolicy ? {
|
|
870
|
-
...baseResourcePolicy,
|
|
871
|
-
effective: {
|
|
872
|
-
skills: [...baseResourcePolicy.effective.skills, ...(roleSelectors?.skills ?? []), ...(rawCallSkills ?? [])],
|
|
873
|
-
extensions: [...baseResourcePolicy.effective.extensions, ...(roleSelectors?.extensions ?? []), ...(rawCallExtensions ?? [])],
|
|
874
|
-
...(baseResourcePolicy.effective.tools === undefined && roleSelectors?.tools === undefined && rawCallTools === undefined ? {} : { tools: [...(baseResourcePolicy.effective.tools ?? []), ...(roleSelectors?.tools ?? []), ...(rawCallTools ?? [])] }),
|
|
875
|
-
},
|
|
876
|
-
selectorSources: { global: baseResourcePolicy.selectorSources.global, project: baseResourcePolicy.selectorSources.project, ...(roleSelectors ? { role: roleSelectors } : {}), ...(Object.keys(callSelectors).length ? { call: callSelectors } : {}) },
|
|
877
|
-
} : undefined;
|
|
878
|
-
if (resourcePolicy) {
|
|
879
|
-
resourcePolicy.selectedTools = resolved.tools;
|
|
880
|
-
resourcePolicy.unmatchedTools = unmatchedResourcePatterns(resourceSelectorLayers(resourcePolicy.selectorSources, "tools").flatMap((layer) => layer ?? []), [...root.tools]);
|
|
881
|
-
}
|
|
882
|
-
const resourcePolicyCeiling = resourcePolicy ? structuredClone(resourcePolicy) : undefined;
|
|
953
|
+
const resourcePolicyCeiling = structuredClone(resourcePolicy);
|
|
883
954
|
const sessionPath = options.sessionPath === undefined || inspection ? options.sessionPath : await attemptSessionInput(options.sessionPath, attempt);
|
|
884
|
-
const extensionSettings =
|
|
885
|
-
root.validateExtensionSettings?.(extensionSettings, { source:
|
|
955
|
+
const extensionSettings = configuration?.settings ?? options.inheritedExtensionSettings ?? root.extensionSettings ?? Object.freeze({});
|
|
956
|
+
root.validateExtensionSettings?.(extensionSettings, { source: "effective", cwd, projectTrusted: resourcePolicy.projectTrusted, ...((root.extensionSettingsPath ?? root.settingsPath) ? { settingsPath: root.extensionSettingsPath ?? root.settingsPath } : {}) });
|
|
886
957
|
if (options.agentNodeId !== undefined) root.onAgentSettings?.(options.agentNodeId, extensionSettings);
|
|
887
|
-
const sessionInput: 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,
|
|
958
|
+
const sessionInput: 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) };
|
|
888
959
|
const setup = { prompt: task, options: sessionInput.options ?? {}, sessionInput, prepared: await preparedAgentSession(sessionInput, task), transport };
|
|
889
960
|
const base = fallbackSetupContext(root, options, setupSignal);
|
|
890
961
|
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" as const } : {}) });
|
|
@@ -902,25 +973,55 @@ async function prepareAgentSetup(root: AgentExecutionRoot, transport: AgentTrans
|
|
|
902
973
|
try {
|
|
903
974
|
if (resourcePolicyWidened(resourcePolicyCeiling, setup.sessionInput.resourcePolicy)) throw new WorkflowError("INVALID_METADATA", "Agent setup widened the prepared resource policy");
|
|
904
975
|
appendResourcePolicyNarrowing(resourcePolicyCeiling, setup.sessionInput.resourcePolicy);
|
|
976
|
+
if (setup.sessionInput.resourcePolicy) {
|
|
977
|
+
const policy = setup.sessionInput.resourcePolicy;
|
|
978
|
+
policy.effective.extensions = policy.effective.extensions.map((selector) => canonicalExtensionSelector(selector, cwd));
|
|
979
|
+
for (const layer of [policy.selectorSources.global, policy.selectorSources.project, policy.selectorSources.call]) if (layer?.extensions !== undefined) layer.extensions = layer.extensions.map((selector) => canonicalExtensionSelector(selector, cwd));
|
|
980
|
+
if (resourcePolicyCeiling.parentSkills === undefined) Reflect.deleteProperty(policy, "parentSkills");
|
|
981
|
+
else policy.parentSkills = [...resourcePolicyCeiling.parentSkills];
|
|
982
|
+
}
|
|
905
983
|
setup.sessionInput.options = setup.options;
|
|
906
984
|
if (changedOption(setup.options, baselineOptions, "model") && typeof setup.options.model === "string") setup.sessionInput.model = parseModel(setup.options.model, setup.sessionInput.model, root.modelAliases, root.knownModels ?? root.availableModels, root.settingsPath);
|
|
907
985
|
if (changedOption(setup.options, baselineOptions, "tools") && Array.isArray(setup.options.tools) && setup.options.tools.every((tool) => typeof tool === "string")) setup.sessionInput.tools = [...setup.options.tools];
|
|
908
|
-
|
|
986
|
+
// Pi derives trust, settings and .pi resources from cwd, so neither the session input nor a cwd option may move it.
|
|
987
|
+
if (changedOption(setup.options, baselineOptions, "cwd") || !sameFilesystemPath(cwd, setup.sessionInput.cwd)) throw new WorkflowError("INVALID_METADATA", "Agent setup cannot change cwd");
|
|
988
|
+
if (!root.availableModels?.has(modelCapability(setup.sessionInput.model)) && root.availableModels !== undefined) throw new WorkflowError("UNKNOWN_MODEL", "Agent setup selected an unavailable model");
|
|
989
|
+
// A custom tool must not reintroduce, by name, a root tool the prepared policy left out (excludeTools, parent ceilings, selectors).
|
|
990
|
+
const shadowed = (setup.sessionInput.customTools ?? []).map(({ name }) => name).find((name) => root.tools.has(name) && !resolved.tools.includes(name));
|
|
991
|
+
if (shadowed) throw new WorkflowError("UNKNOWN_TOOL", `Custom tool reintroduces a tool outside the prepared agent policy: ${shadowed}`);
|
|
992
|
+
// Like Pi's excludeTools, own and inherited exclusions also remove custom tools of those names, including ones setup hooks
|
|
993
|
+
// add, before materialization. A name that matches no root or custom tool excludes nothing and only warns. The names are
|
|
994
|
+
// the frozen configuration's, so re-evaluated options of a later turn, retry or resume cannot change them.
|
|
995
|
+
const excludedTools = new Set(configuration?.excludeTools ?? excludedToolNames(options));
|
|
996
|
+
const excludedCustomTools = (setup.sessionInput.customTools ?? []).filter(({ name }) => excludedTools.has(name)).map(({ name }) => name);
|
|
997
|
+
if (excludedCustomTools.length) {
|
|
998
|
+
setup.sessionInput.customTools = setup.sessionInput.customTools?.filter(({ name }) => !excludedTools.has(name)) ?? [];
|
|
999
|
+
// Hooks may also list their custom tool by name; root names stay subject to the policy checks below.
|
|
1000
|
+
setup.sessionInput.tools = setup.sessionInput.tools.filter((tool) => root.tools.has(tool) || !excludedCustomTools.includes(tool));
|
|
1001
|
+
}
|
|
1002
|
+
for (const tool of excludedTools) if (!options.inheritedExcludeTools?.includes(tool) && !root.tools.has(tool) && !excludedCustomTools.includes(tool)) root.onResourceWarning?.(`excludeTools names no root or custom tool of this agent: ${tool}`);
|
|
909
1003
|
const customToolNames = new Set([...(setup.sessionInput.customTools ?? []).map(({ name }) => name), ...(setup.sessionInput.resultTool ? [setup.sessionInput.resultTool.name] : [])]);
|
|
910
1004
|
const widened = setup.sessionInput.tools.find((tool) => !resolved.tools.includes(tool) && !customToolNames.has(tool));
|
|
911
1005
|
const outsideTool = widened ?? setup.sessionInput.tools.find((tool) => !root.tools.has(tool) && !customToolNames.has(tool));
|
|
912
1006
|
if (outsideTool) throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the prepared agent policy: ${outsideTool}`);
|
|
1007
|
+
if (setup.sessionInput.resourcePolicy) {
|
|
1008
|
+
const policy = setup.sessionInput.resourcePolicy;
|
|
1009
|
+
setup.sessionInput.tools = selectResourcesByLayers(resourceSelectorLayers(policy.selectorSources, "tools"), setup.sessionInput.tools);
|
|
1010
|
+
if (policy.projectTrusted !== resourcePolicyCeiling.projectTrusted) policy.capabilities = await authorizedResources({ ...root, projectTrusted: policy.projectTrusted }, cwd, options);
|
|
1011
|
+
}
|
|
1012
|
+
if (!setup.sessionInput.tools.includes("agent")) setup.sessionInput.customTools = setup.sessionInput.customTools?.filter(({ name }) => name !== "agent" && name !== "get_subagent_result" && name !== "steer_subagent") ?? [];
|
|
1013
|
+
|
|
913
1014
|
} catch (error) {
|
|
914
1015
|
setup.prepared = await preparedAgentSession(setup.sessionInput, task);
|
|
915
1016
|
return { setup, summary: agentSetupSummary(setup, hookNames), failure: { error } };
|
|
916
1017
|
}
|
|
917
|
-
setup.prepared = await preparedAgentSession(setup.sessionInput, inspection ? task :
|
|
1018
|
+
setup.prepared = await preparedAgentSession(setup.sessionInput, inspection ? task : setup.prompt);
|
|
918
1019
|
return { setup, summary: agentSetupSummary(setup, hookNames) };
|
|
919
1020
|
}
|
|
920
1021
|
export async function prepareAgentSetupForInspection(root: AgentExecutionRoot, task: string, options: AgentExecutionOptions, transport: AgentTransport): Promise<PreparedAgentSetup> {
|
|
921
1022
|
const executor = new WorkflowAgentExecutor(root);
|
|
922
|
-
const
|
|
923
|
-
return prepareAgentSetup(root, transport, task, options,
|
|
1023
|
+
const configuration = await executor.prepare(options, root.cwd, root.runContext?.signal, "inspection");
|
|
1024
|
+
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);
|
|
924
1025
|
}
|
|
925
1026
|
function attemptRecord(transport: string, attempt: number, session: WorkflowAgentSession, setup: AgentSetupSummary, stats: AgentAccounting, result?: JsonValue, error?: { code: string; message: string }): AgentAttempt {
|
|
926
1027
|
return { attempt, transport, session: session.reference, ...(result === undefined ? {} : { result }), ...(error ? { error } : {}), accounting: stats, setup };
|
|
@@ -937,52 +1038,89 @@ export function getAgentAttempts(error: unknown): readonly AgentAttempt[] | unde
|
|
|
937
1038
|
}
|
|
938
1039
|
export class WorkflowAgentExecutor {
|
|
939
1040
|
private readonly transport: AgentTransport;
|
|
940
|
-
private readonly warnedRoleTools = new Set<string>();
|
|
941
1041
|
constructor(private readonly root: AgentExecutionRoot, transport: AgentTransport = localAgentTransport) { this.transport = transport; }
|
|
942
1042
|
setRunContext(runContext: Readonly<WorkflowRunContext>): void { this.root.runContext = runContext; }
|
|
943
1043
|
|
|
944
1044
|
resolve(options: AgentExecutionOptions, inheritedTools?: readonly string[]): { model: ModelSpec; requestedModel?: string; tools: readonly string[]; systemPrompt?: string; systemPromptAppend: string; contextFiles?: readonly ContextFileScope[] } {
|
|
945
|
-
|
|
946
|
-
const
|
|
947
|
-
if (
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1045
|
+
if (options.configuration) return { ...options.configuration, ...(options.modelOverride ? { model: options.modelOverride } : {}) };
|
|
1046
|
+
const ceiling = inheritedTools ?? [...this.root.tools];
|
|
1047
|
+
for (const pattern of options.tools ?? []) if (!pattern.startsWith("!") && !resourcePatternHasMagic(pattern) && !ceiling.includes(pattern)) throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the prepared agent policy: ${pattern}`);
|
|
1048
|
+
const tools = options.effectiveTools ?? selectResourcesByLayers([this.root.resourceSelectors?.tools, options.tools], [...(inheritedTools ?? this.root.tools)]);
|
|
1049
|
+
const missing = tools.find((tool) => !ceiling.includes(tool));
|
|
1050
|
+
if (missing) throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the prepared agent policy: ${missing}`);
|
|
1051
|
+
const model = options.modelOverride ?? parseModel(options.model, this.root.model, this.root.modelAliases, this.root.knownModels, this.root.settingsPath);
|
|
1052
|
+
return { model: physicalModel(model, this.root.modelAliases, this.root.knownModels, this.root.settingsPath), tools, systemPromptAppend: "", ...(options.contextFiles === undefined ? {} : { contextFiles: options.contextFiles }) };
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
async prepare(options: AgentExecutionOptions, cwd = this.root.cwd, signal = this.root.runContext?.signal ?? new AbortController().signal, mode: "execution" | "inspection" = "execution"): Promise<PreparedAgentConfiguration> {
|
|
1056
|
+
const root = { ...this.root, projectTrusted: Boolean(this.root.projectTrusted) && options.projectTrusted !== false };
|
|
1057
|
+
const policy = await this.root.agentResourcePolicy?.();
|
|
1058
|
+
const capabilities = await authorizedResources(root, cwd, options);
|
|
1059
|
+
// Children are new spawns of their parent's attempt; their scheduler node keys the configuration their own retries reuse.
|
|
1060
|
+
const persistedKey = options.parent !== undefined ? options.agentNodeId : options.agentIdentity?.handle !== undefined ? `handle:${options.agentIdentity.handle}` : options.agentIdentity ? agentIdentityPath(options.agentIdentity) : options.agentNodeId;
|
|
1061
|
+
const stored = options.configuration ?? (mode === "execution" && persistedKey && this.root.runStore?.load ? (await this.root.runStore.load()).snapshot.agentConfigurations[persistedKey] : undefined);
|
|
1062
|
+
if (stored) {
|
|
1063
|
+
// Without the field, the configuration was prepared before exclusions were frozen. Re-evaluated options cannot prove
|
|
1064
|
+
// which exclusions it had, so it never resumes.
|
|
1065
|
+
if (stored.excludeTools === undefined) throw new WorkflowError("RESUME_INCOMPATIBLE", "Prepared agent configuration predates frozen excludeTools; relaunch the workflow");
|
|
1066
|
+
return validatePreparedConfiguration(root, stored, capabilities, cwd, true);
|
|
954
1067
|
}
|
|
955
|
-
const
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
...(
|
|
963
|
-
...(this.root.
|
|
964
|
-
...(
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1068
|
+
const original = deepFreeze(structuredClone(validateAgentOptions(options.agentOptions ?? {
|
|
1069
|
+
...(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] }),
|
|
1070
|
+
})));
|
|
1071
|
+
const dynamicModelAliasNames = this.root.dynamicModelAliasNames;
|
|
1072
|
+
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 ?? {}) });
|
|
1073
|
+
const context: AgentPreparationContext = 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 });
|
|
1074
|
+
const configuration: AgentPreparation = {
|
|
1075
|
+
...(typeof original.label === "string" ? { label: original.label } : {}), ...(typeof original.model === "string" ? { model: original.model } : {}),
|
|
1076
|
+
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))],
|
|
1077
|
+
...(options.contextFiles === undefined ? {} : { contextFiles: [...options.contextFiles] }), systemPromptAppend: "", settings: structuredClone(defaults.settings),
|
|
1078
|
+
};
|
|
1079
|
+
for (const key of ["tools", "skills", "extensions"] as const) if (Array.isArray(original[key])) {
|
|
1080
|
+
const selectors = validateSelectorList(original[key], "agent options", key, "INVALID_METADATA", false) ?? [];
|
|
1081
|
+
configuration[key] = [...(policy?.effective[key] ?? this.root.resourceSelectors?.[key] ?? []), ...selectors.map((selector) => key === "extensions" ? canonicalExtensionSelector(selector, cwd) : selector)];
|
|
1082
|
+
}
|
|
1083
|
+
if (original.contextFiles !== undefined) configuration.contextFiles = [...(validateContextFileScopes(original.contextFiles, "agent options") ?? [])];
|
|
1084
|
+
const hooks = [...(this.root.agentPreparationHooks ?? [])].sort(byPriorityThenName);
|
|
1085
|
+
const load = this.root.extensionLoad;
|
|
1086
|
+
if (load?.errors.length) {
|
|
1087
|
+
// A failed extension may have owned an option, and its factory may have registered hooks before it failed. Only a
|
|
1088
|
+
// schema whose registration names a loaded extension as `source` proves ownership.
|
|
1089
|
+
const loaded = new Set(load.loaded.map(extensionIdentity));
|
|
1090
|
+
const declared = new Set(hooks.flatMap(({ optionsSchema, source }) => source !== undefined && loaded.has(extensionIdentity(source)) && object(optionsSchema?.properties) ? Object.keys(optionsSchema.properties) : []));
|
|
1091
|
+
const unowned = Object.keys(original).find((key) => !isCoreAgentOption(key) && !declared.has(key));
|
|
1092
|
+
if (unowned !== undefined) throw new WorkflowError("INVALID_METADATA", `Agent option ${unowned} has no owner proven to have loaded while extensions failed to load: ${load.errors.join("; ")}`);
|
|
1093
|
+
}
|
|
1094
|
+
for (const hook of hooks) if (hook.optionsSchema !== undefined && !Value.Check(hook.optionsSchema, original)) throw new WorkflowError("INVALID_METADATA", `Agent options do not match preparation schema: ${hook.name}`);
|
|
1095
|
+
if (typeof original.model === "string" && this.root.blockedAliases?.has(original.model.split(":")[0] ?? "")) throw new WorkflowError("UNKNOWN_MODEL", `Unknown model alias ${original.model} resolved to ${this.root.blockedAliasTargets?.[original.model.split(":")[0] ?? ""] ?? "an unavailable target"}`);
|
|
1096
|
+
for (const hook of hooks) { if (signal.aborted) throw new WorkflowError("CANCELLED", "Agent preparation cancelled"); await hook.prepare(configuration, context); }
|
|
1097
|
+
if (signal.aborted) throw new WorkflowError("CANCELLED", "Agent preparation cancelled");
|
|
1098
|
+
const { model: preparedModel, ...preparedOptions } = configuration;
|
|
1099
|
+
validateAgentOptions(preparedOptions);
|
|
1100
|
+
if (preparedModel !== undefined && (typeof preparedModel !== "string" || !preparedModel.trim())) throw new WorkflowError("INVALID_METADATA", "Prepared agent model must be a non-empty string");
|
|
1101
|
+
if (preparedModel && this.root.blockedAliases?.has(preparedModel.split(":")[0] ?? "")) throw new WorkflowError("UNKNOWN_MODEL", `Unknown model alias ${preparedModel} resolved to ${this.root.blockedAliasTargets?.[preparedModel.split(":")[0] ?? ""] ?? "an unavailable target"}`);
|
|
1102
|
+
configuration.extensions = configuration.extensions.map((selector) => canonicalExtensionSelector(selector, cwd));
|
|
1103
|
+
// A hook that passes on an inherited virtual `workflow/<alias>` root model gets the same physical target as no hook.
|
|
1104
|
+
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);
|
|
1105
|
+
const excludedTools = excludedToolNames({ agentOptions: original, ...(options.inheritedExcludeTools ? { inheritedExcludeTools: options.inheritedExcludeTools } : {}) });
|
|
1106
|
+
const tools = selectResourcesByLayers([configuration.tools], capabilities.tools).filter((tool) => !excludedTools.has(tool));
|
|
1107
|
+
for (const pattern of validateSelectorList(original.tools, "agent options", "tools", "INVALID_METADATA", false) ?? []) if (!pattern.startsWith("!") && !resourcePatternHasMagic(pattern) && !capabilities.tools.includes(pattern)) throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the prepared agent policy: ${pattern}`);
|
|
1108
|
+
for (const pattern of unmatchedResourcePatterns(configuration.tools, capabilities.tools)) this.root.onResourceWarning?.(`Tool selector currently matches no authorized tool: ${pattern}`);
|
|
1109
|
+
const skills = selectResourcesByLayers([configuration.skills], capabilities.skills);
|
|
1110
|
+
const extensions = selectResourcesByLayers([configuration.extensions], capabilities.extensions);
|
|
1111
|
+
const concrete = validatePreparedConfiguration(root, { ...configuration, model, tools, excludeTools: [...excludedTools], skills: [...configuration.skills, "!*", ...skills], extensions: [...configuration.extensions, "!*", ...extensions], projectTrusted: context.projectTrusted }, capabilities, cwd, false);
|
|
1112
|
+
if (mode === "execution") {
|
|
1113
|
+
if (persistedKey && this.root.runStore?.saveAgentConfiguration) await this.root.runStore.saveAgentConfiguration(persistedKey, concrete);
|
|
1114
|
+
await options.onConfiguration?.(concrete);
|
|
1115
|
+
}
|
|
1116
|
+
return concrete;
|
|
979
1117
|
}
|
|
980
1118
|
|
|
981
1119
|
async execute(task: string, options: AgentExecutionOptions, signal?: AbortSignal, customTools: readonly ToolDefinition[] = [], setSteer?: (handler: (message: string) => void | Promise<void>) => void, beforeRetry?: () => void): Promise<AgentExecutionResult> {
|
|
982
1120
|
const executionSignal = signal ?? this.root.runContext?.signal;
|
|
983
1121
|
if (!Number.isInteger(options.retries ?? 0) || (options.retries ?? 0) < 0) throw new WorkflowError("INVALID_METADATA", "retries must be a non-negative integer");
|
|
984
1122
|
if (options.timeoutMs !== undefined && options.timeoutMs !== null && (!Number.isInteger(options.timeoutMs) || options.timeoutMs <= 0)) throw new WorkflowError("INVALID_METADATA", "timeoutMs must be null or a positive integer");
|
|
985
|
-
let resolved
|
|
1123
|
+
let resolved: ReturnType<WorkflowAgentExecutor["resolve"]>;
|
|
986
1124
|
let recoveryModel: ModelSpec | undefined;
|
|
987
1125
|
let cwd: string;
|
|
988
1126
|
if (options.parent) {
|
|
@@ -1004,6 +1142,14 @@ export class WorkflowAgentExecutor {
|
|
|
1004
1142
|
cwd = this.root.cwd;
|
|
1005
1143
|
}
|
|
1006
1144
|
|
|
1145
|
+
const configuration = await this.prepare(options, cwd, executionSignal);
|
|
1146
|
+
const parentSkills = options.parent === undefined ? undefined : options.capabilities?.skills;
|
|
1147
|
+
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) } };
|
|
1148
|
+
resolved = this.resolve(options);
|
|
1149
|
+
if (!resolved.tools.includes("agent")) customTools = customTools.filter(({ name }) => name !== "agent" && name !== "get_subagent_result" && name !== "steer_subagent");
|
|
1150
|
+
// A child's skill ceiling is what its parent session has loaded when it spawns, so withdrawn skills never stay in it.
|
|
1151
|
+
let publishLoadedSkills: (() => void) | undefined;
|
|
1152
|
+
customTools = customTools.map((tool) => tool.name !== "agent" ? tool : { ...tool, execute: (...args: Parameters<typeof tool.execute>) => { publishLoadedSkills?.(); return tool.execute(...args); } } as ToolDefinition);
|
|
1007
1153
|
const attempts: AgentAttempt[] = [];
|
|
1008
1154
|
let maxAttempts = (options.retries ?? 0) + 1;
|
|
1009
1155
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
@@ -1030,7 +1176,7 @@ export class WorkflowAgentExecutor {
|
|
|
1030
1176
|
const resultSchema = Compile(options.schema ?? defaultWorkflowResultSchema);
|
|
1031
1177
|
let resultAccepted = false;
|
|
1032
1178
|
const resultTool = defineTool({
|
|
1033
|
-
name: "workflow_result", label: "Workflow Result", description: "Submit the terminal workflow result", parameters: resultSchema.Type(),
|
|
1179
|
+
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(),
|
|
1034
1180
|
async execute(_id: string, value: unknown) {
|
|
1035
1181
|
if (!resultSchema.Check(value) || !jsonValue(value)) return { content: [{ type: "text" as const, text: "Result does not match the required schema." }], details: {}, isError: true };
|
|
1036
1182
|
if (resultAccepted) return { content: [{ type: "text" as const, text: "Result has already been accepted." }], details: {}, isError: true };
|
|
@@ -1062,11 +1208,23 @@ export class WorkflowAgentExecutor {
|
|
|
1062
1208
|
|
|
1063
1209
|
try {
|
|
1064
1210
|
setupFailed = true;
|
|
1065
|
-
const prepared = await prepareAgentSetup(this.root, this.transport, task, options, resolved, cwd, attempt, attemptSignal, customTools, resultTool, false
|
|
1211
|
+
const prepared = await prepareAgentSetup(this.root, this.transport, task, options, resolved, cwd, attempt, attemptSignal, customTools, resultTool, false);
|
|
1066
1212
|
setup = prepared.setup;
|
|
1067
1213
|
const attemptSetup = prepared.setup;
|
|
1068
1214
|
setupSummary = prepared.summary;
|
|
1069
1215
|
if (prepared.failure) throw prepared.failure.error;
|
|
1216
|
+
let attemptCapabilities: AgentPreparationContext["capabilities"] | undefined;
|
|
1217
|
+
if (options.agentNodeId !== undefined) {
|
|
1218
|
+
const policy = setup.prepared.resourcePolicy;
|
|
1219
|
+
const sources = policy?.selectorSources;
|
|
1220
|
+
const capabilities = policy?.capabilities ?? options.capabilities;
|
|
1221
|
+
attemptCapabilities = {
|
|
1222
|
+
tools: setup.prepared.tools,
|
|
1223
|
+
skills: selectResourcesByLayers(sources ? resourceSelectorLayers(sources, "skills") : [], capabilities?.skills ?? []),
|
|
1224
|
+
extensions: selectResourcesByLayers(sources ? resourceSelectorLayers(sources, "extensions") : [], capabilities?.extensions ?? []),
|
|
1225
|
+
};
|
|
1226
|
+
this.root.onAgentCapabilities?.(options.agentNodeId, attemptCapabilities, policy?.projectTrusted ?? false);
|
|
1227
|
+
}
|
|
1070
1228
|
setupFailed = false;
|
|
1071
1229
|
if (attemptSignal.aborted) throw new WorkflowError("CANCELLED", "Agent cancelled");
|
|
1072
1230
|
await options.onAttempt?.({ attempt, transport: attemptSetup.transport.id, accounting: zeroAccounting(), setup: setupSummary });
|
|
@@ -1101,6 +1259,13 @@ export class WorkflowAgentExecutor {
|
|
|
1101
1259
|
} catch { /* Frozen prepared policies still receive the inspection overlay below. */ }
|
|
1102
1260
|
}
|
|
1103
1261
|
if (policy) setupSummary = { ...setupSummary, resourceSelectors: { ...resourcePolicySummary(policy, attemptSetup.sessionInput.tools), ...(inspection ? { skills: [...inspection.skills], extensions: [...inspection.extensions] } : {}) } };
|
|
1262
|
+
// The session's currently loaded skills, within its authorization, are the skill ceiling its children inherit.
|
|
1263
|
+
const nodeId = options.agentNodeId, selected = attemptCapabilities, live = session;
|
|
1264
|
+
publishLoadedSkills = nodeId === undefined || selected === undefined || live?.getResourceInspection === undefined ? undefined : () => {
|
|
1265
|
+
const loaded = live.getResourceInspection?.();
|
|
1266
|
+
if (loaded) this.root.onAgentCapabilities?.(nodeId, { ...selected, skills: [...loaded.skills] }, attemptSetup.prepared.resourcePolicy?.projectTrusted ?? false);
|
|
1267
|
+
};
|
|
1268
|
+
publishLoadedSkills?.();
|
|
1104
1269
|
},
|
|
1105
1270
|
onBeforeComplete: async () => {
|
|
1106
1271
|
if (options.worktreeOwner) await runStore?.snapshotWorktree(options.worktreeOwner);
|
|
@@ -1127,6 +1292,7 @@ export class WorkflowAgentExecutor {
|
|
|
1127
1292
|
const runtimeRun = { id: transportBase.run.runId, namespaceId: transportBase.run.sessionId, workflowName: transportBase.run.workflow.name };
|
|
1128
1293
|
const providerRecovery = options.providerErrorRecovery;
|
|
1129
1294
|
const runtimeAgent = { id: transportBase.identity.callSite, structuralPath: [...transportBase.identity.structuralPath], ...(options.parent ? { parentId: options.parent } : {}) };
|
|
1295
|
+
const attemptControl = attemptControlText(options, attempt, attempts.at(-1)?.error?.message);
|
|
1130
1296
|
const runtimeRequest = {
|
|
1131
1297
|
task: setup.prepared.initialPrompt ?? setup.prompt,
|
|
1132
1298
|
cwd: setup.prepared.cwd,
|
|
@@ -1135,6 +1301,7 @@ export class WorkflowAgentExecutor {
|
|
|
1135
1301
|
// Pi sessions execute the prepared native definitions; the executor does not duplicate them as neutral request tools.
|
|
1136
1302
|
customTools: [],
|
|
1137
1303
|
...(options.schema ? { resultSchema: options.schema } : {}),
|
|
1304
|
+
...(attemptControl ? { turnControl: attemptControl } : {}),
|
|
1138
1305
|
run: runtimeRun,
|
|
1139
1306
|
agent: runtimeAgent,
|
|
1140
1307
|
signal: attemptSignal,
|
|
@@ -1201,7 +1368,6 @@ export interface ScheduledAgentOptions {
|
|
|
1201
1368
|
extensions?: readonly string[];
|
|
1202
1369
|
worktreeOwner?: string;
|
|
1203
1370
|
model?: string;
|
|
1204
|
-
role?: string;
|
|
1205
1371
|
contextFiles?: readonly ContextFileScope[];
|
|
1206
1372
|
schema?: JsonSchema;
|
|
1207
1373
|
retries?: number;
|
|
@@ -1211,6 +1377,10 @@ export interface ScheduledAgentOptions {
|
|
|
1211
1377
|
agentOptions?: Readonly<Record<string, JsonValue>>;
|
|
1212
1378
|
agentIdentity?: AgentIdentity;
|
|
1213
1379
|
extensionSettings?: Readonly<WorkflowExtensionSettings>;
|
|
1380
|
+
capabilities?: Readonly<{ tools: readonly string[]; skills: readonly string[]; extensions: readonly string[] }>;
|
|
1381
|
+
projectTrusted?: boolean;
|
|
1382
|
+
configuration?: PreparedAgentConfiguration;
|
|
1383
|
+
inheritedExcludeTools?: readonly string[];
|
|
1214
1384
|
}
|
|
1215
1385
|
|
|
1216
1386
|
export type ScheduledAgentResult =
|
|
@@ -1248,13 +1418,41 @@ type ScheduledNode = {
|
|
|
1248
1418
|
resolveCompletion: () => void;
|
|
1249
1419
|
task: () => Promise<void>;
|
|
1250
1420
|
restored: boolean;
|
|
1421
|
+
//NOTE: attempt narrowing is ephemeral; the persisted logical configuration remains unchanged.
|
|
1422
|
+
attemptCapabilities?: AgentPreparationContext["capabilities"];
|
|
1423
|
+
attemptProjectTrusted?: boolean;
|
|
1251
1424
|
steer?: (message: string) => void | Promise<void>;
|
|
1425
|
+
/** Children spawned so far per label; numbers the occurrence of each child's own identity. */
|
|
1426
|
+
childOccurrences?: Map<string, number>;
|
|
1252
1427
|
};
|
|
1253
1428
|
|
|
1254
1429
|
type ScheduledRun = { limit: number; beforeLaunch?: () => void; extensionSettings?: Readonly<WorkflowExtensionSettings> | undefined; logical: number; active: number; nextIndex: number; queue: Array<{ node?: ScheduledNode; start: () => void }> };
|
|
1255
1430
|
export type OwnershipRecord = { id: string; parentId?: string; prompt?: string; label: string; state: ScheduledNode["state"]; options: Readonly<ScheduledAgentOptions> };
|
|
1256
1431
|
type OwnershipWriter = (runId: string, ownership: readonly OwnershipRecord[]) => void | Promise<void>;
|
|
1257
1432
|
|
|
1433
|
+
/**
|
|
1434
|
+
* A child spawned by its parent's `agent` tool shares the parent's structural scope, breadcrumb and worktree, but owns its
|
|
1435
|
+
* call site under the parent's identity and spawn generation, with a per-label occurrence. It never carries the parent's
|
|
1436
|
+
* handle or turn. Children are new spawns: a retried or resumed parent spawns them again under new identities.
|
|
1437
|
+
*/
|
|
1438
|
+
function childIdentity(parent: ScheduledNode, scope: AgentIdentity, label: string): AgentIdentity {
|
|
1439
|
+
const occurrences = parent.childOccurrences ?? new Map<string, number>();
|
|
1440
|
+
parent.childOccurrences = occurrences;
|
|
1441
|
+
const occurrence = (occurrences.get(label) ?? 0) + 1;
|
|
1442
|
+
occurrences.set(label, occurrence);
|
|
1443
|
+
// The parent's scheduler node number marks the spawn generation: a cold-resumed or retried parent runs as a new node.
|
|
1444
|
+
const generation = parent.id.slice(parent.id.lastIndexOf(":") + 1);
|
|
1445
|
+
return { structuralPath: [...scope.structuralPath], callSite: `child:${agentIdentityPath(scope)}/spawn:${generation}/${label}`, occurrence, ...(scope.parentBreadcrumb ? { parentBreadcrumb: scope.parentBreadcrumb } : {}), ...(scope.worktreeOwner ? { worktreeOwner: scope.worktreeOwner } : {}) };
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* Foreign errors keep descriptive core codes such as UNKNOWN_MODEL. Cancellation and budget exhaustion drive node and run
|
|
1449
|
+
* state, so a foreign error may claim CANCELLED only for an aborted node and never BUDGET_EXHAUSTED.
|
|
1450
|
+
*/
|
|
1451
|
+
function scheduledErrorCode(error: unknown, aborted: boolean): WorkflowErrorCode {
|
|
1452
|
+
if (error instanceof WorkflowError) return error.code;
|
|
1453
|
+
const code = errorCode(error);
|
|
1454
|
+
return code === undefined || code === "BUDGET_EXHAUSTED" || (code === "CANCELLED" && !aborted) ? "AGENT_FAILED" : code;
|
|
1455
|
+
}
|
|
1258
1456
|
export class FairAgentScheduler {
|
|
1259
1457
|
readonly #runs = new Map<string, ScheduledRun>();
|
|
1260
1458
|
readonly #nodes = new Map<string, ScheduledNode>();
|
|
@@ -1287,6 +1485,17 @@ export class FairAgentScheduler {
|
|
|
1287
1485
|
if (!run) throw new WorkflowError("INTERNAL_ERROR", `Unknown scheduler run: ${runId}`);
|
|
1288
1486
|
run.extensionSettings = settings === undefined ? undefined : deepFreeze(structuredClone(settings));
|
|
1289
1487
|
}
|
|
1488
|
+
async setConfiguration(agentId: string, configuration: PreparedAgentConfiguration): Promise<void> {
|
|
1489
|
+
const node = this.#node(agentId);
|
|
1490
|
+
node.options = Object.freeze({ ...node.options, configuration: deepFreeze(structuredClone(configuration)), tools: Object.freeze([...configuration.tools]) });
|
|
1491
|
+
this.#persist(node.runId);
|
|
1492
|
+
await this.flush(node.runId);
|
|
1493
|
+
}
|
|
1494
|
+
setAttemptCapabilities(agentId: string, capabilities: AgentPreparationContext["capabilities"], projectTrusted: boolean): void {
|
|
1495
|
+
const node = this.#node(agentId);
|
|
1496
|
+
node.attemptCapabilities = deepFreeze(structuredClone(capabilities));
|
|
1497
|
+
node.attemptProjectTrusted = projectTrusted;
|
|
1498
|
+
}
|
|
1290
1499
|
setExtensionSettings(agentId: string, settings: Readonly<WorkflowExtensionSettings>): void {
|
|
1291
1500
|
const node = this.#node(agentId);
|
|
1292
1501
|
node.options = Object.freeze({ ...node.options, extensionSettings: deepFreeze(structuredClone(settings)) });
|
|
@@ -1297,7 +1506,7 @@ export class FairAgentScheduler {
|
|
|
1297
1506
|
const run = this.#runs.get(runId);
|
|
1298
1507
|
if (!run) throw new WorkflowError("INTERNAL_ERROR", `Unknown scheduler run: ${runId}`);
|
|
1299
1508
|
const parent = parentId ? this.#nodes.get(parentId) : undefined;
|
|
1300
|
-
if (parentId && (!parent || parent.runId !== runId)) throw new WorkflowError("
|
|
1509
|
+
if (parentId && (!parent || parent.runId !== runId)) throw new WorkflowError("UNKNOWN_AGENT", "Parent agent is not owned by this run");
|
|
1301
1510
|
const effective = this.#inherit(run, parent, options);
|
|
1302
1511
|
const id = `${runId}:${String(++this.#nextId)}`;
|
|
1303
1512
|
const tuiIndex = ++run.nextIndex;
|
|
@@ -1311,11 +1520,10 @@ export class FairAgentScheduler {
|
|
|
1311
1520
|
node.state = "running";
|
|
1312
1521
|
this.#persist(runId);
|
|
1313
1522
|
try {
|
|
1314
|
-
const value = await this.runner({ id, runId, tuiIndex, ...(parentId ? { parentId } : {}), prompt, options:
|
|
1523
|
+
const value = await this.runner({ id, runId, tuiIndex, ...(parentId ? { parentId } : {}), prompt, options: node.options, signal: node.controller.signal, setSteer: (handler) => { node.steer = handler; } });
|
|
1315
1524
|
this.#settle(node, { id, ok: true, value });
|
|
1316
1525
|
} catch (error) {
|
|
1317
|
-
|
|
1318
|
-
this.#settle(node, { id, ok: false, error: { code: typed.code, message: typed.message } });
|
|
1526
|
+
this.#settle(node, { id, ok: false, error: { code: scheduledErrorCode(error, node.controller.signal.aborted), message: errorText(error) } });
|
|
1319
1527
|
}
|
|
1320
1528
|
};
|
|
1321
1529
|
this.#nodes.set(id, node);
|
|
@@ -1328,7 +1536,7 @@ export class FairAgentScheduler {
|
|
|
1328
1536
|
async result(parentId: string, childId: string): Promise<ScheduledAgentResult> {
|
|
1329
1537
|
const parent = this.#node(parentId);
|
|
1330
1538
|
const child = this.#node(childId);
|
|
1331
|
-
if (child.parentId !== parentId) throw new WorkflowError("
|
|
1539
|
+
if (child.parentId !== parentId) throw new WorkflowError("UNKNOWN_AGENT", "Results are scoped to direct children");
|
|
1332
1540
|
if (child.collected || !child.promise) throw new WorkflowError("AGENT_RESULT_COLLECTED", "Child result has already been collected; nested results are one-shot");
|
|
1333
1541
|
if (child.collecting) throw new WorkflowError("AGENT_FAILED", "Child result is already being collected");
|
|
1334
1542
|
child.collecting = true;
|
|
@@ -1362,7 +1570,7 @@ export class FairAgentScheduler {
|
|
|
1362
1570
|
|
|
1363
1571
|
async steer(parentId: string, childId: string, message: string): Promise<void> {
|
|
1364
1572
|
const child = this.#node(childId);
|
|
1365
|
-
if (child.parentId !== parentId) throw new WorkflowError("
|
|
1573
|
+
if (child.parentId !== parentId) throw new WorkflowError("UNKNOWN_AGENT", "Steering is scoped to direct children");
|
|
1366
1574
|
if (child.state !== "running" && child.state !== "waiting_for_child") throw new WorkflowError("AGENT_FAILED", "Child is not running");
|
|
1367
1575
|
if (!child.steer) throw new WorkflowError("AGENT_FAILED", "Child has not registered a steering handler");
|
|
1368
1576
|
await child.steer(message);
|
|
@@ -1414,22 +1622,22 @@ export class FairAgentScheduler {
|
|
|
1414
1622
|
this.#dispatch();
|
|
1415
1623
|
}
|
|
1416
1624
|
|
|
1417
|
-
toolsFor(parentId: string
|
|
1625
|
+
toolsFor(parentId: string): ToolDefinition[] {
|
|
1418
1626
|
const parent = this.#node(parentId);
|
|
1419
|
-
if (!parent.options.tools.includes("agent")) return [];
|
|
1627
|
+
if (!(parent.attemptCapabilities?.tools ?? parent.options.tools).includes("agent")) return [];
|
|
1420
1628
|
const agentTool = defineTool({
|
|
1421
|
-
name: "agent", label: "Child Agent", description: "Start a direct child agent",
|
|
1422
|
-
parameters: Type.Object({ prompt: Type.String(), label: Type.String(), tools: Type.Optional(Type.Array(Type.String())),
|
|
1629
|
+
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.",
|
|
1630
|
+
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 }),
|
|
1423
1631
|
outputSchema: Type.Object({ id: Type.String() }),
|
|
1424
1632
|
execute: async (_id, params) => {
|
|
1425
1633
|
if (!isChildAgentToolParams(params)) throw new WorkflowError("INVALID_METADATA", "Invalid child agent parameters");
|
|
1426
1634
|
validateAgentOptions(params);
|
|
1427
1635
|
const outputSchema = params.outputSchema;
|
|
1428
1636
|
if (outputSchema !== undefined) validateSchema(outputSchema, "agent outputSchema");
|
|
1429
|
-
const tools =
|
|
1637
|
+
const tools = parent.attemptCapabilities?.tools ?? parent.options.tools;
|
|
1430
1638
|
const agentOptions = { ...params };
|
|
1431
1639
|
Reflect.deleteProperty(agentOptions, "prompt");
|
|
1432
|
-
const options: ScheduledAgentOptions = { label: params.label, requestedLabel: params.label, cwd: parent.options.cwd, tools, agentOptions, ...(params.skills ? { skills: params.skills } : {}), ...(params.extensions ? { extensions: params.extensions } : {}), ...(params.model ? { model: params.model } : {}), ...(params.
|
|
1640
|
+
const options: ScheduledAgentOptions = { label: params.label, requestedLabel: params.label, cwd: parent.options.cwd, tools, agentOptions, ...(params.skills ? { skills: params.skills } : {}), ...(params.extensions ? { extensions: params.extensions } : {}), ...(params.model ? { model: params.model } : {}), ...(params.contextFiles ? { contextFiles: params.contextFiles } : {}), ...(outputSchema === undefined ? {} : { schema: outputSchema }), ...(params.retries === undefined ? {} : { retries: params.retries }), ...(params.timeoutMs === undefined ? {} : { timeoutMs: params.timeoutMs }) };
|
|
1433
1641
|
const child = this.spawn(parent.runId, params.prompt, options, parentId);
|
|
1434
1642
|
return { content: [{ type: "text" as const, text: JSON.stringify({ id: child.id }) }], details: { id: child.id }, structuredContent: { id: child.id } };
|
|
1435
1643
|
},
|
|
@@ -1488,10 +1696,14 @@ export class FairAgentScheduler {
|
|
|
1488
1696
|
const extensionSettings = options.extensionSettings ?? parent?.options.extensionSettings ?? run.extensionSettings;
|
|
1489
1697
|
if (!parent) 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]) }) } : {}) });
|
|
1490
1698
|
if (options.cwd !== parent.options.cwd) throw new WorkflowError("UNKNOWN_TOOL", "Child cwd cannot differ from its parent");
|
|
1491
|
-
const
|
|
1699
|
+
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);
|
|
1700
|
+
const forbidden = inheritedTools.find((tool: string) => !(parentCapabilities?.tools ?? parent.options.tools).includes(tool));
|
|
1492
1701
|
if (forbidden) throw new WorkflowError("UNKNOWN_TOOL", `Child tool escalates parent boundary: ${forbidden}`);
|
|
1493
|
-
const identity = options.agentIdentity ?? parent.options.agentIdentity;
|
|
1494
|
-
|
|
1702
|
+
const identity = options.agentIdentity ?? (parent.options.agentIdentity ? childIdentity(parent, parent.options.agentIdentity, options.label) : undefined);
|
|
1703
|
+
// Children inherit the exclusions frozen in their parent's configuration, not its re-evaluated options.
|
|
1704
|
+
const inheritedExcludeTools = [...(parent.options.configuration?.excludeTools ?? excludedToolNames(parent.options))];
|
|
1705
|
+
const projectTrusted = (parent.attemptProjectTrusted ?? parent.options.configuration?.projectTrusted ?? parent.options.projectTrusted) !== false && options.projectTrusted !== false;
|
|
1706
|
+
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 } : {}) });
|
|
1495
1707
|
}
|
|
1496
1708
|
|
|
1497
1709
|
#enqueue(runId: string, node: ScheduledNode | undefined, start: () => void): void { this.#runs.get(runId)?.queue.push({ ...(node ? { node } : {}), start }); this.#dispatch(); }
|
|
@@ -1510,7 +1722,7 @@ export class FairAgentScheduler {
|
|
|
1510
1722
|
const item = run.queue.shift() as { node?: ScheduledNode; start: () => void };
|
|
1511
1723
|
if (item.node) {
|
|
1512
1724
|
try { run.beforeLaunch?.(); }
|
|
1513
|
-
catch (error) {
|
|
1725
|
+
catch (error) { this.#settle(item.node, { id: item.node.id, ok: false, error: { code: scheduledErrorCode(error, item.node.controller.signal.aborted), message: errorText(error) } }); continue; }
|
|
1514
1726
|
}
|
|
1515
1727
|
run.active += 1; this.#active += 1; item.start();
|
|
1516
1728
|
}
|
|
@@ -1546,7 +1758,7 @@ export class FairAgentScheduler {
|
|
|
1546
1758
|
|
|
1547
1759
|
#node(id: string): ScheduledNode {
|
|
1548
1760
|
const node = this.#nodes.get(id);
|
|
1549
|
-
if (!node) throw new WorkflowError("
|
|
1761
|
+
if (!node) throw new WorkflowError("UNKNOWN_AGENT", `Unknown owned agent: ${id}`);
|
|
1550
1762
|
return node;
|
|
1551
1763
|
}
|
|
1552
1764
|
|