@marcoscale98/pi-extensible-workflows 5.19.1-fork.1 → 6.2.1-fork.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +77 -0
- package/README.md +2 -2
- package/dist/src/agent-execution.d.ts +43 -10
- package/dist/src/agent-execution.js +365 -127
- package/dist/src/background-widget.d.ts +0 -1
- package/dist/src/background-widget.js +4 -9
- package/dist/src/decoders.d.ts +2 -3
- package/dist/src/decoders.js +33 -37
- package/dist/src/execution.d.ts +2 -0
- package/dist/src/execution.js +5 -5
- package/dist/src/host-delivery.js +4 -8
- package/dist/src/host-navigator.js +5 -2
- package/dist/src/host-phases.d.ts +1 -0
- package/dist/src/host-recovery.d.ts +6 -6
- package/dist/src/host-recovery.js +14 -22
- package/dist/src/host-runtime.js +56 -5
- package/dist/src/host-view.d.ts +1 -1
- package/dist/src/host-view.js +17 -15
- package/dist/src/host.js +62 -87
- package/dist/src/index.d.ts +1 -2
- package/dist/src/index.js +1523 -1630
- package/dist/src/index.js.map +3 -3
- package/dist/src/paths.d.ts +7 -6
- package/dist/src/paths.js +24 -15
- package/dist/src/persistence.d.ts +1 -0
- package/dist/src/persistence.js +1 -0
- package/dist/src/pi-runtime-runner.js +4 -1
- package/dist/src/registry.d.ts +9 -7
- package/dist/src/registry.js +28 -70
- package/dist/src/runtime/agent-runner.d.ts +2 -0
- package/dist/src/session-lease.d.ts +1 -1
- package/dist/src/session-lease.js +45 -11
- package/dist/src/settings.d.ts +6 -3
- package/dist/src/settings.js +36 -47
- package/dist/src/store.d.ts +3 -1
- package/dist/src/store.js +17 -1
- package/dist/src/trajectory.d.ts +0 -1
- package/dist/src/trajectory.js +2 -3
- package/dist/src/types.d.ts +100 -44
- package/dist/src/types.js +4 -7
- package/dist/src/utils.d.ts +3 -2
- package/dist/src/utils.js +20 -10
- package/dist/src/validation.d.ts +8 -2
- package/dist/src/validation.js +122 -86
- package/dist/starter/index.js +8 -9
- package/dist/starter/index.js.map +1 -1
- package/dist/starter/review-loop.js +8 -7
- package/dist/subagents/index.js +654 -814
- package/dist/subagents/index.js.map +3 -3
- package/dist/subagents/src/contracts.d.ts +4 -3
- package/dist/subagents/src/contracts.js +2 -2
- package/dist/subagents/src/index.d.ts +1 -1
- package/dist/subagents/src/index.js +1 -2
- package/dist/subagents/src/manager.js +49 -70
- package/dist/subagents/src/navigator.js +3 -14
- package/dist/subagents/src/view.d.ts +0 -1
- package/dist/subagents/src/view.js +2 -16
- package/dist/subagents/src/worktree.js +1 -1
- package/dist/trajectory/assets/index.html +8 -8
- package/dist/trajectory/index.js +487 -891
- package/dist/trajectory/index.js.map +3 -3
- package/dist/trajectory/src/assets/index.html +8 -8
- package/dist/trajectory/src/index.js +126 -86
- package/dist/trajectory/src/server.js +264 -25
- package/dist/trajectory/src/server.js.map +3 -3
- package/dist/trajectory/src/startup-mutex.d.ts +17 -0
- package/dist/trajectory/src/startup-mutex.js +172 -0
- package/examples/workflow-extension-template/README.md +4 -7
- package/examples/workflow-extension-template/extension.test.mjs +4 -8
- package/examples/workflow-extension-template/index.js +0 -3
- package/package.json +5 -5
- package/skills/pi-extensible-workflows/SKILL.md +10 -8
- package/src/agent-execution.ts +339 -127
- package/src/background-widget.ts +4 -10
- package/src/decoders.ts +31 -37
- package/src/execution.ts +5 -5
- package/src/host-delivery.ts +4 -8
- package/src/host-navigator.ts +4 -2
- package/src/host-phases.ts +1 -1
- package/src/host-recovery.ts +20 -26
- package/src/host-runtime.ts +44 -6
- package/src/host-view.ts +18 -11
- package/src/host.ts +62 -113
- package/src/index.ts +1 -2
- package/src/paths.ts +18 -12
- package/src/persistence.ts +1 -0
- package/src/pi-runtime-runner.ts +3 -1
- package/src/registry.ts +27 -63
- package/src/runtime/agent-runner.ts +2 -0
- package/src/session-lease.ts +34 -6
- package/src/settings.ts +35 -47
- package/src/store.ts +18 -2
- package/src/trajectory.ts +2 -4
- package/src/types.ts +71 -30
- package/src/utils.ts +18 -9
- package/src/validation.ts +102 -79
- package/starter/index.ts +0 -1
- package/starter/review-loop.ts +8 -7
- package/starter/roles/developer.md +0 -1
- package/starter/roles/oracle.md +0 -1
- package/starter/roles/researcher.md +0 -1
- package/starter/roles/reviewer.md +0 -1
- package/starter/roles/scout.md +0 -1
- package/subagents/README.md +8 -14
- package/subagents/src/contracts.ts +3 -4
- package/subagents/src/index.ts +2 -3
- package/subagents/src/manager.ts +45 -125
- package/subagents/src/navigator.ts +4 -13
- package/subagents/src/view.ts +4 -14
- package/subagents/src/worktree.ts +1 -1
- package/trajectory/src/assets/index.html +8 -8
- package/trajectory/src/index.ts +106 -82
- package/trajectory/src/server.ts +44 -8
- package/trajectory/src/startup-mutex.ts +127 -0
- package/dist/src/roles.d.ts +0 -70
- package/dist/src/roles.js +0 -326
- package/dist/starter/roles/developer.md +0 -6
- package/dist/starter/roles/oracle.md +0 -18
- package/dist/starter/roles/researcher.md +0 -20
- package/dist/starter/roles/reviewer.md +0 -19
- package/dist/starter/roles/scout.md +0 -18
- package/examples/workflow-extension-template/roles/reviewer.md +0 -4
- package/src/roles.ts +0 -327
package/src/registry.ts
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { basename, dirname, isAbsolute, join } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
1
|
import { Value } from "typebox/value";
|
|
5
|
-
import type { AgentAttemptAction, JsonSchema, JsonValue, RegisteredAgentSetupHook, WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogModelAlias, WorkflowExtension, WorkflowExtensionSettings, WorkflowExtensionSettingsValidatorContext, WorkflowFunction, WorkflowFunctionContext, WorkflowFunctionSource, WorkflowJournal, WorkflowModelAlias, WorkflowModelAliasResolverContext
|
|
2
|
+
import type { AgentAttemptAction, JsonSchema, JsonValue, RegisteredAgentSetupHook, RegisteredAgentPreparationHook, WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogModelAlias, WorkflowExtension, WorkflowExtensionSettings, WorkflowExtensionSettingsValidatorContext, WorkflowFunction, WorkflowFunctionContext, WorkflowFunctionSource, WorkflowJournal, WorkflowModelAlias, WorkflowModelAliasResolverContext } from "./types.js";
|
|
6
3
|
import type { SubagentRunRequest, SubagentStatus } from "../subagents/src/contracts.js";
|
|
7
4
|
import { byPriorityThenName, deepFreeze, errorCode, errorText, fail, jsonValue, MODEL_ALIAS_NAME, object, validWorkflowExtensionNamespace } from "./utils.js";
|
|
8
5
|
import { loadSettings, resolveWorkflowSettings } from "./settings.js";
|
|
9
6
|
import { validateSchema } from "./utils.js";
|
|
10
|
-
import { canonicalPath } from "./paths.js";
|
|
11
|
-
import { isCorePackageName } from "./package-identity.js";
|
|
12
7
|
|
|
13
8
|
const RESERVED_GLOBALS = new Set(["agent", "shell", "tools", "prompt", "checkpoint", "parallel", "pipeline", "phase", "withWorktree", "log", "args", "Promise", "JSON", "Math", "Date", "eval", "Function", "WebAssembly", "process", "require", "module", "exports", "console", "fetch", "XMLHttpRequest", "WebSocket", "performance", "crypto", "setTimeout", "setInterval", "setImmediate", "queueMicrotask", "Intl", "SharedArrayBuffer", "Atomics", "globalThis", "global", "undefined", "NaN", "Infinity", "extensions", "workflow_catalog"]);
|
|
14
9
|
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
@@ -16,33 +11,6 @@ const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-
|
|
|
16
11
|
const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
17
12
|
export type SubagentStatusObserver = (status: Readonly<SubagentStatus>, request: Readonly<SubagentRunRequest>) => void;
|
|
18
13
|
|
|
19
|
-
function normalizeRoleDirectory(value: unknown): string {
|
|
20
|
-
try {
|
|
21
|
-
if (value instanceof URL) {
|
|
22
|
-
if (value.protocol !== "file:") fail("INVALID_METADATA", "Workflow role directories require file URLs or absolute filesystem paths");
|
|
23
|
-
return fileURLToPath(value);
|
|
24
|
-
}
|
|
25
|
-
if (typeof value === "string") {
|
|
26
|
-
if (value.startsWith("file:")) return fileURLToPath(value);
|
|
27
|
-
if (isAbsolute(value)) return value;
|
|
28
|
-
}
|
|
29
|
-
} catch (error) {
|
|
30
|
-
fail("INVALID_METADATA", `Invalid workflow role directory: ${errorText(error)}`);
|
|
31
|
-
}
|
|
32
|
-
fail("INVALID_METADATA", "Workflow role directories require file URLs or absolute filesystem paths");
|
|
33
|
-
}
|
|
34
|
-
function isBuiltinRoleDirectory(path: string): boolean {
|
|
35
|
-
const canonical = canonicalPath(path);
|
|
36
|
-
const starterDirectory = dirname(canonical);
|
|
37
|
-
if (basename(canonical) !== "roles" || basename(starterDirectory) !== "starter") return false;
|
|
38
|
-
const distributionDirectory = dirname(starterDirectory);
|
|
39
|
-
const packageRoot = basename(distributionDirectory) === "dist" ? dirname(distributionDirectory) : distributionDirectory;
|
|
40
|
-
try {
|
|
41
|
-
const metadata: unknown = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
|
|
42
|
-
return object(metadata) && isCorePackageName(metadata.name);
|
|
43
|
-
} catch { return false; }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
14
|
function catalogSchema(schema: unknown, at: string): JsonSchema {
|
|
47
15
|
validateSchema(schema, at);
|
|
48
16
|
return structuredClone(schema);
|
|
@@ -51,9 +19,9 @@ export class WorkflowRegistry {
|
|
|
51
19
|
readonly #extensions = new Set<Readonly<WorkflowExtension>>();
|
|
52
20
|
readonly #functionSources = new Map<string, WorkflowFunctionSource>();
|
|
53
21
|
readonly #globals = new Map<string, string>();
|
|
22
|
+
readonly #preparationHooks = new Map<string, RegisteredAgentPreparationHook>();
|
|
54
23
|
readonly #hooks = new Map<string, RegisteredAgentSetupHook>();
|
|
55
24
|
readonly #agentAttemptActions = new Map<string, AgentAttemptAction>();
|
|
56
|
-
readonly #roleDirectories = new Map<string, WorkflowRoleDirectoryRegistration>();
|
|
57
25
|
readonly #modelAliases = new Map<string, { name: string; version: string; headline: string; resolve: WorkflowModelAlias["resolve"] }>();
|
|
58
26
|
readonly #settingsValidators = new Set<{ headline: string; validate: NonNullable<WorkflowExtension["validateSettings"]> }>();
|
|
59
27
|
#frozen = false;
|
|
@@ -67,9 +35,10 @@ export class WorkflowRegistry {
|
|
|
67
35
|
register(extension: WorkflowExtension): void {
|
|
68
36
|
if (this.#frozen) fail("REGISTRY_FROZEN", "Workflow extension registration is closed after session_start");
|
|
69
37
|
if (object(extension) && Object.prototype.hasOwnProperty.call(extension, "workflows")) fail("INVALID_METADATA", "Separate registered workflow definitions were removed; register a function with input and output schemas instead");
|
|
70
|
-
if (!object(extension) || Object.keys(extension).some((key) => !["version", "headline", "description", "source", "dependencies", "validateSettings", "functions", "modelAliases", "
|
|
38
|
+
if (!object(extension) || Object.keys(extension).some((key) => !["version", "headline", "description", "source", "dependencies", "validateSettings", "functions", "modelAliases", "agentPreparationHooks", "agentSetupHooks", "agentAttemptActions"].includes(key)) || typeof extension.version !== "string" || !SEMVER.test(extension.version) || typeof extension.headline !== "string" || !extension.headline.trim()) fail("INVALID_METADATA", "Workflow extensions require a semantic version and non-empty headline");
|
|
71
39
|
const functions = extension.functions ?? {};
|
|
72
40
|
const modelAliases = extension.modelAliases ?? {};
|
|
41
|
+
const agentPreparationHooks = extension.agentPreparationHooks ?? {};
|
|
73
42
|
const agentSetupHooks = extension.agentSetupHooks ?? {};
|
|
74
43
|
const agentAttemptActions = extension.agentAttemptActions ?? {};
|
|
75
44
|
const source = extension.source;
|
|
@@ -79,10 +48,7 @@ export class WorkflowRegistry {
|
|
|
79
48
|
if (validateSettings !== undefined && typeof validateSettings !== "function") fail("INVALID_METADATA", "Workflow extension validateSettings must be a function");
|
|
80
49
|
if (source !== undefined && (typeof source !== "string" || !source.trim())) fail("INVALID_METADATA", "Workflow extension source must be a non-empty module URL");
|
|
81
50
|
if (dependencyValues !== undefined && (!Array.isArray(dependencyValues) || dependencyValues.some((dependency) => typeof dependency !== "string" || dependency.trim() !== dependency || !PACKAGE_NAME.test(dependency)) || new Set(dependencyValues).size !== dependencyValues.length)) fail("INVALID_METADATA", "Workflow extension dependencies must be unique, non-empty package names");
|
|
82
|
-
|
|
83
|
-
if (!Array.isArray(roleDirectoryValues)) fail("INVALID_METADATA", "Workflow extension roleDirectories must be an array");
|
|
84
|
-
const roleDirectories = [...new Set(Array.from(roleDirectoryValues, (value) => normalizeRoleDirectory(value)))];
|
|
85
|
-
if (!object(functions) || !object(modelAliases) || !object(agentSetupHooks) || !object(agentAttemptActions) || (Object.keys(functions).length === 0 && Object.keys(modelAliases).length === 0 && Object.keys(agentSetupHooks).length === 0 && Object.keys(agentAttemptActions).length === 0 && roleDirectories.length === 0 && validateSettings === undefined)) fail("INVALID_METADATA", "Workflow extensions require functions, model aliases, agent setup hooks, agent attempt actions, role directories, or a settings validator");
|
|
51
|
+
if (!object(functions) || !object(modelAliases) || !object(agentPreparationHooks) || !object(agentSetupHooks) || !object(agentAttemptActions) || (Object.keys(functions).length === 0 && Object.keys(modelAliases).length === 0 && Object.keys(agentPreparationHooks).length === 0 && Object.keys(agentSetupHooks).length === 0 && Object.keys(agentAttemptActions).length === 0 && validateSettings === undefined)) fail("INVALID_METADATA", "Workflow extensions require functions, model aliases, agent preparation hooks, agent setup hooks, agent attempt actions, or a settings validator");
|
|
86
52
|
const names = Object.keys(functions);
|
|
87
53
|
if (new Set(names).size !== names.length) fail("GLOBAL_COLLISION", "Global name collision inside extension");
|
|
88
54
|
for (const name of names) {
|
|
@@ -101,6 +67,11 @@ export class WorkflowRegistry {
|
|
|
101
67
|
if (!object(alias) || Object.keys(alias).some((key) => key !== "resolve") || typeof alias.resolve !== "function") fail("INVALID_METADATA", `Invalid model alias resolver: ${name}`);
|
|
102
68
|
if (this.#modelAliases.has(name)) fail("DUPLICATE_NAME", `Model alias already registered: ${name}`);
|
|
103
69
|
}
|
|
70
|
+
for (const [name, hook] of Object.entries(agentPreparationHooks)) {
|
|
71
|
+
if (!IDENTIFIER.test(name) || !object(hook) || Object.keys(hook).some((key) => !["priority", "optionsSchema", "prepare"].includes(key)) || typeof hook.prepare !== "function" || hook.priority !== undefined && (typeof hook.priority !== "number" || !Number.isFinite(hook.priority))) fail("INVALID_METADATA", `Invalid agent preparation hook: ${name}`);
|
|
72
|
+
if (this.#preparationHooks.has(name)) fail("DUPLICATE_NAME", `Agent preparation hook already registered: ${name}`);
|
|
73
|
+
if (hook.optionsSchema !== undefined) validateSchema(hook.optionsSchema, `${name} options schema`);
|
|
74
|
+
}
|
|
104
75
|
for (const [name, hook] of Object.entries(agentSetupHooks)) {
|
|
105
76
|
if (!IDENTIFIER.test(name) || !object(hook) || Object.keys(hook).some((key) => !["priority", "setup"].includes(key)) || typeof hook.setup !== "function" || hook.priority !== undefined && (typeof hook.priority !== "number" || !Number.isFinite(hook.priority))) fail("INVALID_METADATA", `Invalid agent setup hook: ${name}`);
|
|
106
77
|
if (this.#hooks.has(name)) fail("DUPLICATE_NAME", `Agent setup hook already registered: ${name}`);
|
|
@@ -110,17 +81,22 @@ export class WorkflowRegistry {
|
|
|
110
81
|
if ((action.visibleStandalone !== undefined) !== (action.runStandalone !== undefined)) fail("INVALID_METADATA", `Standalone agent attempt actions require visibleStandalone and runStandalone: ${name}`);
|
|
111
82
|
if (this.#agentAttemptActions.has(name)) fail("DUPLICATE_NAME", `Agent attempt action already registered: ${name}`);
|
|
112
83
|
}
|
|
113
|
-
const stored = deepFreeze({ ...extension, functions, modelAliases, agentSetupHooks, agentAttemptActions
|
|
84
|
+
const stored = deepFreeze({ ...extension, functions, modelAliases, agentPreparationHooks, agentSetupHooks, agentAttemptActions });
|
|
114
85
|
if (source !== undefined) for (const name of names) this.#functionSources.set(name, Object.freeze({ module: source, export: "default", dependencies: Object.freeze([...dependencies]) }));
|
|
115
86
|
this.#extensions.add(stored);
|
|
116
|
-
for (const directory of roleDirectories) if (!this.#roleDirectories.has(directory)) this.#roleDirectories.set(directory, deepFreeze({ path: directory, extension: { version: extension.version, headline: extension.headline }, ...(isBuiltinRoleDirectory(directory) ? { builtin: true as const } : {}) }));
|
|
117
87
|
for (const name of names) this.#globals.set(name, name);
|
|
118
88
|
for (const [name, alias] of Object.entries(modelAliases)) this.#modelAliases.set(name, { name, version: extension.version, headline: extension.headline, resolve: alias.resolve });
|
|
89
|
+
for (const [name, hook] of Object.entries(agentPreparationHooks)) this.#preparationHooks.set(name, { name, priority: hook.priority ?? 10, prepare: hook.prepare, ...(hook.optionsSchema === undefined ? {} : { optionsSchema: structuredClone(hook.optionsSchema) }), ...(source === undefined ? {} : { source }) });
|
|
119
90
|
for (const [name, hook] of Object.entries(agentSetupHooks)) this.#hooks.set(name, { name, priority: hook.priority ?? 10, setup: hook.setup });
|
|
120
91
|
for (const [name, action] of Object.entries(agentAttemptActions)) this.#agentAttemptActions.set(name, action);
|
|
121
92
|
if (validateSettings !== undefined) this.#settingsValidators.add({ headline: extension.headline, validate: validateSettings });
|
|
122
93
|
}
|
|
123
94
|
|
|
95
|
+
/** Every registration's headline and declared module provenance, for hosts that must prove which extension made it. */
|
|
96
|
+
registrations(): ReadonlyArray<Readonly<{ headline: string; source?: string }>> {
|
|
97
|
+
return [...this.#extensions].map(({ headline, source }) => Object.freeze({ headline, ...(source === undefined ? {} : { source }) }));
|
|
98
|
+
}
|
|
99
|
+
|
|
124
100
|
function(name: string): WorkflowFunction {
|
|
125
101
|
if (!IDENTIFIER.test(name)) fail("MISSING_WORKFLOW", `Registered functions require an unqualified name: ${name}`);
|
|
126
102
|
const fn = [...this.#extensions].find((extension) => extension.functions?.[name])?.functions?.[name];
|
|
@@ -145,7 +121,7 @@ export class WorkflowRegistry {
|
|
|
145
121
|
let source = "global settings";
|
|
146
122
|
try {
|
|
147
123
|
const resolved = context ? resolveWorkflowSettings(context.cwd, context.projectTrusted, context.globalSettingsPath) : undefined;
|
|
148
|
-
aliases = resolved?.effective.modelAliases ?? loadSettings().modelAliases;
|
|
124
|
+
aliases = resolved?.effective.modelAliases ?? (context ? loadSettings().modelAliases : resolveWorkflowSettings(process.cwd(), false).effective.modelAliases);
|
|
149
125
|
if (resolved) { source = resolved.projectTrusted && [resolved.sources.modelAliases, resolved.sources.skills, resolved.sources.extensions, resolved.sources.tools].some((path) => path === resolved.projectSettingsPath) ? "trusted project settings" : "global settings"; settings = { concurrency: resolved.effective.concurrency, backgroundWidget: resolved.effective.backgroundWidget ?? true, modelAliases: resolved.effective.modelAliases ?? {}, skills: resolved.effective.skills ?? [], extensions: Array.isArray(resolved.effective.extensions) ? resolved.effective.extensions : [], tools: resolved.effective.tools ?? [], ...(resolved.effective.extensionSettings === undefined ? {} : { extensionSettings: resolved.effective.extensionSettings }), ...(resolved.effective.worktreePostCreateCommand === undefined ? {} : { worktreePostCreateCommand: resolved.effective.worktreePostCreateCommand }), globalSettingsPath: resolved.globalSettingsPath, projectSettingsPath: resolved.projectSettingsPath, projectTrusted: resolved.projectTrusted, sources: resolved.sources }; }
|
|
150
126
|
} catch { aliases = undefined; }
|
|
151
127
|
const staticEntries: WorkflowCatalogModelAlias[] = Object.keys(aliases ?? {}).map((name) => ({ name, kind: "static", provenance: source }));
|
|
@@ -197,7 +173,7 @@ export class WorkflowRegistry {
|
|
|
197
173
|
for (const { headline, validate } of this.#settingsValidators) {
|
|
198
174
|
try { validate(frozenSettings, Object.freeze({ ...context })); }
|
|
199
175
|
catch (error) {
|
|
200
|
-
const location = context.source
|
|
176
|
+
const location = context.source;
|
|
201
177
|
fail("INVALID_SETTINGS", `Invalid extension settings (${headline}; ${location}): ${errorText(error)}`);
|
|
202
178
|
}
|
|
203
179
|
}
|
|
@@ -217,16 +193,11 @@ export class WorkflowRegistry {
|
|
|
217
193
|
return structuredClone(stored);
|
|
218
194
|
}
|
|
219
195
|
|
|
196
|
+
agentPreparationHooks(): readonly RegisteredAgentPreparationHook[] { return [...this.#preparationHooks.values()].sort(byPriorityThenName); }
|
|
220
197
|
agentSetupHooks(): readonly RegisteredAgentSetupHook[] {
|
|
221
198
|
return [...this.#hooks.values()].sort(byPriorityThenName);
|
|
222
199
|
}
|
|
223
200
|
agentAttemptActions(): Readonly<Record<string, AgentAttemptAction>> { return Object.freeze(Object.fromEntries(this.#agentAttemptActions.entries())); }
|
|
224
|
-
roleDirectories(): readonly string[] {
|
|
225
|
-
return [...this.#roleDirectories.keys()];
|
|
226
|
-
}
|
|
227
|
-
roleDirectoryRegistrations(): readonly WorkflowRoleDirectoryRegistration[] {
|
|
228
|
-
return [...this.#roleDirectories.values()];
|
|
229
|
-
}
|
|
230
201
|
modelAliases(): readonly { name: string; version: string; headline: string; resolve: WorkflowModelAlias["resolve"] }[] { return [...this.#modelAliases.values()].sort((left, right) => left.name.localeCompare(right.name)); }
|
|
231
202
|
async resolveModelAliases(context: Readonly<WorkflowModelAliasResolverContext>, shadowed: ReadonlySet<string> = new Set()): Promise<Readonly<Record<string, string>>> {
|
|
232
203
|
const resolved: Record<string, string> = {};
|
|
@@ -247,7 +218,7 @@ export class WorkflowRegistry {
|
|
|
247
218
|
return Object.freeze(resolved);
|
|
248
219
|
}
|
|
249
220
|
}
|
|
250
|
-
export type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "function" | "functions" | "functionSources" | "catalog" | "catalogIndex" | "catalogDetail" | "globals" | "invokeFunction" | "validateExtensionSettings" | "modelAliases" | "resolveModelAliases" | "
|
|
221
|
+
export type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "function" | "functions" | "functionSources" | "catalog" | "catalogIndex" | "catalogDetail" | "globals" | "invokeFunction" | "validateExtensionSettings" | "modelAliases" | "resolveModelAliases" | "agentPreparationHooks" | "agentSetupHooks" | "agentAttemptActions" | "setSubagentStatusObserver" | "observeSubagentStatus" | "registrations">;
|
|
251
222
|
interface WorkflowRegistryHost { api: WorkflowRegistryApi; activeHosts: number }
|
|
252
223
|
const WORKFLOW_REGISTRY_KEY = Symbol.for("pi-extensible-workflows.workflow-registry");
|
|
253
224
|
const globalRegistry = globalThis as typeof globalThis & Record<symbol, WorkflowRegistryHost | undefined>;
|
|
@@ -269,10 +240,10 @@ function createWorkflowRegistryApi(registry: WorkflowRegistry): WorkflowRegistry
|
|
|
269
240
|
validateExtensionSettings: (...args) => { registry.validateExtensionSettings(...args); },
|
|
270
241
|
modelAliases: () => registry.modelAliases(),
|
|
271
242
|
resolveModelAliases: (...args) => registry.resolveModelAliases(...args),
|
|
272
|
-
|
|
273
|
-
roleDirectoryRegistrations: () => registry.roleDirectoryRegistrations(),
|
|
243
|
+
agentPreparationHooks: () => registry.agentPreparationHooks(),
|
|
274
244
|
agentSetupHooks: () => registry.agentSetupHooks(),
|
|
275
245
|
agentAttemptActions: () => registry.agentAttemptActions(),
|
|
246
|
+
registrations: () => registry.registrations(),
|
|
276
247
|
};
|
|
277
248
|
}
|
|
278
249
|
function workflowRegistryHost(): WorkflowRegistryHost {
|
|
@@ -301,7 +272,9 @@ export function retainWorkflowRegistry(): () => void {
|
|
|
301
272
|
}
|
|
302
273
|
export function loadingRegistry(): WorkflowRegistryApi { return workflowRegistryHost().api; }
|
|
303
274
|
beginWorkflowExtensionLoading();
|
|
304
|
-
export function registerWorkflowExtension(extension: WorkflowExtension): void {
|
|
275
|
+
export function registerWorkflowExtension(extension: WorkflowExtension): void {
|
|
276
|
+
loadingRegistry().register(extension);
|
|
277
|
+
}
|
|
305
278
|
export function workflowCatalog(context?: WorkflowCatalogContext): WorkflowCatalog { return loadingRegistry().catalog(context); }
|
|
306
279
|
export function workflowCatalogIndex(context?: WorkflowCatalogContext): WorkflowCatalogIndex { return loadingRegistry().catalogIndex(context); }
|
|
307
280
|
export function workflowCatalogDetail(name: string, context?: WorkflowCatalogContext): WorkflowCatalogFunction | WorkflowCatalogModelAlias | WorkflowCatalogError { return loadingRegistry().catalogDetail(name, context); }
|
|
@@ -310,13 +283,4 @@ export function registeredWorkflowFunctionSources(): Readonly<Record<string, Wor
|
|
|
310
283
|
const sources = loadingRegistry().functionSources;
|
|
311
284
|
return typeof sources === "function" ? sources() : {};
|
|
312
285
|
}
|
|
313
|
-
export
|
|
314
|
-
const directories = loadingRegistry().roleDirectories;
|
|
315
|
-
return typeof directories === "function" ? directories() : [];
|
|
316
|
-
}
|
|
317
|
-
export function registeredWorkflowRoleDirectoryRegistrations(): readonly WorkflowRoleDirectoryRegistration[] {
|
|
318
|
-
const registrations = loadingRegistry().roleDirectoryRegistrations;
|
|
319
|
-
return typeof registrations === "function" ? registrations() : [];
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
export type { WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogIndexFunction, WorkflowCatalogModelAlias, WorkflowCatalogSettings, WorkflowRoleDirectoryRegistration } from "./types.js";
|
|
286
|
+
export type { WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogIndexFunction, WorkflowCatalogModelAlias, WorkflowCatalogSettings } from "./types.js";
|
|
@@ -121,6 +121,8 @@ export interface RuntimeAgentRunRequest {
|
|
|
121
121
|
readonly customTools: readonly RuntimeTool[];
|
|
122
122
|
/** Explicit result schema; omitted agents use the default string workflow_result schema. */
|
|
123
123
|
readonly resultSchema?: RuntimeJsonSchema;
|
|
124
|
+
/** Transient retry or budget text delivered as a separate steered message after the task, never inside it. */
|
|
125
|
+
readonly turnControl?: string;
|
|
124
126
|
readonly run: RuntimeRunIdentity;
|
|
125
127
|
readonly agent: RuntimeAgentIdentity;
|
|
126
128
|
readonly signal: AbortSignal;
|
package/src/session-lease.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { link, mkdir, open, readFile, readdir, rename, rm, stat } from "node:fs/promises";
|
|
3
4
|
import { homedir } from "node:os";
|
|
@@ -12,12 +13,39 @@ const SESSION_OWNER_FILE = "owner.json";
|
|
|
12
13
|
const SESSION_OWNER_WRITE_GRACE_MS = 30_000;
|
|
13
14
|
const RUN_CREATE_TEMP = /^\.([a-zA-Z0-9._-]+)\.(\d+)\.[0-9a-f-]+\.tmp$/;
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
const START_MARGIN_MS = 60_000;
|
|
17
|
+
let clockTicks: number | undefined;
|
|
18
|
+
|
|
19
|
+
function clockTickRate(): number {
|
|
20
|
+
if (clockTicks !== undefined) return clockTicks;
|
|
21
|
+
try {
|
|
22
|
+
const rate = Number(execFileSync("getconf", ["CLK_TCK"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim());
|
|
23
|
+
clockTicks = Number.isInteger(rate) && rate > 0 ? rate : 100;
|
|
24
|
+
} catch { clockTicks = 100; }
|
|
25
|
+
return clockTicks;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Process start in epoch ms from /proc/<pid>/stat field 22; undefined when unreadable or unparsable.
|
|
29
|
+
async function linuxProcessStart(pid: number, procRoot: string): Promise<number | undefined> {
|
|
30
|
+
try {
|
|
31
|
+
const text = await readFile(join(procRoot, String(pid), "stat"), "utf8");
|
|
32
|
+
// Field 2 (command name) may contain spaces and parentheses, so count fields after the last ")".
|
|
33
|
+
const close = text.lastIndexOf(")");
|
|
34
|
+
const ticks = close < 0 ? Number.NaN : Number(text.slice(close + 2).split(" ")[19]);
|
|
35
|
+
const btime = /^btime (\d+)$/m.exec(await readFile(join(procRoot, "stat"), "utf8"))?.[1];
|
|
36
|
+
if (btime === undefined || !Number.isInteger(ticks) || ticks < 0) return undefined;
|
|
37
|
+
return Number(btime) * 1000 + (ticks / clockTickRate()) * 1000;
|
|
38
|
+
} catch { return undefined; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The /proc/<pid> ctime is not a start time: procfs rebuilds the inode after cache reclaim.
|
|
42
|
+
// A live owner never starts after its stamp; a reused pid starts later.
|
|
43
|
+
export async function processAlive(pid: number, startedAt?: number, procRoot = "/proc"): Promise<boolean> {
|
|
44
|
+
try { process.kill(pid, 0); } catch (error) { return !isNodeError(error, "ESRCH"); }
|
|
45
|
+
if (startedAt === undefined || process.platform !== "linux") return true;
|
|
46
|
+
const start = await linuxProcessStart(pid, procRoot);
|
|
47
|
+
if (start !== undefined) return start <= startedAt + START_MARGIN_MS;
|
|
16
48
|
try { process.kill(pid, 0); } catch (error) { return !isNodeError(error, "ESRCH"); }
|
|
17
|
-
if (startedAt !== undefined && process.platform === "linux") {
|
|
18
|
-
try { if ((await stat(`/proc/${String(pid)}`)).ctimeMs > startedAt) return false; }
|
|
19
|
-
catch (error) { if (isNodeError(error, "ENOENT")) return false; }
|
|
20
|
-
}
|
|
21
49
|
return true;
|
|
22
50
|
}
|
|
23
51
|
export async function hasLiveSessionLease(cwd: string, sessionId: string, home = homedir()): Promise<boolean> {
|
|
@@ -73,7 +101,7 @@ export async function acquireSessionLease(cwd: string, sessionId: string, home =
|
|
|
73
101
|
const path = join(directory, SESSION_OWNER_FILE);
|
|
74
102
|
for (;;) {
|
|
75
103
|
const token = randomUUID();
|
|
76
|
-
const owner: SessionOwner = { pid: process.pid, token, startedAt:
|
|
104
|
+
const owner: SessionOwner = { pid: process.pid, token, startedAt: Date.now() };
|
|
77
105
|
try {
|
|
78
106
|
const handle = await open(path, "wx", 0o600);
|
|
79
107
|
try { await handle.writeFile(`${JSON.stringify(owner)}\n`, "utf8"); } finally { await handle.close(); }
|
package/src/settings.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { atomicWriteFile } from "./persistence.js";
|
|
2
2
|
import { mkdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { getAgentDir, type ToolAnnotations } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { AgentResourcePolicy, AgentResourceSelectorSources, AgentResourceSelectors, AgentResourceSelectorSet, ContextFileScope, JsonValue, WorkflowExtensionSettings, WorkflowRetentionSettings, WorkflowSettings, WorkflowSettingsOverrides, WorkflowSettingsResolution, WorkflowSettingsSources } from "./types.js";
|
|
3
6
|
import { homedir } from "node:os";
|
|
4
|
-
import {
|
|
7
|
+
import { resolve } from "node:path";
|
|
5
8
|
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
9
|
import { canonicalPath, workflowProjectSettingsPath } from "./paths.js";
|
|
11
10
|
export { workflowProjectSettingsPath } from "./paths.js";
|
|
11
|
+
import { isContextFileScope } from "./types.js";
|
|
12
|
+
import { annotateModelAliasError, deepFreeze, errorText, fail, isNodeError, modelCapability, object, positiveInteger, unknownModel, validateModelAliases } from "./utils.js";
|
|
13
|
+
import { jsonValue, resourcePatternHasMagic, validateResourcePattern, validWorkflowExtensionNamespace } from "./utils.js";
|
|
12
14
|
|
|
13
|
-
const
|
|
15
|
+
const WORKFLOW_DIRECTORY = "pi-extensible-workflows";
|
|
14
16
|
export const DEFAULT_SETTINGS: Readonly<WorkflowSettings> = Object.freeze({ concurrency: 8, backgroundWidget: true });
|
|
15
|
-
export function workflowSettingsPath(agentDir = getAgentDir()): string { return join(agentDir,
|
|
17
|
+
export function workflowSettingsPath(agentDir = getAgentDir()): string { return join(agentDir, WORKFLOW_DIRECTORY, "settings.json"); }
|
|
16
18
|
function normalizedResourcePath(value: string, settingsPath: string): string {
|
|
17
19
|
// Built-in extensions are named `builtin:<name>`, not by a path.
|
|
18
20
|
if (value === "*" || value.startsWith("builtin:")) return value;
|
|
@@ -64,18 +66,19 @@ function selectorsFromSettings(settings: Readonly<WorkflowSettings | WorkflowSet
|
|
|
64
66
|
};
|
|
65
67
|
}
|
|
66
68
|
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,
|
|
69
|
+
export function validateContextFileScopes(value: unknown, sourcePath: string): readonly ContextFileScope[] | undefined {
|
|
68
70
|
if (value === undefined) return undefined;
|
|
69
|
-
if (!Array.isArray(value) || !value.every(isContextFileScope)) fail("INVALID_METADATA", `${
|
|
71
|
+
if (!Array.isArray(value) || !value.every(isContextFileScope)) fail("INVALID_METADATA", `${sourcePath}.contextFiles must be an array containing only global, project, or cwd`);
|
|
70
72
|
return [...value];
|
|
71
73
|
}
|
|
72
74
|
export function validateWorkflowExtensionSettings(value: unknown, settingsPath: string, errorCode: "INVALID_SETTINGS" | "INVALID_METADATA" = "INVALID_SETTINGS"): WorkflowExtensionSettings | undefined {
|
|
73
75
|
if (value === undefined) return undefined;
|
|
76
|
+
if (!object(value) || !jsonValue(value)) fail(errorCode, `${settingsPath}.extensionSettings must be a JSON object`);
|
|
77
|
+
for (const namespace of Object.keys(value)) if (!validWorkflowExtensionNamespace(namespace)) fail(errorCode, `${settingsPath}.extensionSettings contains an invalid namespace: ${namespace}`);
|
|
78
|
+
const generic = value;
|
|
74
79
|
const base = `${settingsPath}.extensionSettings`;
|
|
75
|
-
if (!object(value)) fail(errorCode, `${base} must be an object`);
|
|
76
80
|
const normalized: Record<string, JsonValue> = {};
|
|
77
|
-
for (const [namespace, raw] of Object.entries(
|
|
78
|
-
if (!validWorkflowExtensionNamespace(namespace)) fail(errorCode, `${base} contains an invalid namespace: ${namespace}`);
|
|
81
|
+
for (const [namespace, raw] of Object.entries(generic)) {
|
|
79
82
|
if (namespace === "herdr") {
|
|
80
83
|
if (!object(raw)) fail(errorCode, `${base}.herdr must be an object`);
|
|
81
84
|
if (Object.keys(raw).some((key) => key !== "enableFullyInspectableMode")) fail(errorCode, `${base}.herdr contains an unsupported setting`);
|
|
@@ -90,7 +93,6 @@ export function validateWorkflowExtensionSettings(value: unknown, settingsPath:
|
|
|
90
93
|
normalized.trajectory = Object.freeze({ ...(raw.port === undefined ? {} : { port: raw.port }) });
|
|
91
94
|
continue;
|
|
92
95
|
}
|
|
93
|
-
if (!jsonValue(raw)) fail(errorCode, `${base}.${namespace} must be JSON-compatible`);
|
|
94
96
|
normalized[namespace] = structuredClone(raw);
|
|
95
97
|
}
|
|
96
98
|
return deepFreeze(normalized as WorkflowExtensionSettings);
|
|
@@ -157,41 +159,22 @@ export function loadCodemodeToolsSetting(agentDir?: string): WorkflowSettings["c
|
|
|
157
159
|
try { return loadSettings(workflowSettingsPath(agentDir)).codemodeTools; } catch { return undefined; }
|
|
158
160
|
}
|
|
159
161
|
export function loadSettingsOverrides(path: string): Readonly<WorkflowSettingsOverrides> { return parseSettings(path, true); }
|
|
160
|
-
export function resolveWorkflowSettings(cwd: string, projectTrusted: boolean, globalSettingsPath = workflowSettingsPath()): WorkflowSettingsResolution {
|
|
162
|
+
export function resolveWorkflowSettings(cwd: string, projectTrusted: boolean, globalSettingsPath = workflowSettingsPath()): WorkflowSettingsResolution & { selectorSources: AgentResourceSelectorSources } {
|
|
161
163
|
const projectSettingsPath = workflowProjectSettingsPath(cwd);
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
skills: [...(
|
|
170
|
-
extensions: [...(
|
|
171
|
-
...(
|
|
164
|
+
const consumerGlobal = loadSettings(globalSettingsPath);
|
|
165
|
+
const consumerProject: Readonly<WorkflowSettingsOverrides> = projectTrusted ? loadSettingsOverrides(projectSettingsPath) : Object.freeze({});
|
|
166
|
+
const global = consumerGlobal;
|
|
167
|
+
const project = consumerProject;
|
|
168
|
+
const selectorSources = { global: selectorsFromSettings(global), project: selectorsFromSettings(project) };
|
|
169
|
+
const effective: Readonly<WorkflowSettings> = Object.freeze({
|
|
170
|
+
...global, ...project,
|
|
171
|
+
skills: [...(global.skills ?? []), ...(project.skills ?? [])],
|
|
172
|
+
extensions: [...(global.extensions ?? []), ...(project.extensions ?? [])],
|
|
173
|
+
...(global.tools === undefined && project.tools === undefined ? {} : { tools: [...(global.tools ?? []), ...(project.tools ?? [])] }),
|
|
172
174
|
});
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
|
|
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 };
|
|
175
|
+
const sources: WorkflowSettingsSources = { concurrency: project.concurrency === undefined ? globalSettingsPath : projectSettingsPath, modelAliases: project.modelAliases === undefined ? globalSettingsPath : projectSettingsPath };
|
|
176
|
+
for (const key of ["skills", "extensions", "tools", "extensionSettings", "retention", "worktreePostCreateCommand"] as const) if (effective[key] !== undefined) sources[key] = project[key] === undefined ? globalSettingsPath : projectSettingsPath;
|
|
177
|
+
return { globalSettingsPath, projectSettingsPath, projectTrusted, global, project, effective, sources, selectorSources };
|
|
195
178
|
}
|
|
196
179
|
export function validateModelAliasAvailability(aliases: Readonly<Record<string, string>>, names: readonly string[], availableModels: ReadonlySet<string>, knownModels: ReadonlySet<string>, settingsPath?: string): void {
|
|
197
180
|
for (const name of names) {
|
|
@@ -206,7 +189,7 @@ export function resolveAgentResourcePolicy(cwd: string, projectTrusted: boolean,
|
|
|
206
189
|
const global = selectorSet(selectorsFromSettings(resolved.global));
|
|
207
190
|
const project = selectorSet(selectorsFromSettings(resolved.project));
|
|
208
191
|
const effective = selectorSet(selectorsFromSettings(resolved.effective));
|
|
209
|
-
return { globalSettingsPath: resolved.globalSettingsPath, projectSettingsPath: resolved.projectSettingsPath, projectTrusted, global, project, effective, unmatchedSkills: [], unmatchedExtensions: [], unmatchedTools: [], selectorSources:
|
|
192
|
+
return { globalSettingsPath: resolved.globalSettingsPath, projectSettingsPath: resolved.projectSettingsPath, projectTrusted, global, project, effective, unmatchedSkills: [], unmatchedExtensions: [], unmatchedTools: [], selectorSources: resolved.selectorSources };
|
|
210
193
|
}
|
|
211
194
|
export function saveModelAliases(path = workflowSettingsPath(), aliases: Readonly<Record<string, string>> = {}): void {
|
|
212
195
|
const normalized = validateModelAliases(aliases, path);
|
|
@@ -222,3 +205,8 @@ export function saveModelAliases(path = workflowSettingsPath(), aliases: Readonl
|
|
|
222
205
|
atomicWriteFile(path, `${JSON.stringify({ ...parsed, modelAliases: normalized }, null, 2)}\n`, true);
|
|
223
206
|
}
|
|
224
207
|
|
|
208
|
+
|
|
209
|
+
export function canonicalExtensionSelector(selector: string, cwd: string): string {
|
|
210
|
+
const negated = selector.startsWith("!");
|
|
211
|
+
return `${negated ? "!" : ""}${normalizedResourcePath(negated ? selector.slice(1) : selector, join(cwd, "settings.json"))}`;
|
|
212
|
+
}
|
package/src/store.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { access, mkdir, readdir, readFile, realpath, rename, rm, stat, writeFile
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
|
-
import { HARD_TERMINAL_RUN_STATES, WorkflowError, type JsonValue, type LaunchSnapshot, type WorkflowErrorCode, type WorkflowRunEvent } from "./types.js";
|
|
7
|
+
import { HARD_TERMINAL_RUN_STATES, WorkflowError, type PreparedAgentConfiguration, type JsonValue, type LaunchSnapshot, type WorkflowErrorCode, type WorkflowLaunchMode, type WorkflowRunEvent } from "./types.js";
|
|
8
8
|
import { coerceWorkflowError, errorText, isNodeError, loadLaunchSnapshot, object, positiveInteger, SerialLane } from "./utils.js";
|
|
9
9
|
import { budgetUsage } from "./budget.js";
|
|
10
10
|
import {
|
|
@@ -32,7 +32,7 @@ function summaryFromRun(run: PersistedRun, directory: string, journal: Journal,
|
|
|
32
32
|
const failedAt = run.failedAt ?? run.error?.failedAt;
|
|
33
33
|
const replayablePaths = [...new Set([...(run.retry?.completedPaths ?? []), ...Object.keys(journal.completed)])];
|
|
34
34
|
const incompletePaths = [...new Set([...(run.retry?.incompletePaths ?? []), ...(failedAt ? [failedAt] : [])])];
|
|
35
|
-
return { schemaVersion: 1, runId: run.id, sessionId: run.sessionId, workflowName: run.workflowName, state: run.state, createdAt, updatedAt: now, ...(previous?.terminalAt || HARD_TERMINAL_RUN_STATES.has(run.state) ? { terminalAt: previous?.terminalAt ?? now } : {}), usage: budgetUsage(run.usage), agents: run.agents.map(({ id, name, label, state,
|
|
35
|
+
return { schemaVersion: 1, runId: run.id, sessionId: run.sessionId, workflowName: run.workflowName, state: run.state, createdAt, updatedAt: now, ...(previous?.terminalAt || HARD_TERMINAL_RUN_STATES.has(run.state) ? { terminalAt: previous?.terminalAt ?? now } : {}), usage: budgetUsage(run.usage), agents: run.agents.map(({ id, name, label, state, attempts }) => ({ id, name, ...(label ? { label } : {}), state, attempts })), ...(run.error ? { error: run.error } : {}), ...(failedAt ? { failedAt } : {}), replayablePaths, incompletePaths, artifacts: summaryArtifacts(directory) };
|
|
36
36
|
}
|
|
37
37
|
function systemPromptStoragePath(directory: string): string { return join(directory, SYSTEM_PROMPT_STORAGE); }
|
|
38
38
|
function systemPromptRecordsPath(directory: string): string { return join(systemPromptStoragePath(directory), SYSTEM_PROMPT_RECORDS); }
|
|
@@ -163,6 +163,22 @@ export class RunStore {
|
|
|
163
163
|
return write;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
async saveAgentConfiguration(key: string, configuration: PreparedAgentConfiguration): Promise<void> {
|
|
167
|
+
await this.launchSnapshotLane.run(async () => {
|
|
168
|
+
const snapshot = decodeLaunchSnapshot(await json(join(this.directory, "snapshot.json")));
|
|
169
|
+
if (!snapshot) throw new WorkflowError("RESUME_INCOMPATIBLE", "Workflow launch snapshot identity version is incompatible");
|
|
170
|
+
await atomicJson(join(this.directory, "snapshot.json"), { ...snapshot, agentConfigurations: { ...snapshot.agentConfigurations, [key]: configuration } });
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async setLaunchMode(launchMode: WorkflowLaunchMode): Promise<void> {
|
|
175
|
+
await this.launchSnapshotLane.run(async () => {
|
|
176
|
+
const snapshot = decodeLaunchSnapshot(await json(join(this.directory, "snapshot.json")));
|
|
177
|
+
if (!snapshot) throw new WorkflowError("RESUME_INCOMPATIBLE", "Workflow launch snapshot identity version is incompatible");
|
|
178
|
+
await atomicJson(join(this.directory, "snapshot.json"), { ...snapshot, launchMode });
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
166
182
|
async saveSnapshot(snapshot: Readonly<LaunchSnapshot>): Promise<void> {
|
|
167
183
|
const write = this.launchSnapshotLane.run(() => atomicJson(join(this.directory, "snapshot.json"), snapshot));
|
|
168
184
|
await write;
|
package/src/trajectory.ts
CHANGED
|
@@ -66,7 +66,6 @@ export type TrajectorySubagent = {
|
|
|
66
66
|
sessionId: string;
|
|
67
67
|
cwd: string;
|
|
68
68
|
label?: string;
|
|
69
|
-
role?: string;
|
|
70
69
|
mode: "background" | "foreground";
|
|
71
70
|
state: SubagentStatus["state"];
|
|
72
71
|
startedAt?: number;
|
|
@@ -282,8 +281,8 @@ async function resolveResourceInspection(inspection: ResourceInspection | undefi
|
|
|
282
281
|
let discovered: DiscoveredResources;
|
|
283
282
|
try { discovered = await discoveredResources(cwd); } catch { return inspection; }
|
|
284
283
|
const sources = inspection.selectorSources ?? { global: inspection.selectors, project: {} };
|
|
285
|
-
const skills = inspection.skills.length ? inspection.skills : selectResourcesByLayers([sources.global.skills, sources.project.skills, sources.
|
|
286
|
-
const extensions = inspection.extensions.length ? inspection.extensions : selectResourcesByLayers([sources.global.extensions, sources.project.extensions, sources.
|
|
284
|
+
const skills = inspection.skills.length ? inspection.skills : selectResourcesByLayers([sources.global.skills, sources.project.skills, sources.call?.skills], discovered.skills);
|
|
285
|
+
const extensions = inspection.extensions.length ? inspection.extensions : selectResourcesByLayers([sources.global.extensions, sources.project.extensions, sources.call?.extensions].map((layer) => layer?.map((selector) => canonicalExtensionSelector(selector, selectorCwd))), discovered.extensions);
|
|
287
286
|
return skills === inspection.skills && extensions === inspection.extensions ? inspection : { ...inspection, skills, extensions };
|
|
288
287
|
}
|
|
289
288
|
export async function withResolvedAttemptResources(attempt: AgentAttemptSummary, cwd: string): Promise<AgentAttemptSummary> {
|
|
@@ -565,7 +564,6 @@ async function loadTrajectorySubagent(directory: string, cwd: string, sessionId:
|
|
|
565
564
|
return {
|
|
566
565
|
id, sessionId, cwd, mode: request.mode ?? "background", state: status.state, request, tools, transcript,
|
|
567
566
|
...(request.label === undefined ? {} : { label: request.label }),
|
|
568
|
-
...(request.role === undefined ? {} : { role: request.role }),
|
|
569
567
|
...(status.startedAt === undefined ? {} : { startedAt: status.startedAt }),
|
|
570
568
|
...(status.finishedAt === undefined ? {} : { finishedAt: status.finishedAt }),
|
|
571
569
|
...(status.attempts === undefined ? {} : { attempts: status.attempts }),
|