pi-extensible-workflows 3.0.0 → 3.1.0
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/README.md +15 -10
- package/dist/src/agent-execution.d.ts +4 -80
- package/dist/src/agent-execution.js +20 -10
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +48 -2
- package/dist/src/doctor-cleanup.d.ts +41 -0
- package/dist/src/doctor-cleanup.js +597 -0
- package/dist/src/doctor.d.ts +7 -2
- package/dist/src/doctor.js +127 -22
- package/dist/src/host.d.ts +40 -1
- package/dist/src/host.js +517 -129
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +2 -1
- package/dist/src/persistence.d.ts +3 -0
- package/dist/src/persistence.js +49 -1
- package/dist/src/registry.d.ts +21 -9
- package/dist/src/registry.js +131 -21
- package/dist/src/session-inspector.js +4 -2
- package/dist/src/types.d.ts +134 -7
- package/dist/src/utils.d.ts +6 -0
- package/dist/src/utils.js +45 -5
- package/dist/src/validation.d.ts +6 -2
- package/dist/src/validation.js +123 -31
- package/package.json +3 -2
- package/skills/pi-extensible-workflows/SKILL.md +50 -16
- package/src/agent-execution.ts +23 -37
- package/src/cli.ts +33 -2
- package/src/doctor-cleanup.ts +337 -0
- package/src/doctor.ts +117 -24
- package/src/host.ts +455 -122
- package/src/index.ts +4 -2
- package/src/persistence.ts +35 -1
- package/src/registry.ts +108 -25
- package/src/session-inspector.ts +4 -2
- package/src/types.ts +52 -7
- package/src/utils.ts +39 -5
- package/src/validation.ts +103 -31
package/dist/src/utils.d.ts
CHANGED
|
@@ -13,12 +13,18 @@ export declare function asWorkflowError(error: unknown): WorkflowError;
|
|
|
13
13
|
export declare function fail(code: WorkflowErrorCode, message: string): never;
|
|
14
14
|
export declare function parseThinking(value: unknown): ModelSpec["thinking"] | undefined;
|
|
15
15
|
export declare function parseModelReference(value: string): ModelSpec;
|
|
16
|
+
export declare function annotateModelAliasError(error: unknown, name: string): unknown;
|
|
17
|
+
export declare function modelAliasErrorName(error: unknown): string | undefined;
|
|
16
18
|
export declare function modelAliasName(value: string, aliases: Readonly<Record<string, string>>): string | undefined;
|
|
17
19
|
export declare function validateModelAliases(value: unknown, settingsPath?: string): Readonly<Record<string, string>>;
|
|
18
20
|
export declare function unknownModel(value: string, target: string | undefined, settingsPath?: string): never;
|
|
19
21
|
export declare function resolveModelReference(value: string, aliases?: Readonly<Record<string, string>>, knownModels?: ReadonlySet<string>, settingsPath?: string): ModelSpec;
|
|
20
22
|
export declare function modelCapability(value: string | ModelSpec, aliases?: Readonly<Record<string, string>>, knownModels?: ReadonlySet<string>, settingsPath?: string): string;
|
|
21
23
|
export declare function aliasDrift(previous: Readonly<Record<string, string>>, current: Readonly<Record<string, string>>): string[];
|
|
24
|
+
export declare function validateResourcePattern(pattern: string): void;
|
|
25
|
+
export declare function resourcePatternMatches(resource: string, pattern: string): boolean;
|
|
26
|
+
export declare function disabledResources(patterns: readonly string[], resources: readonly string[]): string[];
|
|
27
|
+
export declare function unmatchedResourcePatterns(patterns: readonly string[], resources: readonly string[]): string[];
|
|
22
28
|
export declare function mergeAgentResourceExclusions(...values: (AgentResourceExclusions | undefined)[]): AgentResourceExclusions;
|
|
23
29
|
export declare function createLaunchSnapshot(input: Omit<import("./types.js").LaunchSnapshot, "identityVersion"> & {
|
|
24
30
|
identityVersion?: number;
|
package/dist/src/utils.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ERROR_CODES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, WorkflowError } from "./types.js";
|
|
2
|
+
import { Minimatch } from "minimatch";
|
|
2
3
|
export function object(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
|
|
3
4
|
export { object as isObject };
|
|
4
5
|
export function jsonValue(value, seen = new Set()) {
|
|
@@ -61,7 +62,21 @@ export function parseModelReference(value) {
|
|
|
61
62
|
fail("UNKNOWN_MODEL", `Invalid thinking level: ${thinking}`);
|
|
62
63
|
return { provider: match[1], model: match[2], ...(thinking ? { thinking: thinking } : {}) };
|
|
63
64
|
}
|
|
64
|
-
|
|
65
|
+
const MODEL_ALIAS_ERROR_NAME = Symbol("modelAliasErrorName");
|
|
66
|
+
function aliasError(message, settingsPath, name) {
|
|
67
|
+
const error = new WorkflowError("CONFIG_ERROR", `${message} (settings: ${settingsPath})`);
|
|
68
|
+
if (name)
|
|
69
|
+
Object.defineProperty(error, MODEL_ALIAS_ERROR_NAME, { value: name, configurable: true });
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
export function annotateModelAliasError(error, name) {
|
|
73
|
+
if (error instanceof WorkflowError)
|
|
74
|
+
Object.defineProperty(error, MODEL_ALIAS_ERROR_NAME, { value: name, configurable: true });
|
|
75
|
+
return error;
|
|
76
|
+
}
|
|
77
|
+
export function modelAliasErrorName(error) {
|
|
78
|
+
return error instanceof WorkflowError ? error[MODEL_ALIAS_ERROR_NAME] : undefined;
|
|
79
|
+
}
|
|
65
80
|
export function modelAliasName(value, aliases) {
|
|
66
81
|
const name = /^([^/:\s]+)(?::[^:\s]+)?$/.exec(value)?.[1];
|
|
67
82
|
return name && Object.prototype.hasOwnProperty.call(aliases, name) ? name : undefined;
|
|
@@ -72,9 +87,9 @@ export function validateModelAliases(value, settingsPath = "workflow settings")
|
|
|
72
87
|
const aliases = {};
|
|
73
88
|
for (const [name, target] of Object.entries(value)) {
|
|
74
89
|
if (!MODEL_ALIAS_NAME.test(name))
|
|
75
|
-
aliasError(`Invalid model alias name: ${name}`, settingsPath);
|
|
90
|
+
aliasError(`Invalid model alias name: ${name}`, settingsPath, name);
|
|
76
91
|
if (typeof target !== "string" || !target.trim())
|
|
77
|
-
aliasError(`Invalid model alias target for ${name}`, settingsPath);
|
|
92
|
+
aliasError(`Invalid model alias target for ${name}`, settingsPath, name);
|
|
78
93
|
aliases[name] = target;
|
|
79
94
|
}
|
|
80
95
|
for (const name of Object.keys(aliases)) {
|
|
@@ -82,7 +97,7 @@ export function validateModelAliases(value, settingsPath = "workflow settings")
|
|
|
82
97
|
resolveModelReference(name, aliases);
|
|
83
98
|
}
|
|
84
99
|
catch (error) {
|
|
85
|
-
aliasError(`Invalid model alias target for ${name}: ${errorText(error)}`, settingsPath);
|
|
100
|
+
aliasError(`Invalid model alias target for ${name}: ${errorText(error)}`, settingsPath, name);
|
|
86
101
|
}
|
|
87
102
|
}
|
|
88
103
|
return Object.freeze(aliases);
|
|
@@ -123,6 +138,31 @@ export function modelCapability(value, aliases, knownModels, settingsPath) {
|
|
|
123
138
|
export function aliasDrift(previous, current) {
|
|
124
139
|
return [...new Set([...Object.keys(previous), ...Object.keys(current)])].sort().flatMap((name) => previous[name] === current[name] ? [] : [`${name}: ${previous[name] ?? "(missing)"} -> ${current[name] ?? "(missing)"}`]);
|
|
125
140
|
}
|
|
126
|
-
|
|
141
|
+
const RESOURCE_PATTERN_OPTIONS = { dot: true, nonegate: true, nocomment: true };
|
|
142
|
+
function resourcePatternBody(pattern) { return pattern.startsWith("!") ? pattern.slice(1) : pattern; }
|
|
143
|
+
function resourcePatternPath(value) { return value.replaceAll("\\", "/"); }
|
|
144
|
+
export function validateResourcePattern(pattern) {
|
|
145
|
+
const body = resourcePatternBody(pattern);
|
|
146
|
+
if (!body)
|
|
147
|
+
throw new Error(`Empty minimatch pattern ${JSON.stringify(pattern)}`);
|
|
148
|
+
const matcher = new Minimatch(resourcePatternPath(body), RESOURCE_PATTERN_OPTIONS);
|
|
149
|
+
if (matcher.makeRe() === false)
|
|
150
|
+
throw new Error(`Invalid minimatch pattern ${JSON.stringify(pattern)}`);
|
|
151
|
+
}
|
|
152
|
+
export function resourcePatternMatches(resource, pattern) { return new Minimatch(resourcePatternPath(resourcePatternBody(pattern)), RESOURCE_PATTERN_OPTIONS).match(resourcePatternPath(resource)); }
|
|
153
|
+
export function disabledResources(patterns, resources) {
|
|
154
|
+
const disabled = new Set();
|
|
155
|
+
for (const resource of resources) {
|
|
156
|
+
let excluded = false;
|
|
157
|
+
for (const pattern of patterns)
|
|
158
|
+
if (resourcePatternMatches(resource, pattern))
|
|
159
|
+
excluded = !pattern.startsWith("!");
|
|
160
|
+
if (excluded)
|
|
161
|
+
disabled.add(resource);
|
|
162
|
+
}
|
|
163
|
+
return [...disabled];
|
|
164
|
+
}
|
|
165
|
+
export function unmatchedResourcePatterns(patterns, resources) { return patterns.filter((pattern) => !resources.some((resource) => resourcePatternMatches(resource, pattern))); }
|
|
166
|
+
export function mergeAgentResourceExclusions(...values) { return { skills: values.flatMap((value) => value?.skills ?? []), extensions: values.flatMap((value) => value?.extensions ?? []) }; }
|
|
127
167
|
export function createLaunchSnapshot(input) { return deepFreeze(structuredClone({ ...input, launchKind: input.launchKind ?? (input.functionName ? "function" : "inline"), identityVersion: input.identityVersion ?? LAUNCH_SNAPSHOT_IDENTITY_VERSION })); }
|
|
128
168
|
export function loadLaunchSnapshot(input) { return deepFreeze(structuredClone(input)); }
|
package/dist/src/validation.d.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import type { AgentDefinition, AgentResourcePolicy, CheckpointInput, JsonSchema, JsonValue, LaunchSnapshot, PreflightCapabilities, PreflightResult, ShellOptions, StaticWorkflowCall, ValidatedWorkflowLaunch, WorkflowMetadata, WorkflowSettings, WorkflowValidationContext, WorkflowValidationParameters } from "./types.js";
|
|
1
|
+
import type { AgentDefinition, AgentResourcePolicy, CheckpointInput, JsonSchema, JsonValue, LaunchSnapshot, PreflightCapabilities, PreflightResult, ShellOptions, StaticWorkflowCall, ValidatedWorkflowLaunch, WorkflowMetadata, WorkflowRoleDirectoryRegistration, WorkflowSettings, WorkflowSettingsOverrides, WorkflowSettingsResolution, WorkflowValidationContext, WorkflowValidationParameters } from "./types.js";
|
|
2
2
|
import type { WorkflowRegistryApi } from "./registry.js";
|
|
3
3
|
export declare const DEFAULT_SETTINGS: Readonly<WorkflowSettings>;
|
|
4
4
|
export declare function validateCheckpoint(value: unknown): CheckpointInput;
|
|
5
5
|
export declare function workflowSettingsPath(agentDir?: string): string;
|
|
6
6
|
export declare function workflowProjectSettingsPath(cwd: string): string;
|
|
7
7
|
export declare function loadSettings(path?: string): Readonly<WorkflowSettings>;
|
|
8
|
+
export declare function loadSettingsOverrides(path: string): Readonly<WorkflowSettingsOverrides>;
|
|
9
|
+
export declare function resolveWorkflowSettings(cwd: string, projectTrusted: boolean, globalSettingsPath?: string): WorkflowSettingsResolution;
|
|
10
|
+
export declare function validateModelAliasAvailability(aliases: Readonly<Record<string, string>>, names: readonly string[], availableModels: ReadonlySet<string>, knownModels: ReadonlySet<string>, settingsPath?: string): void;
|
|
8
11
|
export declare function resolveAgentResourcePolicy(cwd: string, projectTrusted: boolean, globalSettingsPath?: string): AgentResourcePolicy;
|
|
9
12
|
export declare function saveModelAliases(path?: string, aliases?: Readonly<Record<string, string>>): void;
|
|
10
13
|
export declare function parseRoleMarkdown(content: string, strict?: boolean, rolePath?: string): AgentDefinition;
|
|
11
14
|
export declare function workflowRoleDirectories(agentDir?: string): readonly string[];
|
|
12
|
-
export
|
|
15
|
+
export type WorkflowRoleDirectoryInput = string | WorkflowRoleDirectoryRegistration;
|
|
16
|
+
export declare function loadAgentDefinitions(cwd: string, agentDir?: string, projectTrusted?: boolean, extensionRoleDirectories?: readonly WorkflowRoleDirectoryInput[]): Readonly<Record<string, AgentDefinition>>;
|
|
13
17
|
export declare function instrumentWorkflow(script: string): string;
|
|
14
18
|
export declare function workflowPrompt(template: string, values: Readonly<Record<string, JsonValue>>): string;
|
|
15
19
|
export declare function validateSchema(schema: unknown, at?: string): asserts schema is JsonSchema;
|
package/dist/src/validation.js
CHANGED
|
@@ -8,7 +8,8 @@ import { Script } from "node:vm";
|
|
|
8
8
|
import { Value } from "typebox/value";
|
|
9
9
|
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { WorkflowError } from "./types.js";
|
|
11
|
-
import {
|
|
11
|
+
import { registeredWorkflowRoleDirectoryRegistrations } from "./registry.js";
|
|
12
|
+
import { annotateModelAliasError, deepFreeze, errorText, fail, jsonValue, modelAliasName, modelCapability, object, parseThinking, positiveInteger, resolveModelReference, unknownModel, validateModelAliases, validateResourcePattern } from "./utils.js";
|
|
12
13
|
import { WORKFLOW_CALL_KINDS } from "./types.js";
|
|
13
14
|
export const DEFAULT_SETTINGS = Object.freeze({ concurrency: 8 });
|
|
14
15
|
export function validateCheckpoint(value) {
|
|
@@ -23,11 +24,27 @@ export function validateCheckpoint(value) {
|
|
|
23
24
|
export function workflowSettingsPath(agentDir = getAgentDir()) { return join(agentDir, ROLE_DIRECTORY, "settings.json"); }
|
|
24
25
|
export function workflowProjectSettingsPath(cwd) { return join(cwd, ".pi", ROLE_DIRECTORY, "settings.json"); }
|
|
25
26
|
const EMPTY_AGENT_RESOURCE_EXCLUSIONS = Object.freeze({ skills: [], extensions: [] });
|
|
27
|
+
function resourcePatternHasMagic(value) { return /[*?\x5b\x5d{}()]/.test(value); }
|
|
26
28
|
function normalizedResourcePath(value, settingsPath) {
|
|
27
29
|
let expanded = value === "~" ? homedir() : value.startsWith("~/") || value.startsWith("~\\") ? join(homedir(), value.slice(2)) : value;
|
|
28
30
|
if (expanded.startsWith("file://"))
|
|
29
31
|
expanded = fileURLToPath(expanded);
|
|
30
32
|
const resolved = resolve(dirname(settingsPath), expanded);
|
|
33
|
+
if (expanded === "**" || expanded.startsWith("**/") || expanded.startsWith("**\\"))
|
|
34
|
+
return expanded;
|
|
35
|
+
if (resourcePatternHasMagic(expanded)) {
|
|
36
|
+
const magicIndex = resolved.search(/[*?\x5b\x5d{}()]/);
|
|
37
|
+
const separatorIndex = Math.max(resolved.lastIndexOf("/", magicIndex), resolved.lastIndexOf("\\", magicIndex));
|
|
38
|
+
const rootBoundary = separatorIndex === 0 || (separatorIndex === 2 && /^[A-Za-z]:[\\/]/.test(resolved));
|
|
39
|
+
const prefix = rootBoundary ? resolved.slice(0, separatorIndex + 1) : separatorIndex >= 0 ? resolved.slice(0, separatorIndex) : resolved;
|
|
40
|
+
const suffix = rootBoundary ? resolved.slice(separatorIndex + 1) : separatorIndex >= 0 ? resolved.slice(separatorIndex) : "";
|
|
41
|
+
try {
|
|
42
|
+
return `${realpathSync(prefix)}${suffix}`;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return resolved;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
31
48
|
try {
|
|
32
49
|
return realpathSync(resolved);
|
|
33
50
|
}
|
|
@@ -51,35 +68,41 @@ function validateAgentResourceExclusions(value, settingsPath, errorCode = "INVAL
|
|
|
51
68
|
continue;
|
|
52
69
|
if (!Array.isArray(entries))
|
|
53
70
|
fail(errorCode, `${base}.${kind} must be an array`);
|
|
54
|
-
const seen = new Set();
|
|
55
71
|
for (const [index, entry] of entries.entries()) {
|
|
56
72
|
if (typeof entry !== "string" || !entry.trim())
|
|
57
73
|
fail(errorCode, `${base}.${kind}[${String(index)}] must be a non-empty string`);
|
|
58
74
|
let selector = entry.trim();
|
|
59
75
|
if (kind === "extensions") {
|
|
76
|
+
const negated = selector.startsWith("!");
|
|
77
|
+
const body = negated ? selector.slice(1) : selector;
|
|
78
|
+
if (!body)
|
|
79
|
+
fail(errorCode, `${base}.${kind}[${String(index)}] must be a valid minimatch pattern: Empty minimatch pattern ${JSON.stringify(selector)}`);
|
|
60
80
|
try {
|
|
61
|
-
selector = normalizedResourcePath(
|
|
81
|
+
selector = `${negated ? "!" : ""}${normalizedResourcePath(body, settingsPath)}`;
|
|
62
82
|
}
|
|
63
83
|
catch (error) {
|
|
64
84
|
fail(errorCode, `${base}.${kind}[${String(index)}] must be a valid path: ${errorText(error)}`);
|
|
65
85
|
}
|
|
66
86
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
normalized[kind].push(selector);
|
|
87
|
+
try {
|
|
88
|
+
validateResourcePattern(selector);
|
|
70
89
|
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
fail(errorCode, `${base}.${kind}[${String(index)}] must be a valid minimatch pattern: ${errorText(error)}`);
|
|
92
|
+
}
|
|
93
|
+
normalized[kind].push(selector);
|
|
71
94
|
}
|
|
72
95
|
}
|
|
73
96
|
return Object.freeze({ skills: Object.freeze(normalized.skills), extensions: Object.freeze(normalized.extensions) });
|
|
74
97
|
}
|
|
75
|
-
|
|
98
|
+
function parseSettings(path, partial) {
|
|
76
99
|
let parsed;
|
|
77
100
|
try {
|
|
78
101
|
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
79
102
|
}
|
|
80
103
|
catch (error) {
|
|
81
104
|
if (error.code === "ENOENT")
|
|
82
|
-
return DEFAULT_SETTINGS;
|
|
105
|
+
return partial ? Object.freeze({}) : DEFAULT_SETTINGS;
|
|
83
106
|
fail("CONFIG_ERROR", `Invalid workflow settings JSON at ${path}: ${errorText(error)}`);
|
|
84
107
|
}
|
|
85
108
|
if (!object(parsed))
|
|
@@ -88,19 +111,51 @@ export function loadSettings(path = workflowSettingsPath()) {
|
|
|
88
111
|
const unknown = Object.keys(parsed).find((key) => !allowed.has(key));
|
|
89
112
|
if (unknown)
|
|
90
113
|
fail("INVALID_SETTINGS", `Unknown workflow setting at ${path}: ${unknown}`);
|
|
91
|
-
const concurrency = parsed.concurrency === undefined ? DEFAULT_SETTINGS.concurrency : parsed.concurrency;
|
|
92
|
-
if (!positiveInteger(concurrency) || concurrency > 16)
|
|
114
|
+
const concurrency = parsed.concurrency === undefined ? (partial ? undefined : DEFAULT_SETTINGS.concurrency) : parsed.concurrency;
|
|
115
|
+
if (concurrency !== undefined && (!positiveInteger(concurrency) || concurrency > 16))
|
|
93
116
|
fail("INVALID_SETTINGS", `${path}.concurrency must be an integer from 1 to 16`);
|
|
94
117
|
const modelAliases = parsed.modelAliases === undefined ? undefined : validateModelAliases(parsed.modelAliases, path);
|
|
95
118
|
const disabledAgentResources = validateAgentResourceExclusions(parsed.disabledAgentResources, path);
|
|
96
|
-
return Object.freeze({ concurrency, ...(modelAliases ? {
|
|
119
|
+
return Object.freeze({ ...(concurrency === undefined ? {} : { concurrency }), ...(modelAliases === undefined ? {} : { modelAliases }), ...(disabledAgentResources === undefined ? {} : { disabledAgentResources }) });
|
|
97
120
|
}
|
|
98
|
-
export function
|
|
121
|
+
export function loadSettings(path = workflowSettingsPath()) { return parseSettings(path, false); }
|
|
122
|
+
export function loadSettingsOverrides(path) { return parseSettings(path, true); }
|
|
123
|
+
export function resolveWorkflowSettings(cwd, projectTrusted, globalSettingsPath = workflowSettingsPath()) {
|
|
99
124
|
const projectSettingsPath = workflowProjectSettingsPath(cwd);
|
|
100
|
-
const global = loadSettings(globalSettingsPath)
|
|
101
|
-
const project = projectTrusted ?
|
|
102
|
-
const
|
|
103
|
-
|
|
125
|
+
const global = loadSettings(globalSettingsPath);
|
|
126
|
+
const project = projectTrusted ? loadSettingsOverrides(projectSettingsPath) : Object.freeze({});
|
|
127
|
+
const projectHas = (key) => Object.prototype.hasOwnProperty.call(project, key);
|
|
128
|
+
const sources = {
|
|
129
|
+
concurrency: projectHas("concurrency") ? projectSettingsPath : globalSettingsPath,
|
|
130
|
+
modelAliases: projectHas("modelAliases") ? projectSettingsPath : globalSettingsPath,
|
|
131
|
+
disabledAgentResources: projectHas("disabledAgentResources") ? projectSettingsPath : globalSettingsPath,
|
|
132
|
+
};
|
|
133
|
+
const effective = Object.freeze({
|
|
134
|
+
concurrency: project.concurrency ?? global.concurrency,
|
|
135
|
+
...(projectHas("modelAliases") ? { modelAliases: project.modelAliases } : global.modelAliases === undefined ? {} : { modelAliases: global.modelAliases }),
|
|
136
|
+
...(projectHas("disabledAgentResources") ? { disabledAgentResources: project.disabledAgentResources } : global.disabledAgentResources === undefined ? {} : { disabledAgentResources: global.disabledAgentResources }),
|
|
137
|
+
});
|
|
138
|
+
return { globalSettingsPath, projectSettingsPath, projectTrusted, global, project, effective, sources };
|
|
139
|
+
}
|
|
140
|
+
export function validateModelAliasAvailability(aliases, names, availableModels, knownModels, settingsPath) {
|
|
141
|
+
for (const name of names) {
|
|
142
|
+
try {
|
|
143
|
+
const target = modelCapability(name, aliases, knownModels, settingsPath);
|
|
144
|
+
if (!availableModels.has(target))
|
|
145
|
+
unknownModel(name, target, settingsPath);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
throw annotateModelAliasError(error, name);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
export function resolveAgentResourcePolicy(cwd, projectTrusted, globalSettingsPath = workflowSettingsPath()) {
|
|
153
|
+
const resolved = resolveWorkflowSettings(cwd, projectTrusted, globalSettingsPath);
|
|
154
|
+
const empty = EMPTY_AGENT_RESOURCE_EXCLUSIONS;
|
|
155
|
+
const global = resolved.global.disabledAgentResources ?? empty;
|
|
156
|
+
const project = resolved.project.disabledAgentResources ?? empty;
|
|
157
|
+
const effective = resolved.effective.disabledAgentResources ?? empty;
|
|
158
|
+
return { globalSettingsPath: resolved.globalSettingsPath, projectSettingsPath: resolved.projectSettingsPath, projectTrusted, global, project, effective, unmatchedSkills: [], unmatchedExtensions: [] };
|
|
104
159
|
}
|
|
105
160
|
export function saveModelAliases(path = workflowSettingsPath(), aliases = {}) {
|
|
106
161
|
const normalized = validateModelAliases(aliases, path);
|
|
@@ -175,23 +230,60 @@ export function workflowRoleDirectories(agentDir = getAgentDir()) {
|
|
|
175
230
|
function projectRoleDirectories(root) {
|
|
176
231
|
return [join(root, ROLE_DIRECTORY, "roles")];
|
|
177
232
|
}
|
|
178
|
-
function
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
233
|
+
function roleDirectorySources(dirs) {
|
|
234
|
+
const seen = new Set();
|
|
235
|
+
return dirs.flatMap((value) => {
|
|
236
|
+
const source = typeof value === "string" ? { path: value } : { path: value.path, extension: value.extension };
|
|
237
|
+
if (seen.has(source.path))
|
|
238
|
+
return [];
|
|
239
|
+
seen.add(source.path);
|
|
240
|
+
return [source];
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
function roleDirectoryLabel(source, extension = true) {
|
|
244
|
+
return source.extension ? `Extension "${source.extension.headline}" (${source.extension.version})` : extension ? "Registered workflow extension" : "Standard workflow";
|
|
245
|
+
}
|
|
246
|
+
function scanRoleFiles(dirs, extension) {
|
|
247
|
+
const files = [];
|
|
248
|
+
for (const source of roleDirectorySources(dirs)) {
|
|
249
|
+
let entries;
|
|
250
|
+
try {
|
|
251
|
+
entries = readdirSync(source.path, { withFileTypes: true });
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
if (!extension && error.code === "ENOENT")
|
|
255
|
+
continue;
|
|
256
|
+
fail("INVALID_METADATA", `${roleDirectoryLabel(source, extension)} role directory "${source.path}" could not be scanned: ${errorText(error)}`);
|
|
257
|
+
}
|
|
258
|
+
for (const entry of entries)
|
|
259
|
+
if (entry.isFile() && extname(entry.name) === ".md")
|
|
260
|
+
files.push({ name: basename(entry.name, ".md"), path: join(source.path, entry.name), source });
|
|
183
261
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
262
|
+
return files.sort((left, right) => left.name.localeCompare(right.name) || left.path.localeCompare(right.path));
|
|
263
|
+
}
|
|
264
|
+
function readRoleDefinitions(dirs, extension = false) {
|
|
265
|
+
const files = scanRoleFiles(dirs, extension);
|
|
266
|
+
if (extension) {
|
|
267
|
+
const byName = new Map();
|
|
268
|
+
for (const file of files)
|
|
269
|
+
byName.set(file.name, [...(byName.get(file.name) ?? []), file]);
|
|
270
|
+
for (const [name, matches] of byName)
|
|
271
|
+
if (matches.length > 1)
|
|
272
|
+
fail("INVALID_METADATA", `Duplicate extension role "${name}": ${matches.map(({ path, source }) => `${roleDirectoryLabel(source)} role directory "${source.path}" (${path})`).join("; ")}`);
|
|
188
273
|
}
|
|
274
|
+
return Object.fromEntries(files.map(({ name, path, source }) => {
|
|
275
|
+
try {
|
|
276
|
+
return [name, parseRoleMarkdown(readFileSync(path, "utf8"), true, path)];
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
if (extension)
|
|
280
|
+
fail("INVALID_METADATA", `${roleDirectoryLabel(source)} role directory "${source.path}" contains invalid role "${name}" at "${path}": ${errorText(error)}`);
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
}));
|
|
189
284
|
}
|
|
190
|
-
function
|
|
191
|
-
return
|
|
192
|
-
}
|
|
193
|
-
export function loadAgentDefinitions(cwd, agentDir = getAgentDir(), projectTrusted = true) {
|
|
194
|
-
return deepFreeze({ ...readRoleDefinitions(workflowRoleDirectories(agentDir)), ...(projectTrusted ? readRoleDefinitions(projectRoleDirectories(join(cwd, ".pi"))) : {}) });
|
|
285
|
+
export function loadAgentDefinitions(cwd, agentDir = getAgentDir(), projectTrusted = true, extensionRoleDirectories = registeredWorkflowRoleDirectoryRegistrations()) {
|
|
286
|
+
return deepFreeze({ ...readRoleDefinitions(extensionRoleDirectories, true), ...readRoleDefinitions(workflowRoleDirectories(agentDir)), ...(projectTrusted ? readRoleDefinitions(projectRoleDirectories(join(cwd, ".pi"))) : {}) });
|
|
195
287
|
}
|
|
196
288
|
function validateRolePolicies(definitions, roles, availableModels, rootTools, aliases = {}, knownModels = availableModels, settingsPath) {
|
|
197
289
|
for (const role of roles) {
|
|
@@ -709,7 +801,7 @@ export function validateWorkflowLaunchWithRegistry(params, context, registry) {
|
|
|
709
801
|
if (!script)
|
|
710
802
|
fail("INVALID_SYNTAX", "Provide script or registered function");
|
|
711
803
|
const metadata = validateWorkflowMetadata({ name: workflowName, ...(typeof params.description === "string" ? { description: params.description } : fn?.description ? { description: fn.description } : {}) });
|
|
712
|
-
const globalAgentDefinitions = loadAgentDefinitions(context.cwd, context.agentDir, false);
|
|
804
|
+
const globalAgentDefinitions = loadAgentDefinitions(context.cwd, context.agentDir, false, registry && typeof registry.roleDirectoryRegistrations === "function" ? registry.roleDirectoryRegistrations() : registry && typeof registry.roleDirectories === "function" ? registry.roleDirectories() : undefined);
|
|
713
805
|
const projectAgentDefinitions = context.projectTrusted ? readRoleDefinitions(projectRoleDirectories(join(context.cwd, ".pi"))) : {};
|
|
714
806
|
const agentDefinitions = deepFreeze({ ...globalAgentDefinitions, ...projectAgentDefinitions });
|
|
715
807
|
const aliases = context.modelAliases ?? {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-extensible-workflows",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Deterministic multi-agent workflow orchestration for Pi",
|
|
5
5
|
"homepage": "https://vekexasia.github.io/pi-extensible-workflows/",
|
|
6
6
|
"repository": {
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
},
|
|
65
65
|
"license": "MIT",
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"acorn": "^8.17.0"
|
|
67
|
+
"acorn": "^8.17.0",
|
|
68
|
+
"minimatch": "^10.2.5"
|
|
68
69
|
}
|
|
69
70
|
}
|
|
@@ -4,30 +4,49 @@ description: Use when the task is complex enough to require multiple subagents o
|
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# pi-extensible-workflows
|
|
7
|
+
|
|
7
8
|
Use `workflow` only for genuinely multi-agent orchestration; one agent uses ordinary tools or `Agent` directly. Give phases distinct responsibilities and keep result flow explicit.
|
|
8
9
|
|
|
9
10
|
## Example
|
|
10
|
-
```js
|
|
11
|
-
const reportSchema = { type: "object", properties: { summary: { type: "string" }, findings: { type: "array", items: { type: "string" } } }, required: ["summary", "findings"], additionalProperties: false };
|
|
12
11
|
|
|
12
|
+
```js
|
|
13
|
+
const reportSchema = {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
summary: { type: "string" },
|
|
17
|
+
findings: { type: "array", items: { type: "string" } },
|
|
18
|
+
},
|
|
19
|
+
required: ["summary", "findings"],
|
|
20
|
+
additionalProperties: false,
|
|
21
|
+
};
|
|
13
22
|
|
|
14
23
|
const reports = await parallel("research", {
|
|
15
|
-
first: () =>
|
|
16
|
-
|
|
24
|
+
first: () =>
|
|
25
|
+
agent("Research the first target.", {
|
|
26
|
+
role: "scout",
|
|
27
|
+
outputSchema: reportSchema,
|
|
28
|
+
}),
|
|
29
|
+
second: () =>
|
|
30
|
+
agent("Research the second target.", {
|
|
31
|
+
role: "scout",
|
|
32
|
+
outputSchema: reportSchema,
|
|
33
|
+
}),
|
|
17
34
|
});
|
|
18
35
|
|
|
19
|
-
return agent(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
);
|
|
36
|
+
return agent(prompt("Review these reports:\n\n{reports}", { reports }), {
|
|
37
|
+
role: "reviewer",
|
|
38
|
+
outputSchema: reportSchema,
|
|
39
|
+
});
|
|
23
40
|
```
|
|
24
41
|
|
|
25
42
|
Inline launches require a non-empty `name`. Registered function launches must omit `name`; they use `workflow` as the run name:
|
|
43
|
+
|
|
26
44
|
```json
|
|
27
45
|
{ "workflow": "workflowName", "args": { "issue": 42 } }
|
|
28
46
|
```
|
|
47
|
+
|
|
29
48
|
Inside the workflow, read `args.issue` (`args` is `null` when omitted). `workflow_stop` requires the exact run ID; foreground results retain their value and completed `runId`, while background launches return `runId` immediately. A terminal `parentRunId` reuses matching named `withWorktree` scopes but does not replay journal results. For an explicitly failed run, use the exact `runId` from failure diagnostics with `workflow_retry({ runId })`: diagnostics list replayable and incomplete paths, artifacts, and valid named worktrees; the tool creates a linked child, replays completed agent, shell, function, and checkpoint operations, and executes incomplete work. Retry versus per-agent `retries` and `workflow_resume` is always explicit; external side effects before failure are not guaranteed exactly once.
|
|
30
|
-
Inspect tool `workflow_catalog` result at least once before creating the first workflow for a task.
|
|
49
|
+
Inspect tool `workflow_catalog` result at least once before creating the first workflow for a task.
|
|
31
50
|
|
|
32
51
|
Workflow JavaScript has no imports, filesystem, network, process, or timers. Delegate that work to agents. `shell(command, options)` is the trusted host RPC for deterministic gates: it inherits the workflow or active-worktree cwd, merges string `env` overrides, and returns `{ exitCode, stdout, stderr }`; nonzero exits are results, but launch failures and timeouts fail with `SHELL_FAILED`.
|
|
33
52
|
|
|
@@ -37,20 +56,24 @@ Example use of `shell`:
|
|
|
37
56
|
// ... other code
|
|
38
57
|
const testRes = await shell("yarn test", { env: { CI: "1" } });
|
|
39
58
|
if (testRes.exitCode === 0) {
|
|
40
|
-
|
|
41
|
-
|
|
59
|
+
// success path
|
|
60
|
+
return { ok: true };
|
|
42
61
|
}
|
|
43
62
|
```
|
|
44
63
|
|
|
45
64
|
Most of the times using `shell()` to perform mutations is an antipattern. Use it mainly for verifications or idempotent actions.
|
|
46
65
|
|
|
47
66
|
## `agent()` options
|
|
67
|
+
|
|
48
68
|
```typescript
|
|
49
69
|
export interface AgentOptions {
|
|
50
|
-
label?: string;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
70
|
+
label?: string;
|
|
71
|
+
model?: string;
|
|
72
|
+
role?: string;
|
|
73
|
+
tools?: string[];
|
|
74
|
+
thinking?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
75
|
+
outputSchema?: JsonSchema;
|
|
76
|
+
retries?: number;
|
|
54
77
|
timeoutMs?: number | null;
|
|
55
78
|
[key: string]: JsonValue;
|
|
56
79
|
}
|
|
@@ -61,24 +84,34 @@ Extensions may add JSON-compatible agent options such as `advisor: true`; core k
|
|
|
61
84
|
Agent calls are unnamed. Direct calls receive hidden source call-site identity; aliases are unsupported, and calls from one source site must not race outside `parallel` or `pipeline`, whose structural keys make replay deterministic.
|
|
62
85
|
|
|
63
86
|
## Passing agent results
|
|
87
|
+
|
|
64
88
|
Use independent `agent(prompt, options)` calls and pass each completed result explicitly to the next prompt. This keeps workflow execution deterministic and makes replay state local to each call:
|
|
89
|
+
|
|
65
90
|
```js
|
|
66
91
|
const findings = await agent("Inspect the implementation.");
|
|
67
|
-
const fix = await agent(
|
|
92
|
+
const fix = await agent(
|
|
93
|
+
prompt("Propose the smallest fix from these findings:\n\n{findings}", {
|
|
94
|
+
findings,
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
68
97
|
return { findings, fix };
|
|
69
98
|
```
|
|
70
99
|
|
|
71
100
|
## Worktrees
|
|
101
|
+
|
|
72
102
|
Use `withWorktree(name, callback)` for top-level agents that collaborate in one explicitly named worktree scope:
|
|
103
|
+
|
|
73
104
|
```js
|
|
74
105
|
const result = await withWorktree("issue", async ({ path, branch }) => {
|
|
75
106
|
const report = await agent("Implement the issue");
|
|
76
107
|
return { path, branch, report };
|
|
77
108
|
});
|
|
78
109
|
```
|
|
110
|
+
|
|
79
111
|
Entering the scope materializes its worktree before the callback. The callback receives a frozen reference containing only the real string `path` and `branch`; callbacks may ignore the argument, and their bare return value is preserved. Concurrent agents share mutable files, so assign non-conflicting work or coordinate explicitly.
|
|
80
112
|
|
|
81
113
|
Branches may call any workflow function, not only `agent()`. Use separate named scopes when parallel branches need isolated worktrees:
|
|
114
|
+
|
|
82
115
|
```js
|
|
83
116
|
const results = await parallel("implementation", {
|
|
84
117
|
api: () => withWorktree("api", () => agent("Implement the API")),
|
|
@@ -89,6 +122,7 @@ const results = await parallel("implementation", {
|
|
|
89
122
|
Registered extension functions receive `withWorktree` in context and can compose other registered functions with `context.invoke("reviewRepository", { focus: "security" })`. Their public inputs and outputs remain JSON; callbacks cannot cross the extension boundary.
|
|
90
123
|
|
|
91
124
|
## Rules
|
|
125
|
+
|
|
92
126
|
- Use `log(messageString)` for brief operator status.
|
|
93
127
|
- A role owns execution policy: with `role`, do not set `model`, `thinking`, or `tools`; only task options such as `outputSchema`, retries, timeout, or a `withWorktree` scope may accompany it.
|
|
94
128
|
- Use `parallel()` for independent tasks with different flows and `pipeline()` when every keyed item follows the same ordered stages; do not duplicate identical chains in `parallel()`. Signatures are `parallel(operationName, tasksRecord)` and `pipeline(operationName, itemsRecord, stagesRecord)`; keys are stable task, item, and stage names.
|
package/src/agent-execution.ts
CHANGED
|
@@ -2,13 +2,14 @@ import { realpathSync } from "node:fs";
|
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { Type } from "@earendil-works/pi-ai";
|
|
4
4
|
import { Value } from "typebox/value";
|
|
5
|
-
import { createAgentSession, DefaultPackageManager, DefaultResourceLoader, getAgentDir, ModelRuntime, SessionManager, SettingsManager, type
|
|
5
|
+
import { createAgentSession, DefaultPackageManager, DefaultResourceLoader, getAgentDir, ModelRuntime, SessionManager, SettingsManager, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
7
7
|
type AgentMessage = { role: string; content?: unknown; stopReason?: string; errorMessage?: string; usage?: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: { total: number } } };
|
|
8
|
-
import type { AgentIdentity, AgentResourceExclusions, AgentResourcePolicy, AgentSetupSummary, JsonSchema, JsonValue, ModelSpec, WorkflowRunContext } from "./types.js";
|
|
9
|
-
import { jsonObject, mergeAgentResourceExclusions, modelAliasName, modelCapability, resolveModelReference } from "./utils.js";
|
|
8
|
+
import type { AgentIdentity, AgentResourceExclusions, AgentResourcePolicy, AgentSetup, AgentSetupSummary, JsonSchema, JsonValue, ModelSpec, NativeSession, RegisteredAgentSetupHook, SessionFactory, SessionInput, WorkflowRunContext } from "./types.js";
|
|
9
|
+
import { jsonObject, disabledResources, mergeAgentResourceExclusions, modelAliasName, modelCapability, resolveModelReference, unmatchedResourcePatterns } from "./utils.js";
|
|
10
10
|
import { WorkflowError } from "./types.js";
|
|
11
11
|
import type { RunStore } from "./persistence.js";
|
|
12
|
+
export type { AgentSetup, AgentSetupContext, AgentSetupHook, NativeSession, RegisteredAgentSetupHook, SessionFactory, SessionInput } from "./types.js";
|
|
12
13
|
export interface AgentBudgetHooks {
|
|
13
14
|
beforeAttempt(): void;
|
|
14
15
|
beforeTurn(): void;
|
|
@@ -66,29 +67,6 @@ export interface AgentActivity { kind: "reasoning" | "tool" | "text"; text: stri
|
|
|
66
67
|
export interface AgentProgress { accounting: AgentAccounting; toolCalls: readonly AgentToolCallProgress[]; activity?: AgentActivity; persist: boolean }
|
|
67
68
|
export interface AgentAttempt { attempt: number; sessionId: string; sessionFile: string; result?: JsonValue; error?: { code: string; message: string }; accounting: AgentAccounting; setup?: AgentSetupSummary }
|
|
68
69
|
export interface AgentExecutionResult { value: JsonValue; attempts: readonly AgentAttempt[]; cwd: string }
|
|
69
|
-
export interface AgentSetup { prompt: string; options: Record<string, JsonValue>; sessionInput: SessionInput; createSession: SessionFactory }
|
|
70
|
-
export interface AgentSetupContext { readonly run: Readonly<WorkflowRunContext>; readonly identity: Readonly<AgentIdentity>; readonly attempt: number; readonly signal: AbortSignal }
|
|
71
|
-
export interface AgentSetupHook { priority?: number; setup: (agent: AgentSetup, context: Readonly<AgentSetupContext>) => void | Promise<void> }
|
|
72
|
-
export interface RegisteredAgentSetupHook { name: string; priority: number; setup: AgentSetupHook["setup"] }
|
|
73
|
-
type NativeSessionStats = Pick<SessionStats, "tokens" | "cost">;
|
|
74
|
-
export interface NativeSession {
|
|
75
|
-
readonly sessionId: string;
|
|
76
|
-
readonly sessionFile: string | undefined;
|
|
77
|
-
readonly messages: readonly AgentMessage[];
|
|
78
|
-
getSessionStats(): NativeSessionStats;
|
|
79
|
-
readonly systemPrompt?: string;
|
|
80
|
-
readonly model?: { provider: string; model?: string; id?: string };
|
|
81
|
-
readonly agent?: { state: { tools: readonly { name: string }[] } };
|
|
82
|
-
getLeafId?: () => string | null;
|
|
83
|
-
getToolDefinitions?: () => unknown;
|
|
84
|
-
subscribe?(listener: (event: AgentSessionEvent) => void): () => void;
|
|
85
|
-
prompt(text: string): Promise<void>;
|
|
86
|
-
steer?(text: string): Promise<void>;
|
|
87
|
-
abort?(): Promise<void>;
|
|
88
|
-
dispose(): void;
|
|
89
|
-
}
|
|
90
|
-
export interface SessionInput { cwd: string; model: ModelSpec; tools: string[]; sessionLabel: string; agentDir?: string; customTools?: ToolDefinition[]; resultTool?: ToolDefinition; systemPromptAppend?: string; extensionFactories?: InlineExtension[]; resourcePolicy?: AgentResourcePolicy; options?: Record<string, JsonValue> }
|
|
91
|
-
export type SessionFactory = (input: SessionInput) => Promise<NativeSession>;
|
|
92
70
|
|
|
93
71
|
function parseModel(value: string | undefined, fallback: ModelSpec, thinking?: ThinkingLevel, aliases: Readonly<Record<string, string>> = {}, knownModels?: ReadonlySet<string>, settingsPath?: string): ModelSpec {
|
|
94
72
|
if (!value) return { ...fallback, ...(thinking ? { thinking } : {}) };
|
|
@@ -129,7 +107,7 @@ function terminalProviderError(error: WorkflowError): TerminalProviderError | un
|
|
|
129
107
|
return typeof candidate.provider === "string" && typeof candidate.model === "string" && typeof candidate.error === "string" ? { provider: candidate.provider, model: candidate.model, error: candidate.error } : undefined;
|
|
130
108
|
}
|
|
131
109
|
|
|
132
|
-
function accounting(stats:
|
|
110
|
+
function accounting(stats: ReturnType<NativeSession["getSessionStats"]>): AgentAccounting {
|
|
133
111
|
return { input: stats.tokens.input, output: stats.tokens.output, cacheRead: stats.tokens.cacheRead, cacheWrite: stats.tokens.cacheWrite, cost: stats.cost };
|
|
134
112
|
}
|
|
135
113
|
function canonicalSourcePath(path: string): string { try { return realpathSync(path); } catch { return resolve(path); } }
|
|
@@ -151,14 +129,17 @@ export async function createNativeAgentSession(input: SessionInput): Promise<Nat
|
|
|
151
129
|
settingsManager.setProjectTrusted(policy.projectTrusted);
|
|
152
130
|
const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager });
|
|
153
131
|
const resolved = await packageManager.resolve();
|
|
154
|
-
const
|
|
155
|
-
const
|
|
132
|
+
const discoveredExtensions = [...new Set(resolved.extensions.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => canonicalSourcePath(path)))];
|
|
133
|
+
const excludedExtensions = new Set(disabledResources(policy.effective.extensions, discoveredExtensions));
|
|
134
|
+
const extensionPaths = discoveredExtensions.filter((path) => !excludedExtensions.has(path));
|
|
135
|
+
Object.assign(policy, { excludedExtensions: [...excludedExtensions], unmatchedExtensions: unmatchedResourcePatterns(policy.effective.extensions, discoveredExtensions) });
|
|
156
136
|
const skillPaths = [...new Set(resolved.skills.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => path))];
|
|
157
|
-
const updateSkillMatches = (skills: readonly { name: string }[]) => {
|
|
158
|
-
const names = new Set(skills.map(({ name }) => name));
|
|
159
|
-
|
|
137
|
+
const updateSkillMatches = (skills: readonly { name: string }[]): Set<string> => {
|
|
138
|
+
const names = [...new Set(skills.map(({ name }) => name))];
|
|
139
|
+
const excludedSkills = disabledResources(policy.effective.skills, names);
|
|
140
|
+
Object.assign(policy, { excludedSkills, unmatchedSkills: unmatchedResourcePatterns(policy.effective.skills, names) });
|
|
141
|
+
return new Set(excludedSkills);
|
|
160
142
|
};
|
|
161
|
-
const disabledSkills = new Set(policy.effective.skills);
|
|
162
143
|
resourceLoader = new DefaultResourceLoader({
|
|
163
144
|
cwd: input.cwd,
|
|
164
145
|
agentDir,
|
|
@@ -169,14 +150,12 @@ export async function createNativeAgentSession(input: SessionInput): Promise<Nat
|
|
|
169
150
|
additionalSkillPaths: skillPaths,
|
|
170
151
|
...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}),
|
|
171
152
|
skillsOverride: (base) => {
|
|
172
|
-
updateSkillMatches(base.skills);
|
|
153
|
+
const disabledSkills = updateSkillMatches(base.skills);
|
|
173
154
|
return { ...base, skills: base.skills.filter(({ name }) => !disabledSkills.has(name)) };
|
|
174
155
|
},
|
|
175
156
|
...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}),
|
|
176
157
|
});
|
|
177
158
|
await resourceLoader.reload();
|
|
178
|
-
const discoveredExtensions = new Set(resolved.extensions.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => canonicalSourcePath(path)));
|
|
179
|
-
Object.assign(policy, { unmatchedExtensions: policy.effective.extensions.filter((path) => !discoveredExtensions.has(canonicalSourcePath(path))) });
|
|
180
159
|
} else if (input.systemPromptAppend || input.extensionFactories?.length) {
|
|
181
160
|
resourceLoader = new DefaultResourceLoader({ cwd: input.cwd, agentDir, ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}), ...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}) });
|
|
182
161
|
await resourceLoader.reload();
|
|
@@ -218,7 +197,7 @@ function fallbackSetupContext(root: AgentExecutionRoot, options: AgentExecutionO
|
|
|
218
197
|
return { run, identity: Object.freeze({ ...identity, structuralPath: Object.freeze([...identity.structuralPath]) }) };
|
|
219
198
|
}
|
|
220
199
|
function resourcePolicySummary(policy: AgentResourcePolicy): NonNullable<AgentSetupSummary["disabledAgentResources"]> {
|
|
221
|
-
return { skills: [...policy.effective.skills], extensions: [...policy.effective.extensions], unmatchedSkills: [...policy.unmatchedSkills], unmatchedExtensions: [...policy.unmatchedExtensions] };
|
|
200
|
+
return { skills: [...policy.effective.skills], extensions: [...policy.effective.extensions], excludedSkills: [...(policy.excludedSkills ?? [])], excludedExtensions: [...(policy.excludedExtensions ?? [])], unmatchedSkills: [...policy.unmatchedSkills], unmatchedExtensions: [...policy.unmatchedExtensions] };
|
|
222
201
|
}
|
|
223
202
|
async function prepareAgentSetup(root: AgentExecutionRoot, createSession: SessionFactory, task: string, options: AgentExecutionOptions, resolved: { model: ModelSpec; tools: readonly string[]; systemPromptAppend: string }, cwd: string, attempt: number, signal: AbortSignal | undefined, customTools: readonly ToolDefinition[], resultTool: ToolDefinition | undefined): Promise<{ setup: AgentSetup; summary: AgentSetupSummary }> {
|
|
224
203
|
const setupSignal = signal ?? root.runContext?.signal ?? new AbortController().signal;
|
|
@@ -518,6 +497,13 @@ export class FairAgentScheduler {
|
|
|
518
497
|
this.#runs.set(runId, { limit, ...(beforeLaunch ? { beforeLaunch } : {}), logical: 0, active: 0, queue: [] });
|
|
519
498
|
this.#runOrder.push(runId);
|
|
520
499
|
}
|
|
500
|
+
updateRunLimit(runId: string, limit: number): void {
|
|
501
|
+
const run = this.#runs.get(runId);
|
|
502
|
+
if (!run) throw new WorkflowError("INTERNAL_ERROR", `Unknown scheduler run: ${runId}`);
|
|
503
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > this.sessionLimit) throw new WorkflowError("INVALID_SETTINGS", "Invalid run concurrency");
|
|
504
|
+
run.limit = limit;
|
|
505
|
+
this.#dispatch();
|
|
506
|
+
}
|
|
521
507
|
|
|
522
508
|
spawn(runId: string, prompt: string, options: ScheduledAgentOptions, parentId?: string): { id: string; result: Promise<ScheduledAgentResult> } {
|
|
523
509
|
const run = this.#runs.get(runId);
|