@marcoscale98/pi-extensible-workflows 5.14.1-fork.1 → 5.19.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 +135 -0
- package/dist/src/agent-execution.js +114 -87
- package/dist/src/background-widget.d.ts +61 -0
- package/dist/src/background-widget.js +23 -16
- package/dist/src/decoders.js +28 -1
- package/dist/src/execution.d.ts +4 -2
- package/dist/src/execution.js +43 -2
- package/dist/src/host-delivery.js +1 -0
- package/dist/src/host-navigator.d.ts +18 -0
- package/dist/src/host-navigator.js +71 -13
- package/dist/src/host-recovery.d.ts +5 -2
- package/dist/src/host-recovery.js +3 -3
- package/dist/src/host-view.d.ts +21 -1
- package/dist/src/host-view.js +138 -47
- package/dist/src/host.d.ts +2 -2
- package/dist/src/host.js +246 -60
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +1346 -622
- package/dist/src/index.js.map +3 -3
- package/dist/src/paths.d.ts +2 -0
- package/dist/src/paths.js +2 -0
- package/dist/src/registry.js +3 -2
- package/dist/src/roles.d.ts +70 -0
- package/dist/src/roles.js +326 -0
- package/dist/src/script-tools.d.ts +27 -0
- package/dist/src/script-tools.js +87 -0
- package/dist/src/settings.d.ts +26 -0
- package/dist/src/settings.js +277 -0
- package/dist/src/trajectory.d.ts +39 -4
- package/dist/src/trajectory.js +82 -5
- package/dist/src/types.d.ts +30 -3
- package/dist/src/types.js +1 -1
- package/dist/src/utils.d.ts +21 -1
- package/dist/src/utils.js +40 -0
- package/dist/src/validation.d.ts +10 -17
- package/dist/src/validation.js +29 -458
- package/dist/starter/index.js +0 -2
- package/dist/starter/index.js.map +1 -1
- package/dist/starter/review-loop.d.ts +0 -13
- package/dist/starter/review-loop.js +0 -2
- package/dist/subagents/index.js +1344 -798
- package/dist/subagents/index.js.map +3 -3
- package/dist/subagents/src/contracts.d.ts +49 -29
- package/dist/subagents/src/contracts.js +18 -7
- package/dist/subagents/src/index.d.ts +1 -1
- package/dist/subagents/src/index.js +15 -7
- package/dist/subagents/src/manager.js +32 -13
- package/dist/subagents/src/navigator.js +396 -247
- package/dist/subagents/src/view.d.ts +24 -2
- package/dist/subagents/src/view.js +107 -80
- package/dist/trajectory/assets/index.html +370 -232
- package/dist/trajectory/assets/prism.min.js +13 -0
- package/dist/trajectory/index.js +445 -277
- package/dist/trajectory/index.js.map +3 -3
- package/dist/trajectory/src/assets/index.html +370 -232
- package/dist/trajectory/src/assets/prism.min.js +13 -0
- package/dist/trajectory/src/export.js +4 -2
- package/dist/trajectory/src/index.d.ts +2 -0
- package/dist/trajectory/src/index.js +151 -60
- package/dist/trajectory/src/server.js +138 -11
- package/dist/trajectory/src/server.js.map +2 -2
- package/package.json +7 -6
- package/skills/pi-extensible-workflows/SKILL.md +2 -0
- package/src/agent-execution.ts +110 -80
- package/src/background-widget.ts +26 -19
- package/src/decoders.ts +22 -1
- package/src/execution.ts +41 -4
- package/src/host-delivery.ts +1 -0
- package/src/host-navigator.ts +58 -14
- package/src/host-recovery.ts +8 -7
- package/src/host-view.ts +116 -42
- package/src/host.ts +223 -55
- package/src/index.ts +3 -1
- package/src/paths.ts +3 -0
- package/src/registry.ts +3 -2
- package/src/roles.ts +327 -0
- package/src/script-tools.ts +76 -0
- package/src/settings.ts +224 -0
- package/src/trajectory.ts +97 -9
- package/src/types.ts +12 -5
- package/src/utils.ts +36 -1
- package/src/validation.ts +30 -356
- package/starter/review-loop.ts +0 -2
- package/subagents/README.md +3 -3
- package/subagents/src/contracts.ts +22 -7
- package/subagents/src/index.ts +18 -7
- package/subagents/src/manager.ts +31 -13
- package/subagents/src/navigator.ts +336 -180
- package/subagents/src/view.ts +108 -76
- package/trajectory/src/assets/index.html +370 -232
- package/trajectory/src/assets/prism.min.js +13 -0
- package/trajectory/src/export.ts +4 -2
- package/trajectory/src/index.ts +124 -47
- package/trajectory/src/server.ts +114 -14
package/src/validation.ts
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
6
3
|
import * as acorn from "acorn";
|
|
7
4
|
import { Script } from "node:vm";
|
|
8
|
-
import {
|
|
9
|
-
import type { AgentDefinition, AgentResourceSelectors, AgentResourceSelectorSet, AgentResourcePolicy, CheckpointInput, ContextFileScope, JsonSchema, JsonValue, PreflightCapabilities, PreflightResult, ShellOptions, StaticWorkflowCall, StaticWorkflowExecution, StaticWorkflowScope, ValidatedWorkflowLaunch, WorkflowCallKind, WorkflowErrorCode, WorkflowExtensionMetadata, WorkflowExtensionSettings, WorkflowMetadata, WorkflowRetentionSettings, WorkflowRoleDirectoryRegistration, WorkflowSettings, WorkflowSettingsOverrides, WorkflowSettingsResolution, WorkflowSettingsSources, WorkflowValidationContext, WorkflowValidationParameters } from "./types.js";
|
|
5
|
+
import type { AgentDefinition, CheckpointInput, JsonSchema, JsonValue, PreflightCapabilities, PreflightResult, ShellOptions, StaticWorkflowCall, StaticWorkflowExecution, StaticWorkflowScope, ValidatedWorkflowLaunch, WorkflowCallKind, WorkflowErrorCode, WorkflowMetadata, WorkflowValidationContext, WorkflowValidationParameters } from "./types.js";
|
|
10
6
|
import type { WorkflowRegistryApi } from "./registry.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
export {
|
|
7
|
+
import { assertModelThinking, validateSchema, deepFreeze, errorText, fail, jsonObject, jsonValue, mergeWorkflowExtensionSettings, modelAliasName, modelCapability, object, positiveInteger, resolveModelReference, resourcePatternHasMagic, unknownModel } from "./utils.js";
|
|
8
|
+
import { WORKFLOW_CALL_KINDS } from "./types.js";
|
|
9
|
+
export { validateSchema } from "./utils.js";
|
|
10
|
+
import { loadAgentDefinitions, loadProjectAgentDefinitions } from "./roles.js";
|
|
11
|
+
export { loadAgentDefinitions, loadProjectAgentDefinitions, parseRoleMarkdown, workflowRoleDirectories } from "./roles.js";
|
|
12
|
+
export type { WorkflowRoleDirectoryInput } from "./roles.js";
|
|
13
|
+
|
|
14
|
+
import { validateContextFileScopes, validateSelectorList } from "./settings.js";
|
|
15
|
+
export { DEFAULT_SETTINGS, loadCodemodeToolsSetting, loadSettings, loadSettingsOverrides, resolveAgentResourcePolicy, resolveWorkflowSettings, saveModelAliases, validateContextFileScopes, validateModelAliasAvailability, validateSelectorList, validateWorkflowExtensionSettings, workflowProjectSettingsPath, workflowSettingsPath, workflowToolExposure } from "./settings.js";
|
|
16
16
|
|
|
17
|
-
export const DEFAULT_SETTINGS: Readonly<WorkflowSettings> = Object.freeze({ concurrency: 8, backgroundWidget: true });
|
|
18
17
|
export function validateCheckpoint(value: unknown): CheckpointInput {
|
|
19
18
|
if (!object(value) || Object.keys(value).some((key) => !["name", "prompt", "context"].includes(key)) || typeof value.name !== "string" || value.name.trim() === "" || typeof value.prompt !== "string" || !jsonValue(value.context)) fail("INVALID_METADATA", "checkpoint requires only name, prompt, and JSON context");
|
|
20
19
|
if (Buffer.byteLength(value.prompt) > 1024) fail("INVALID_METADATA", "checkpoint prompt exceeds 1024 UTF-8 bytes");
|
|
@@ -22,342 +21,6 @@ export function validateCheckpoint(value: unknown): CheckpointInput {
|
|
|
22
21
|
return { name: value.name, prompt: value.prompt, context: value.context };
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
export function workflowSettingsPath(agentDir = getAgentDir()): string { return join(agentDir, ROLE_DIRECTORY, "settings.json"); }
|
|
26
|
-
function normalizedResourcePath(value: string, settingsPath: string): string {
|
|
27
|
-
if (value === "*") return value;
|
|
28
|
-
let expanded = value === "~" ? homedir() : value.startsWith("~/") || value.startsWith("~\\") ? join(homedir(), value.slice(2)) : value;
|
|
29
|
-
if (expanded.startsWith("file://")) expanded = fileURLToPath(expanded);
|
|
30
|
-
const resolved = resolve(dirname(settingsPath), expanded);
|
|
31
|
-
if (expanded === "**" || expanded.startsWith("**/") || expanded.startsWith("**\\")) return expanded;
|
|
32
|
-
if (resourcePatternHasMagic(expanded)) {
|
|
33
|
-
const magicIndex = resolved.search(/[*?\x5b\x5d{}()]/);
|
|
34
|
-
const separatorIndex = Math.max(resolved.lastIndexOf("/", magicIndex), resolved.lastIndexOf("\\", magicIndex));
|
|
35
|
-
const rootBoundary = separatorIndex === 0 || (separatorIndex === 2 && /^[A-Za-z]:[\\/]/.test(resolved));
|
|
36
|
-
const prefix = rootBoundary ? resolved.slice(0, separatorIndex + 1) : separatorIndex >= 0 ? resolved.slice(0, separatorIndex) : resolved;
|
|
37
|
-
const suffix = rootBoundary ? resolved.slice(separatorIndex + 1) : separatorIndex >= 0 ? resolved.slice(separatorIndex) : "";
|
|
38
|
-
return `${canonicalPath(prefix)}${suffix}`;
|
|
39
|
-
}
|
|
40
|
-
return canonicalPath(resolved);
|
|
41
|
-
}
|
|
42
|
-
function validateSelectorList(value: unknown, path: string, kind: "skills" | "extensions" | "tools", errorCode: "INVALID_SETTINGS" | "INVALID_METADATA" = "INVALID_SETTINGS", normalizeExtensions = true): readonly string[] | undefined {
|
|
43
|
-
if (value === undefined) return undefined;
|
|
44
|
-
if (!Array.isArray(value)) fail(errorCode, `${path}.${kind} must be an array`);
|
|
45
|
-
const normalized: string[] = [];
|
|
46
|
-
for (const [index, entry] of value.entries()) {
|
|
47
|
-
if (typeof entry !== "string" || !entry.trim()) fail(errorCode, `${path}.${kind}[${String(index)}] must be a non-empty string`);
|
|
48
|
-
let selector = entry.trim();
|
|
49
|
-
if (kind === "extensions" && normalizeExtensions) {
|
|
50
|
-
const negated = selector.startsWith("!");
|
|
51
|
-
const body = negated ? selector.slice(1) : selector;
|
|
52
|
-
if (!body) fail(errorCode, `${path}.${kind}[${String(index)}] must be a valid minimatch pattern: Empty minimatch pattern ${JSON.stringify(selector)}`);
|
|
53
|
-
try { selector = `${negated ? "!" : ""}${normalizedResourcePath(body, path)}`; } catch (error) { fail(errorCode, `${path}.${kind}[${String(index)}] must be a valid path: ${errorText(error)}`); }
|
|
54
|
-
}
|
|
55
|
-
try { validateResourcePattern(selector); } catch (error) { fail(errorCode, `${path}.${kind}[${String(index)}] must be a valid minimatch pattern: ${errorText(error)}`); }
|
|
56
|
-
normalized.push(selector);
|
|
57
|
-
}
|
|
58
|
-
return Object.freeze(normalized);
|
|
59
|
-
}
|
|
60
|
-
function validateWorktreePostCreateCommand(value: unknown, settingsPath: string): readonly string[] | undefined {
|
|
61
|
-
if (value === undefined) return undefined;
|
|
62
|
-
if (!Array.isArray(value) || value.length === 0) fail("INVALID_SETTINGS", `${settingsPath}.worktreePostCreateCommand must be a non-empty array`);
|
|
63
|
-
return Object.freeze(value.map((entry, index) => {
|
|
64
|
-
if (typeof entry !== "string" || !entry.trim()) fail("INVALID_SETTINGS", `${settingsPath}.worktreePostCreateCommand[${String(index)}] must be a non-empty string`);
|
|
65
|
-
return entry;
|
|
66
|
-
}));
|
|
67
|
-
}
|
|
68
|
-
function selectorsFromSettings(settings: Readonly<WorkflowSettings | WorkflowSettingsOverrides>): AgentResourceSelectors {
|
|
69
|
-
return {
|
|
70
|
-
...(settings.skills === undefined ? {} : { skills: settings.skills }),
|
|
71
|
-
...(settings.extensions === undefined ? {} : { extensions: settings.extensions }),
|
|
72
|
-
...(settings.tools === undefined ? {} : { tools: settings.tools }),
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
function selectorSet(value: AgentResourceSelectors | undefined): AgentResourceSelectorSet { return { skills: [...(value?.skills ?? [])], extensions: [...(value?.extensions ?? [])], ...(value?.tools === undefined ? {} : { tools: [...value.tools] }) }; }
|
|
76
|
-
function validateContextFileScopes(value: unknown, rolePath: string): readonly ContextFileScope[] | undefined {
|
|
77
|
-
if (value === undefined) return undefined;
|
|
78
|
-
if (!Array.isArray(value) || !value.every(isContextFileScope)) fail("INVALID_METADATA", `${rolePath}.contextFiles must be an array containing only global, project, or cwd`);
|
|
79
|
-
return [...value];
|
|
80
|
-
}
|
|
81
|
-
export function validateWorkflowExtensionSettings(value: unknown, settingsPath: string, errorCode: "INVALID_SETTINGS" | "INVALID_METADATA" = "INVALID_SETTINGS", applyDefaults = true): WorkflowExtensionSettings | undefined {
|
|
82
|
-
if (value === undefined) return undefined;
|
|
83
|
-
const base = `${settingsPath}.extensionSettings`;
|
|
84
|
-
if (!object(value)) fail(errorCode, `${base} must be an object`);
|
|
85
|
-
const normalized: Record<string, JsonValue> = {};
|
|
86
|
-
for (const [namespace, raw] of Object.entries(value)) {
|
|
87
|
-
if (!validWorkflowExtensionNamespace(namespace)) fail(errorCode, `${base} contains an invalid namespace: ${namespace}`);
|
|
88
|
-
if (namespace === "herdr") {
|
|
89
|
-
if (!object(raw)) fail(errorCode, `${base}.herdr must be an object`);
|
|
90
|
-
if (Object.keys(raw).some((key) => key !== "enableFullyInspectableMode")) fail(errorCode, `${base}.herdr contains an unsupported setting`);
|
|
91
|
-
if (raw.enableFullyInspectableMode !== undefined && typeof raw.enableFullyInspectableMode !== "boolean") fail(errorCode, `${base}.herdr.enableFullyInspectableMode must be a boolean`);
|
|
92
|
-
normalized.herdr = Object.freeze({ ...(raw.enableFullyInspectableMode === undefined ? {} : { enableFullyInspectableMode: raw.enableFullyInspectableMode }) });
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
if (namespace === "trajectory") {
|
|
96
|
-
if (!object(raw)) fail(errorCode, `${base}.trajectory must be an object`);
|
|
97
|
-
if (Object.keys(raw).some((key) => key !== "port" && key !== "themes")) fail(errorCode, `${base}.trajectory contains an unsupported setting`);
|
|
98
|
-
if (raw.port !== undefined && (!positiveInteger(raw.port) || raw.port > 65535)) fail(errorCode, `${base}.trajectory.port must be an integer from 1 to 65535`);
|
|
99
|
-
if (raw.themes !== undefined && typeof raw.themes !== "boolean") fail(errorCode, `${base}.trajectory.themes must be a boolean`);
|
|
100
|
-
normalized.trajectory = Object.freeze({ ...(raw.port === undefined ? {} : { port: raw.port }), ...(raw.themes === undefined ? (applyDefaults ? { themes: false } : {}) : { themes: raw.themes }) });
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
if (!jsonValue(raw)) fail(errorCode, `${base}.${namespace} must be JSON-compatible`);
|
|
104
|
-
normalized[namespace] = structuredClone(raw);
|
|
105
|
-
}
|
|
106
|
-
return deepFreeze(normalized as WorkflowExtensionSettings);
|
|
107
|
-
}
|
|
108
|
-
function positiveRetentionInteger(value: unknown): value is number { return positiveInteger(value) && Number.isSafeInteger(value); }
|
|
109
|
-
function validateRetention(value: unknown, settingsPath: string): Readonly<WorkflowRetentionSettings> | undefined {
|
|
110
|
-
if (value === undefined) return undefined;
|
|
111
|
-
const base = `${settingsPath}.retention`;
|
|
112
|
-
if (!object(value)) fail("INVALID_SETTINGS", `${base} must be an object`);
|
|
113
|
-
const unknown = Object.keys(value).find((key) => key !== "olderThanDays" && key !== "maxTerminalRuns");
|
|
114
|
-
if (unknown) fail("INVALID_SETTINGS", `Unknown retention setting at ${base}: ${unknown}`);
|
|
115
|
-
if (value.olderThanDays !== undefined && !positiveRetentionInteger(value.olderThanDays)) fail("INVALID_SETTINGS", `${base}.olderThanDays must be a positive integer`);
|
|
116
|
-
if (value.maxTerminalRuns !== undefined && !positiveRetentionInteger(value.maxTerminalRuns)) fail("INVALID_SETTINGS", `${base}.maxTerminalRuns must be a positive integer`);
|
|
117
|
-
return Object.freeze({ ...(value.olderThanDays === undefined ? {} : { olderThanDays: value.olderThanDays }), ...(value.maxTerminalRuns === undefined ? {} : { maxTerminalRuns: value.maxTerminalRuns }) });
|
|
118
|
-
}
|
|
119
|
-
function parseSettings(path: string, partial: false): Readonly<WorkflowSettings>;
|
|
120
|
-
function parseSettings(path: string, partial: true): Readonly<WorkflowSettingsOverrides>;
|
|
121
|
-
function parseSettings(path: string, partial: boolean): Readonly<WorkflowSettings | WorkflowSettingsOverrides> {
|
|
122
|
-
let parsed: unknown;
|
|
123
|
-
try { parsed = JSON.parse(readFileSync(path, "utf8")); }
|
|
124
|
-
catch (error) {
|
|
125
|
-
if (isNodeError(error, "ENOENT")) return partial ? Object.freeze({}) : DEFAULT_SETTINGS;
|
|
126
|
-
fail("CONFIG_ERROR", `Invalid workflow settings JSON at ${path}: ${errorText(error)}`);
|
|
127
|
-
}
|
|
128
|
-
if (!object(parsed)) fail("INVALID_SETTINGS", `Workflow settings at ${path} must be an object`);
|
|
129
|
-
const allowed = new Set(["concurrency", "modelAliases", "skills", "extensions", "extensionSettings", "tools", "retention", "worktreePostCreateCommand", ...(partial ? [] : ["backgroundWidget"]) ]);
|
|
130
|
-
const unknown = Object.keys(parsed).find((key) => !allowed.has(key));
|
|
131
|
-
if (Object.prototype.hasOwnProperty.call(parsed, "disabledAgentResources")) fail("INVALID_SETTINGS", `disabledAgentResources is no longer supported; use skills, extensions, and tools selectors (settings: ${path})`);
|
|
132
|
-
if (unknown) fail("INVALID_SETTINGS", `Unknown workflow setting at ${path}: ${unknown}`);
|
|
133
|
-
const concurrency = parsed.concurrency === undefined ? (partial ? undefined : DEFAULT_SETTINGS.concurrency) : parsed.concurrency;
|
|
134
|
-
if (concurrency !== undefined && (!positiveInteger(concurrency) || concurrency > 16)) fail("INVALID_SETTINGS", `${path}.concurrency must be an integer from 1 to 16`);
|
|
135
|
-
const backgroundWidget = parsed.backgroundWidget === undefined ? (partial ? undefined : DEFAULT_SETTINGS.backgroundWidget) : parsed.backgroundWidget;
|
|
136
|
-
if (backgroundWidget !== undefined && typeof backgroundWidget !== "boolean") fail("INVALID_SETTINGS", `${path}.backgroundWidget must be a boolean`);
|
|
137
|
-
const modelAliases = parsed.modelAliases === undefined ? undefined : validateModelAliases(parsed.modelAliases, path);
|
|
138
|
-
const skills = validateSelectorList(parsed.skills, path, "skills");
|
|
139
|
-
const tools = validateSelectorList(parsed.tools, path, "tools");
|
|
140
|
-
const extensions = validateSelectorList(parsed.extensions, path, "extensions");
|
|
141
|
-
const extensionSettings = parsed.extensionSettings === undefined ? undefined : validateWorkflowExtensionSettings(parsed.extensionSettings, path, "INVALID_SETTINGS", !partial);
|
|
142
|
-
const retention = validateRetention(parsed.retention, path);
|
|
143
|
-
const worktreePostCreateCommand = validateWorktreePostCreateCommand(parsed.worktreePostCreateCommand, path);
|
|
144
|
-
return Object.freeze({
|
|
145
|
-
...(concurrency === undefined ? {} : { concurrency }), ...(backgroundWidget === undefined ? {} : { backgroundWidget }), ...(modelAliases === undefined ? {} : { modelAliases }),
|
|
146
|
-
...(skills === undefined ? {} : { skills }), ...(extensions === undefined ? {} : { extensions }),
|
|
147
|
-
...(extensionSettings === undefined ? {} : { extensionSettings }), ...(tools === undefined ? {} : { tools }), ...(retention === undefined ? {} : { retention }),
|
|
148
|
-
...(worktreePostCreateCommand === undefined ? {} : { worktreePostCreateCommand }),
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
export function loadSettings(path = workflowSettingsPath()): Readonly<WorkflowSettings> { return parseSettings(path, false); }
|
|
152
|
-
export function loadSettingsOverrides(path: string): Readonly<WorkflowSettingsOverrides> { return parseSettings(path, true); }
|
|
153
|
-
export function resolveWorkflowSettings(cwd: string, projectTrusted: boolean, globalSettingsPath = workflowSettingsPath()): WorkflowSettingsResolution {
|
|
154
|
-
const projectSettingsPath = workflowProjectSettingsPath(cwd);
|
|
155
|
-
const global = loadSettings(globalSettingsPath);
|
|
156
|
-
const project: Readonly<WorkflowSettingsOverrides> = projectTrusted ? loadSettingsOverrides(projectSettingsPath) : Object.freeze({});
|
|
157
|
-
const projectHas = (key: keyof WorkflowSettingsOverrides): boolean => Object.prototype.hasOwnProperty.call(project, key);
|
|
158
|
-
const sourceFor = (key: "skills" | "extensions" | "tools"): string => projectHas(key) ? projectSettingsPath : globalSettingsPath;
|
|
159
|
-
const globalSelectors = selectorsFromSettings(global);
|
|
160
|
-
const projectSelectors = selectorsFromSettings(project);
|
|
161
|
-
const effectiveSelectors = selectorSet({
|
|
162
|
-
skills: [...(globalSelectors.skills ?? []), ...(projectSelectors.skills ?? [])],
|
|
163
|
-
extensions: [...(globalSelectors.extensions ?? []), ...(projectSelectors.extensions ?? [])],
|
|
164
|
-
...(globalSelectors.tools === undefined && projectSelectors.tools === undefined ? {} : { tools: [...(globalSelectors.tools ?? []), ...(projectSelectors.tools ?? [])] }),
|
|
165
|
-
});
|
|
166
|
-
const hasExtensionSelectors = global.extensions !== undefined || project.extensions !== undefined;
|
|
167
|
-
const extensionSettings = projectHas("extensionSettings") ? project.extensionSettings : global.extensionSettings;
|
|
168
|
-
const sources: WorkflowSettingsSources = {
|
|
169
|
-
concurrency: projectHas("concurrency") ? projectSettingsPath : globalSettingsPath,
|
|
170
|
-
modelAliases: projectHas("modelAliases") ? projectSettingsPath : globalSettingsPath,
|
|
171
|
-
skills: sourceFor("skills"), extensions: sourceFor("extensions"), tools: sourceFor("tools"),
|
|
172
|
-
...(extensionSettings === undefined ? {} : { extensionSettings: projectHas("extensionSettings") ? projectSettingsPath : globalSettingsPath }),
|
|
173
|
-
...(project.retention === undefined && global.retention === undefined ? {} : { retention: project.retention === undefined ? globalSettingsPath : projectSettingsPath }),
|
|
174
|
-
...(projectHas("worktreePostCreateCommand") || global.worktreePostCreateCommand !== undefined ? { worktreePostCreateCommand: projectHas("worktreePostCreateCommand") ? projectSettingsPath : globalSettingsPath } : {}),
|
|
175
|
-
};
|
|
176
|
-
const effective = Object.freeze({
|
|
177
|
-
concurrency: project.concurrency ?? global.concurrency,
|
|
178
|
-
backgroundWidget: global.backgroundWidget ?? true,
|
|
179
|
-
...(projectHas("modelAliases") ? { modelAliases: project.modelAliases } : global.modelAliases === undefined ? {} : { modelAliases: global.modelAliases }),
|
|
180
|
-
...(effectiveSelectors.skills.length ? { skills: effectiveSelectors.skills } : global.skills !== undefined || project.skills !== undefined ? { skills: effectiveSelectors.skills } : {}),
|
|
181
|
-
...(hasExtensionSelectors ? { extensions: effectiveSelectors.extensions } : {}),
|
|
182
|
-
...(extensionSettings === undefined ? {} : { extensionSettings }),
|
|
183
|
-
...(effectiveSelectors.tools?.length ? { tools: effectiveSelectors.tools } : global.tools !== undefined || project.tools !== undefined ? { tools: effectiveSelectors.tools } : {}),
|
|
184
|
-
...((project.retention ?? global.retention) === undefined ? {} : { retention: project.retention ?? global.retention }),
|
|
185
|
-
...(projectHas("worktreePostCreateCommand") ? { worktreePostCreateCommand: project.worktreePostCreateCommand } : global.worktreePostCreateCommand === undefined ? {} : { worktreePostCreateCommand: global.worktreePostCreateCommand }),
|
|
186
|
-
});
|
|
187
|
-
return { globalSettingsPath, projectSettingsPath, projectTrusted, global, project, effective, sources };
|
|
188
|
-
}
|
|
189
|
-
export function validateModelAliasAvailability(aliases: Readonly<Record<string, string>>, names: readonly string[], availableModels: ReadonlySet<string>, knownModels: ReadonlySet<string>, settingsPath?: string): void {
|
|
190
|
-
for (const name of names) {
|
|
191
|
-
try {
|
|
192
|
-
const target = modelCapability(name, aliases, knownModels, settingsPath);
|
|
193
|
-
if (!availableModels.has(target)) unknownModel(name, target, settingsPath);
|
|
194
|
-
} catch (error) { throw annotateModelAliasError(error, name); }
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
export function resolveAgentResourcePolicy(cwd: string, projectTrusted: boolean, globalSettingsPath = workflowSettingsPath()): AgentResourcePolicy {
|
|
198
|
-
const resolved = resolveWorkflowSettings(cwd, projectTrusted, globalSettingsPath);
|
|
199
|
-
const global = selectorSet(selectorsFromSettings(resolved.global));
|
|
200
|
-
const project = selectorSet(selectorsFromSettings(resolved.project));
|
|
201
|
-
const effective = selectorSet(selectorsFromSettings(resolved.effective));
|
|
202
|
-
return { globalSettingsPath: resolved.globalSettingsPath, projectSettingsPath: resolved.projectSettingsPath, projectTrusted, global, project, effective, unmatchedSkills: [], unmatchedExtensions: [], unmatchedTools: [], selectorSources: { global: selectorsFromSettings(resolved.global), project: selectorsFromSettings(resolved.project) } };
|
|
203
|
-
}
|
|
204
|
-
export function saveModelAliases(path = workflowSettingsPath(), aliases: Readonly<Record<string, string>> = {}): void {
|
|
205
|
-
const normalized = validateModelAliases(aliases, path);
|
|
206
|
-
let parsed: unknown = {};
|
|
207
|
-
try {
|
|
208
|
-
loadSettings(path);
|
|
209
|
-
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
210
|
-
} catch (error) {
|
|
211
|
-
if (!isNodeError(error, "ENOENT")) throw error;
|
|
212
|
-
}
|
|
213
|
-
if (!object(parsed)) fail("INVALID_SETTINGS", `Workflow settings at ${path} must be an object`);
|
|
214
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
215
|
-
atomicWriteFile(path, `${JSON.stringify({ ...parsed, modelAliases: normalized }, null, 2)}\n`, true);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export function parseRoleMarkdown(content: string, strict = false, rolePath?: string): AgentDefinition {
|
|
219
|
-
if (!strict) {
|
|
220
|
-
if (!content.startsWith("---\n")) return { prompt: content };
|
|
221
|
-
const end = content.indexOf("\n---", 4);
|
|
222
|
-
if (end < 0) return { prompt: content };
|
|
223
|
-
const meta: Record<string, string> = {};
|
|
224
|
-
for (const line of content.slice(4, end).split("\n")) {
|
|
225
|
-
const match = /^(model|tools|skills|extensions|description|overrideSystemPrompt|override_system_prompt|is_system_prompt|contextFiles|extensionSettings|disabledAgentResources|thinking)\s*:\s*(.+)$/.exec(line.trim());
|
|
226
|
-
if (match?.[1] === "disabledAgentResources") fail("INVALID_METADATA", "disabledAgentResources is no longer supported; use skills, extensions, and tools selectors");
|
|
227
|
-
if (match?.[1] === "thinking") fail("INVALID_METADATA", "Role thinking is not supported; put it on model as provider/model:thinking");
|
|
228
|
-
if (match?.[1] && match[2]) meta[match[1]] = match[2].trim();
|
|
229
|
-
}
|
|
230
|
-
const unquote = (v: string) => v.replace(/^['"]|['"]$/g, "");
|
|
231
|
-
const parseList = (value: string | undefined): string[] | undefined => value === undefined ? undefined : value.replace(/^\[|\]$/g, "").split(",").map((entry) => unquote(entry.trim())).filter(Boolean);
|
|
232
|
-
const tools = parseList(meta.tools);
|
|
233
|
-
const skills = parseList(meta.skills);
|
|
234
|
-
const extensions = parseList(meta.extensions);
|
|
235
|
-
let extensionSettings: unknown;
|
|
236
|
-
if (meta.extensionSettings !== undefined) {
|
|
237
|
-
try { extensionSettings = JSON.parse(meta.extensionSettings) as unknown; }
|
|
238
|
-
catch (error) { fail("INVALID_METADATA", `Invalid role extensionSettings: ${errorText(error)}`); }
|
|
239
|
-
}
|
|
240
|
-
const rolePathValue = rolePath ?? "<role>";
|
|
241
|
-
const normalizedExtensionSettings = validateWorkflowExtensionSettings(extensionSettings, rolePathValue, "INVALID_METADATA", false);
|
|
242
|
-
const definition: AgentDefinition = { prompt: content.slice(end + 4).replace(/^\n/, "") };
|
|
243
|
-
if (meta.model) {
|
|
244
|
-
const model = unquote(meta.model);
|
|
245
|
-
assertModelThinking(model, "Role model");
|
|
246
|
-
definition.model = model;
|
|
247
|
-
}
|
|
248
|
-
if (meta.description) definition.description = unquote(meta.description);
|
|
249
|
-
if (tools) definition.tools = tools;
|
|
250
|
-
if (skills) definition.skills = skills;
|
|
251
|
-
if (extensions) definition.extensions = extensions;
|
|
252
|
-
if (normalizedExtensionSettings !== undefined) definition.extensionSettings = normalizedExtensionSettings;
|
|
253
|
-
const overrideSystemPrompt = meta.overrideSystemPrompt ?? meta.override_system_prompt ?? meta.is_system_prompt;
|
|
254
|
-
const contextFiles = meta.contextFiles ? meta.contextFiles.replace(/^\[|\]$/g, "").split(",").map((scope) => unquote(scope.trim())).filter(Boolean) : undefined;
|
|
255
|
-
const normalizedContextFiles = validateContextFileScopes(contextFiles, "role");
|
|
256
|
-
if (overrideSystemPrompt) definition.overrideSystemPrompt = overrideSystemPrompt === "true";
|
|
257
|
-
if (normalizedContextFiles) definition.contextFiles = normalizedContextFiles;
|
|
258
|
-
return definition;
|
|
259
|
-
}
|
|
260
|
-
const normalized = content.replace(/\r\n?/g, "\n");
|
|
261
|
-
if (normalized.startsWith("---\n") && normalized.indexOf("\n---", 3) < 0) fail("INVALID_METADATA", "Role frontmatter is missing its closing delimiter");
|
|
262
|
-
let parsed: ReturnType<typeof parseFrontmatter>;
|
|
263
|
-
try { parsed = parseFrontmatter(content); }
|
|
264
|
-
catch (error) { fail("INVALID_METADATA", `Invalid role frontmatter: ${errorText(error)}`); }
|
|
265
|
-
if (!object(parsed.frontmatter)) fail("INVALID_METADATA", "Role frontmatter must be an object");
|
|
266
|
-
const { model, tools, skills, extensions, extensionSettings, description, contextFiles } = parsed.frontmatter;
|
|
267
|
-
if (Object.prototype.hasOwnProperty.call(parsed.frontmatter, "disabledAgentResources")) fail("INVALID_METADATA", "disabledAgentResources is no longer supported; use skills, extensions, and tools selectors");
|
|
268
|
-
if (Object.prototype.hasOwnProperty.call(parsed.frontmatter, "thinking")) fail("INVALID_METADATA", "Role thinking is not supported; put it on model as provider/model:thinking");
|
|
269
|
-
const overrideSystemPrompt = parsed.frontmatter.overrideSystemPrompt ?? parsed.frontmatter.override_system_prompt ?? parsed.frontmatter.is_system_prompt;
|
|
270
|
-
if (model !== undefined && (typeof model !== "string" || model.trim() === "")) fail("INVALID_METADATA", "Role model must be a non-empty string");
|
|
271
|
-
if (description !== undefined && (typeof description !== "string" || description.trim() === "" || description.length > 1024 || /[\r\n]/.test(description))) fail("INVALID_METADATA", "Role description must be a non-empty single-line string of at most 1024 characters");
|
|
272
|
-
if (overrideSystemPrompt !== undefined && typeof overrideSystemPrompt !== "boolean") fail("INVALID_METADATA", "Role overrideSystemPrompt must be a boolean");
|
|
273
|
-
const normalizedContextFiles = validateContextFileScopes(contextFiles, rolePath ?? "<role>");
|
|
274
|
-
const rolePathValue = rolePath ?? "<role>";
|
|
275
|
-
const normalizedTools = validateSelectorList(tools, rolePathValue, "tools", "INVALID_METADATA");
|
|
276
|
-
const normalizedSkills = validateSelectorList(skills, rolePathValue, "skills", "INVALID_METADATA");
|
|
277
|
-
const normalizedExtensions = validateSelectorList(extensions, rolePathValue, "extensions", "INVALID_METADATA");
|
|
278
|
-
const normalizedExtensionSettings = validateWorkflowExtensionSettings(extensionSettings, rolePathValue, "INVALID_METADATA", false);
|
|
279
|
-
const normalizedDescription = typeof description === "string" ? description.trim() : undefined;
|
|
280
|
-
const normalizedModel = typeof model === "string" ? model.trim() : undefined;
|
|
281
|
-
if (normalizedModel !== undefined) assertModelThinking(normalizedModel, "Role model");
|
|
282
|
-
const definition: AgentDefinition = { prompt: parsed.body };
|
|
283
|
-
if (normalizedDescription !== undefined) definition.description = normalizedDescription;
|
|
284
|
-
if (normalizedModel !== undefined) definition.model = normalizedModel;
|
|
285
|
-
if (normalizedTools !== undefined) definition.tools = normalizedTools;
|
|
286
|
-
if (normalizedSkills !== undefined) definition.skills = normalizedSkills;
|
|
287
|
-
if (normalizedExtensions !== undefined) definition.extensions = normalizedExtensions;
|
|
288
|
-
if (normalizedExtensionSettings !== undefined) definition.extensionSettings = normalizedExtensionSettings;
|
|
289
|
-
if (typeof overrideSystemPrompt === "boolean") definition.overrideSystemPrompt = overrideSystemPrompt;
|
|
290
|
-
if (normalizedContextFiles !== undefined) definition.contextFiles = normalizedContextFiles;
|
|
291
|
-
return definition;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const ROLE_DIRECTORY = "pi-extensible-workflows";
|
|
295
|
-
|
|
296
|
-
export function workflowRoleDirectories(agentDir = getAgentDir()): readonly string[] {
|
|
297
|
-
return [join(agentDir, ROLE_DIRECTORY, "roles")];
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function projectRoleDirectories(root: string): readonly string[] {
|
|
301
|
-
return [join(root, ROLE_DIRECTORY, "roles")];
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
type RoleDirectorySource = { path: string; extension?: WorkflowExtensionMetadata; builtin?: true };
|
|
305
|
-
export type WorkflowRoleDirectoryInput = string | WorkflowRoleDirectoryRegistration;
|
|
306
|
-
type RoleFile = { name: string; path: string; source: RoleDirectorySource };
|
|
307
|
-
function roleDirectorySources(dirs: readonly WorkflowRoleDirectoryInput[]): RoleDirectorySource[] {
|
|
308
|
-
const seen = new Set<string>();
|
|
309
|
-
return dirs.flatMap((value) => {
|
|
310
|
-
const source = typeof value === "string" ? { path: value } : { path: value.path, extension: value.extension, ...(value.builtin === true ? { builtin: true as const } : {}) };
|
|
311
|
-
if (seen.has(source.path)) return [];
|
|
312
|
-
seen.add(source.path);
|
|
313
|
-
return [source];
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
function roleDirectoryLabel(source: RoleDirectorySource, extension = true): string {
|
|
317
|
-
return source.extension ? `Extension "${source.extension.headline}" (${source.extension.version})` : extension ? "Registered workflow extension" : "Standard workflow";
|
|
318
|
-
}
|
|
319
|
-
function isRoleFile(dir: string, entry: import("node:fs").Dirent): boolean {
|
|
320
|
-
if (extname(entry.name) !== ".md") return false;
|
|
321
|
-
if (entry.isFile()) return true;
|
|
322
|
-
if (!entry.isSymbolicLink()) return false;
|
|
323
|
-
try { return statSync(join(dir, entry.name)).isFile(); }
|
|
324
|
-
catch (error) { if (isNodeError(error, "ENOENT")) return false; throw error; }
|
|
325
|
-
}
|
|
326
|
-
function scanRoleFiles(dirs: readonly WorkflowRoleDirectoryInput[], extension: boolean): RoleFile[] {
|
|
327
|
-
const files: RoleFile[] = [];
|
|
328
|
-
for (const source of roleDirectorySources(dirs)) {
|
|
329
|
-
let entries: import("node:fs").Dirent[];
|
|
330
|
-
try { entries = readdirSync(source.path, { withFileTypes: true }); }
|
|
331
|
-
catch (error) {
|
|
332
|
-
if (!extension && isNodeError(error, "ENOENT")) continue;
|
|
333
|
-
fail("INVALID_METADATA", `${roleDirectoryLabel(source, extension)} role directory "${source.path}" could not be scanned: ${errorText(error)}`);
|
|
334
|
-
}
|
|
335
|
-
for (const entry of entries) if (isRoleFile(source.path, entry)) files.push({ name: basename(entry.name, ".md"), path: join(source.path, entry.name), source });
|
|
336
|
-
}
|
|
337
|
-
return files.sort((left, right) => left.name.localeCompare(right.name) || left.path.localeCompare(right.path));
|
|
338
|
-
}
|
|
339
|
-
function readRoleDefinitions(dirs: readonly WorkflowRoleDirectoryInput[], extension = false): Record<string, AgentDefinition> {
|
|
340
|
-
const files = scanRoleFiles(dirs, extension);
|
|
341
|
-
const starterFiles = files.filter(({ source }) => source.builtin === true);
|
|
342
|
-
const regularFiles = files.filter(({ source }) => source.builtin !== true);
|
|
343
|
-
if (extension) {
|
|
344
|
-
const byName = new Map<string, RoleFile[]>();
|
|
345
|
-
for (const file of regularFiles) byName.set(file.name, [...(byName.get(file.name) ?? []), file]);
|
|
346
|
-
for (const [name, matches] of byName) if (matches.length > 1) fail("INVALID_METADATA", `Duplicate extension role "${name}": ${matches.map(({ path, source }) => `${roleDirectoryLabel(source)} role directory "${source.path}" (${path})`).join("; ")}`);
|
|
347
|
-
}
|
|
348
|
-
const read = (roleFiles: readonly RoleFile[]): Record<string, AgentDefinition> => Object.fromEntries(roleFiles.map(({ name, path, source }) => {
|
|
349
|
-
try { return [name, parseRoleMarkdown(readFileSync(path, "utf8"), true, path)]; }
|
|
350
|
-
catch (error) {
|
|
351
|
-
if (extension) fail("INVALID_METADATA", `${roleDirectoryLabel(source)} role directory "${source.path}" contains invalid role "${name}" at "${path}": ${errorText(error)}`);
|
|
352
|
-
throw error;
|
|
353
|
-
}
|
|
354
|
-
}));
|
|
355
|
-
return extension ? { ...read(starterFiles), ...read(regularFiles) } : read(files);
|
|
356
|
-
}
|
|
357
|
-
export function loadProjectAgentDefinitions(cwd: string): Readonly<Record<string, AgentDefinition>> { return readRoleDefinitions(projectRoleDirectories(join(cwd, ".pi"))); }
|
|
358
|
-
export function loadAgentDefinitions(cwd: string, agentDir = getAgentDir(), projectTrusted = true, extensionRoleDirectories: readonly WorkflowRoleDirectoryInput[] = registeredWorkflowRoleDirectoryRegistrations()): Readonly<Record<string, AgentDefinition>> {
|
|
359
|
-
return deepFreeze({ ...readRoleDefinitions(extensionRoleDirectories, true), ...readRoleDefinitions(workflowRoleDirectories(agentDir)), ...(projectTrusted ? readRoleDefinitions(projectRoleDirectories(join(cwd, ".pi"))) : {}) });
|
|
360
|
-
}
|
|
361
24
|
function validateRolePolicies(definitions: Readonly<Record<string, AgentDefinition>>, roles: readonly string[], availableModels: ReadonlySet<string>, aliases: Readonly<Record<string, string>> = {}, knownModels = availableModels, settingsPath?: string): void {
|
|
362
25
|
for (const role of roles) {
|
|
363
26
|
const definition = definitions[role];
|
|
@@ -556,6 +219,24 @@ function callHasTrailingComma(source: string, call: WorkflowCall): boolean {
|
|
|
556
219
|
return current?.type.label === ")" && previous?.type.label === ",";
|
|
557
220
|
}
|
|
558
221
|
|
|
222
|
+
/**
|
|
223
|
+
* The `tools.<id>` members a workflow script names statically. Any other use of `tools`, such as a
|
|
224
|
+
* local declaration that shadows the global or passing it around, makes the set unknowable: undefined.
|
|
225
|
+
*/
|
|
226
|
+
export function scriptToolReferences(script: string): string[] | undefined {
|
|
227
|
+
const body = workflowBody(script);
|
|
228
|
+
if (!body.trim()) return [];
|
|
229
|
+
const names = new Set<string>();
|
|
230
|
+
const opaque = (node: acorn.AnyNode, parent?: acorn.AnyNode): boolean => {
|
|
231
|
+
if (node.type === "Identifier" && node.name === "tools") {
|
|
232
|
+
if (parent?.type === "MemberExpression" && parent.object === node && !parent.computed && parent.property.type === "Identifier") names.add(parent.property.name);
|
|
233
|
+
else if (!(parent?.type === "Property" && parent.key === node && !parent.computed && !parent.shorthand || parent?.type === "MemberExpression" && parent.property === node && !parent.computed)) return true;
|
|
234
|
+
}
|
|
235
|
+
return astChildren(node).some((child) => opaque(child, node));
|
|
236
|
+
};
|
|
237
|
+
return opaque(parseWorkflow(body)) ? undefined : [...names];
|
|
238
|
+
}
|
|
239
|
+
|
|
559
240
|
export function instrumentWorkflow(script: string): string {
|
|
560
241
|
const body = workflowBody(script);
|
|
561
242
|
if (!body.trim()) return body;
|
|
@@ -628,13 +309,6 @@ export function workflowPrompt(template: string, values: Readonly<Record<string,
|
|
|
628
309
|
});
|
|
629
310
|
}
|
|
630
311
|
|
|
631
|
-
export function validateSchema(schema: unknown, at = "schema"): asserts schema is JsonSchema {
|
|
632
|
-
if (!object(schema) || Object.getPrototypeOf(schema) !== Object.prototype || !jsonValue(schema)) fail("INVALID_SCHEMA", `${at} must be a plain JSON-compatible Schema object`);
|
|
633
|
-
if (typeof schema.type !== "string" && !Array.isArray(schema.type) && schema.$ref === undefined && schema.anyOf === undefined && schema.oneOf === undefined && schema.allOf === undefined && schema.const === undefined && schema.enum === undefined) fail("INVALID_SCHEMA", `${at} has no JSON Schema shape`);
|
|
634
|
-
if (schema.required !== undefined && (!Array.isArray(schema.required) || schema.required.some((key) => typeof key !== "string"))) fail("INVALID_SCHEMA", `${at}.required must be an array of strings`);
|
|
635
|
-
if (schema.properties !== undefined && !object(schema.properties)) fail("INVALID_SCHEMA", `${at}.properties must be an object`);
|
|
636
|
-
}
|
|
637
|
-
|
|
638
312
|
const AGENT_OPTION_KEYS = new Set(["label", "model", "tools", "skills", "extensions", "contextFiles", "role", "outputSchema", "retries", "timeoutMs"]);
|
|
639
313
|
function validateAgentOption(key: string, value: unknown, aliases?: Readonly<Record<string, string>>, knownModels?: ReadonlySet<string>, settingsPath?: string): void {
|
|
640
314
|
switch (key) {
|
package/starter/review-loop.ts
CHANGED
package/subagents/README.md
CHANGED
|
@@ -44,7 +44,7 @@ subagents_run({ prompt: "Summarize README.md.", mode: "foreground" })
|
|
|
44
44
|
|
|
45
45
|
## Interactive inspection
|
|
46
46
|
|
|
47
|
-
In Pi's TUI, `/subagents` opens
|
|
47
|
+
In Pi's TUI, `/subagents` opens the `/workflow` dashboard layout for the durable standalone runs of the current session: the run list sits beside the selected run's details at 80 columns or wider, and narrower terminals drill down from the list to the details. The details show the run ID and the same activity, stall warning, state, model, role, tools, attempts, duration, cumulative token accounting, cost, and error fields as a `/workflow` agent; a running run shows its live activity, which `subagents_inspect` leaves out. Requests without a role show `Role: (none)`. While a run is active the view refreshes the active runs and the selection every second; runs launched while it is open appear after an action or when it reopens. `a` or `enter` opens registered standalone agent actions, editor controls for the prompt, the live system prompt, and the result, `Steer` and `Stop` while running, `Retry` for failed or stopped runs, and the `/workflow` run actions that apply: `Delete`, `Delete all completed`, `Delete all failed`, `Copy run path`, and `Copy agent ID`. Stop and every deletion ask for confirmation, and a run whose worktree still awaits cleanup cannot be deleted. Outside the TUI, `/subagents` opens a picker with the same run summary and bulk deletions as the `/workflow` picker. Background runs appear below the editor in the workflow widget's frame, one row per run with its model, tokens, cost, elapsed time, and quiet or stalled warning, and each leaves a receipt in the transcript when it settles. Inspection does not launch a new LLM call; lifecycle actions may steer, stop, retry, or delete a run.
|
|
48
48
|
|
|
49
49
|
## Tools
|
|
50
50
|
|
|
@@ -53,14 +53,14 @@ Every tool schema is a closed object. Unknown properties are rejected. The model
|
|
|
53
53
|
| Tool | Input schema |
|
|
54
54
|
| --- | --- |
|
|
55
55
|
| `subagents_run` | `{ prompt: string, mode?: "background" \| "foreground", label?: string, model?: string, tools?: string[], skills?: string[], extensions?: string[], contextFiles?: string[], role?: string, worktree?: string, outputSchema?: object, retries?: integer 0..255, timeoutMs?: positive integer \| null }` |
|
|
56
|
-
| `subagents_inspect` | `{ id?: string }` |
|
|
56
|
+
| `subagents_inspect` | `{ id?: string, scope?: "session" \| "all" }` |
|
|
57
57
|
| `subagents_steer` | `{ id: string, message: string }` |
|
|
58
58
|
| `subagents_stop` | `{ id: string }` |
|
|
59
59
|
| `subagents_retry` | `{ id: string }` |
|
|
60
60
|
|
|
61
61
|
`prompt` is the only required `subagents_run` property. `mode` defaults to "background". Resource candidates start enabled, so plain positive top-level selector lists are additive and top-level `[]` adds no matches. Use `["!*", "read", "grep"]` to restrict a selector to those resources, or `["!*"]` to select none. A `role` string selects an existing workflow role. Model, tools, skills, extensions, and contextFiles are top-level `AgentOptions` overrides applied after the role file. Concrete models are `provider/model:thinking`. Capability selectors use ordered minimatch rules and are applied after global, trusted-project, and role selectors. Use `tools: ["*"]` to re-enable all tools after a role restriction.
|
|
62
62
|
|
|
63
|
-
`subagents_inspect({})` returns
|
|
63
|
+
`subagents_inspect({})` returns the current Pi session's run summaries ordered by start time; `subagents_inspect({ scope: "all" })` lists every stored run. `subagents_inspect({ id })` returns the detailed lifecycle record, including state, start and finish timestamps, and the live snapshot under `progress`: `state`, cumulative `accounting`, `toolCalls`, `activity`, and `lastEventAt`. The snapshot state never includes the effective system prompt, and inspection has no `usage` field; token totals are derived from accounting. Materialized worktree path and branch are included when available. For completed runs it also includes `value`; for failed runs it includes `error`. A running run has no terminal value yet. Unknown IDs fail with `RUN_NOT_FOUND`.
|
|
64
64
|
|
|
65
65
|
## Launching and concurrency
|
|
66
66
|
|
|
@@ -41,6 +41,7 @@ export const SUBAGENTS_RUN_PARAMETERS = Type.Object({
|
|
|
41
41
|
|
|
42
42
|
export const SUBAGENTS_INSPECT_PARAMETERS = Type.Object({
|
|
43
43
|
id: Type.Optional(Type.String({ description: "Subagent ID; omit to list ordered run summaries" })),
|
|
44
|
+
scope: Type.Optional(Type.Union([Type.Literal("session"), Type.Literal("all")], { description: "List scope when id is omitted: session (default) lists runs from the current Pi session; all lists every stored run" })),
|
|
44
45
|
}, { additionalProperties: false });
|
|
45
46
|
|
|
46
47
|
export const SUBAGENTS_ID_PARAMETERS = Type.Object({
|
|
@@ -60,13 +61,23 @@ export const SUBAGENTS_RETRY_PARAMETERS = Type.Object({
|
|
|
60
61
|
id: Type.String({ description: "Failed or stopped subagent ID to retry" }),
|
|
61
62
|
}, { additionalProperties: false });
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
// Structured results codemode scripts receive instead of the JSON text; nested records stay loose so they cannot drift from persistence.
|
|
65
|
+
export const SUBAGENT_STATUS_OUTPUT = Type.Object({
|
|
66
|
+
id: Type.String(),
|
|
67
|
+
state: Type.String(),
|
|
68
|
+
sessionId: Type.Optional(Type.String()),
|
|
69
|
+
startedAt: Type.Optional(Type.Number()),
|
|
70
|
+
finishedAt: Type.Optional(Type.Number()),
|
|
71
|
+
attempts: Type.Optional(Type.Number()),
|
|
72
|
+
attemptDetails: Type.Optional(Type.Array(Type.Unknown())),
|
|
73
|
+
worktree: Type.Optional(Type.Object({ path: Type.String(), branch: Type.String() })),
|
|
74
|
+
error: Type.Optional(Type.Object({ code: Type.String(), message: Type.String() })),
|
|
75
|
+
progress: Type.Optional(Type.Unknown()),
|
|
76
|
+
value: Type.Optional(Type.Unknown()),
|
|
77
|
+
});
|
|
78
|
+
// One run when id is given, otherwise the ordered run summaries; the summaries stay short in the codemode description.
|
|
79
|
+
export const SUBAGENTS_INSPECT_OUTPUT = Type.Union([SUBAGENT_STATUS_OUTPUT, Type.Array(Type.Object({ id: Type.String(), state: Type.String() }))]);
|
|
80
|
+
export const SUBAGENTS_STEER_OUTPUT = Type.Object({ id: Type.String(), accepted: Type.Boolean() });
|
|
70
81
|
|
|
71
82
|
export type SubagentRunRequest = Static<typeof SUBAGENTS_RUN_PARAMETERS>;
|
|
72
83
|
export function normalizeSubagentRunRequest(value: unknown): SubagentRunRequest {
|
|
@@ -114,6 +125,8 @@ export interface SubagentManagerContext {
|
|
|
114
125
|
readonly extensionContext: ExtensionContext;
|
|
115
126
|
readonly waitForForeground?: boolean;
|
|
116
127
|
readonly includeAttemptMetadata?: boolean;
|
|
128
|
+
/** Adds the live progress and activity of an active run; tool results leave streamed activity out. */
|
|
129
|
+
readonly includeActivity?: boolean;
|
|
117
130
|
}
|
|
118
131
|
export interface SubagentAttemptActionData {
|
|
119
132
|
readonly attempt: AgentAttemptSummary;
|
|
@@ -160,6 +173,8 @@ export interface SubagentManager {
|
|
|
160
173
|
steer(request: Readonly<SubagentSteerRequest>, context: Readonly<SubagentManagerContext>): Promise<unknown>;
|
|
161
174
|
stop(request: Readonly<SubagentIdRequest>, context: Readonly<SubagentManagerContext>): Promise<unknown>;
|
|
162
175
|
retry(request: Readonly<SubagentIdRequest>, context: Readonly<SubagentManagerContext>): Promise<unknown>;
|
|
176
|
+
/** Removes a settled run record; used by the /subagents navigator only. */
|
|
177
|
+
delete?(request: Readonly<SubagentIdRequest>, context: Readonly<SubagentManagerContext>): Promise<unknown>;
|
|
163
178
|
getAttemptActionData?(id: string): Readonly<SubagentAttemptActionData> | undefined;
|
|
164
179
|
dispose?(): Promise<void>;
|
|
165
180
|
}
|
package/subagents/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { defineTool, getAgentDir, type AgentToolResult, type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Value } from "typebox/value";
|
|
4
|
-
import { WorkflowError, loadingRegistry } from "../../src/index.js";
|
|
4
|
+
import { WorkflowError, loadCodemodeToolsSetting, loadingRegistry, reachableTools, workflowToolExposure, type JsonValue } from "../../src/index.js";
|
|
5
5
|
import { clearSubagentManager, setSubagentManager } from "../../src/subagent-manager-handle.js";
|
|
6
6
|
import type { SubagentIdRequest, SubagentInspectRequest, SubagentManager, SubagentManagerContext, SubagentNotification, SubagentsExtension, SubagentsExtensionOptions, SubagentRunRequest, SubagentStatus, SubagentSteerRequest } from "./contracts.js";
|
|
7
7
|
import { createSubagentManager } from "./manager.js";
|
|
@@ -9,10 +9,13 @@ import { registerSubagentNavigator } from "./navigator.js";
|
|
|
9
9
|
import { createSubagentBackgroundWidget, renderSubagentCall, renderSubagentControlCall, renderSubagentControlResult, renderSubagentInspectCall, renderSubagentInspectResult, renderSubagentResult } from "./view.js";
|
|
10
10
|
import {
|
|
11
11
|
normalizeSubagentRunRequest,
|
|
12
|
+
SUBAGENT_STATUS_OUTPUT,
|
|
12
13
|
SUBAGENTS_ID_PARAMETERS,
|
|
14
|
+
SUBAGENTS_INSPECT_OUTPUT,
|
|
13
15
|
SUBAGENTS_INSPECT_PARAMETERS,
|
|
14
16
|
SUBAGENTS_RETRY_PARAMETERS,
|
|
15
17
|
SUBAGENTS_RUN_PARAMETERS,
|
|
18
|
+
SUBAGENTS_STEER_OUTPUT,
|
|
16
19
|
SUBAGENTS_STEER_PARAMETERS,
|
|
17
20
|
SUBAGENTS_STOP_PARAMETERS,
|
|
18
21
|
} from "./contracts.js";
|
|
@@ -23,7 +26,7 @@ export { createSubagentManager, createUnavailableSubagentManager } from "./manag
|
|
|
23
26
|
export { createRunStoreWorktreeAdapter, defaultWorktreeHome } from "./worktree.js";
|
|
24
27
|
export type { SubagentWorktreeAdapter, SubagentWorktreeContext, SubagentWorktreeHandle, SubagentWorktreeRunStore } from "./worktree.js";
|
|
25
28
|
|
|
26
|
-
type SubagentsExtensionAPI = Pick<ExtensionAPI, "registerTool"> & Partial<Pick<ExtensionAPI, "getActiveTools" | "on" | "sendMessage" | "registerCommand">>;
|
|
29
|
+
type SubagentsExtensionAPI = Pick<ExtensionAPI, "registerTool"> & Partial<Pick<ExtensionAPI, "getActiveTools" | "getAllTools" | "on" | "sendMessage" | "registerCommand" | "appendEntry" | "registerEntryRenderer">>;
|
|
27
30
|
|
|
28
31
|
function validateSubagentRunRequest(value: unknown): SubagentRunRequest {
|
|
29
32
|
return normalizeSubagentRunRequest(value);
|
|
@@ -55,7 +58,7 @@ function serialize(value: unknown): string {
|
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
function toolResult(value: unknown): AgentToolResult<unknown> {
|
|
58
|
-
return { content: [{ type: "text", text: serialize(value) }], details: value };
|
|
61
|
+
return { content: [{ type: "text", text: serialize(value) }], details: value, structuredContent: value as JsonValue };
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
function managerContext(toolCallId: string, signal: AbortSignal | undefined, onUpdate: ((value: AgentToolResult<unknown>) => void) | undefined, context: ExtensionContext): SubagentManagerContext {
|
|
@@ -78,6 +81,7 @@ export function createSubagentTools(manager: SubagentManager): readonly ToolDefi
|
|
|
78
81
|
"Do not poll a running ID; use subagents_inspect({ id }) when you need its current status or terminal result.",
|
|
79
82
|
],
|
|
80
83
|
parameters: SUBAGENTS_RUN_PARAMETERS,
|
|
84
|
+
outputSchema: SUBAGENT_STATUS_OUTPUT,
|
|
81
85
|
async execute(toolCallId, params, signal, onUpdate, context) {
|
|
82
86
|
return toolResult(await manager.run(validateSubagentRunRequest(params), managerContext(toolCallId, signal, onUpdate, context)));
|
|
83
87
|
},
|
|
@@ -87,8 +91,9 @@ export function createSubagentTools(manager: SubagentManager): readonly ToolDefi
|
|
|
87
91
|
defineTool({
|
|
88
92
|
name: "subagents_inspect",
|
|
89
93
|
label: "Subagents Inspect",
|
|
90
|
-
description: "Inspect durable subagent runs. Omit id for ordered run summaries, or provide id for detailed status, progress, activity, accounting, tool calls, timestamps, worktree metadata, and a terminal value or error when available.",
|
|
94
|
+
description: "Inspect durable subagent runs. Omit id for ordered run summaries of the current session (scope all for every session), or provide id for detailed status, progress, activity, accounting, tool calls, timestamps, worktree metadata, and a terminal value or error when available.",
|
|
91
95
|
parameters: SUBAGENTS_INSPECT_PARAMETERS,
|
|
96
|
+
outputSchema: SUBAGENTS_INSPECT_OUTPUT,
|
|
92
97
|
async execute(toolCallId, params, signal, onUpdate, context) {
|
|
93
98
|
return toolResult(await manager.inspect(validateSubagentInspectRequest(params), managerContext(toolCallId, signal, onUpdate, context)));
|
|
94
99
|
},
|
|
@@ -100,6 +105,7 @@ export function createSubagentTools(manager: SubagentManager): readonly ToolDefi
|
|
|
100
105
|
label: "Subagents Steer",
|
|
101
106
|
description: "Send a message to a running subagent. The message is queued safely if its steering handler is not ready; settled runs cannot be steered.",
|
|
102
107
|
parameters: SUBAGENTS_STEER_PARAMETERS,
|
|
108
|
+
outputSchema: SUBAGENTS_STEER_OUTPUT,
|
|
103
109
|
async execute(toolCallId, params, signal, onUpdate, context) {
|
|
104
110
|
return toolResult(await manager.steer(validateSubagentSteerRequest(params), managerContext(toolCallId, signal, onUpdate, context)));
|
|
105
111
|
},
|
|
@@ -111,6 +117,7 @@ export function createSubagentTools(manager: SubagentManager): readonly ToolDefi
|
|
|
111
117
|
label: "Subagents Stop",
|
|
112
118
|
description: "Stop one running subagent, abort its active session, persist state \"stopped\", and clean its worktree without affecting sibling runs.",
|
|
113
119
|
parameters: SUBAGENTS_STOP_PARAMETERS,
|
|
120
|
+
outputSchema: SUBAGENT_STATUS_OUTPUT,
|
|
114
121
|
async execute(toolCallId, params, signal, onUpdate, context) {
|
|
115
122
|
return toolResult(await manager.stop(validateSubagentIdRequest(params, "subagents_stop"), managerContext(toolCallId, signal, onUpdate, context)));
|
|
116
123
|
},
|
|
@@ -122,6 +129,7 @@ export function createSubagentTools(manager: SubagentManager): readonly ToolDefi
|
|
|
122
129
|
label: "Subagents Retry",
|
|
123
130
|
description: "Start a fresh run from a failed or stopped subagent's persisted request. The new run gets a new ID and preserves its original background or foreground mode.",
|
|
124
131
|
parameters: SUBAGENTS_RETRY_PARAMETERS,
|
|
132
|
+
outputSchema: SUBAGENT_STATUS_OUTPUT,
|
|
125
133
|
async execute(toolCallId, params, signal, onUpdate, context) {
|
|
126
134
|
return toolResult(await manager.retry(validateSubagentIdRequest(params, "subagents_retry"), managerContext(toolCallId, signal, onUpdate, context)));
|
|
127
135
|
},
|
|
@@ -162,7 +170,8 @@ export function createSubagentsExtension(options: SubagentsExtensionOptions = {}
|
|
|
162
170
|
|
|
163
171
|
export function registerSubagentsExtension(pi: SubagentsExtensionAPI, options: SubagentsExtensionOptions = {}): SubagentsExtension {
|
|
164
172
|
const getActiveTools = pi.getActiveTools;
|
|
165
|
-
const
|
|
173
|
+
const getAllTools = pi.getAllTools;
|
|
174
|
+
const activeTools = getActiveTools === undefined ? undefined : () => reachableTools({ getActiveTools: () => getActiveTools.call(pi), ...(getAllTools === undefined ? {} : { getAllTools: () => getAllTools.call(pi) }) });
|
|
166
175
|
const sendMessage = pi.sendMessage;
|
|
167
176
|
const notify = sendMessage === undefined ? undefined : (notification: SubagentNotification): void => {
|
|
168
177
|
sendMessage.call(pi, { customType: "subagents", content: notificationContent(notification), display: true, details: notification }, { deliverAs: "steer", triggerTurn: true });
|
|
@@ -170,9 +179,11 @@ export function registerSubagentsExtension(pi: SubagentsExtensionAPI, options: S
|
|
|
170
179
|
const onResourceWarning = sendMessage === undefined ? undefined : (message: string): void => {
|
|
171
180
|
sendMessage.call(pi, { customType: "subagents", content: `Warning: ${message}`, display: true }, { deliverAs: "steer" });
|
|
172
181
|
};
|
|
173
|
-
const
|
|
182
|
+
const appendEntry = pi.appendEntry;
|
|
183
|
+
const widget = createSubagentBackgroundWidget({ ...(appendEntry === undefined ? {} : { appendEntry: (customType, data) => { appendEntry.call(pi, customType, data); } }), ...(pi.registerEntryRenderer === undefined ? {} : { registerEntryRenderer: pi.registerEntryRenderer.bind(pi) }) });
|
|
174
184
|
const extension = createSubagentsExtension(options, activeTools, notify, (status, request) => { widget.update(status, request); const registry = loadingRegistry(); if (typeof registry.observeSubagentStatus === "function") registry.observeSubagentStatus(status, request); }, onResourceWarning);
|
|
175
|
-
|
|
185
|
+
const codemodeTools = loadCodemodeToolsSetting(options.managerDependencies?.agentDir);
|
|
186
|
+
for (const tool of extension.tools) pi.registerTool({ ...tool, ...workflowToolExposure(tool.name, codemodeTools) });
|
|
176
187
|
if (pi.registerCommand !== undefined) registerSubagentNavigator(pi.registerCommand.bind(pi), extension.manager, storageDirectory(options), options.clipboard);
|
|
177
188
|
if (pi.on !== undefined) {
|
|
178
189
|
setSubagentManager(extension.manager);
|