@marcoscale98/pi-extensible-workflows 5.17.0-fork.1 → 6.2.1-fork.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +145 -0
- package/README.md +2 -2
- package/dist/src/agent-execution.d.ts +43 -10
- package/dist/src/agent-execution.js +442 -162
- package/dist/src/background-widget.d.ts +5 -1
- package/dist/src/background-widget.js +5 -10
- package/dist/src/decoders.d.ts +2 -3
- package/dist/src/decoders.js +59 -37
- package/dist/src/execution.d.ts +6 -2
- package/dist/src/execution.js +48 -7
- package/dist/src/host-delivery.js +5 -8
- package/dist/src/host-navigator.js +5 -2
- package/dist/src/host-phases.d.ts +1 -0
- package/dist/src/host-recovery.d.ts +11 -8
- package/dist/src/host-recovery.js +14 -22
- package/dist/src/host-runtime.js +56 -5
- package/dist/src/host-view.d.ts +6 -1
- package/dist/src/host-view.js +46 -20
- package/dist/src/host.d.ts +2 -2
- package/dist/src/host.js +246 -118
- package/dist/src/index.d.ts +1 -2
- package/dist/src/index.js +1985 -1690
- package/dist/src/index.js.map +3 -3
- package/dist/src/paths.d.ts +8 -5
- package/dist/src/paths.js +24 -13
- package/dist/src/persistence.d.ts +1 -0
- package/dist/src/persistence.js +1 -0
- package/dist/src/pi-runtime-runner.js +4 -1
- package/dist/src/registry.d.ts +9 -7
- package/dist/src/registry.js +29 -71
- package/dist/src/runtime/agent-runner.d.ts +2 -0
- package/dist/src/script-tools.d.ts +27 -0
- package/dist/src/script-tools.js +87 -0
- package/dist/src/session-lease.d.ts +1 -1
- package/dist/src/session-lease.js +45 -11
- package/dist/src/settings.d.ts +19 -3
- package/dist/src/settings.js +63 -50
- package/dist/src/store.d.ts +3 -1
- package/dist/src/store.js +17 -1
- package/dist/src/trajectory.d.ts +12 -3
- package/dist/src/trajectory.js +32 -11
- package/dist/src/types.d.ts +129 -45
- package/dist/src/types.js +5 -8
- package/dist/src/utils.d.ts +21 -1
- package/dist/src/utils.js +46 -6
- package/dist/src/validation.d.ts +14 -3
- package/dist/src/validation.js +143 -87
- package/dist/starter/index.js +8 -11
- package/dist/starter/index.js.map +1 -1
- package/dist/starter/review-loop.d.ts +0 -13
- package/dist/starter/review-loop.js +8 -9
- package/dist/subagents/index.js +904 -956
- package/dist/subagents/index.js.map +3 -3
- package/dist/subagents/src/contracts.d.ts +48 -31
- package/dist/subagents/src/contracts.js +20 -9
- package/dist/subagents/src/index.d.ts +1 -1
- package/dist/subagents/src/index.js +14 -8
- package/dist/subagents/src/manager.js +52 -73
- package/dist/subagents/src/navigator.js +3 -14
- package/dist/subagents/src/view.d.ts +1 -1
- package/dist/subagents/src/view.js +15 -68
- package/dist/subagents/src/worktree.js +1 -1
- package/dist/trajectory/assets/index.html +35 -13
- package/dist/trajectory/index.js +576 -932
- package/dist/trajectory/index.js.map +3 -3
- package/dist/trajectory/src/assets/index.html +35 -13
- package/dist/trajectory/src/index.js +134 -104
- package/dist/trajectory/src/server.js +268 -27
- package/dist/trajectory/src/server.js.map +3 -3
- package/dist/trajectory/src/startup-mutex.d.ts +17 -0
- package/dist/trajectory/src/startup-mutex.js +172 -0
- package/examples/workflow-extension-template/README.md +4 -7
- package/examples/workflow-extension-template/extension.test.mjs +4 -8
- package/examples/workflow-extension-template/index.js +0 -3
- package/package.json +10 -10
- package/skills/pi-extensible-workflows/SKILL.md +12 -8
- package/src/agent-execution.ts +412 -164
- package/src/background-widget.ts +5 -11
- package/src/decoders.ts +51 -37
- package/src/execution.ts +46 -9
- package/src/host-delivery.ts +5 -8
- package/src/host-navigator.ts +4 -2
- package/src/host-phases.ts +1 -1
- package/src/host-recovery.ts +23 -28
- package/src/host-runtime.ts +44 -6
- package/src/host-view.ts +41 -16
- package/src/host.ts +229 -142
- package/src/index.ts +1 -2
- package/src/paths.ts +18 -9
- package/src/persistence.ts +1 -0
- package/src/pi-runtime-runner.ts +3 -1
- package/src/registry.ts +28 -64
- package/src/runtime/agent-runner.ts +2 -0
- package/src/script-tools.ts +76 -0
- package/src/session-lease.ts +34 -6
- package/src/settings.ts +56 -50
- package/src/store.ts +18 -2
- package/src/trajectory.ts +32 -15
- package/src/types.ts +82 -34
- package/src/utils.ts +42 -5
- package/src/validation.ts +121 -80
- package/starter/index.ts +0 -1
- package/starter/review-loop.ts +8 -9
- package/starter/roles/developer.md +0 -1
- package/starter/roles/oracle.md +0 -1
- package/starter/roles/researcher.md +0 -1
- package/starter/roles/reviewer.md +0 -1
- package/starter/roles/scout.md +0 -1
- package/subagents/README.md +10 -16
- package/subagents/src/contracts.ts +21 -11
- package/subagents/src/index.ts +17 -8
- package/subagents/src/manager.ts +48 -128
- package/subagents/src/navigator.ts +4 -13
- package/subagents/src/view.ts +16 -63
- package/subagents/src/worktree.ts +1 -1
- package/trajectory/src/assets/index.html +35 -13
- package/trajectory/src/index.ts +112 -91
- package/trajectory/src/server.ts +46 -10
- package/trajectory/src/startup-mutex.ts +127 -0
- package/dist/src/roles.d.ts +0 -70
- package/dist/src/roles.js +0 -325
- package/dist/starter/roles/developer.md +0 -6
- package/dist/starter/roles/oracle.md +0 -18
- package/dist/starter/roles/researcher.md +0 -20
- package/dist/starter/roles/reviewer.md +0 -19
- package/dist/starter/roles/scout.md +0 -18
- package/examples/workflow-extension-template/roles/reviewer.md +0 -4
- package/src/roles.ts +0 -326
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, 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, 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,29 +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 } from "./paths.js";
|
|
59
|
-
import {
|
|
58
|
+
import { isCoreAgentOption, validateAgentOptions, validateSchema } from "./validation.js";
|
|
59
|
+
import { canonicalPath, extensionIdentity, piResourceContains, piResourcePath, sameFilesystemPath } from "./paths.js";
|
|
60
|
+
import { isCorePackageName } from "./package-identity.js";
|
|
61
|
+
import { canonicalExtensionSelector, validateContextFileScopes, validateSelectorList, validateWorkflowExtensionSettings } from "./settings.js";
|
|
62
|
+
import { agentIdentityPath } from "./execution.js";
|
|
60
63
|
import type { RunStore } from "./persistence.js";
|
|
61
|
-
type AgentExecutionRunStore = Pick<RunStore, "recordSystemPrompt" | "validateWorktree" | "worktree" | "snapshotWorktree"
|
|
64
|
+
type AgentExecutionRunStore = Pick<RunStore, "recordSystemPrompt" | "validateWorktree" | "worktree" | "snapshotWorktree"> & Partial<Pick<RunStore, "load" | "saveAgentConfiguration">>;
|
|
62
65
|
const localToolTimingExtension = createToolTimingExtension();
|
|
63
|
-
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";
|
|
64
67
|
export interface AgentBudgetHooks {
|
|
65
68
|
beforeAttempt(): void;
|
|
66
69
|
beforeTurn(): void;
|
|
67
70
|
afterTurn(accounting: AgentAccounting, final: boolean): void;
|
|
68
71
|
instruction(): string | undefined;
|
|
69
72
|
}
|
|
70
|
-
export type { AgentDefinition } from "./types.js";
|
|
71
73
|
export interface AgentProviderFailure { label: string; provider: string; model: string; error: string }
|
|
72
74
|
export type AgentProviderRecovery = "retry" | "abort" | { model: string };
|
|
73
75
|
export interface AgentExecutionOptions {
|
|
@@ -86,7 +88,6 @@ export interface AgentExecutionOptions {
|
|
|
86
88
|
skills?: readonly string[];
|
|
87
89
|
extensions?: readonly string[];
|
|
88
90
|
effectiveTools?: readonly string[];
|
|
89
|
-
role?: string;
|
|
90
91
|
contextFiles?: readonly ContextFileScope[];
|
|
91
92
|
schema?: JsonSchema;
|
|
92
93
|
retries?: number;
|
|
@@ -100,31 +101,46 @@ export interface AgentExecutionOptions {
|
|
|
100
101
|
agentIdentity?: AgentIdentity;
|
|
101
102
|
inheritedExtensionSettings?: Readonly<WorkflowExtensionSettings>;
|
|
102
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>;
|
|
103
111
|
}
|
|
104
112
|
export interface AgentExecutionRoot {
|
|
113
|
+
projectTrusted?: boolean;
|
|
105
114
|
cwd: string;
|
|
106
115
|
model: ModelSpec;
|
|
107
116
|
tools: ReadonlySet<string>;
|
|
108
117
|
extensionSettings?: Readonly<WorkflowExtensionSettings> | undefined;
|
|
109
118
|
validateExtensionSettings?: (settings: Readonly<WorkflowExtensionSettings> | undefined, context: WorkflowExtensionSettingsValidatorContext) => void;
|
|
110
119
|
onAgentSettings?: (agentNodeId: string, settings: Readonly<WorkflowExtensionSettings>) => void;
|
|
120
|
+
onAgentCapabilities?: (agentNodeId: string, capabilities: AgentPreparationContext["capabilities"], projectTrusted: boolean) => void;
|
|
111
121
|
resourceSelectors?: AgentResourceSelectors;
|
|
112
|
-
agentDefinitions?: Readonly<Record<string, AgentDefinition>>;
|
|
113
122
|
agentDir?: string;
|
|
114
123
|
additionalSkillPaths?: readonly string[];
|
|
115
124
|
availableModels?: ReadonlySet<string>;
|
|
116
125
|
knownModels?: ReadonlySet<string>;
|
|
117
126
|
modelAliases?: Readonly<Record<string, string>>;
|
|
127
|
+
dynamicModelAliasNames?: readonly string[];
|
|
118
128
|
blockedAliases?: ReadonlySet<string>;
|
|
119
129
|
blockedAliasTargets?: Readonly<Record<string, string>>;
|
|
120
130
|
settingsPath?: string;
|
|
121
131
|
extensionSettingsPath?: string;
|
|
122
132
|
runStore?: AgentExecutionRunStore;
|
|
123
133
|
providerPause?: () => Promise<void>;
|
|
134
|
+
agentPreparationHooks?: readonly RegisteredAgentPreparationHook[];
|
|
124
135
|
agentSetupHooks?: readonly RegisteredAgentSetupHook[];
|
|
125
136
|
agentResourcePolicy?: () => AgentResourcePolicy | Promise<AgentResourcePolicy>;
|
|
126
137
|
runContext?: Readonly<WorkflowRunContext>;
|
|
127
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[] }>;
|
|
128
144
|
}
|
|
129
145
|
export interface AgentToolCallProgress { id: string; name: string; state: "running" | "completed" | "failed" }
|
|
130
146
|
export interface AgentProgress { accounting: AgentAccounting; toolCalls: readonly AgentToolCallProgress[]; state?: WorkflowAgentSessionState; activity?: AgentActivity; lastEventAt?: number; persist: boolean }
|
|
@@ -137,20 +153,67 @@ function parseModel(value: string | undefined, fallback: ModelSpec, aliases: Rea
|
|
|
137
153
|
return resolveModelReference(value, aliases, knownModels, settingsPath);
|
|
138
154
|
}
|
|
139
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
|
+
|
|
140
196
|
function accounting(stats: WorkflowAgentSessionStats): AgentAccounting {
|
|
141
197
|
return { input: stats.tokens.input, output: stats.tokens.output, cacheRead: stats.tokens.cacheRead, cacheWrite: stats.tokens.cacheWrite, cost: stats.cost };
|
|
142
198
|
}
|
|
143
199
|
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
200
|
+
const WORKFLOW_HOST_ENTRY_PATHS = ["src/index.ts", "dist/src/index.js", "starter/index.ts", "dist/starter/index.js", "subagents/index.ts", "dist/subagents/index.js"];
|
|
201
|
+
function isWorkflowHostEntry(path: string): boolean {
|
|
202
|
+
const entryPath = canonicalPath(path);
|
|
203
|
+
let packageRoot = dirname(entryPath);
|
|
204
|
+
while (packageRoot !== dirname(packageRoot)) {
|
|
205
|
+
const packageMetadataPath = join(packageRoot, "package.json");
|
|
206
|
+
if (existsSync(packageMetadataPath)) {
|
|
207
|
+
try {
|
|
208
|
+
const metadata: unknown = JSON.parse(readFileSync(packageMetadataPath, "utf8"));
|
|
209
|
+
if (typeof metadata !== "object" || metadata === null || !("name" in metadata) || !isCorePackageName(metadata.name)) return false;
|
|
210
|
+
return WORKFLOW_HOST_ENTRY_PATHS.some((entry) => canonicalPath(join(packageRoot, entry)) === entryPath);
|
|
211
|
+
} catch { return false; }
|
|
212
|
+
}
|
|
213
|
+
packageRoot = dirname(packageRoot);
|
|
214
|
+
}
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
154
217
|
const WORKFLOW_DIRECTORY = "pi-extensible-workflows";
|
|
155
218
|
function workflowSystemPromptPath(cwd: string, agentDir: string, projectTrusted: boolean): string | undefined {
|
|
156
219
|
const projectPath = join(cwd, ".pi", WORKFLOW_DIRECTORY, "SYSTEM.md");
|
|
@@ -184,24 +247,29 @@ function loadPiPackageDirectory(): Promise<PiPackageDirectoryResolution> {
|
|
|
184
247
|
}
|
|
185
248
|
}).catch((error: unknown) => ({ error: `could not load @earendil-works/pi-coding-agent package metadata: ${errorText(error)}` }));
|
|
186
249
|
}
|
|
187
|
-
|
|
188
|
-
|
|
250
|
+
const nativeCoreModules = new Map<string, Promise<unknown>>();
|
|
251
|
+
/** Pi does not export these core modules, so inspection loads them from the installed package. */
|
|
252
|
+
async function loadNativeCoreModule(file: string, label: string): Promise<unknown> {
|
|
189
253
|
const packageDirectory = await loadPiPackageDirectory();
|
|
190
|
-
let
|
|
254
|
+
let modulePath: string | undefined;
|
|
191
255
|
if (packageDirectory.directory) {
|
|
192
|
-
const candidate = resolve(packageDirectory.directory, "dist/core
|
|
193
|
-
if (existsSync(candidate))
|
|
256
|
+
const candidate = resolve(packageDirectory.directory, "dist/core", file);
|
|
257
|
+
if (existsSync(candidate)) modulePath = candidate;
|
|
194
258
|
}
|
|
195
|
-
|
|
259
|
+
modulePath ??= resolve(dirname(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"))), "core", file);
|
|
196
260
|
const packageDirectoryError = packageDirectory.error ? ` Package directory lookup failed: ${packageDirectory.error}` : "";
|
|
197
|
-
|
|
261
|
+
let loaded = nativeCoreModules.get(file);
|
|
262
|
+
if (!loaded) { loaded = import(modulePath).catch((error: unknown) => { throw new Error(`Could not load Pi ${label}.${packageDirectoryError}`, { cause: error }); }); nativeCoreModules.set(file, loaded); }
|
|
263
|
+
return loaded;
|
|
198
264
|
}
|
|
199
265
|
async function expandPromptTemplateForInspection(text: string, templates: readonly { name: string; content: string }[]): Promise<string> {
|
|
200
|
-
return (await
|
|
266
|
+
return (await loadNativeCoreModule("prompt-templates.js", "prompt templates") as NativePromptTemplateModule).expandPromptTemplate(text, templates);
|
|
267
|
+
}
|
|
268
|
+
async function buildSystemPromptForInspection(options: unknown): Promise<string> {
|
|
269
|
+
return (await loadNativeCoreModule("system-prompt.js", "system prompt builder") as { buildSystemPrompt(options: unknown): string }).buildSystemPrompt(options);
|
|
201
270
|
}
|
|
202
271
|
type NativePromptSession = PiSession & {
|
|
203
|
-
readonly _extensionRunner?: { hasHandlers(event: string): boolean; emitInput(text: string, images: undefined, source: "interactive"): Promise<unknown>; emitBeforeAgentStart(prompt: string, images: undefined,
|
|
204
|
-
readonly _baseSystemPrompt?: string;
|
|
272
|
+
readonly _extensionRunner?: { hasHandlers(event: string): boolean; emitInput(text: string, images: undefined, source: "interactive"): Promise<unknown>; emitBeforeAgentStart(prompt: string, images: undefined, options: unknown): Promise<unknown>; onError(listener: (error: unknown) => void): () => void };
|
|
205
273
|
readonly _baseSystemPromptOptions?: unknown;
|
|
206
274
|
readonly _expandSkillCommand?: (text: string) => string;
|
|
207
275
|
readonly promptTemplates?: readonly { name: string; content: string }[];
|
|
@@ -210,12 +278,11 @@ async function preparePiPrompt(native: PiSession, text: string): Promise<PiPromp
|
|
|
210
278
|
const session = native as NativePromptSession;
|
|
211
279
|
const diagnostics: Array<{ type: "error"; message: string; source?: string }> = [];
|
|
212
280
|
const runner = session._extensionRunner;
|
|
213
|
-
const baseSystemPrompt =
|
|
281
|
+
const baseSystemPrompt = session.systemPrompt ?? "";
|
|
214
282
|
const baseOptions = session._baseSystemPromptOptions;
|
|
215
283
|
const expandSkillCommand = session._expandSkillCommand;
|
|
216
284
|
const templates = session.promptTemplates;
|
|
217
285
|
if (!runner) diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: extension runner is missing", source: "Pi session" });
|
|
218
|
-
if (typeof session._baseSystemPrompt !== "string") diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: base system prompt is missing", source: "Pi session" });
|
|
219
286
|
if (baseOptions === undefined) diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: base system prompt options are missing", source: "Pi session" });
|
|
220
287
|
if (typeof expandSkillCommand !== "function") diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: skill expansion is missing", source: "Pi session" });
|
|
221
288
|
if (!Array.isArray(templates)) diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: prompt templates are missing", source: "Pi session" });
|
|
@@ -237,9 +304,11 @@ async function preparePiPrompt(native: PiSession, text: string): Promise<PiPromp
|
|
|
237
304
|
try { expandedPrompt = await expandPromptTemplateForInspection(skillExpanded, Array.isArray(templates) ? templates : []); }
|
|
238
305
|
catch (error) { diagnostics.push({ type: "error", message: `Pi prompt template expansion is unavailable: ${errorText(error)}`, source: "Pi session" }); }
|
|
239
306
|
}
|
|
240
|
-
const result = !inputHandled && runner && baseOptions !== undefined ? await runner.emitBeforeAgentStart(expandedPrompt, undefined,
|
|
241
|
-
const prepared = result as { messages?: readonly unknown[];
|
|
242
|
-
|
|
307
|
+
const result = !inputHandled && runner && baseOptions !== undefined ? await runner.emitBeforeAgentStart(expandedPrompt, undefined, baseOptions) : undefined;
|
|
308
|
+
const prepared = result as { messages?: readonly unknown[]; systemPromptOptions?: unknown } | undefined;
|
|
309
|
+
const systemPromptOptions = prepared?.systemPromptOptions ?? baseOptions;
|
|
310
|
+
const systemPrompt = prepared?.systemPromptOptions === undefined ? baseSystemPrompt : await buildSystemPromptForInspection(prepared.systemPromptOptions);
|
|
311
|
+
return { prompt: text, expandedPrompt, systemPrompt, systemPromptOptions, messages: prepared?.messages ?? [], inputHandled, diagnostics };
|
|
243
312
|
} finally { unsubscribe?.(); }
|
|
244
313
|
}
|
|
245
314
|
|
|
@@ -281,6 +350,57 @@ export function flushExtensionProviders(resourceLoader: DefaultResourceLoader, m
|
|
|
281
350
|
return failures;
|
|
282
351
|
}
|
|
283
352
|
|
|
353
|
+
/**
|
|
354
|
+
* The CLI adds Pi's built-in extensions itself; SDK sessions only get them from these public
|
|
355
|
+
* factories (docs/sdk.md). As `builtin` entries they obey `-builtin:<name>` settings, and like the
|
|
356
|
+
* CLI's they are replaceable by an extension that registers the same tools or commands.
|
|
357
|
+
* NOTE: Pi does not export the built-in llama.cpp extension, so workflow agents cannot use models it
|
|
358
|
+
* provides. Declare the server as a provider in models.json instead.
|
|
359
|
+
*/
|
|
360
|
+
function builtinExtensions(): InlineExtension[] {
|
|
361
|
+
return [
|
|
362
|
+
{ name: "codemode", factory: createCodemodeExtension(), replaceable: true, builtin: true },
|
|
363
|
+
{ name: "tool-search", factory: createToolSearchExtension(), replaceable: true, builtin: true },
|
|
364
|
+
{ name: "mcp", factory: createMcpExtension(), replaceable: true, builtin: true },
|
|
365
|
+
];
|
|
366
|
+
}
|
|
367
|
+
const BUILTIN_EXTENSION_PREFIX = "builtin:";
|
|
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
|
+
}
|
|
403
|
+
|
|
284
404
|
async function createLocalPiSessionHandle(input: SessionInput, sessionStartEvent?: WorkflowSessionStartEvent): Promise<LocalPiSessionHandle> {
|
|
285
405
|
const agentDir = input.agentDir ?? getAgentDir();
|
|
286
406
|
const systemPromptSource = workflowSystemPromptPath(input.cwd, agentDir, input.resourcePolicy?.projectTrusted ?? true);
|
|
@@ -293,33 +413,34 @@ async function createLocalPiSessionHandle(input: SessionInput, sessionStartEvent
|
|
|
293
413
|
const tools = [...new Set([...input.tools, ...customTools.map(({ name }) => name)])];
|
|
294
414
|
let settingsManager: SettingsManager;
|
|
295
415
|
let resourceLoader: DefaultResourceLoader;
|
|
416
|
+
const builtins = builtinExtensions();
|
|
417
|
+
const builtinNames = builtins.flatMap((extension) => typeof extension === "function" ? [] : [extension.name]);
|
|
418
|
+
const extensionFactories = [...builtins, ...(input.extensionFactories ?? [])];
|
|
296
419
|
const policy = input.resourcePolicy;
|
|
297
420
|
if (policy) {
|
|
298
421
|
settingsManager = SettingsManager.create(input.cwd, agentDir, { projectTrusted: false });
|
|
299
422
|
settingsManager.setProjectTrusted(policy.projectTrusted);
|
|
300
|
-
const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager });
|
|
423
|
+
const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager, builtinExtensions: builtinNames });
|
|
301
424
|
const resolved = await packageManager.resolve();
|
|
302
|
-
const discoveredExtensions = [...new Set(resolved.extensions.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) =>
|
|
425
|
+
const discoveredExtensions = [...new Set(resolved.extensions.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => extensionIdentity(path)))];
|
|
303
426
|
const selectorSources = policy.selectorSources;
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
selectorSources,
|
|
307
|
-
resources: { extensions: discoveredExtensions },
|
|
308
|
-
});
|
|
309
|
-
const selectedExtensions = selection.selectedExtensions ?? [];
|
|
310
|
-
const extensionPaths = selectedExtensions.filter((path) => !WORKFLOW_HOST_ENTRIES.has(path));
|
|
427
|
+
const selectedExtensions = selectResourcesByLayers(resourceSelectorLayers(selectorSources, "extensions"), discoveredExtensions).filter((path) => policy.capabilities === undefined || policy.capabilities.extensions.includes(path));
|
|
428
|
+
const extensionPaths = selectedExtensions.filter((path) => path.startsWith(BUILTIN_EXTENSION_PREFIX) || !isWorkflowHostEntry(path));
|
|
311
429
|
policy.selectedExtensions = selectedExtensions;
|
|
312
|
-
policy.unmatchedExtensions =
|
|
430
|
+
policy.unmatchedExtensions = unmatchedResourcePatterns(policy.effective.extensions, discoveredExtensions);
|
|
313
431
|
const skillPaths = [...new Set(resolved.skills.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => path))];
|
|
314
|
-
const updateSkillMatches = (skills: readonly { name: string }[]): Set<string> => {
|
|
432
|
+
const updateSkillMatches = (skills: readonly { name: string; filePath: string }[], extensionSkillPaths: readonly string[]): Set<string> => {
|
|
315
433
|
const names = [...new Set(skills.map(({ name }) => name))];
|
|
316
|
-
const
|
|
317
|
-
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];
|
|
318
439
|
policy.selectedSkills = selectedSkills;
|
|
319
|
-
policy.unmatchedSkills =
|
|
440
|
+
policy.unmatchedSkills = unmatchedResourcePatterns(policy.effective.skills, names);
|
|
320
441
|
return new Set(names.filter((name) => !selectedSkills.includes(name)));
|
|
321
442
|
};
|
|
322
|
-
|
|
443
|
+
const workflowLoader: WorkflowResourceLoader = new WorkflowResourceLoader(input.cwd, {
|
|
323
444
|
cwd: input.cwd,
|
|
324
445
|
agentDir,
|
|
325
446
|
settingsManager,
|
|
@@ -327,22 +448,23 @@ async function createLocalPiSessionHandle(input: SessionInput, sessionStartEvent
|
|
|
327
448
|
additionalExtensionPaths: extensionPaths,
|
|
328
449
|
noSkills: true,
|
|
329
450
|
additionalSkillPaths: [...new Set([...skillPaths, ...(input.additionalSkillPaths ?? [])])],
|
|
330
|
-
|
|
451
|
+
extensionFactories,
|
|
331
452
|
...(contextFilesOverride ? { agentsFilesOverride: contextFilesOverride } : {}),
|
|
332
453
|
skillsOverride: (base) => {
|
|
333
|
-
const disabledSkills = updateSkillMatches(base.skills);
|
|
454
|
+
const disabledSkills = updateSkillMatches(base.skills, workflowLoader.extensionSkillPaths);
|
|
334
455
|
return { ...base, skills: base.skills.filter(({ name }) => !disabledSkills.has(name)) };
|
|
335
456
|
},
|
|
336
457
|
...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}),
|
|
337
458
|
...systemPromptOptions,
|
|
338
459
|
});
|
|
460
|
+
resourceLoader = workflowLoader;
|
|
339
461
|
await resourceLoader.reload();
|
|
340
462
|
} else {
|
|
341
463
|
settingsManager = SettingsManager.create(input.cwd, agentDir, { projectTrusted: true });
|
|
342
|
-
const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager });
|
|
464
|
+
const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager, builtinExtensions: builtinNames });
|
|
343
465
|
const resolved = await packageManager.resolve();
|
|
344
|
-
const extensionPaths = [...new Set(resolved.extensions.filter(({ enabled }) => enabled).map(({ path }) =>
|
|
345
|
-
resourceLoader = new DefaultResourceLoader({ cwd: input.cwd, agentDir, settingsManager, noExtensions: true, additionalExtensionPaths: extensionPaths, ...(input.additionalSkillPaths?.length ? { additionalSkillPaths: [...input.additionalSkillPaths] } : {}),
|
|
466
|
+
const extensionPaths = [...new Set(resolved.extensions.filter(({ enabled }) => enabled).map(({ path }) => extensionIdentity(path)).filter((path) => path.startsWith(BUILTIN_EXTENSION_PREFIX) || !isWorkflowHostEntry(path)))];
|
|
467
|
+
resourceLoader = new DefaultResourceLoader({ cwd: input.cwd, agentDir, settingsManager, noExtensions: true, additionalExtensionPaths: extensionPaths, ...(input.additionalSkillPaths?.length ? { additionalSkillPaths: [...input.additionalSkillPaths] } : {}), extensionFactories, ...(contextFilesOverride ? { agentsFilesOverride: contextFilesOverride } : {}), ...systemPromptOptions, ...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}) });
|
|
346
468
|
await resourceLoader.reload();
|
|
347
469
|
}
|
|
348
470
|
const providerFailures = flushExtensionProviders(resourceLoader, modelRuntime);
|
|
@@ -353,11 +475,11 @@ async function createLocalPiSessionHandle(input: SessionInput, sessionStartEvent
|
|
|
353
475
|
// missing, so the failure travels with the complaint rather than being
|
|
354
476
|
// swallowed — otherwise a misconfigured gateway reads as a typo in a model
|
|
355
477
|
// name.
|
|
356
|
-
const because = providerFailures.length > 0 ? ` (provider registration failed — ${providerFailures.join("; ")})` : "";
|
|
478
|
+
const because = providerFailures.length > 0 ? ` (provider registration failed — ${providerFailures.join("; ")})` : isLlamaModel(input.model) ? " (Pi does not expose its built-in llama.cpp provider to extension sessions; declare the llama.cpp server as a provider in models.json and use that model instead)" : "";
|
|
357
479
|
throw new WorkflowError("UNKNOWN_MODEL", `Unknown model: ${input.model.provider}/${input.model.model}${because}`);
|
|
358
480
|
}
|
|
359
481
|
const effectiveSessionStartEvent = input.settings === undefined && sessionStartEvent === undefined ? undefined : { ...(sessionStartEvent ?? { type: "session_start" as const, reason: "startup" as const }), settings: input.settings ?? Object.freeze({}) };
|
|
360
|
-
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 });
|
|
361
483
|
const nativeDispose = session.dispose.bind(session);
|
|
362
484
|
let disposal: Promise<void> | undefined;
|
|
363
485
|
const shutdown = (reason: LocalSessionShutdownReason, targetSessionFile?: string): Promise<void> => {
|
|
@@ -378,13 +500,13 @@ async function createLocalPiSessionHandle(input: SessionInput, sessionStartEvent
|
|
|
378
500
|
await shutdown("quit").catch(() => undefined);
|
|
379
501
|
throw error;
|
|
380
502
|
}
|
|
381
|
-
const resourcePaths = { extensions: resourceLoader.getExtensions().extensions.filter(({ path }) => !path.startsWith("<")).map(({ resolvedPath }) =>
|
|
503
|
+
const resourcePaths = { extensions: resourceLoader.getExtensions().extensions.filter(({ path }) => !path.startsWith("<")).map(({ resolvedPath }) => extensionIdentity(resolvedPath)), skills: resourceLoader.getSkills().skills.map(({ filePath }) => canonicalPath(filePath)) };
|
|
382
504
|
const resourceInspection = (): PiResourceInspection => {
|
|
383
505
|
const extensions = resourceLoader.getExtensions();
|
|
384
506
|
const skills = resourceLoader.getSkills();
|
|
385
507
|
const diagnostics = [...extensions.errors.map(({ path, error }) => ({ type: "error" as const, message: error, source: path })), ...skills.diagnostics.map(({ type, path, message }) => ({ type, message, ...(path ? { source: path } : {}) }))];
|
|
386
508
|
const systemSource = systemPromptSource;
|
|
387
|
-
return { extensions: resourceLoader.getExtensions().extensions.filter(({ path }) => !path.startsWith("<")).map(({ resolvedPath }) =>
|
|
509
|
+
return { extensions: resourceLoader.getExtensions().extensions.filter(({ path }) => !path.startsWith("<")).map(({ resolvedPath }) => extensionIdentity(resolvedPath)), skills: skills.skills.map(({ name }) => name), diagnostics, ...(systemSource ? { systemPromptSource: systemSource } : resourceLoader.getSystemPrompt() !== undefined ? { systemPromptSource: "Pi resource loader" } : {}) };
|
|
388
510
|
};
|
|
389
511
|
const managedSession = Object.assign(session, {
|
|
390
512
|
getLeafId: () => manager.getLeafId(),
|
|
@@ -637,6 +759,11 @@ export async function createLocalWorkflowAgentSession(prepared: Readonly<Prepare
|
|
|
637
759
|
return session;
|
|
638
760
|
}
|
|
639
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
|
+
}
|
|
640
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]); }
|
|
641
768
|
interface ChildAgentToolParams {
|
|
642
769
|
prompt: string;
|
|
@@ -645,7 +772,6 @@ interface ChildAgentToolParams {
|
|
|
645
772
|
skills?: string[];
|
|
646
773
|
extensions?: string[];
|
|
647
774
|
model?: string;
|
|
648
|
-
role?: string;
|
|
649
775
|
contextFiles?: ContextFileScope[];
|
|
650
776
|
outputSchema?: unknown;
|
|
651
777
|
retries?: number;
|
|
@@ -658,7 +784,6 @@ function isChildAgentToolParams(value: unknown): value is ChildAgentToolParams &
|
|
|
658
784
|
if (value.skills !== undefined && (!Array.isArray(value.skills) || value.skills.some((skill) => typeof skill !== "string"))) return false;
|
|
659
785
|
if (value.extensions !== undefined && (!Array.isArray(value.extensions) || value.extensions.some((extension) => typeof extension !== "string"))) return false;
|
|
660
786
|
if (value.model !== undefined && typeof value.model !== "string") return false;
|
|
661
|
-
if (value.role !== undefined && (typeof value.role !== "string" || !value.role.trim())) return false;
|
|
662
787
|
if (value.contextFiles !== undefined && (!Array.isArray(value.contextFiles) || !value.contextFiles.every(isContextFileScope))) return false;
|
|
663
788
|
if (value.outputSchema !== undefined && !jsonObject(value.outputSchema)) return false;
|
|
664
789
|
if (value.retries !== undefined && (typeof value.retries !== "number" || !Number.isInteger(value.retries) || value.retries < 0)) return false;
|
|
@@ -681,7 +806,7 @@ function resourcePolicySummary(policy: AgentResourcePolicy, tools: readonly stri
|
|
|
681
806
|
const RESOURCE_SELECTOR_KEYS = ["skills", "extensions", "tools"] as const;
|
|
682
807
|
type ResourceSelectorKey = (typeof RESOURCE_SELECTOR_KEYS)[number];
|
|
683
808
|
function resourceSelectorLayers(sources: AgentResourceSelectorSources, key: ResourceSelectorKey): readonly (readonly string[] | undefined)[] {
|
|
684
|
-
return [sources.global[key], sources.project[key], sources.
|
|
809
|
+
return [sources.global[key], sources.project[key], sources.call?.[key]];
|
|
685
810
|
}
|
|
686
811
|
function selectorListWidened(ceiling: readonly string[], candidate: readonly string[]): boolean {
|
|
687
812
|
let candidateIndex = 0;
|
|
@@ -738,7 +863,7 @@ function appendResourcePolicyNarrowing(ceiling: AgentResourcePolicy | undefined,
|
|
|
738
863
|
};
|
|
739
864
|
}
|
|
740
865
|
function packageRoot(start: string): string | undefined {
|
|
741
|
-
let current = dirname(
|
|
866
|
+
let current = dirname(canonicalPath(start));
|
|
742
867
|
for (;;) {
|
|
743
868
|
const candidates = [current, join(current, "..", "@earendil-works", "pi-coding-agent"), join(current, "..", "pi-coding-agent")];
|
|
744
869
|
for (const candidate of candidates) {
|
|
@@ -785,10 +910,9 @@ async function resolvePiRuntime(): Promise<PiRuntimeResolution> {
|
|
|
785
910
|
}
|
|
786
911
|
}
|
|
787
912
|
const piRuntimeResolution = resolvePiRuntime();
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
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");
|
|
792
916
|
}
|
|
793
917
|
async function preparedAgentSession(input: SessionInput, initialPrompt?: string): Promise<Readonly<PreparedAgentSession>> {
|
|
794
918
|
const { runtime: piRuntime, error: piRuntimeError } = await piRuntimeResolution;
|
|
@@ -812,45 +936,26 @@ async function attemptSessionInput(source: string, attempt: number): Promise<str
|
|
|
812
936
|
await copyFile(source, target);
|
|
813
937
|
return target;
|
|
814
938
|
}
|
|
815
|
-
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> {
|
|
816
940
|
const setupSignal = signal ?? root.runContext?.signal ?? new AbortController().signal;
|
|
817
941
|
const baselineOptions = structuredClone(options.agentOptions ?? {});
|
|
818
942
|
const baseResourcePolicy = await root.agentResourcePolicy?.();
|
|
819
|
-
const
|
|
820
|
-
const
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
if (Array.isArray(value) && value.every((entry) => typeof entry === "string")) return value;
|
|
829
|
-
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,
|
|
830
952
|
};
|
|
831
|
-
const
|
|
832
|
-
const rawCallExtensions = selectorValue("extensions", options.extensions)?.map((selector) => canonicalExtensionSelector(selector, cwd));
|
|
833
|
-
const rawCallTools = selectorValue("tools", options.tools);
|
|
834
|
-
const callSelectors: AgentResourceSelectors = { ...(rawCallSkills === undefined ? {} : { skills: rawCallSkills }), ...(rawCallExtensions === undefined ? {} : { extensions: rawCallExtensions }), ...(rawCallTools === undefined ? {} : { tools: rawCallTools }) };
|
|
835
|
-
const resourcePolicy: AgentResourcePolicy | undefined = baseResourcePolicy ? {
|
|
836
|
-
...baseResourcePolicy,
|
|
837
|
-
effective: {
|
|
838
|
-
skills: [...baseResourcePolicy.effective.skills, ...(roleSelectors?.skills ?? []), ...(rawCallSkills ?? [])],
|
|
839
|
-
extensions: [...baseResourcePolicy.effective.extensions, ...(roleSelectors?.extensions ?? []), ...(rawCallExtensions ?? [])],
|
|
840
|
-
...(baseResourcePolicy.effective.tools === undefined && roleSelectors?.tools === undefined && rawCallTools === undefined ? {} : { tools: [...(baseResourcePolicy.effective.tools ?? []), ...(roleSelectors?.tools ?? []), ...(rawCallTools ?? [])] }),
|
|
841
|
-
},
|
|
842
|
-
selectorSources: { global: baseResourcePolicy.selectorSources.global, project: baseResourcePolicy.selectorSources.project, ...(roleSelectors ? { role: roleSelectors } : {}), ...(Object.keys(callSelectors).length ? { call: callSelectors } : {}) },
|
|
843
|
-
} : undefined;
|
|
844
|
-
if (resourcePolicy) {
|
|
845
|
-
resourcePolicy.selectedTools = resolved.tools;
|
|
846
|
-
resourcePolicy.unmatchedTools = unmatchedResourcePatterns(resourceSelectorLayers(resourcePolicy.selectorSources, "tools").flatMap((layer) => layer ?? []), [...root.tools]);
|
|
847
|
-
}
|
|
848
|
-
const resourcePolicyCeiling = resourcePolicy ? structuredClone(resourcePolicy) : undefined;
|
|
953
|
+
const resourcePolicyCeiling = structuredClone(resourcePolicy);
|
|
849
954
|
const sessionPath = options.sessionPath === undefined || inspection ? options.sessionPath : await attemptSessionInput(options.sessionPath, attempt);
|
|
850
|
-
const extensionSettings =
|
|
851
|
-
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 } : {}) });
|
|
852
957
|
if (options.agentNodeId !== undefined) root.onAgentSettings?.(options.agentNodeId, extensionSettings);
|
|
853
|
-
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) };
|
|
854
959
|
const setup = { prompt: task, options: sessionInput.options ?? {}, sessionInput, prepared: await preparedAgentSession(sessionInput, task), transport };
|
|
855
960
|
const base = fallbackSetupContext(root, options, setupSignal);
|
|
856
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 } : {}) });
|
|
@@ -868,25 +973,55 @@ async function prepareAgentSetup(root: AgentExecutionRoot, transport: AgentTrans
|
|
|
868
973
|
try {
|
|
869
974
|
if (resourcePolicyWidened(resourcePolicyCeiling, setup.sessionInput.resourcePolicy)) throw new WorkflowError("INVALID_METADATA", "Agent setup widened the prepared resource policy");
|
|
870
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
|
+
}
|
|
871
983
|
setup.sessionInput.options = setup.options;
|
|
872
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);
|
|
873
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];
|
|
874
|
-
|
|
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}`);
|
|
875
1003
|
const customToolNames = new Set([...(setup.sessionInput.customTools ?? []).map(({ name }) => name), ...(setup.sessionInput.resultTool ? [setup.sessionInput.resultTool.name] : [])]);
|
|
876
1004
|
const widened = setup.sessionInput.tools.find((tool) => !resolved.tools.includes(tool) && !customToolNames.has(tool));
|
|
877
1005
|
const outsideTool = widened ?? setup.sessionInput.tools.find((tool) => !root.tools.has(tool) && !customToolNames.has(tool));
|
|
878
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
|
+
|
|
879
1014
|
} catch (error) {
|
|
880
1015
|
setup.prepared = await preparedAgentSession(setup.sessionInput, task);
|
|
881
1016
|
return { setup, summary: agentSetupSummary(setup, hookNames), failure: { error } };
|
|
882
1017
|
}
|
|
883
|
-
setup.prepared = await preparedAgentSession(setup.sessionInput, inspection ? task :
|
|
1018
|
+
setup.prepared = await preparedAgentSession(setup.sessionInput, inspection ? task : setup.prompt);
|
|
884
1019
|
return { setup, summary: agentSetupSummary(setup, hookNames) };
|
|
885
1020
|
}
|
|
886
1021
|
export async function prepareAgentSetupForInspection(root: AgentExecutionRoot, task: string, options: AgentExecutionOptions, transport: AgentTransport): Promise<PreparedAgentSetup> {
|
|
887
1022
|
const executor = new WorkflowAgentExecutor(root);
|
|
888
|
-
const
|
|
889
|
-
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);
|
|
890
1025
|
}
|
|
891
1026
|
function attemptRecord(transport: string, attempt: number, session: WorkflowAgentSession, setup: AgentSetupSummary, stats: AgentAccounting, result?: JsonValue, error?: { code: string; message: string }): AgentAttempt {
|
|
892
1027
|
return { attempt, transport, session: session.reference, ...(result === undefined ? {} : { result }), ...(error ? { error } : {}), accounting: stats, setup };
|
|
@@ -903,52 +1038,89 @@ export function getAgentAttempts(error: unknown): readonly AgentAttempt[] | unde
|
|
|
903
1038
|
}
|
|
904
1039
|
export class WorkflowAgentExecutor {
|
|
905
1040
|
private readonly transport: AgentTransport;
|
|
906
|
-
private readonly warnedRoleTools = new Set<string>();
|
|
907
1041
|
constructor(private readonly root: AgentExecutionRoot, transport: AgentTransport = localAgentTransport) { this.transport = transport; }
|
|
908
1042
|
setRunContext(runContext: Readonly<WorkflowRunContext>): void { this.root.runContext = runContext; }
|
|
909
1043
|
|
|
910
1044
|
resolve(options: AgentExecutionOptions, inheritedTools?: readonly string[]): { model: ModelSpec; requestedModel?: string; tools: readonly string[]; systemPrompt?: string; systemPromptAppend: string; contextFiles?: readonly ContextFileScope[] } {
|
|
911
|
-
|
|
912
|
-
const
|
|
913
|
-
if (
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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);
|
|
920
1067
|
}
|
|
921
|
-
const
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
...(
|
|
929
|
-
...(this.root.
|
|
930
|
-
...(
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
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;
|
|
945
1117
|
}
|
|
946
1118
|
|
|
947
1119
|
async execute(task: string, options: AgentExecutionOptions, signal?: AbortSignal, customTools: readonly ToolDefinition[] = [], setSteer?: (handler: (message: string) => void | Promise<void>) => void, beforeRetry?: () => void): Promise<AgentExecutionResult> {
|
|
948
1120
|
const executionSignal = signal ?? this.root.runContext?.signal;
|
|
949
1121
|
if (!Number.isInteger(options.retries ?? 0) || (options.retries ?? 0) < 0) throw new WorkflowError("INVALID_METADATA", "retries must be a non-negative integer");
|
|
950
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");
|
|
951
|
-
let resolved
|
|
1123
|
+
let resolved: ReturnType<WorkflowAgentExecutor["resolve"]>;
|
|
952
1124
|
let recoveryModel: ModelSpec | undefined;
|
|
953
1125
|
let cwd: string;
|
|
954
1126
|
if (options.parent) {
|
|
@@ -970,6 +1142,14 @@ export class WorkflowAgentExecutor {
|
|
|
970
1142
|
cwd = this.root.cwd;
|
|
971
1143
|
}
|
|
972
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);
|
|
973
1153
|
const attempts: AgentAttempt[] = [];
|
|
974
1154
|
let maxAttempts = (options.retries ?? 0) + 1;
|
|
975
1155
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
@@ -996,7 +1176,7 @@ export class WorkflowAgentExecutor {
|
|
|
996
1176
|
const resultSchema = Compile(options.schema ?? defaultWorkflowResultSchema);
|
|
997
1177
|
let resultAccepted = false;
|
|
998
1178
|
const resultTool = defineTool({
|
|
999
|
-
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(),
|
|
1000
1180
|
async execute(_id: string, value: unknown) {
|
|
1001
1181
|
if (!resultSchema.Check(value) || !jsonValue(value)) return { content: [{ type: "text" as const, text: "Result does not match the required schema." }], details: {}, isError: true };
|
|
1002
1182
|
if (resultAccepted) return { content: [{ type: "text" as const, text: "Result has already been accepted." }], details: {}, isError: true };
|
|
@@ -1028,11 +1208,23 @@ export class WorkflowAgentExecutor {
|
|
|
1028
1208
|
|
|
1029
1209
|
try {
|
|
1030
1210
|
setupFailed = true;
|
|
1031
|
-
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);
|
|
1032
1212
|
setup = prepared.setup;
|
|
1033
1213
|
const attemptSetup = prepared.setup;
|
|
1034
1214
|
setupSummary = prepared.summary;
|
|
1035
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
|
+
}
|
|
1036
1228
|
setupFailed = false;
|
|
1037
1229
|
if (attemptSignal.aborted) throw new WorkflowError("CANCELLED", "Agent cancelled");
|
|
1038
1230
|
await options.onAttempt?.({ attempt, transport: attemptSetup.transport.id, accounting: zeroAccounting(), setup: setupSummary });
|
|
@@ -1067,6 +1259,13 @@ export class WorkflowAgentExecutor {
|
|
|
1067
1259
|
} catch { /* Frozen prepared policies still receive the inspection overlay below. */ }
|
|
1068
1260
|
}
|
|
1069
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?.();
|
|
1070
1269
|
},
|
|
1071
1270
|
onBeforeComplete: async () => {
|
|
1072
1271
|
if (options.worktreeOwner) await runStore?.snapshotWorktree(options.worktreeOwner);
|
|
@@ -1093,6 +1292,7 @@ export class WorkflowAgentExecutor {
|
|
|
1093
1292
|
const runtimeRun = { id: transportBase.run.runId, namespaceId: transportBase.run.sessionId, workflowName: transportBase.run.workflow.name };
|
|
1094
1293
|
const providerRecovery = options.providerErrorRecovery;
|
|
1095
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);
|
|
1096
1296
|
const runtimeRequest = {
|
|
1097
1297
|
task: setup.prepared.initialPrompt ?? setup.prompt,
|
|
1098
1298
|
cwd: setup.prepared.cwd,
|
|
@@ -1101,6 +1301,7 @@ export class WorkflowAgentExecutor {
|
|
|
1101
1301
|
// Pi sessions execute the prepared native definitions; the executor does not duplicate them as neutral request tools.
|
|
1102
1302
|
customTools: [],
|
|
1103
1303
|
...(options.schema ? { resultSchema: options.schema } : {}),
|
|
1304
|
+
...(attemptControl ? { turnControl: attemptControl } : {}),
|
|
1104
1305
|
run: runtimeRun,
|
|
1105
1306
|
agent: runtimeAgent,
|
|
1106
1307
|
signal: attemptSignal,
|
|
@@ -1167,7 +1368,6 @@ export interface ScheduledAgentOptions {
|
|
|
1167
1368
|
extensions?: readonly string[];
|
|
1168
1369
|
worktreeOwner?: string;
|
|
1169
1370
|
model?: string;
|
|
1170
|
-
role?: string;
|
|
1171
1371
|
contextFiles?: readonly ContextFileScope[];
|
|
1172
1372
|
schema?: JsonSchema;
|
|
1173
1373
|
retries?: number;
|
|
@@ -1177,6 +1377,10 @@ export interface ScheduledAgentOptions {
|
|
|
1177
1377
|
agentOptions?: Readonly<Record<string, JsonValue>>;
|
|
1178
1378
|
agentIdentity?: AgentIdentity;
|
|
1179
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[];
|
|
1180
1384
|
}
|
|
1181
1385
|
|
|
1182
1386
|
export type ScheduledAgentResult =
|
|
@@ -1214,13 +1418,41 @@ type ScheduledNode = {
|
|
|
1214
1418
|
resolveCompletion: () => void;
|
|
1215
1419
|
task: () => Promise<void>;
|
|
1216
1420
|
restored: boolean;
|
|
1421
|
+
//NOTE: attempt narrowing is ephemeral; the persisted logical configuration remains unchanged.
|
|
1422
|
+
attemptCapabilities?: AgentPreparationContext["capabilities"];
|
|
1423
|
+
attemptProjectTrusted?: boolean;
|
|
1217
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>;
|
|
1218
1427
|
};
|
|
1219
1428
|
|
|
1220
1429
|
type ScheduledRun = { limit: number; beforeLaunch?: () => void; extensionSettings?: Readonly<WorkflowExtensionSettings> | undefined; logical: number; active: number; nextIndex: number; queue: Array<{ node?: ScheduledNode; start: () => void }> };
|
|
1221
1430
|
export type OwnershipRecord = { id: string; parentId?: string; prompt?: string; label: string; state: ScheduledNode["state"]; options: Readonly<ScheduledAgentOptions> };
|
|
1222
1431
|
type OwnershipWriter = (runId: string, ownership: readonly OwnershipRecord[]) => void | Promise<void>;
|
|
1223
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
|
+
}
|
|
1224
1456
|
export class FairAgentScheduler {
|
|
1225
1457
|
readonly #runs = new Map<string, ScheduledRun>();
|
|
1226
1458
|
readonly #nodes = new Map<string, ScheduledNode>();
|
|
@@ -1253,6 +1485,17 @@ export class FairAgentScheduler {
|
|
|
1253
1485
|
if (!run) throw new WorkflowError("INTERNAL_ERROR", `Unknown scheduler run: ${runId}`);
|
|
1254
1486
|
run.extensionSettings = settings === undefined ? undefined : deepFreeze(structuredClone(settings));
|
|
1255
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
|
+
}
|
|
1256
1499
|
setExtensionSettings(agentId: string, settings: Readonly<WorkflowExtensionSettings>): void {
|
|
1257
1500
|
const node = this.#node(agentId);
|
|
1258
1501
|
node.options = Object.freeze({ ...node.options, extensionSettings: deepFreeze(structuredClone(settings)) });
|
|
@@ -1263,7 +1506,7 @@ export class FairAgentScheduler {
|
|
|
1263
1506
|
const run = this.#runs.get(runId);
|
|
1264
1507
|
if (!run) throw new WorkflowError("INTERNAL_ERROR", `Unknown scheduler run: ${runId}`);
|
|
1265
1508
|
const parent = parentId ? this.#nodes.get(parentId) : undefined;
|
|
1266
|
-
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");
|
|
1267
1510
|
const effective = this.#inherit(run, parent, options);
|
|
1268
1511
|
const id = `${runId}:${String(++this.#nextId)}`;
|
|
1269
1512
|
const tuiIndex = ++run.nextIndex;
|
|
@@ -1277,11 +1520,10 @@ export class FairAgentScheduler {
|
|
|
1277
1520
|
node.state = "running";
|
|
1278
1521
|
this.#persist(runId);
|
|
1279
1522
|
try {
|
|
1280
|
-
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; } });
|
|
1281
1524
|
this.#settle(node, { id, ok: true, value });
|
|
1282
1525
|
} catch (error) {
|
|
1283
|
-
|
|
1284
|
-
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) } });
|
|
1285
1527
|
}
|
|
1286
1528
|
};
|
|
1287
1529
|
this.#nodes.set(id, node);
|
|
@@ -1294,7 +1536,7 @@ export class FairAgentScheduler {
|
|
|
1294
1536
|
async result(parentId: string, childId: string): Promise<ScheduledAgentResult> {
|
|
1295
1537
|
const parent = this.#node(parentId);
|
|
1296
1538
|
const child = this.#node(childId);
|
|
1297
|
-
if (child.parentId !== parentId) throw new WorkflowError("
|
|
1539
|
+
if (child.parentId !== parentId) throw new WorkflowError("UNKNOWN_AGENT", "Results are scoped to direct children");
|
|
1298
1540
|
if (child.collected || !child.promise) throw new WorkflowError("AGENT_RESULT_COLLECTED", "Child result has already been collected; nested results are one-shot");
|
|
1299
1541
|
if (child.collecting) throw new WorkflowError("AGENT_FAILED", "Child result is already being collected");
|
|
1300
1542
|
child.collecting = true;
|
|
@@ -1328,7 +1570,7 @@ export class FairAgentScheduler {
|
|
|
1328
1570
|
|
|
1329
1571
|
async steer(parentId: string, childId: string, message: string): Promise<void> {
|
|
1330
1572
|
const child = this.#node(childId);
|
|
1331
|
-
if (child.parentId !== parentId) throw new WorkflowError("
|
|
1573
|
+
if (child.parentId !== parentId) throw new WorkflowError("UNKNOWN_AGENT", "Steering is scoped to direct children");
|
|
1332
1574
|
if (child.state !== "running" && child.state !== "waiting_for_child") throw new WorkflowError("AGENT_FAILED", "Child is not running");
|
|
1333
1575
|
if (!child.steer) throw new WorkflowError("AGENT_FAILED", "Child has not registered a steering handler");
|
|
1334
1576
|
await child.steer(message);
|
|
@@ -1380,29 +1622,31 @@ export class FairAgentScheduler {
|
|
|
1380
1622
|
this.#dispatch();
|
|
1381
1623
|
}
|
|
1382
1624
|
|
|
1383
|
-
toolsFor(parentId: string
|
|
1625
|
+
toolsFor(parentId: string): ToolDefinition[] {
|
|
1384
1626
|
const parent = this.#node(parentId);
|
|
1385
|
-
if (!parent.options.tools.includes("agent")) return [];
|
|
1627
|
+
if (!(parent.attemptCapabilities?.tools ?? parent.options.tools).includes("agent")) return [];
|
|
1386
1628
|
const agentTool = defineTool({
|
|
1387
|
-
name: "agent", label: "Child Agent", description: "Start a direct child agent",
|
|
1388
|
-
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 }),
|
|
1631
|
+
outputSchema: Type.Object({ id: Type.String() }),
|
|
1389
1632
|
execute: async (_id, params) => {
|
|
1390
1633
|
if (!isChildAgentToolParams(params)) throw new WorkflowError("INVALID_METADATA", "Invalid child agent parameters");
|
|
1391
1634
|
validateAgentOptions(params);
|
|
1392
1635
|
const outputSchema = params.outputSchema;
|
|
1393
1636
|
if (outputSchema !== undefined) validateSchema(outputSchema, "agent outputSchema");
|
|
1394
|
-
const tools =
|
|
1637
|
+
const tools = parent.attemptCapabilities?.tools ?? parent.options.tools;
|
|
1395
1638
|
const agentOptions = { ...params };
|
|
1396
1639
|
Reflect.deleteProperty(agentOptions, "prompt");
|
|
1397
|
-
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 }) };
|
|
1398
1641
|
const child = this.spawn(parent.runId, params.prompt, options, parentId);
|
|
1399
|
-
return { content: [{ type: "text" as const, text: JSON.stringify({ id: child.id }) }], details: { id: child.id } };
|
|
1642
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ id: child.id }) }], details: { id: child.id }, structuredContent: { id: child.id } };
|
|
1400
1643
|
},
|
|
1401
1644
|
});
|
|
1402
1645
|
const resultTool = defineTool({
|
|
1403
1646
|
name: "get_subagent_result", label: "Child Result", description: "Wait for a direct child and return its result once; repeated retrieval fails with AGENT_RESULT_COLLECTED",
|
|
1404
1647
|
parameters: Type.Object({ id: Type.String() }),
|
|
1405
|
-
|
|
1648
|
+
outputSchema: Type.Object({ id: Type.String(), ok: Type.Boolean(), value: Type.Optional(Type.Unknown()), error: Type.Optional(Type.Object({ code: Type.String(), message: Type.String() })) }),
|
|
1649
|
+
execute: async (_id, params) => { const value = await this.result(parentId, params.id); if (!value.ok && value.error.code === "BUDGET_EXHAUSTED") throw new WorkflowError("BUDGET_EXHAUSTED", value.error.message); return { content: [{ type: "text" as const, text: JSON.stringify(value) }], details: value, structuredContent: value }; }
|
|
1406
1650
|
});
|
|
1407
1651
|
const steerTool = defineTool({
|
|
1408
1652
|
name: "steer_subagent", label: "Steer Child", description: "Steer a running direct child",
|
|
@@ -1452,10 +1696,14 @@ export class FairAgentScheduler {
|
|
|
1452
1696
|
const extensionSettings = options.extensionSettings ?? parent?.options.extensionSettings ?? run.extensionSettings;
|
|
1453
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]) }) } : {}) });
|
|
1454
1698
|
if (options.cwd !== parent.options.cwd) throw new WorkflowError("UNKNOWN_TOOL", "Child cwd cannot differ from its parent");
|
|
1455
|
-
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));
|
|
1456
1701
|
if (forbidden) throw new WorkflowError("UNKNOWN_TOOL", `Child tool escalates parent boundary: ${forbidden}`);
|
|
1457
|
-
const identity = options.agentIdentity ?? parent.options.agentIdentity;
|
|
1458
|
-
|
|
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 } : {}) });
|
|
1459
1707
|
}
|
|
1460
1708
|
|
|
1461
1709
|
#enqueue(runId: string, node: ScheduledNode | undefined, start: () => void): void { this.#runs.get(runId)?.queue.push({ ...(node ? { node } : {}), start }); this.#dispatch(); }
|
|
@@ -1474,7 +1722,7 @@ export class FairAgentScheduler {
|
|
|
1474
1722
|
const item = run.queue.shift() as { node?: ScheduledNode; start: () => void };
|
|
1475
1723
|
if (item.node) {
|
|
1476
1724
|
try { run.beforeLaunch?.(); }
|
|
1477
|
-
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; }
|
|
1478
1726
|
}
|
|
1479
1727
|
run.active += 1; this.#active += 1; item.start();
|
|
1480
1728
|
}
|
|
@@ -1510,7 +1758,7 @@ export class FairAgentScheduler {
|
|
|
1510
1758
|
|
|
1511
1759
|
#node(id: string): ScheduledNode {
|
|
1512
1760
|
const node = this.#nodes.get(id);
|
|
1513
|
-
if (!node) throw new WorkflowError("
|
|
1761
|
+
if (!node) throw new WorkflowError("UNKNOWN_AGENT", `Unknown owned agent: ${id}`);
|
|
1514
1762
|
return node;
|
|
1515
1763
|
}
|
|
1516
1764
|
|