@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/settings.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { atomicWriteFile } from "./persistence.js";
|
|
2
|
+
import { mkdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { getAgentDir, type ToolAnnotations } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import type { AgentResourcePolicy, AgentResourceSelectors, AgentResourceSelectorSet, ContextFileScope, JsonValue, WorkflowExtensionSettings, WorkflowRetentionSettings, WorkflowSettings, WorkflowSettingsOverrides, WorkflowSettingsResolution, WorkflowSettingsSources } from "./types.js";
|
|
8
|
+
import { isContextFileScope } from "./types.js";
|
|
9
|
+
import { annotateModelAliasError, deepFreeze, errorText, fail, isNodeError, jsonValue, modelCapability, object, positiveInteger, resourcePatternHasMagic, unknownModel, validateModelAliases, validateResourcePattern, validWorkflowExtensionNamespace } from "./utils.js";
|
|
10
|
+
import { canonicalPath, workflowProjectSettingsPath } from "./paths.js";
|
|
11
|
+
export { workflowProjectSettingsPath } from "./paths.js";
|
|
12
|
+
|
|
13
|
+
const ROLE_DIRECTORY = "pi-extensible-workflows";
|
|
14
|
+
export const DEFAULT_SETTINGS: Readonly<WorkflowSettings> = Object.freeze({ concurrency: 8, backgroundWidget: true });
|
|
15
|
+
export function workflowSettingsPath(agentDir = getAgentDir()): string { return join(agentDir, ROLE_DIRECTORY, "settings.json"); }
|
|
16
|
+
function normalizedResourcePath(value: string, settingsPath: string): string {
|
|
17
|
+
// Built-in extensions are named `builtin:<name>`, not by a path.
|
|
18
|
+
if (value === "*" || value.startsWith("builtin:")) return value;
|
|
19
|
+
let expanded = value === "~" ? homedir() : value.startsWith("~/") || value.startsWith("~\\") ? join(homedir(), value.slice(2)) : value;
|
|
20
|
+
if (expanded.startsWith("file://")) expanded = fileURLToPath(expanded);
|
|
21
|
+
const resolved = resolve(dirname(settingsPath), expanded);
|
|
22
|
+
if (expanded === "**" || expanded.startsWith("**/") || expanded.startsWith("**\\")) return expanded;
|
|
23
|
+
if (resourcePatternHasMagic(expanded)) {
|
|
24
|
+
const magicIndex = resolved.search(/[*?\x5b\x5d{}()]/);
|
|
25
|
+
const separatorIndex = Math.max(resolved.lastIndexOf("/", magicIndex), resolved.lastIndexOf("\\", magicIndex));
|
|
26
|
+
const rootBoundary = separatorIndex === 0 || (separatorIndex === 2 && /^[A-Za-z]:[\\/]/.test(resolved));
|
|
27
|
+
const prefix = rootBoundary ? resolved.slice(0, separatorIndex + 1) : separatorIndex >= 0 ? resolved.slice(0, separatorIndex) : resolved;
|
|
28
|
+
const suffix = rootBoundary ? resolved.slice(separatorIndex + 1) : separatorIndex >= 0 ? resolved.slice(separatorIndex) : "";
|
|
29
|
+
return `${canonicalPath(prefix)}${suffix}`;
|
|
30
|
+
}
|
|
31
|
+
return canonicalPath(resolved);
|
|
32
|
+
}
|
|
33
|
+
export function validateSelectorList(value: unknown, path: string, kind: "skills" | "extensions" | "tools", errorCode: "INVALID_SETTINGS" | "INVALID_METADATA" = "INVALID_SETTINGS", normalizeExtensions = true): readonly string[] | undefined {
|
|
34
|
+
if (value === undefined) return undefined;
|
|
35
|
+
if (!Array.isArray(value)) fail(errorCode, `${path}.${kind} must be an array`);
|
|
36
|
+
const normalized: string[] = [];
|
|
37
|
+
for (const [index, entry] of value.entries()) {
|
|
38
|
+
if (typeof entry !== "string" || !entry.trim()) fail(errorCode, `${path}.${kind}[${String(index)}] must be a non-empty string`);
|
|
39
|
+
let selector = entry.trim();
|
|
40
|
+
if (kind === "extensions" && normalizeExtensions) {
|
|
41
|
+
const negated = selector.startsWith("!");
|
|
42
|
+
const body = negated ? selector.slice(1) : selector;
|
|
43
|
+
if (!body) fail(errorCode, `${path}.${kind}[${String(index)}] must be a valid minimatch pattern: Empty minimatch pattern ${JSON.stringify(selector)}`);
|
|
44
|
+
try { selector = `${negated ? "!" : ""}${normalizedResourcePath(body, path)}`; } catch (error) { fail(errorCode, `${path}.${kind}[${String(index)}] must be a valid path: ${errorText(error)}`); }
|
|
45
|
+
}
|
|
46
|
+
try { validateResourcePattern(selector); } catch (error) { fail(errorCode, `${path}.${kind}[${String(index)}] must be a valid minimatch pattern: ${errorText(error)}`); }
|
|
47
|
+
normalized.push(selector);
|
|
48
|
+
}
|
|
49
|
+
return Object.freeze(normalized);
|
|
50
|
+
}
|
|
51
|
+
function validateWorktreePostCreateCommand(value: unknown, settingsPath: string): readonly string[] | undefined {
|
|
52
|
+
if (value === undefined) return undefined;
|
|
53
|
+
if (!Array.isArray(value) || value.length === 0) fail("INVALID_SETTINGS", `${settingsPath}.worktreePostCreateCommand must be a non-empty array`);
|
|
54
|
+
return Object.freeze(value.map((entry, index) => {
|
|
55
|
+
if (typeof entry !== "string" || !entry.trim()) fail("INVALID_SETTINGS", `${settingsPath}.worktreePostCreateCommand[${String(index)}] must be a non-empty string`);
|
|
56
|
+
return entry;
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
function selectorsFromSettings(settings: Readonly<WorkflowSettings | WorkflowSettingsOverrides>): AgentResourceSelectors {
|
|
60
|
+
return {
|
|
61
|
+
...(settings.skills === undefined ? {} : { skills: settings.skills }),
|
|
62
|
+
...(settings.extensions === undefined ? {} : { extensions: settings.extensions }),
|
|
63
|
+
...(settings.tools === undefined ? {} : { tools: settings.tools }),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function selectorSet(value: AgentResourceSelectors | undefined): AgentResourceSelectorSet { return { skills: [...(value?.skills ?? [])], extensions: [...(value?.extensions ?? [])], ...(value?.tools === undefined ? {} : { tools: [...value.tools] }) }; }
|
|
67
|
+
export function validateContextFileScopes(value: unknown, rolePath: string): readonly ContextFileScope[] | undefined {
|
|
68
|
+
if (value === undefined) return undefined;
|
|
69
|
+
if (!Array.isArray(value) || !value.every(isContextFileScope)) fail("INVALID_METADATA", `${rolePath}.contextFiles must be an array containing only global, project, or cwd`);
|
|
70
|
+
return [...value];
|
|
71
|
+
}
|
|
72
|
+
export function validateWorkflowExtensionSettings(value: unknown, settingsPath: string, errorCode: "INVALID_SETTINGS" | "INVALID_METADATA" = "INVALID_SETTINGS"): WorkflowExtensionSettings | undefined {
|
|
73
|
+
if (value === undefined) return undefined;
|
|
74
|
+
const base = `${settingsPath}.extensionSettings`;
|
|
75
|
+
if (!object(value)) fail(errorCode, `${base} must be an object`);
|
|
76
|
+
const normalized: Record<string, JsonValue> = {};
|
|
77
|
+
for (const [namespace, raw] of Object.entries(value)) {
|
|
78
|
+
if (!validWorkflowExtensionNamespace(namespace)) fail(errorCode, `${base} contains an invalid namespace: ${namespace}`);
|
|
79
|
+
if (namespace === "herdr") {
|
|
80
|
+
if (!object(raw)) fail(errorCode, `${base}.herdr must be an object`);
|
|
81
|
+
if (Object.keys(raw).some((key) => key !== "enableFullyInspectableMode")) fail(errorCode, `${base}.herdr contains an unsupported setting`);
|
|
82
|
+
if (raw.enableFullyInspectableMode !== undefined && typeof raw.enableFullyInspectableMode !== "boolean") fail(errorCode, `${base}.herdr.enableFullyInspectableMode must be a boolean`);
|
|
83
|
+
normalized.herdr = Object.freeze({ ...(raw.enableFullyInspectableMode === undefined ? {} : { enableFullyInspectableMode: raw.enableFullyInspectableMode }) });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (namespace === "trajectory") {
|
|
87
|
+
if (!object(raw)) fail(errorCode, `${base}.trajectory must be an object`);
|
|
88
|
+
if (Object.keys(raw).some((key) => key !== "port")) fail(errorCode, `${base}.trajectory contains an unsupported setting`);
|
|
89
|
+
if (raw.port !== undefined && (!positiveInteger(raw.port) || raw.port > 65535)) fail(errorCode, `${base}.trajectory.port must be an integer from 1 to 65535`);
|
|
90
|
+
normalized.trajectory = Object.freeze({ ...(raw.port === undefined ? {} : { port: raw.port }) });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (!jsonValue(raw)) fail(errorCode, `${base}.${namespace} must be JSON-compatible`);
|
|
94
|
+
normalized[namespace] = structuredClone(raw);
|
|
95
|
+
}
|
|
96
|
+
return deepFreeze(normalized as WorkflowExtensionSettings);
|
|
97
|
+
}
|
|
98
|
+
function positiveRetentionInteger(value: unknown): value is number { return positiveInteger(value) && Number.isSafeInteger(value); }
|
|
99
|
+
function validateRetention(value: unknown, settingsPath: string): Readonly<WorkflowRetentionSettings> | undefined {
|
|
100
|
+
if (value === undefined) return undefined;
|
|
101
|
+
const base = `${settingsPath}.retention`;
|
|
102
|
+
if (!object(value)) fail("INVALID_SETTINGS", `${base} must be an object`);
|
|
103
|
+
const unknown = Object.keys(value).find((key) => key !== "olderThanDays" && key !== "maxTerminalRuns");
|
|
104
|
+
if (unknown) fail("INVALID_SETTINGS", `Unknown retention setting at ${base}: ${unknown}`);
|
|
105
|
+
if (value.olderThanDays !== undefined && !positiveRetentionInteger(value.olderThanDays)) fail("INVALID_SETTINGS", `${base}.olderThanDays must be a positive integer`);
|
|
106
|
+
if (value.maxTerminalRuns !== undefined && !positiveRetentionInteger(value.maxTerminalRuns)) fail("INVALID_SETTINGS", `${base}.maxTerminalRuns must be a positive integer`);
|
|
107
|
+
return Object.freeze({ ...(value.olderThanDays === undefined ? {} : { olderThanDays: value.olderThanDays }), ...(value.maxTerminalRuns === undefined ? {} : { maxTerminalRuns: value.maxTerminalRuns }) });
|
|
108
|
+
}
|
|
109
|
+
function parseSettings(path: string, partial: false): Readonly<WorkflowSettings>;
|
|
110
|
+
function parseSettings(path: string, partial: true): Readonly<WorkflowSettingsOverrides>;
|
|
111
|
+
function parseSettings(path: string, partial: boolean): Readonly<WorkflowSettings | WorkflowSettingsOverrides> {
|
|
112
|
+
let parsed: unknown;
|
|
113
|
+
try { parsed = JSON.parse(readFileSync(path, "utf8")); }
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (isNodeError(error, "ENOENT")) return partial ? Object.freeze({}) : DEFAULT_SETTINGS;
|
|
116
|
+
fail("CONFIG_ERROR", `Invalid workflow settings JSON at ${path}: ${errorText(error)}`);
|
|
117
|
+
}
|
|
118
|
+
if (!object(parsed)) fail("INVALID_SETTINGS", `Workflow settings at ${path} must be an object`);
|
|
119
|
+
const allowed = new Set(["concurrency", "modelAliases", "skills", "extensions", "extensionSettings", "tools", "retention", "worktreePostCreateCommand", ...(partial ? [] : ["backgroundWidget", "codemodeTools"]) ]);
|
|
120
|
+
const unknown = Object.keys(parsed).find((key) => !allowed.has(key));
|
|
121
|
+
if (Object.prototype.hasOwnProperty.call(parsed, "disabledAgentResources")) fail("INVALID_SETTINGS", `disabledAgentResources is no longer supported; use skills, extensions, and tools selectors (settings: ${path})`);
|
|
122
|
+
if (unknown) fail("INVALID_SETTINGS", `Unknown workflow setting at ${path}: ${unknown}`);
|
|
123
|
+
const concurrency = parsed.concurrency === undefined ? (partial ? undefined : DEFAULT_SETTINGS.concurrency) : parsed.concurrency;
|
|
124
|
+
if (concurrency !== undefined && (!positiveInteger(concurrency) || concurrency > 16)) fail("INVALID_SETTINGS", `${path}.concurrency must be an integer from 1 to 16`);
|
|
125
|
+
const backgroundWidget = parsed.backgroundWidget === undefined ? (partial ? undefined : DEFAULT_SETTINGS.backgroundWidget) : parsed.backgroundWidget;
|
|
126
|
+
if (backgroundWidget !== undefined && typeof backgroundWidget !== "boolean") fail("INVALID_SETTINGS", `${path}.backgroundWidget must be a boolean`);
|
|
127
|
+
const codemodeTools = parsed.codemodeTools;
|
|
128
|
+
if (codemodeTools !== undefined && codemodeTools !== "all" && codemodeTools !== "read-only" && codemodeTools !== "none") fail("INVALID_SETTINGS", `${path}.codemodeTools must be "all", "read-only", or "none"`);
|
|
129
|
+
const modelAliases = parsed.modelAliases === undefined ? undefined : validateModelAliases(parsed.modelAliases, path);
|
|
130
|
+
const skills = validateSelectorList(parsed.skills, path, "skills");
|
|
131
|
+
const tools = validateSelectorList(parsed.tools, path, "tools");
|
|
132
|
+
const extensions = validateSelectorList(parsed.extensions, path, "extensions");
|
|
133
|
+
const extensionSettings = parsed.extensionSettings === undefined ? undefined : validateWorkflowExtensionSettings(parsed.extensionSettings, path, "INVALID_SETTINGS");
|
|
134
|
+
const retention = validateRetention(parsed.retention, path);
|
|
135
|
+
const worktreePostCreateCommand = validateWorktreePostCreateCommand(parsed.worktreePostCreateCommand, path);
|
|
136
|
+
return Object.freeze({
|
|
137
|
+
...(concurrency === undefined ? {} : { concurrency }), ...(backgroundWidget === undefined ? {} : { backgroundWidget }), ...(codemodeTools === undefined ? {} : { codemodeTools }), ...(modelAliases === undefined ? {} : { modelAliases }),
|
|
138
|
+
...(skills === undefined ? {} : { skills }), ...(extensions === undefined ? {} : { extensions }),
|
|
139
|
+
...(extensionSettings === undefined ? {} : { extensionSettings }), ...(tools === undefined ? {} : { tools }), ...(retention === undefined ? {} : { retention }),
|
|
140
|
+
...(worktreePostCreateCommand === undefined ? {} : { worktreePostCreateCommand }),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
export function loadSettings(path = workflowSettingsPath()): Readonly<WorkflowSettings> { return parseSettings(path, false); }
|
|
144
|
+
const READ_ONLY_WORKFLOW_TOOLS: ReadonlySet<string> = new Set(["workflow_status", "workflow_catalog", "subagents_inspect"]);
|
|
145
|
+
/**
|
|
146
|
+
* Exposure and annotations of a workflow or subagent tool. Under the global `codemodeTools` setting,
|
|
147
|
+
* `model-only` keeps the model's direct access and stops codemode scripts from calling the tool.
|
|
148
|
+
* Tools are registered once at load, so the setting is global and applies after a reload.
|
|
149
|
+
* Read-only tools carry `readOnlyHint` so permission extensions need not confirm them.
|
|
150
|
+
*/
|
|
151
|
+
export function workflowToolExposure(name: string, codemodeTools: WorkflowSettings["codemodeTools"]): { exposure?: "model-only"; annotations?: ToolAnnotations } {
|
|
152
|
+
const readOnly = READ_ONLY_WORKFLOW_TOOLS.has(name);
|
|
153
|
+
return { ...(codemodeTools === "none" || (codemodeTools === "read-only" && !readOnly) ? { exposure: "model-only" as const } : {}), ...(readOnly ? { annotations: { readOnlyHint: true } } : {}) };
|
|
154
|
+
}
|
|
155
|
+
/** The `codemodeTools` setting at load time. Invalid settings keep the default; launches report them. */
|
|
156
|
+
export function loadCodemodeToolsSetting(agentDir?: string): WorkflowSettings["codemodeTools"] {
|
|
157
|
+
try { return loadSettings(workflowSettingsPath(agentDir)).codemodeTools; } catch { return undefined; }
|
|
158
|
+
}
|
|
159
|
+
export function loadSettingsOverrides(path: string): Readonly<WorkflowSettingsOverrides> { return parseSettings(path, true); }
|
|
160
|
+
export function resolveWorkflowSettings(cwd: string, projectTrusted: boolean, globalSettingsPath = workflowSettingsPath()): WorkflowSettingsResolution {
|
|
161
|
+
const projectSettingsPath = workflowProjectSettingsPath(cwd);
|
|
162
|
+
const global = loadSettings(globalSettingsPath);
|
|
163
|
+
const project: Readonly<WorkflowSettingsOverrides> = projectTrusted ? loadSettingsOverrides(projectSettingsPath) : Object.freeze({});
|
|
164
|
+
const projectHas = (key: keyof WorkflowSettingsOverrides): boolean => Object.prototype.hasOwnProperty.call(project, key);
|
|
165
|
+
const sourceFor = (key: "skills" | "extensions" | "tools"): string => projectHas(key) ? projectSettingsPath : globalSettingsPath;
|
|
166
|
+
const globalSelectors = selectorsFromSettings(global);
|
|
167
|
+
const projectSelectors = selectorsFromSettings(project);
|
|
168
|
+
const effectiveSelectors = selectorSet({
|
|
169
|
+
skills: [...(globalSelectors.skills ?? []), ...(projectSelectors.skills ?? [])],
|
|
170
|
+
extensions: [...(globalSelectors.extensions ?? []), ...(projectSelectors.extensions ?? [])],
|
|
171
|
+
...(globalSelectors.tools === undefined && projectSelectors.tools === undefined ? {} : { tools: [...(globalSelectors.tools ?? []), ...(projectSelectors.tools ?? [])] }),
|
|
172
|
+
});
|
|
173
|
+
const hasExtensionSelectors = global.extensions !== undefined || project.extensions !== undefined;
|
|
174
|
+
const extensionSettings = projectHas("extensionSettings") ? project.extensionSettings : global.extensionSettings;
|
|
175
|
+
const sources: WorkflowSettingsSources = {
|
|
176
|
+
concurrency: projectHas("concurrency") ? projectSettingsPath : globalSettingsPath,
|
|
177
|
+
modelAliases: projectHas("modelAliases") ? projectSettingsPath : globalSettingsPath,
|
|
178
|
+
skills: sourceFor("skills"), extensions: sourceFor("extensions"), tools: sourceFor("tools"),
|
|
179
|
+
...(extensionSettings === undefined ? {} : { extensionSettings: projectHas("extensionSettings") ? projectSettingsPath : globalSettingsPath }),
|
|
180
|
+
...(project.retention === undefined && global.retention === undefined ? {} : { retention: project.retention === undefined ? globalSettingsPath : projectSettingsPath }),
|
|
181
|
+
...(projectHas("worktreePostCreateCommand") || global.worktreePostCreateCommand !== undefined ? { worktreePostCreateCommand: projectHas("worktreePostCreateCommand") ? projectSettingsPath : globalSettingsPath } : {}),
|
|
182
|
+
};
|
|
183
|
+
const effective = Object.freeze({
|
|
184
|
+
concurrency: project.concurrency ?? global.concurrency,
|
|
185
|
+
backgroundWidget: global.backgroundWidget ?? true,
|
|
186
|
+
...(projectHas("modelAliases") ? { modelAliases: project.modelAliases } : global.modelAliases === undefined ? {} : { modelAliases: global.modelAliases }),
|
|
187
|
+
...(effectiveSelectors.skills.length ? { skills: effectiveSelectors.skills } : global.skills !== undefined || project.skills !== undefined ? { skills: effectiveSelectors.skills } : {}),
|
|
188
|
+
...(hasExtensionSelectors ? { extensions: effectiveSelectors.extensions } : {}),
|
|
189
|
+
...(extensionSettings === undefined ? {} : { extensionSettings }),
|
|
190
|
+
...(effectiveSelectors.tools?.length ? { tools: effectiveSelectors.tools } : global.tools !== undefined || project.tools !== undefined ? { tools: effectiveSelectors.tools } : {}),
|
|
191
|
+
...((project.retention ?? global.retention) === undefined ? {} : { retention: project.retention ?? global.retention }),
|
|
192
|
+
...(projectHas("worktreePostCreateCommand") ? { worktreePostCreateCommand: project.worktreePostCreateCommand } : global.worktreePostCreateCommand === undefined ? {} : { worktreePostCreateCommand: global.worktreePostCreateCommand }),
|
|
193
|
+
});
|
|
194
|
+
return { globalSettingsPath, projectSettingsPath, projectTrusted, global, project, effective, sources };
|
|
195
|
+
}
|
|
196
|
+
export function validateModelAliasAvailability(aliases: Readonly<Record<string, string>>, names: readonly string[], availableModels: ReadonlySet<string>, knownModels: ReadonlySet<string>, settingsPath?: string): void {
|
|
197
|
+
for (const name of names) {
|
|
198
|
+
try {
|
|
199
|
+
const target = modelCapability(name, aliases, knownModels, settingsPath);
|
|
200
|
+
if (!availableModels.has(target)) unknownModel(name, target, settingsPath);
|
|
201
|
+
} catch (error) { throw annotateModelAliasError(error, name); }
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
export function resolveAgentResourcePolicy(cwd: string, projectTrusted: boolean, globalSettingsPath = workflowSettingsPath()): AgentResourcePolicy {
|
|
205
|
+
const resolved = resolveWorkflowSettings(cwd, projectTrusted, globalSettingsPath);
|
|
206
|
+
const global = selectorSet(selectorsFromSettings(resolved.global));
|
|
207
|
+
const project = selectorSet(selectorsFromSettings(resolved.project));
|
|
208
|
+
const effective = selectorSet(selectorsFromSettings(resolved.effective));
|
|
209
|
+
return { globalSettingsPath: resolved.globalSettingsPath, projectSettingsPath: resolved.projectSettingsPath, projectTrusted, global, project, effective, unmatchedSkills: [], unmatchedExtensions: [], unmatchedTools: [], selectorSources: { global: selectorsFromSettings(resolved.global), project: selectorsFromSettings(resolved.project) } };
|
|
210
|
+
}
|
|
211
|
+
export function saveModelAliases(path = workflowSettingsPath(), aliases: Readonly<Record<string, string>> = {}): void {
|
|
212
|
+
const normalized = validateModelAliases(aliases, path);
|
|
213
|
+
let parsed: unknown = {};
|
|
214
|
+
try {
|
|
215
|
+
loadSettings(path);
|
|
216
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (!isNodeError(error, "ENOENT")) throw error;
|
|
219
|
+
}
|
|
220
|
+
if (!object(parsed)) fail("INVALID_SETTINGS", `Workflow settings at ${path} must be an object`);
|
|
221
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
222
|
+
atomicWriteFile(path, `${JSON.stringify({ ...parsed, modelAliases: normalized }, null, 2)}\n`, true);
|
|
223
|
+
}
|
|
224
|
+
|
package/src/trajectory.ts
CHANGED
|
@@ -3,20 +3,30 @@ import { open, readFile, readdir, stat } from "node:fs/promises";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { basename, dirname, join, resolve } from "node:path";
|
|
5
5
|
import { createBashToolDefinition, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, DefaultPackageManager, getAgentDir, SettingsManager, DEFAULT_MAX_BYTES } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import { listRunIds, RunStore, type AwaitingCheckpoint, type EffectiveSystemPrompt, type PersistedRun } from "./persistence.js";
|
|
6
|
+
import { listRunIds, RunStore, type AwaitingCheckpoint, type CompletedOperation, type EffectiveSystemPrompt, type PersistedRun } from "./persistence.js";
|
|
7
7
|
import { navigatorAttentionSortByState } from "./host-view.js";
|
|
8
|
-
import type { AgentAttemptSummary, JsonValue, LaunchSnapshot, WorkflowAgentSessionReference } from "./types.js";
|
|
8
|
+
import type { AgentAttemptSummary, AgentRecord, JsonValue, LaunchSnapshot, WorkflowAgentSessionReference, WorkflowErrorCode, WorkflowScriptCall } from "./types.js";
|
|
9
9
|
import { normalizeSubagentRunRequest, type SubagentProgress, type SubagentRunRequest, type SubagentStatus } from "../subagents/src/contracts.js";
|
|
10
10
|
import { statusValue, subagentErrorValue } from "../subagents/src/decode.js";
|
|
11
11
|
import { isNodeError, jsonValue, object, resourcePatternHasMagic, selectResourcesByLayers } from "./utils.js";
|
|
12
12
|
import type { TrajectoryAction, TrajectoryTarget } from "./trajectory-contracts.js";
|
|
13
13
|
import { canonicalPath, sameFilesystemPath } from "./paths.js";
|
|
14
|
+
import { TOOL_TIMING_ENTRY_TYPE } from "./tool-timing.js";
|
|
14
15
|
|
|
15
16
|
export const TRAJECTORY_MAX_TRANSCRIPT_BYTES = 2 * 1024 * 1024;
|
|
16
17
|
const TRAJECTORY_MAX_NON_TIMING_ENTRIES = 400;
|
|
17
18
|
|
|
19
|
+
export type TrajectoryAgentOutput =
|
|
20
|
+
| { readonly status: "available"; readonly value: JsonValue; readonly bytes: number }
|
|
21
|
+
| { readonly status: "pending" }
|
|
22
|
+
| { readonly status: "failed" | "cancelled"; readonly code: string; readonly message: string }
|
|
23
|
+
| { readonly status: "truncated"; readonly kind: "result" | "failure"; readonly bytes: number; readonly path?: string }
|
|
24
|
+
| { readonly status: "unavailable" };
|
|
25
|
+
export type TrajectoryAgent = AgentRecord & { readonly output: TrajectoryAgentOutput };
|
|
26
|
+
export type TrajectoryScriptCall = WorkflowScriptCall & { readonly output: TrajectoryAgentOutput | { readonly status: "completed" } | { readonly status: "failed" | "cancelled"; readonly code: WorkflowErrorCode } };
|
|
27
|
+
export type TrajectoryRunRecord = Omit<PersistedRun, "agents" | "scriptCalls"> & { readonly agents: readonly TrajectoryAgent[]; readonly scriptCalls?: readonly TrajectoryScriptCall[] };
|
|
18
28
|
export type TrajectoryRun = {
|
|
19
|
-
run:
|
|
29
|
+
run: TrajectoryRunRecord;
|
|
20
30
|
snapshot: Readonly<LaunchSnapshot>;
|
|
21
31
|
awaiting: readonly AwaitingCheckpoint[];
|
|
22
32
|
createdAt?: string;
|
|
@@ -26,6 +36,31 @@ export type TrajectoryTranscriptStatus = "available" | "empty" | "missing" | "fa
|
|
|
26
36
|
export type TrajectoryTranscriptMetadata = { readonly revision: number; readonly status: TrajectoryTranscriptStatus; readonly bytes?: number; readonly timing?: readonly unknown[]; };
|
|
27
37
|
export type TrajectoryRunMetadata = Omit<TrajectoryRun, "transcripts"> & { transcripts: Readonly<Record<string, TrajectoryTranscriptMetadata>> };
|
|
28
38
|
export type TrajectorySubagentArtifact = { readonly truncated: true; readonly path: string; readonly bytes: number };
|
|
39
|
+
type SubagentOutputOptions = {
|
|
40
|
+
readonly state: SubagentStatus["state"];
|
|
41
|
+
readonly statusError: SubagentStatus["error"];
|
|
42
|
+
readonly attempt: AgentAttemptSummary | undefined;
|
|
43
|
+
readonly result: JsonValue | undefined;
|
|
44
|
+
readonly resultTruncated: boolean;
|
|
45
|
+
readonly resultPath: string;
|
|
46
|
+
readonly resultBytes: number | undefined;
|
|
47
|
+
readonly failure: { readonly code: string; readonly message: string } | undefined;
|
|
48
|
+
readonly failureTruncated: boolean;
|
|
49
|
+
readonly failurePath: string;
|
|
50
|
+
readonly failureBytes: number | undefined;
|
|
51
|
+
};
|
|
52
|
+
function subagentOutput(options: SubagentOutputOptions): TrajectoryAgentOutput {
|
|
53
|
+
if (options.resultTruncated) return { status: "truncated", kind: "result", path: options.resultPath, bytes: options.resultBytes ?? 0 };
|
|
54
|
+
if (options.result !== undefined) return { status: "available", value: options.result, bytes: serializedJsonBytes(options.result) };
|
|
55
|
+
if (options.failureTruncated) return { status: "truncated", kind: "failure", path: options.failurePath, bytes: options.failureBytes ?? 0 };
|
|
56
|
+
if (options.failure !== undefined) return { status: "failed", code: options.failure.code, message: options.failure.message };
|
|
57
|
+
if (options.state === "failed" || options.state === "stopped") {
|
|
58
|
+
const error = options.statusError ?? options.attempt?.error;
|
|
59
|
+
return { status: options.state === "stopped" ? "cancelled" : "failed", code: error?.code ?? (options.state === "stopped" ? "CANCELLED" : "AGENT_FAILED"), message: error?.message ?? `Subagent ${options.state}` };
|
|
60
|
+
}
|
|
61
|
+
if (options.state === "completed") return { status: "unavailable" };
|
|
62
|
+
return { status: "pending" };
|
|
63
|
+
}
|
|
29
64
|
export type TrajectorySubagent = {
|
|
30
65
|
id: string;
|
|
31
66
|
sessionId: string;
|
|
@@ -47,6 +82,7 @@ export type TrajectorySubagent = {
|
|
|
47
82
|
attempt?: AgentAttemptSummary;
|
|
48
83
|
result?: JsonValue | TrajectorySubagentArtifact;
|
|
49
84
|
failure?: { readonly code: string; readonly message: string } | TrajectorySubagentArtifact;
|
|
85
|
+
output: TrajectoryAgentOutput;
|
|
50
86
|
transcript: readonly unknown[];
|
|
51
87
|
};
|
|
52
88
|
export type TrajectorySubagentMetadata = Omit<TrajectorySubagent, "transcript"> & { transcript: TrajectoryTranscriptMetadata };
|
|
@@ -69,7 +105,6 @@ export type TrajectoryPublisherInput = {
|
|
|
69
105
|
cwd: string;
|
|
70
106
|
sessionId: string;
|
|
71
107
|
port?: number;
|
|
72
|
-
themes: boolean;
|
|
73
108
|
loadRuns: TrajectoryRunLoader;
|
|
74
109
|
loadSubagents: TrajectorySubagentLoader;
|
|
75
110
|
loadMetadata: TrajectoryPublisherMetadataLoader;
|
|
@@ -91,7 +126,7 @@ function transcriptToolCallId(value: unknown): string | undefined {
|
|
|
91
126
|
for (const part of message.content) if (object(part) && typeof part.id === "string") return part.id;
|
|
92
127
|
return undefined;
|
|
93
128
|
}
|
|
94
|
-
function isTimingTranscriptEntry(value: unknown): boolean { return object(value) && value.type === "custom" && value.customType ===
|
|
129
|
+
export function isTimingTranscriptEntry(value: unknown): boolean { return object(value) && value.type === "custom" && value.customType === TOOL_TIMING_ENTRY_TYPE; }
|
|
95
130
|
function timingToolCallId(value: unknown): string | undefined {
|
|
96
131
|
if (!object(value) || !isTimingTranscriptEntry(value) || !object(value.data)) return undefined;
|
|
97
132
|
return typeof value.data.toolCallId === "string" ? value.data.toolCallId : undefined;
|
|
@@ -210,7 +245,8 @@ export function withPiToolDescriptionsForTools(tools: readonly string[], cwd: st
|
|
|
210
245
|
function canonicalExtensionSelector(selector: string, base: string): string {
|
|
211
246
|
const negated = selector.startsWith("!");
|
|
212
247
|
const body = negated ? selector.slice(1) : selector;
|
|
213
|
-
|
|
248
|
+
// Built-in extensions are named `builtin:<name>`, not by a path.
|
|
249
|
+
if (body === "*" || body === "**" || body.startsWith("**/") || body.startsWith("builtin:")) return selector;
|
|
214
250
|
const resolved = resolve(base, body);
|
|
215
251
|
if (resourcePatternHasMagic(body)) return `${negated ? "!" : ""}${resolved}`;
|
|
216
252
|
return `${negated ? "!" : ""}${canonicalPath(resolved)}`;
|
|
@@ -277,11 +313,62 @@ function withoutAgentActivities(run: PersistedRun): PersistedRun {
|
|
|
277
313
|
return agents.every((agent, index) => agent === run.agents[index]) ? run : { ...run, agents };
|
|
278
314
|
}
|
|
279
315
|
|
|
316
|
+
function serializedJsonBytes(value: JsonValue): number {
|
|
317
|
+
const serialized = JSON.stringify(value);
|
|
318
|
+
return Buffer.byteLength(serialized, "utf8");
|
|
319
|
+
}
|
|
320
|
+
function failedAgentOutput(agent: PersistedRun["agents"][number]): TrajectoryAgentOutput {
|
|
321
|
+
const error = [...(agent.attemptDetails ?? [])].reverse().find((attempt) => attempt.error)?.error;
|
|
322
|
+
const status = agent.state === "cancelled" ? "cancelled" : "failed";
|
|
323
|
+
return { status, code: error?.code ?? (status === "cancelled" ? "CANCELLED" : "AGENT_FAILED"), message: error?.message ?? `Agent ${agent.state}` };
|
|
324
|
+
}
|
|
325
|
+
function operationOutput(operation: CompletedOperation): TrajectoryAgentOutput {
|
|
326
|
+
const bytes = serializedJsonBytes(operation.value);
|
|
327
|
+
return bytes > DEFAULT_MAX_BYTES ? { status: "truncated", kind: "result", bytes } : { status: "available", value: operation.value, bytes };
|
|
328
|
+
}
|
|
329
|
+
function outputForCall(call: WorkflowScriptCall, operation: CompletedOperation | undefined): TrajectoryScriptCall["output"] {
|
|
330
|
+
if (operation !== undefined) return call.kind === "tool" ? { status: "completed" } : operationOutput(operation);
|
|
331
|
+
if (call.error) return { status: call.error.code === "CANCELLED" ? "cancelled" : "failed", code: call.error.code, ...(call.kind === "shell" ? { message: call.error.message ?? "Shell failed" } : {}) };
|
|
332
|
+
return call.finishedAt === undefined ? { status: "pending" } : { status: "unavailable" };
|
|
333
|
+
}
|
|
334
|
+
function outputForAgent(agent: PersistedRun["agents"][number], operation: CompletedOperation | undefined): TrajectoryAgentOutput {
|
|
335
|
+
if (operation !== undefined) return operationOutput(operation);
|
|
336
|
+
if (agent.state === "failed" || agent.state === "cancelled") return failedAgentOutput(agent);
|
|
337
|
+
if (agent.state === "completed") return { status: "unavailable" };
|
|
338
|
+
return { status: "pending" };
|
|
339
|
+
}
|
|
340
|
+
export function applyTrajectoryAgentOutputs(run: PersistedRun, operations: readonly CompletedOperation[]): TrajectoryRunRecord {
|
|
341
|
+
const byPath = new Map(operations.map((operation) => [operation.path, operation]));
|
|
342
|
+
const agents: readonly TrajectoryAgent[] = run.agents.map((agent) => ({ ...agent, output: outputForAgent(agent, agent.resultPath === undefined ? undefined : byPath.get(agent.resultPath)) }));
|
|
343
|
+
const scriptCalls = run.scriptCalls?.map((call): TrajectoryScriptCall => {
|
|
344
|
+
const recorded = { ...call, output: outputForCall(call, byPath.get(call.path)) };
|
|
345
|
+
if (call.kind === "tool") {
|
|
346
|
+
delete recorded.input;
|
|
347
|
+
delete recorded.inputBytes;
|
|
348
|
+
if (call.error) recorded.error = { code: call.error.code };
|
|
349
|
+
}
|
|
350
|
+
return recorded;
|
|
351
|
+
});
|
|
352
|
+
const rest: Omit<PersistedRun, "scriptCalls"> = run;
|
|
353
|
+
return { ...rest, agents, ...(scriptCalls === undefined ? {} : { scriptCalls }) };
|
|
354
|
+
}
|
|
355
|
+
function overlayTrajectoryRun(run: TrajectoryRunRecord, overlay: (run: PersistedRun) => PersistedRun): TrajectoryRunRecord {
|
|
356
|
+
const overlaid: Omit<PersistedRun, "scriptCalls"> = overlay(run);
|
|
357
|
+
const outputs = new Map(run.agents.map((agent) => [agent.id, agent.output]));
|
|
358
|
+
const agents: readonly TrajectoryAgent[] = overlaid.agents.map((agent) => {
|
|
359
|
+
const previous = outputs.get(agent.id);
|
|
360
|
+
const output = previous?.status === "available" || previous?.status === "truncated" ? previous : outputForAgent(agent, undefined);
|
|
361
|
+
return { ...agent, output };
|
|
362
|
+
});
|
|
363
|
+
return { ...overlaid, agents, ...(run.scriptCalls === undefined ? {} : { scriptCalls: run.scriptCalls }) };
|
|
364
|
+
}
|
|
280
365
|
async function loadTrajectoryRun(store: RunStore, includeTranscripts = true): Promise<TrajectoryRun> {
|
|
281
366
|
const value = await store.load();
|
|
282
367
|
const summary = await store.loadSummary().catch(() => undefined);
|
|
283
368
|
const prompts = await store.systemPrompts().catch(() => []);
|
|
284
|
-
const
|
|
369
|
+
const prepared = withoutAgentActivities(await withResolvedResources(withPiToolDescriptions(applySystemPrompts(value.run, prompts)), store.cwd));
|
|
370
|
+
const operations = await store.replayableOperations().catch(() => []);
|
|
371
|
+
const run = applyTrajectoryAgentOutputs(prepared, operations);
|
|
285
372
|
return { run, snapshot: value.snapshot, awaiting: await store.awaitingCheckpoints(), ...(summary?.createdAt === undefined ? {} : { createdAt: summary.createdAt }), transcripts: includeTranscripts ? await runTranscripts(run) : {} };
|
|
286
373
|
}
|
|
287
374
|
|
|
@@ -290,7 +377,7 @@ export async function loadTrajectoryRuns(cwd: string, sessionId: string, home =
|
|
|
290
377
|
for (const runId of await listRunIds(cwd, sessionId, home, false)) {
|
|
291
378
|
try {
|
|
292
379
|
const value = await loadTrajectoryRun(new RunStore(cwd, sessionId, runId, home));
|
|
293
|
-
loaded.push(overlay ? { ...value, run:
|
|
380
|
+
loaded.push(overlay ? { ...value, run: overlayTrajectoryRun(value.run, overlay) } : value);
|
|
294
381
|
} catch { /* Ignore corrupt or concurrently removed runs. */ }
|
|
295
382
|
}
|
|
296
383
|
return loaded;
|
|
@@ -323,7 +410,7 @@ function createCachedRunLoader<Value extends { run: PersistedRun }, Signature>(c
|
|
|
323
410
|
entry = { value, stateMtimeMs: (await fileMtime(join(store.directory, "state.json"))) ?? 0, journalMtimeMs: (await fileMtime(join(store.directory, "journal.json"))) ?? 0, transcripts };
|
|
324
411
|
cache.set(runId, entry);
|
|
325
412
|
}
|
|
326
|
-
loaded.push(overlay ? { ...entry.value, run:
|
|
413
|
+
loaded.push(overlay ? { ...entry.value, run: overlayTrajectoryRun(entry.value.run as TrajectoryRunRecord, overlay) } : entry.value);
|
|
327
414
|
} catch { cache.delete(runId); /* Ignore corrupt or concurrently removed runs. */ }
|
|
328
415
|
}
|
|
329
416
|
return loaded;
|
|
@@ -490,6 +577,7 @@ async function loadTrajectorySubagent(directory: string, cwd: string, sessionId:
|
|
|
490
577
|
...(attempt === undefined ? {} : { attempt }),
|
|
491
578
|
...(result === undefined ? {} : { result }),
|
|
492
579
|
...(failure === undefined ? {} : { failure }),
|
|
580
|
+
output: subagentOutput({ state: status.state, statusError: status.error, attempt, result: resultValue === undefined ? undefined : boundedSubagentJson(resultValue), resultTruncated, resultPath, resultBytes, failure: failureValue === undefined ? undefined : subagentErrorValue(failureValue), failureTruncated, failurePath, failureBytes }),
|
|
493
581
|
};
|
|
494
582
|
} catch { return undefined; }
|
|
495
583
|
}
|
package/src/types.ts
CHANGED
|
@@ -29,7 +29,7 @@ export const WORKFLOW_BLOCKED_EVENT = "workflow:blocked";
|
|
|
29
29
|
export const ERROR_CODES = [
|
|
30
30
|
"CONFIG_ERROR", "INVALID_SETTINGS", "INVALID_SYNTAX", "INVALID_METADATA", "DUPLICATE_NAME", "INVALID_SCHEMA", "UNKNOWN_MODEL", "UNKNOWN_TOOL", "UNKNOWN_AGENT_TYPE",
|
|
31
31
|
"RUN_OWNED", "RUN_NOT_FOUND", "REGISTRY_FROZEN", "GLOBAL_COLLISION", "MISSING_WORKFLOW", "RPC_LIMIT_EXCEEDED", "SHELL_FAILED", "AGENT_TIMEOUT", "AGENT_FAILED", "AGENT_RESULT_COLLECTED", "RESULT_INVALID",
|
|
32
|
-
"CANCELLED", "WORKER_UNRESPONSIVE", "WORKTREE_FAILED", "RESUME_INCOMPATIBLE", "BUDGET_EXHAUSTED", "INTERNAL_ERROR",
|
|
32
|
+
"TOOL_FAILED", "CANCELLED", "WORKER_UNRESPONSIVE", "WORKTREE_FAILED", "RESUME_INCOMPATIBLE", "BUDGET_EXHAUSTED", "INTERNAL_ERROR",
|
|
33
33
|
] as const;
|
|
34
34
|
|
|
35
35
|
export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
@@ -90,15 +90,16 @@ export interface WorkflowModelAliasResolverContext { cwd: string; projectTrusted
|
|
|
90
90
|
export interface WorkflowModelAlias { resolve: (context: Readonly<WorkflowModelAliasResolverContext>) => string | Promise<string> }
|
|
91
91
|
export interface WorkflowMetadata { name: string; description?: string }
|
|
92
92
|
export interface HerdrExtensionSettings { enableFullyInspectableMode?: boolean }
|
|
93
|
-
export interface TrajectoryExtensionSettings { port?: number
|
|
93
|
+
export interface TrajectoryExtensionSettings { port?: number }
|
|
94
94
|
export interface WorkflowExtensionSettings { readonly [namespace: string]: JsonValue; readonly herdr?: Readonly<HerdrExtensionSettings>; readonly trajectory?: Readonly<TrajectoryExtensionSettings> }
|
|
95
95
|
export type WorkflowExtensionSettingsSource = "global" | "project" | "role" | "effective";
|
|
96
96
|
export interface WorkflowExtensionSettingsValidatorContext { source: WorkflowExtensionSettingsSource; cwd: string; projectTrusted: boolean; settingsPath?: string; role?: string }
|
|
97
97
|
export type WorkflowExtensionSettingsValidator = (settings: Readonly<WorkflowExtensionSettings>, context: Readonly<WorkflowExtensionSettingsValidatorContext>) => void;
|
|
98
98
|
export interface WorkflowSessionStartEvent { type: "session_start"; reason: "startup" | "reload" | "new" | "resume" | "fork"; previousSessionFile?: string; settings: Readonly<WorkflowExtensionSettings> }
|
|
99
99
|
export interface WorkflowRetentionSettings { olderThanDays?: number; maxTerminalRuns?: number }
|
|
100
|
-
export
|
|
101
|
-
export
|
|
100
|
+
export type CodemodeToolsSetting = "all" | "read-only" | "none";
|
|
101
|
+
export interface WorkflowSettings { concurrency: number; backgroundWidget?: boolean; codemodeTools?: CodemodeToolsSetting; modelAliases?: Readonly<Record<string, string>>; skills?: readonly string[]; extensions?: readonly string[]; extensionSettings?: Readonly<WorkflowExtensionSettings>; tools?: readonly string[]; retention?: Readonly<WorkflowRetentionSettings>; worktreePostCreateCommand?: readonly string[] }
|
|
102
|
+
export type WorkflowSettingsOverrides = Partial<Omit<WorkflowSettings, "backgroundWidget" | "codemodeTools">>;
|
|
102
103
|
export interface WorkflowSettingsSources { concurrency: string; modelAliases: string; skills?: string; extensions?: string; tools?: string; extensionSettings?: string; retention?: string; worktreePostCreateCommand?: string }
|
|
103
104
|
export interface WorkflowSettingsResolution { globalSettingsPath: string; projectSettingsPath: string; projectTrusted: boolean; global: Readonly<WorkflowSettings>; project: Readonly<WorkflowSettingsOverrides>; effective: Readonly<WorkflowSettings>; sources: Readonly<WorkflowSettingsSources> }
|
|
104
105
|
export interface AgentResourceSelectors { skills?: readonly string[]; extensions?: readonly string[]; tools?: readonly string[] }
|
|
@@ -178,6 +179,8 @@ export interface WorkflowRunEvent { type: string; message: string; timestamp?: n
|
|
|
178
179
|
export interface WorkflowRetryProvenance { sourceRunId: string; lineageRootRunId: string; completedPaths: readonly string[]; incompletePaths: readonly string[]; namedWorktrees: readonly string[] }
|
|
179
180
|
export interface WorkflowPhaseRecord { phase: string; afterAgent: number }
|
|
180
181
|
export interface WorkflowPhaseShellActivity { phaseIndex: number; active: number; startedAt: number }
|
|
182
|
+
/** A script call recorded for inspection. Tool payloads stay in the replay journal, not this record. Oversized shell inputs are JSON prefixes, with `inputBytes` their full size. */
|
|
183
|
+
export interface WorkflowScriptCall { kind: "shell" | "tool"; name: string; path: string; input?: JsonValue; inputBytes?: number; phase?: string; startedAt: number; finishedAt?: number; error?: { code: WorkflowErrorCode; message?: string } }
|
|
181
184
|
export interface RunRecord {
|
|
182
185
|
id: string;
|
|
183
186
|
workflowName: string;
|
|
@@ -194,6 +197,8 @@ export interface RunRecord {
|
|
|
194
197
|
activeShells?: number;
|
|
195
198
|
activeShellStartedAt?: number;
|
|
196
199
|
activeShellsByPhase?: readonly WorkflowPhaseShellActivity[];
|
|
200
|
+
activeTools?: readonly { name: string; startedAt: number }[]; // Script tool calls in flight: a live overlay for rendering, never persisted.
|
|
201
|
+
scriptCalls?: readonly WorkflowScriptCall[];
|
|
197
202
|
error?: WorkflowErrorShape;
|
|
198
203
|
failedAt?: string;
|
|
199
204
|
budget?: WorkflowBudget;
|
|
@@ -356,7 +361,9 @@ export interface FunctionIdentity { path: string; structuralPath: readonly strin
|
|
|
356
361
|
export type AgentContinuity = "fresh" | "continued";
|
|
357
362
|
export interface AgentIdentity { structuralPath: readonly string[]; callSite: string; occurrence: number; parentBreadcrumb?: string; worktreeOwner?: string; handle?: string; turn?: number }
|
|
358
363
|
export interface ShellIdentity { structuralPath: readonly string[]; callSite: string; occurrence: number; worktreeOwner?: string }
|
|
359
|
-
|
|
364
|
+
/** A script `tools.<id>(args)` call: the n-th call of that tool within one structural scope. */
|
|
365
|
+
export interface ToolIdentity { structuralPath: readonly string[]; occurrence: number }
|
|
366
|
+
export interface WorkflowBridge { agent?: (prompt: string, options: Readonly<Record<string, JsonValue>>, signal: AbortSignal, identity: AgentIdentity) => Promise<JsonValue>; shell?: (command: string, options: ShellOptions, signal: AbortSignal, identity: ShellIdentity) => Promise<ShellResult>; checkpoint?: (input: Readonly<Record<string, JsonValue>>, signal: AbortSignal) => boolean | Promise<boolean>; function?: (name: string, input: Readonly<Record<string, JsonValue>>, signal: AbortSignal, identity: FunctionIdentity) => Promise<JsonValue>; worktree?: (owner: string, signal: AbortSignal) => Promise<Readonly<WorkflowWorktreeReference>>; functions?: Readonly<Record<string, { name: string }>>; phase?: (name: string) => void | Promise<void>; log?: (message: string) => void | Promise<void>; tool?: (identifier: string, args: Readonly<Record<string, JsonValue>>, signal: AbortSignal, identity: ToolIdentity) => Promise<JsonValue> }
|
|
360
367
|
export interface WorkflowExecution { result: Promise<JsonValue>; cancel: () => void }
|
|
361
368
|
export interface StaticWorkflowScope { kind: "parallel" | "pipeline"; name: string | null; key: string | null }
|
|
362
369
|
export type StaticWorkflowExecution = "parallel" | "sequential";
|
package/src/utils.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { ERROR_CODES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, THINKING_LEVELS, WorkflowError, type JsonSchema, type JsonValue, type ModelSpec, type ThinkingLevel, type WorkflowErrorCode, type WorkflowExtensionSettings } from "./types.js";
|
|
2
3
|
import { Minimatch } from "minimatch";
|
|
3
4
|
export class SerialLane {
|
|
4
5
|
#tail: Promise<void> = Promise.resolve();
|
|
@@ -18,6 +19,15 @@ export function sanitizeDisplayText(value: string): string { return value.replac
|
|
|
18
19
|
export { object as isObject };
|
|
19
20
|
function isStringKey(key: PropertyKey): key is string { return typeof key === "string"; }
|
|
20
21
|
function stringKeyValue(value: object, key: string): unknown { return object(value) ? value[key] : undefined; }
|
|
22
|
+
function sortedJson(value: JsonValue): JsonValue {
|
|
23
|
+
if (Array.isArray(value)) return value.map(sortedJson);
|
|
24
|
+
if (value === null || typeof value !== "object") return value;
|
|
25
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortedJson(value[key] as JsonValue)]));
|
|
26
|
+
}
|
|
27
|
+
/** A short digest of a JSON value that ignores object key order. */
|
|
28
|
+
export function jsonDigest(value: JsonValue): string {
|
|
29
|
+
return createHash("sha256").update(JSON.stringify(sortedJson(value))).digest("hex").slice(0, 16);
|
|
30
|
+
}
|
|
21
31
|
export function jsonValue(value: unknown, seen = new Set<object>()): value is JsonValue {
|
|
22
32
|
if (value === null || typeof value === "boolean" || typeof value === "string") return true;
|
|
23
33
|
if (typeof value === "number") return Number.isFinite(value);
|
|
@@ -153,6 +163,24 @@ export function resolveModelReference(value: string, aliases: Readonly<Record<st
|
|
|
153
163
|
};
|
|
154
164
|
return resolveReference(value, []);
|
|
155
165
|
}
|
|
166
|
+
type ToolSource = { getActiveTools(): string[]; getAllTools?(): readonly { name: string; exposure?: string }[] };
|
|
167
|
+
/**
|
|
168
|
+
* Tools a session can reach, and so the ceiling for its agents: the declared tools plus the ones
|
|
169
|
+
* only codemode scripts call or tool_search loads, such as MCP tools with the default exposure.
|
|
170
|
+
* Agent sessions allow tools by name, so leaving these out would make them unreachable there.
|
|
171
|
+
*/
|
|
172
|
+
export function reachableTools(pi: ToolSource): string[] {
|
|
173
|
+
const undeclared = pi.getAllTools?.().filter(({ exposure }) => exposure === "codemode" || exposure === "deferred").map(({ name }) => name) ?? [];
|
|
174
|
+
return [...new Set([...pi.getActiveTools(), ...undeclared])];
|
|
175
|
+
}
|
|
176
|
+
/** Provider of the virtual models that expose workflow model aliases in `/model`. */
|
|
177
|
+
export const VIRTUAL_MODEL_PROVIDER = "workflow";
|
|
178
|
+
/** Child sessions do not load the workflow extension, so they receive the alias target instead of its virtual model. */
|
|
179
|
+
export function physicalModel(spec: ModelSpec, aliases: Readonly<Record<string, string>> = {}, knownModels?: ReadonlySet<string>, settingsPath?: string): ModelSpec {
|
|
180
|
+
if (spec.provider !== VIRTUAL_MODEL_PROVIDER || !modelAliasName(spec.model, aliases)) return spec;
|
|
181
|
+
const target = resolveModelReference(spec.model, aliases, knownModels, settingsPath);
|
|
182
|
+
return spec.thinking === undefined ? target : { ...target, thinking: spec.thinking };
|
|
183
|
+
}
|
|
156
184
|
export function modelCapability(value: string | ModelSpec, aliases?: Readonly<Record<string, string>>, knownModels?: ReadonlySet<string>, settingsPath?: string): string {
|
|
157
185
|
const parsed = typeof value === "string" ? resolveModelReference(value, aliases, knownModels, settingsPath) : value;
|
|
158
186
|
return `${parsed.provider}/${parsed.model}`;
|
|
@@ -190,3 +218,10 @@ export function resourcePatternHasMagic(pattern: string): boolean { return /[*?\
|
|
|
190
218
|
export function unmatchedResourcePatterns(patterns: readonly string[], resources: readonly string[]): string[] { return patterns.filter((pattern) => !resources.some((resource) => resourcePatternMatches(resource, pattern))); }
|
|
191
219
|
export function createLaunchSnapshot(input: Omit<import("./types.js").LaunchSnapshot, "identityVersion"> & { identityVersion?: number }): Readonly<import("./types.js").LaunchSnapshot> { return deepFreeze(structuredClone({ ...input, identityVersion: input.identityVersion ?? LAUNCH_SNAPSHOT_IDENTITY_VERSION })); }
|
|
192
220
|
export function loadLaunchSnapshot(input: import("./types.js").LaunchSnapshot): Readonly<import("./types.js").LaunchSnapshot> { return deepFreeze(structuredClone(input)); }
|
|
221
|
+
|
|
222
|
+
export function validateSchema(schema: unknown, at = "schema"): asserts schema is JsonSchema {
|
|
223
|
+
if (!object(schema) || Object.getPrototypeOf(schema) !== Object.prototype || !jsonValue(schema)) fail("INVALID_SCHEMA", `${at} must be a plain JSON-compatible Schema object`);
|
|
224
|
+
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`);
|
|
225
|
+
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`);
|
|
226
|
+
if (schema.properties !== undefined && !object(schema.properties)) fail("INVALID_SCHEMA", `${at}.properties must be an object`);
|
|
227
|
+
}
|