@marcoscale98/pi-extensible-workflows 5.14.1-fork.1 → 5.17.0-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 +67 -0
- package/dist/src/agent-execution.js +36 -51
- package/dist/src/background-widget.d.ts +56 -0
- package/dist/src/background-widget.js +22 -15
- package/dist/src/decoders.js +2 -1
- package/dist/src/host-navigator.d.ts +18 -0
- package/dist/src/host-navigator.js +71 -13
- package/dist/src/host-view.d.ts +16 -1
- package/dist/src/host-view.js +109 -42
- package/dist/src/host.js +57 -24
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +867 -545
- package/dist/src/index.js.map +3 -3
- package/dist/src/registry.js +2 -1
- package/dist/src/roles.d.ts +70 -0
- package/dist/src/roles.js +325 -0
- package/dist/src/settings.d.ts +13 -0
- package/dist/src/settings.js +253 -0
- package/dist/src/trajectory.d.ts +29 -4
- package/dist/src/trajectory.js +58 -3
- package/dist/src/types.d.ts +0 -1
- package/dist/src/utils.d.ts +2 -1
- package/dist/src/utils.js +10 -0
- package/dist/src/validation.d.ts +5 -17
- package/dist/src/validation.js +9 -458
- package/dist/subagents/index.js +1132 -694
- package/dist/subagents/index.js.map +3 -3
- package/dist/subagents/src/contracts.d.ts +4 -0
- package/dist/subagents/src/index.d.ts +1 -1
- package/dist/subagents/src/index.js +2 -1
- package/dist/subagents/src/manager.js +29 -10
- package/dist/subagents/src/navigator.js +396 -247
- package/dist/subagents/src/view.d.ts +23 -2
- package/dist/subagents/src/view.js +96 -30
- package/dist/trajectory/assets/index.html +347 -231
- package/dist/trajectory/assets/prism.min.js +13 -0
- package/dist/trajectory/index.js +377 -257
- package/dist/trajectory/index.js.map +3 -3
- package/dist/trajectory/src/assets/index.html +347 -231
- 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 +143 -42
- package/dist/trajectory/src/server.js +134 -9
- package/dist/trajectory/src/server.js.map +1 -1
- package/package.json +2 -1
- package/src/agent-execution.ts +36 -42
- package/src/background-widget.ts +25 -18
- package/src/decoders.ts +2 -1
- package/src/host-navigator.ts +58 -14
- package/src/host-view.ts +93 -37
- package/src/host.ts +51 -21
- package/src/index.ts +3 -1
- package/src/registry.ts +2 -1
- package/src/roles.ts +326 -0
- package/src/settings.ts +206 -0
- package/src/trajectory.ts +76 -7
- package/src/types.ts +1 -1
- package/src/utils.ts +8 -1
- package/src/validation.ts +12 -356
- package/subagents/README.md +1 -1
- package/subagents/src/contracts.ts +4 -0
- package/subagents/src/index.ts +3 -2
- package/subagents/src/manager.ts +28 -10
- package/subagents/src/navigator.ts +336 -180
- package/subagents/src/view.ts +97 -28
- package/trajectory/src/assets/index.html +347 -231
- package/trajectory/src/assets/prism.min.js +13 -0
- package/trajectory/src/export.ts +4 -2
- package/trajectory/src/index.ts +118 -38
- package/trajectory/src/server.ts +112 -12
package/src/trajectory.ts
CHANGED
|
@@ -3,9 +3,9 @@ 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 } 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";
|
|
@@ -15,8 +15,16 @@ import { canonicalPath, sameFilesystemPath } from "./paths.js";
|
|
|
15
15
|
export const TRAJECTORY_MAX_TRANSCRIPT_BYTES = 2 * 1024 * 1024;
|
|
16
16
|
const TRAJECTORY_MAX_NON_TIMING_ENTRIES = 400;
|
|
17
17
|
|
|
18
|
+
export type TrajectoryAgentOutput =
|
|
19
|
+
| { readonly status: "available"; readonly value: JsonValue; readonly bytes: number }
|
|
20
|
+
| { readonly status: "pending" }
|
|
21
|
+
| { readonly status: "failed" | "cancelled"; readonly code: string; readonly message: string }
|
|
22
|
+
| { readonly status: "truncated"; readonly kind: "result" | "failure"; readonly bytes: number; readonly path?: string }
|
|
23
|
+
| { readonly status: "unavailable" };
|
|
24
|
+
export type TrajectoryAgent = AgentRecord & { readonly output: TrajectoryAgentOutput };
|
|
25
|
+
export type TrajectoryRunRecord = Omit<PersistedRun, "agents"> & { readonly agents: readonly TrajectoryAgent[] };
|
|
18
26
|
export type TrajectoryRun = {
|
|
19
|
-
run:
|
|
27
|
+
run: TrajectoryRunRecord;
|
|
20
28
|
snapshot: Readonly<LaunchSnapshot>;
|
|
21
29
|
awaiting: readonly AwaitingCheckpoint[];
|
|
22
30
|
createdAt?: string;
|
|
@@ -26,6 +34,31 @@ export type TrajectoryTranscriptStatus = "available" | "empty" | "missing" | "fa
|
|
|
26
34
|
export type TrajectoryTranscriptMetadata = { readonly revision: number; readonly status: TrajectoryTranscriptStatus; readonly bytes?: number; readonly timing?: readonly unknown[]; };
|
|
27
35
|
export type TrajectoryRunMetadata = Omit<TrajectoryRun, "transcripts"> & { transcripts: Readonly<Record<string, TrajectoryTranscriptMetadata>> };
|
|
28
36
|
export type TrajectorySubagentArtifact = { readonly truncated: true; readonly path: string; readonly bytes: number };
|
|
37
|
+
type SubagentOutputOptions = {
|
|
38
|
+
readonly state: SubagentStatus["state"];
|
|
39
|
+
readonly statusError: SubagentStatus["error"];
|
|
40
|
+
readonly attempt: AgentAttemptSummary | undefined;
|
|
41
|
+
readonly result: JsonValue | undefined;
|
|
42
|
+
readonly resultTruncated: boolean;
|
|
43
|
+
readonly resultPath: string;
|
|
44
|
+
readonly resultBytes: number | undefined;
|
|
45
|
+
readonly failure: { readonly code: string; readonly message: string } | undefined;
|
|
46
|
+
readonly failureTruncated: boolean;
|
|
47
|
+
readonly failurePath: string;
|
|
48
|
+
readonly failureBytes: number | undefined;
|
|
49
|
+
};
|
|
50
|
+
function subagentOutput(options: SubagentOutputOptions): TrajectoryAgentOutput {
|
|
51
|
+
if (options.resultTruncated) return { status: "truncated", kind: "result", path: options.resultPath, bytes: options.resultBytes ?? 0 };
|
|
52
|
+
if (options.result !== undefined) return { status: "available", value: options.result, bytes: serializedJsonBytes(options.result) };
|
|
53
|
+
if (options.failureTruncated) return { status: "truncated", kind: "failure", path: options.failurePath, bytes: options.failureBytes ?? 0 };
|
|
54
|
+
if (options.failure !== undefined) return { status: "failed", code: options.failure.code, message: options.failure.message };
|
|
55
|
+
if (options.state === "failed" || options.state === "stopped") {
|
|
56
|
+
const error = options.statusError ?? options.attempt?.error;
|
|
57
|
+
return { status: options.state === "stopped" ? "cancelled" : "failed", code: error?.code ?? (options.state === "stopped" ? "CANCELLED" : "AGENT_FAILED"), message: error?.message ?? `Subagent ${options.state}` };
|
|
58
|
+
}
|
|
59
|
+
if (options.state === "completed") return { status: "unavailable" };
|
|
60
|
+
return { status: "pending" };
|
|
61
|
+
}
|
|
29
62
|
export type TrajectorySubagent = {
|
|
30
63
|
id: string;
|
|
31
64
|
sessionId: string;
|
|
@@ -47,6 +80,7 @@ export type TrajectorySubagent = {
|
|
|
47
80
|
attempt?: AgentAttemptSummary;
|
|
48
81
|
result?: JsonValue | TrajectorySubagentArtifact;
|
|
49
82
|
failure?: { readonly code: string; readonly message: string } | TrajectorySubagentArtifact;
|
|
83
|
+
output: TrajectoryAgentOutput;
|
|
50
84
|
transcript: readonly unknown[];
|
|
51
85
|
};
|
|
52
86
|
export type TrajectorySubagentMetadata = Omit<TrajectorySubagent, "transcript"> & { transcript: TrajectoryTranscriptMetadata };
|
|
@@ -69,7 +103,6 @@ export type TrajectoryPublisherInput = {
|
|
|
69
103
|
cwd: string;
|
|
70
104
|
sessionId: string;
|
|
71
105
|
port?: number;
|
|
72
|
-
themes: boolean;
|
|
73
106
|
loadRuns: TrajectoryRunLoader;
|
|
74
107
|
loadSubagents: TrajectorySubagentLoader;
|
|
75
108
|
loadMetadata: TrajectoryPublisherMetadataLoader;
|
|
@@ -277,11 +310,46 @@ function withoutAgentActivities(run: PersistedRun): PersistedRun {
|
|
|
277
310
|
return agents.every((agent, index) => agent === run.agents[index]) ? run : { ...run, agents };
|
|
278
311
|
}
|
|
279
312
|
|
|
313
|
+
function serializedJsonBytes(value: JsonValue): number {
|
|
314
|
+
const serialized = JSON.stringify(value);
|
|
315
|
+
return Buffer.byteLength(serialized, "utf8");
|
|
316
|
+
}
|
|
317
|
+
function failedAgentOutput(agent: PersistedRun["agents"][number]): TrajectoryAgentOutput {
|
|
318
|
+
const error = [...(agent.attemptDetails ?? [])].reverse().find((attempt) => attempt.error)?.error;
|
|
319
|
+
const status = agent.state === "cancelled" ? "cancelled" : "failed";
|
|
320
|
+
return { status, code: error?.code ?? (status === "cancelled" ? "CANCELLED" : "AGENT_FAILED"), message: error?.message ?? `Agent ${agent.state}` };
|
|
321
|
+
}
|
|
322
|
+
function outputForAgent(agent: PersistedRun["agents"][number], operation: CompletedOperation | undefined): TrajectoryAgentOutput {
|
|
323
|
+
if (operation !== undefined) {
|
|
324
|
+
const bytes = serializedJsonBytes(operation.value);
|
|
325
|
+
return bytes > DEFAULT_MAX_BYTES ? { status: "truncated", kind: "result", bytes } : { status: "available", value: operation.value, bytes };
|
|
326
|
+
}
|
|
327
|
+
if (agent.state === "failed" || agent.state === "cancelled") return failedAgentOutput(agent);
|
|
328
|
+
if (agent.state === "completed") return { status: "unavailable" };
|
|
329
|
+
return { status: "pending" };
|
|
330
|
+
}
|
|
331
|
+
export function applyTrajectoryAgentOutputs(run: PersistedRun, operations: readonly CompletedOperation[]): TrajectoryRunRecord {
|
|
332
|
+
const byPath = new Map(operations.map((operation) => [operation.path, operation]));
|
|
333
|
+
const agents: readonly TrajectoryAgent[] = run.agents.map((agent) => ({ ...agent, output: outputForAgent(agent, agent.resultPath === undefined ? undefined : byPath.get(agent.resultPath)) }));
|
|
334
|
+
return { ...run, agents };
|
|
335
|
+
}
|
|
336
|
+
function overlayTrajectoryRun(run: TrajectoryRunRecord, overlay: (run: PersistedRun) => PersistedRun): TrajectoryRunRecord {
|
|
337
|
+
const overlaid = overlay(run);
|
|
338
|
+
const outputs = new Map(run.agents.map((agent) => [agent.id, agent.output]));
|
|
339
|
+
const agents: readonly TrajectoryAgent[] = overlaid.agents.map((agent) => {
|
|
340
|
+
const previous = outputs.get(agent.id);
|
|
341
|
+
const output = previous?.status === "available" || previous?.status === "truncated" ? previous : outputForAgent(agent, undefined);
|
|
342
|
+
return { ...agent, output };
|
|
343
|
+
});
|
|
344
|
+
return { ...overlaid, agents };
|
|
345
|
+
}
|
|
280
346
|
async function loadTrajectoryRun(store: RunStore, includeTranscripts = true): Promise<TrajectoryRun> {
|
|
281
347
|
const value = await store.load();
|
|
282
348
|
const summary = await store.loadSummary().catch(() => undefined);
|
|
283
349
|
const prompts = await store.systemPrompts().catch(() => []);
|
|
284
|
-
const
|
|
350
|
+
const prepared = withoutAgentActivities(await withResolvedResources(withPiToolDescriptions(applySystemPrompts(value.run, prompts)), store.cwd));
|
|
351
|
+
const operations = await store.replayableOperations().catch(() => []);
|
|
352
|
+
const run = applyTrajectoryAgentOutputs(prepared, operations);
|
|
285
353
|
return { run, snapshot: value.snapshot, awaiting: await store.awaitingCheckpoints(), ...(summary?.createdAt === undefined ? {} : { createdAt: summary.createdAt }), transcripts: includeTranscripts ? await runTranscripts(run) : {} };
|
|
286
354
|
}
|
|
287
355
|
|
|
@@ -290,7 +358,7 @@ export async function loadTrajectoryRuns(cwd: string, sessionId: string, home =
|
|
|
290
358
|
for (const runId of await listRunIds(cwd, sessionId, home, false)) {
|
|
291
359
|
try {
|
|
292
360
|
const value = await loadTrajectoryRun(new RunStore(cwd, sessionId, runId, home));
|
|
293
|
-
loaded.push(overlay ? { ...value, run:
|
|
361
|
+
loaded.push(overlay ? { ...value, run: overlayTrajectoryRun(value.run, overlay) } : value);
|
|
294
362
|
} catch { /* Ignore corrupt or concurrently removed runs. */ }
|
|
295
363
|
}
|
|
296
364
|
return loaded;
|
|
@@ -323,7 +391,7 @@ function createCachedRunLoader<Value extends { run: PersistedRun }, Signature>(c
|
|
|
323
391
|
entry = { value, stateMtimeMs: (await fileMtime(join(store.directory, "state.json"))) ?? 0, journalMtimeMs: (await fileMtime(join(store.directory, "journal.json"))) ?? 0, transcripts };
|
|
324
392
|
cache.set(runId, entry);
|
|
325
393
|
}
|
|
326
|
-
loaded.push(overlay ? { ...entry.value, run:
|
|
394
|
+
loaded.push(overlay ? { ...entry.value, run: overlayTrajectoryRun(entry.value.run as TrajectoryRunRecord, overlay) } : entry.value);
|
|
327
395
|
} catch { cache.delete(runId); /* Ignore corrupt or concurrently removed runs. */ }
|
|
328
396
|
}
|
|
329
397
|
return loaded;
|
|
@@ -490,6 +558,7 @@ async function loadTrajectorySubagent(directory: string, cwd: string, sessionId:
|
|
|
490
558
|
...(attempt === undefined ? {} : { attempt }),
|
|
491
559
|
...(result === undefined ? {} : { result }),
|
|
492
560
|
...(failure === undefined ? {} : { failure }),
|
|
561
|
+
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
562
|
};
|
|
494
563
|
} catch { return undefined; }
|
|
495
564
|
}
|
package/src/types.ts
CHANGED
|
@@ -90,7 +90,7 @@ 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 }
|
package/src/utils.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ERROR_CODES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, THINKING_LEVELS, WorkflowError, type JsonValue, type ModelSpec, type ThinkingLevel, type WorkflowErrorCode, type WorkflowExtensionSettings } from "./types.js";
|
|
1
|
+
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
2
|
import { Minimatch } from "minimatch";
|
|
3
3
|
export class SerialLane {
|
|
4
4
|
#tail: Promise<void> = Promise.resolve();
|
|
@@ -190,3 +190,10 @@ export function resourcePatternHasMagic(pattern: string): boolean { return /[*?\
|
|
|
190
190
|
export function unmatchedResourcePatterns(patterns: readonly string[], resources: readonly string[]): string[] { return patterns.filter((pattern) => !resources.some((resource) => resourcePatternMatches(resource, pattern))); }
|
|
191
191
|
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
192
|
export function loadLaunchSnapshot(input: import("./types.js").LaunchSnapshot): Readonly<import("./types.js").LaunchSnapshot> { return deepFreeze(structuredClone(input)); }
|
|
193
|
+
|
|
194
|
+
export function validateSchema(schema: unknown, at = "schema"): asserts schema is JsonSchema {
|
|
195
|
+
if (!object(schema) || Object.getPrototypeOf(schema) !== Object.prototype || !jsonValue(schema)) fail("INVALID_SCHEMA", `${at} must be a plain JSON-compatible Schema object`);
|
|
196
|
+
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`);
|
|
197
|
+
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`);
|
|
198
|
+
if (schema.properties !== undefined && !object(schema.properties)) fail("INVALID_SCHEMA", `${at}.properties must be an object`);
|
|
199
|
+
}
|
package/src/validation.ts
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
6
3
|
import * as acorn from "acorn";
|
|
7
4
|
import { Script } from "node:vm";
|
|
8
|
-
import {
|
|
9
|
-
import type { AgentDefinition, AgentResourceSelectors, AgentResourceSelectorSet, AgentResourcePolicy, CheckpointInput, ContextFileScope, JsonSchema, JsonValue, PreflightCapabilities, PreflightResult, ShellOptions, StaticWorkflowCall, StaticWorkflowExecution, StaticWorkflowScope, ValidatedWorkflowLaunch, WorkflowCallKind, WorkflowErrorCode, WorkflowExtensionMetadata, WorkflowExtensionSettings, WorkflowMetadata, WorkflowRetentionSettings, WorkflowRoleDirectoryRegistration, WorkflowSettings, WorkflowSettingsOverrides, WorkflowSettingsResolution, WorkflowSettingsSources, WorkflowValidationContext, WorkflowValidationParameters } from "./types.js";
|
|
5
|
+
import type { AgentDefinition, CheckpointInput, JsonSchema, JsonValue, PreflightCapabilities, PreflightResult, ShellOptions, StaticWorkflowCall, StaticWorkflowExecution, StaticWorkflowScope, ValidatedWorkflowLaunch, WorkflowCallKind, WorkflowErrorCode, WorkflowMetadata, WorkflowValidationContext, WorkflowValidationParameters } from "./types.js";
|
|
10
6
|
import type { WorkflowRegistryApi } from "./registry.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
export {
|
|
7
|
+
import { assertModelThinking, validateSchema, deepFreeze, errorText, fail, jsonObject, jsonValue, mergeWorkflowExtensionSettings, modelAliasName, modelCapability, object, positiveInteger, resolveModelReference, resourcePatternHasMagic, unknownModel } from "./utils.js";
|
|
8
|
+
import { WORKFLOW_CALL_KINDS } from "./types.js";
|
|
9
|
+
export { validateSchema } from "./utils.js";
|
|
10
|
+
import { loadAgentDefinitions, loadProjectAgentDefinitions } from "./roles.js";
|
|
11
|
+
export { loadAgentDefinitions, loadProjectAgentDefinitions, parseRoleMarkdown, workflowRoleDirectories } from "./roles.js";
|
|
12
|
+
export type { WorkflowRoleDirectoryInput } from "./roles.js";
|
|
13
|
+
|
|
14
|
+
import { validateContextFileScopes, validateSelectorList } from "./settings.js";
|
|
15
|
+
export { DEFAULT_SETTINGS, loadSettings, loadSettingsOverrides, resolveAgentResourcePolicy, resolveWorkflowSettings, saveModelAliases, validateContextFileScopes, validateModelAliasAvailability, validateSelectorList, validateWorkflowExtensionSettings, workflowProjectSettingsPath, workflowSettingsPath } from "./settings.js";
|
|
16
16
|
|
|
17
|
-
export const DEFAULT_SETTINGS: Readonly<WorkflowSettings> = Object.freeze({ concurrency: 8, backgroundWidget: true });
|
|
18
17
|
export function validateCheckpoint(value: unknown): CheckpointInput {
|
|
19
18
|
if (!object(value) || Object.keys(value).some((key) => !["name", "prompt", "context"].includes(key)) || typeof value.name !== "string" || value.name.trim() === "" || typeof value.prompt !== "string" || !jsonValue(value.context)) fail("INVALID_METADATA", "checkpoint requires only name, prompt, and JSON context");
|
|
20
19
|
if (Buffer.byteLength(value.prompt) > 1024) fail("INVALID_METADATA", "checkpoint prompt exceeds 1024 UTF-8 bytes");
|
|
@@ -22,342 +21,6 @@ export function validateCheckpoint(value: unknown): CheckpointInput {
|
|
|
22
21
|
return { name: value.name, prompt: value.prompt, context: value.context };
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
export function workflowSettingsPath(agentDir = getAgentDir()): string { return join(agentDir, ROLE_DIRECTORY, "settings.json"); }
|
|
26
|
-
function normalizedResourcePath(value: string, settingsPath: string): string {
|
|
27
|
-
if (value === "*") return value;
|
|
28
|
-
let expanded = value === "~" ? homedir() : value.startsWith("~/") || value.startsWith("~\\") ? join(homedir(), value.slice(2)) : value;
|
|
29
|
-
if (expanded.startsWith("file://")) expanded = fileURLToPath(expanded);
|
|
30
|
-
const resolved = resolve(dirname(settingsPath), expanded);
|
|
31
|
-
if (expanded === "**" || expanded.startsWith("**/") || expanded.startsWith("**\\")) return expanded;
|
|
32
|
-
if (resourcePatternHasMagic(expanded)) {
|
|
33
|
-
const magicIndex = resolved.search(/[*?\x5b\x5d{}()]/);
|
|
34
|
-
const separatorIndex = Math.max(resolved.lastIndexOf("/", magicIndex), resolved.lastIndexOf("\\", magicIndex));
|
|
35
|
-
const rootBoundary = separatorIndex === 0 || (separatorIndex === 2 && /^[A-Za-z]:[\\/]/.test(resolved));
|
|
36
|
-
const prefix = rootBoundary ? resolved.slice(0, separatorIndex + 1) : separatorIndex >= 0 ? resolved.slice(0, separatorIndex) : resolved;
|
|
37
|
-
const suffix = rootBoundary ? resolved.slice(separatorIndex + 1) : separatorIndex >= 0 ? resolved.slice(separatorIndex) : "";
|
|
38
|
-
return `${canonicalPath(prefix)}${suffix}`;
|
|
39
|
-
}
|
|
40
|
-
return canonicalPath(resolved);
|
|
41
|
-
}
|
|
42
|
-
function validateSelectorList(value: unknown, path: string, kind: "skills" | "extensions" | "tools", errorCode: "INVALID_SETTINGS" | "INVALID_METADATA" = "INVALID_SETTINGS", normalizeExtensions = true): readonly string[] | undefined {
|
|
43
|
-
if (value === undefined) return undefined;
|
|
44
|
-
if (!Array.isArray(value)) fail(errorCode, `${path}.${kind} must be an array`);
|
|
45
|
-
const normalized: string[] = [];
|
|
46
|
-
for (const [index, entry] of value.entries()) {
|
|
47
|
-
if (typeof entry !== "string" || !entry.trim()) fail(errorCode, `${path}.${kind}[${String(index)}] must be a non-empty string`);
|
|
48
|
-
let selector = entry.trim();
|
|
49
|
-
if (kind === "extensions" && normalizeExtensions) {
|
|
50
|
-
const negated = selector.startsWith("!");
|
|
51
|
-
const body = negated ? selector.slice(1) : selector;
|
|
52
|
-
if (!body) fail(errorCode, `${path}.${kind}[${String(index)}] must be a valid minimatch pattern: Empty minimatch pattern ${JSON.stringify(selector)}`);
|
|
53
|
-
try { selector = `${negated ? "!" : ""}${normalizedResourcePath(body, path)}`; } catch (error) { fail(errorCode, `${path}.${kind}[${String(index)}] must be a valid path: ${errorText(error)}`); }
|
|
54
|
-
}
|
|
55
|
-
try { validateResourcePattern(selector); } catch (error) { fail(errorCode, `${path}.${kind}[${String(index)}] must be a valid minimatch pattern: ${errorText(error)}`); }
|
|
56
|
-
normalized.push(selector);
|
|
57
|
-
}
|
|
58
|
-
return Object.freeze(normalized);
|
|
59
|
-
}
|
|
60
|
-
function validateWorktreePostCreateCommand(value: unknown, settingsPath: string): readonly string[] | undefined {
|
|
61
|
-
if (value === undefined) return undefined;
|
|
62
|
-
if (!Array.isArray(value) || value.length === 0) fail("INVALID_SETTINGS", `${settingsPath}.worktreePostCreateCommand must be a non-empty array`);
|
|
63
|
-
return Object.freeze(value.map((entry, index) => {
|
|
64
|
-
if (typeof entry !== "string" || !entry.trim()) fail("INVALID_SETTINGS", `${settingsPath}.worktreePostCreateCommand[${String(index)}] must be a non-empty string`);
|
|
65
|
-
return entry;
|
|
66
|
-
}));
|
|
67
|
-
}
|
|
68
|
-
function selectorsFromSettings(settings: Readonly<WorkflowSettings | WorkflowSettingsOverrides>): AgentResourceSelectors {
|
|
69
|
-
return {
|
|
70
|
-
...(settings.skills === undefined ? {} : { skills: settings.skills }),
|
|
71
|
-
...(settings.extensions === undefined ? {} : { extensions: settings.extensions }),
|
|
72
|
-
...(settings.tools === undefined ? {} : { tools: settings.tools }),
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
function selectorSet(value: AgentResourceSelectors | undefined): AgentResourceSelectorSet { return { skills: [...(value?.skills ?? [])], extensions: [...(value?.extensions ?? [])], ...(value?.tools === undefined ? {} : { tools: [...value.tools] }) }; }
|
|
76
|
-
function validateContextFileScopes(value: unknown, rolePath: string): readonly ContextFileScope[] | undefined {
|
|
77
|
-
if (value === undefined) return undefined;
|
|
78
|
-
if (!Array.isArray(value) || !value.every(isContextFileScope)) fail("INVALID_METADATA", `${rolePath}.contextFiles must be an array containing only global, project, or cwd`);
|
|
79
|
-
return [...value];
|
|
80
|
-
}
|
|
81
|
-
export function validateWorkflowExtensionSettings(value: unknown, settingsPath: string, errorCode: "INVALID_SETTINGS" | "INVALID_METADATA" = "INVALID_SETTINGS", applyDefaults = true): WorkflowExtensionSettings | undefined {
|
|
82
|
-
if (value === undefined) return undefined;
|
|
83
|
-
const base = `${settingsPath}.extensionSettings`;
|
|
84
|
-
if (!object(value)) fail(errorCode, `${base} must be an object`);
|
|
85
|
-
const normalized: Record<string, JsonValue> = {};
|
|
86
|
-
for (const [namespace, raw] of Object.entries(value)) {
|
|
87
|
-
if (!validWorkflowExtensionNamespace(namespace)) fail(errorCode, `${base} contains an invalid namespace: ${namespace}`);
|
|
88
|
-
if (namespace === "herdr") {
|
|
89
|
-
if (!object(raw)) fail(errorCode, `${base}.herdr must be an object`);
|
|
90
|
-
if (Object.keys(raw).some((key) => key !== "enableFullyInspectableMode")) fail(errorCode, `${base}.herdr contains an unsupported setting`);
|
|
91
|
-
if (raw.enableFullyInspectableMode !== undefined && typeof raw.enableFullyInspectableMode !== "boolean") fail(errorCode, `${base}.herdr.enableFullyInspectableMode must be a boolean`);
|
|
92
|
-
normalized.herdr = Object.freeze({ ...(raw.enableFullyInspectableMode === undefined ? {} : { enableFullyInspectableMode: raw.enableFullyInspectableMode }) });
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
if (namespace === "trajectory") {
|
|
96
|
-
if (!object(raw)) fail(errorCode, `${base}.trajectory must be an object`);
|
|
97
|
-
if (Object.keys(raw).some((key) => key !== "port" && key !== "themes")) fail(errorCode, `${base}.trajectory contains an unsupported setting`);
|
|
98
|
-
if (raw.port !== undefined && (!positiveInteger(raw.port) || raw.port > 65535)) fail(errorCode, `${base}.trajectory.port must be an integer from 1 to 65535`);
|
|
99
|
-
if (raw.themes !== undefined && typeof raw.themes !== "boolean") fail(errorCode, `${base}.trajectory.themes must be a boolean`);
|
|
100
|
-
normalized.trajectory = Object.freeze({ ...(raw.port === undefined ? {} : { port: raw.port }), ...(raw.themes === undefined ? (applyDefaults ? { themes: false } : {}) : { themes: raw.themes }) });
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
if (!jsonValue(raw)) fail(errorCode, `${base}.${namespace} must be JSON-compatible`);
|
|
104
|
-
normalized[namespace] = structuredClone(raw);
|
|
105
|
-
}
|
|
106
|
-
return deepFreeze(normalized as WorkflowExtensionSettings);
|
|
107
|
-
}
|
|
108
|
-
function positiveRetentionInteger(value: unknown): value is number { return positiveInteger(value) && Number.isSafeInteger(value); }
|
|
109
|
-
function validateRetention(value: unknown, settingsPath: string): Readonly<WorkflowRetentionSettings> | undefined {
|
|
110
|
-
if (value === undefined) return undefined;
|
|
111
|
-
const base = `${settingsPath}.retention`;
|
|
112
|
-
if (!object(value)) fail("INVALID_SETTINGS", `${base} must be an object`);
|
|
113
|
-
const unknown = Object.keys(value).find((key) => key !== "olderThanDays" && key !== "maxTerminalRuns");
|
|
114
|
-
if (unknown) fail("INVALID_SETTINGS", `Unknown retention setting at ${base}: ${unknown}`);
|
|
115
|
-
if (value.olderThanDays !== undefined && !positiveRetentionInteger(value.olderThanDays)) fail("INVALID_SETTINGS", `${base}.olderThanDays must be a positive integer`);
|
|
116
|
-
if (value.maxTerminalRuns !== undefined && !positiveRetentionInteger(value.maxTerminalRuns)) fail("INVALID_SETTINGS", `${base}.maxTerminalRuns must be a positive integer`);
|
|
117
|
-
return Object.freeze({ ...(value.olderThanDays === undefined ? {} : { olderThanDays: value.olderThanDays }), ...(value.maxTerminalRuns === undefined ? {} : { maxTerminalRuns: value.maxTerminalRuns }) });
|
|
118
|
-
}
|
|
119
|
-
function parseSettings(path: string, partial: false): Readonly<WorkflowSettings>;
|
|
120
|
-
function parseSettings(path: string, partial: true): Readonly<WorkflowSettingsOverrides>;
|
|
121
|
-
function parseSettings(path: string, partial: boolean): Readonly<WorkflowSettings | WorkflowSettingsOverrides> {
|
|
122
|
-
let parsed: unknown;
|
|
123
|
-
try { parsed = JSON.parse(readFileSync(path, "utf8")); }
|
|
124
|
-
catch (error) {
|
|
125
|
-
if (isNodeError(error, "ENOENT")) return partial ? Object.freeze({}) : DEFAULT_SETTINGS;
|
|
126
|
-
fail("CONFIG_ERROR", `Invalid workflow settings JSON at ${path}: ${errorText(error)}`);
|
|
127
|
-
}
|
|
128
|
-
if (!object(parsed)) fail("INVALID_SETTINGS", `Workflow settings at ${path} must be an object`);
|
|
129
|
-
const allowed = new Set(["concurrency", "modelAliases", "skills", "extensions", "extensionSettings", "tools", "retention", "worktreePostCreateCommand", ...(partial ? [] : ["backgroundWidget"]) ]);
|
|
130
|
-
const unknown = Object.keys(parsed).find((key) => !allowed.has(key));
|
|
131
|
-
if (Object.prototype.hasOwnProperty.call(parsed, "disabledAgentResources")) fail("INVALID_SETTINGS", `disabledAgentResources is no longer supported; use skills, extensions, and tools selectors (settings: ${path})`);
|
|
132
|
-
if (unknown) fail("INVALID_SETTINGS", `Unknown workflow setting at ${path}: ${unknown}`);
|
|
133
|
-
const concurrency = parsed.concurrency === undefined ? (partial ? undefined : DEFAULT_SETTINGS.concurrency) : parsed.concurrency;
|
|
134
|
-
if (concurrency !== undefined && (!positiveInteger(concurrency) || concurrency > 16)) fail("INVALID_SETTINGS", `${path}.concurrency must be an integer from 1 to 16`);
|
|
135
|
-
const backgroundWidget = parsed.backgroundWidget === undefined ? (partial ? undefined : DEFAULT_SETTINGS.backgroundWidget) : parsed.backgroundWidget;
|
|
136
|
-
if (backgroundWidget !== undefined && typeof backgroundWidget !== "boolean") fail("INVALID_SETTINGS", `${path}.backgroundWidget must be a boolean`);
|
|
137
|
-
const modelAliases = parsed.modelAliases === undefined ? undefined : validateModelAliases(parsed.modelAliases, path);
|
|
138
|
-
const skills = validateSelectorList(parsed.skills, path, "skills");
|
|
139
|
-
const tools = validateSelectorList(parsed.tools, path, "tools");
|
|
140
|
-
const extensions = validateSelectorList(parsed.extensions, path, "extensions");
|
|
141
|
-
const extensionSettings = parsed.extensionSettings === undefined ? undefined : validateWorkflowExtensionSettings(parsed.extensionSettings, path, "INVALID_SETTINGS", !partial);
|
|
142
|
-
const retention = validateRetention(parsed.retention, path);
|
|
143
|
-
const worktreePostCreateCommand = validateWorktreePostCreateCommand(parsed.worktreePostCreateCommand, path);
|
|
144
|
-
return Object.freeze({
|
|
145
|
-
...(concurrency === undefined ? {} : { concurrency }), ...(backgroundWidget === undefined ? {} : { backgroundWidget }), ...(modelAliases === undefined ? {} : { modelAliases }),
|
|
146
|
-
...(skills === undefined ? {} : { skills }), ...(extensions === undefined ? {} : { extensions }),
|
|
147
|
-
...(extensionSettings === undefined ? {} : { extensionSettings }), ...(tools === undefined ? {} : { tools }), ...(retention === undefined ? {} : { retention }),
|
|
148
|
-
...(worktreePostCreateCommand === undefined ? {} : { worktreePostCreateCommand }),
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
export function loadSettings(path = workflowSettingsPath()): Readonly<WorkflowSettings> { return parseSettings(path, false); }
|
|
152
|
-
export function loadSettingsOverrides(path: string): Readonly<WorkflowSettingsOverrides> { return parseSettings(path, true); }
|
|
153
|
-
export function resolveWorkflowSettings(cwd: string, projectTrusted: boolean, globalSettingsPath = workflowSettingsPath()): WorkflowSettingsResolution {
|
|
154
|
-
const projectSettingsPath = workflowProjectSettingsPath(cwd);
|
|
155
|
-
const global = loadSettings(globalSettingsPath);
|
|
156
|
-
const project: Readonly<WorkflowSettingsOverrides> = projectTrusted ? loadSettingsOverrides(projectSettingsPath) : Object.freeze({});
|
|
157
|
-
const projectHas = (key: keyof WorkflowSettingsOverrides): boolean => Object.prototype.hasOwnProperty.call(project, key);
|
|
158
|
-
const sourceFor = (key: "skills" | "extensions" | "tools"): string => projectHas(key) ? projectSettingsPath : globalSettingsPath;
|
|
159
|
-
const globalSelectors = selectorsFromSettings(global);
|
|
160
|
-
const projectSelectors = selectorsFromSettings(project);
|
|
161
|
-
const effectiveSelectors = selectorSet({
|
|
162
|
-
skills: [...(globalSelectors.skills ?? []), ...(projectSelectors.skills ?? [])],
|
|
163
|
-
extensions: [...(globalSelectors.extensions ?? []), ...(projectSelectors.extensions ?? [])],
|
|
164
|
-
...(globalSelectors.tools === undefined && projectSelectors.tools === undefined ? {} : { tools: [...(globalSelectors.tools ?? []), ...(projectSelectors.tools ?? [])] }),
|
|
165
|
-
});
|
|
166
|
-
const hasExtensionSelectors = global.extensions !== undefined || project.extensions !== undefined;
|
|
167
|
-
const extensionSettings = projectHas("extensionSettings") ? project.extensionSettings : global.extensionSettings;
|
|
168
|
-
const sources: WorkflowSettingsSources = {
|
|
169
|
-
concurrency: projectHas("concurrency") ? projectSettingsPath : globalSettingsPath,
|
|
170
|
-
modelAliases: projectHas("modelAliases") ? projectSettingsPath : globalSettingsPath,
|
|
171
|
-
skills: sourceFor("skills"), extensions: sourceFor("extensions"), tools: sourceFor("tools"),
|
|
172
|
-
...(extensionSettings === undefined ? {} : { extensionSettings: projectHas("extensionSettings") ? projectSettingsPath : globalSettingsPath }),
|
|
173
|
-
...(project.retention === undefined && global.retention === undefined ? {} : { retention: project.retention === undefined ? globalSettingsPath : projectSettingsPath }),
|
|
174
|
-
...(projectHas("worktreePostCreateCommand") || global.worktreePostCreateCommand !== undefined ? { worktreePostCreateCommand: projectHas("worktreePostCreateCommand") ? projectSettingsPath : globalSettingsPath } : {}),
|
|
175
|
-
};
|
|
176
|
-
const effective = Object.freeze({
|
|
177
|
-
concurrency: project.concurrency ?? global.concurrency,
|
|
178
|
-
backgroundWidget: global.backgroundWidget ?? true,
|
|
179
|
-
...(projectHas("modelAliases") ? { modelAliases: project.modelAliases } : global.modelAliases === undefined ? {} : { modelAliases: global.modelAliases }),
|
|
180
|
-
...(effectiveSelectors.skills.length ? { skills: effectiveSelectors.skills } : global.skills !== undefined || project.skills !== undefined ? { skills: effectiveSelectors.skills } : {}),
|
|
181
|
-
...(hasExtensionSelectors ? { extensions: effectiveSelectors.extensions } : {}),
|
|
182
|
-
...(extensionSettings === undefined ? {} : { extensionSettings }),
|
|
183
|
-
...(effectiveSelectors.tools?.length ? { tools: effectiveSelectors.tools } : global.tools !== undefined || project.tools !== undefined ? { tools: effectiveSelectors.tools } : {}),
|
|
184
|
-
...((project.retention ?? global.retention) === undefined ? {} : { retention: project.retention ?? global.retention }),
|
|
185
|
-
...(projectHas("worktreePostCreateCommand") ? { worktreePostCreateCommand: project.worktreePostCreateCommand } : global.worktreePostCreateCommand === undefined ? {} : { worktreePostCreateCommand: global.worktreePostCreateCommand }),
|
|
186
|
-
});
|
|
187
|
-
return { globalSettingsPath, projectSettingsPath, projectTrusted, global, project, effective, sources };
|
|
188
|
-
}
|
|
189
|
-
export function validateModelAliasAvailability(aliases: Readonly<Record<string, string>>, names: readonly string[], availableModels: ReadonlySet<string>, knownModels: ReadonlySet<string>, settingsPath?: string): void {
|
|
190
|
-
for (const name of names) {
|
|
191
|
-
try {
|
|
192
|
-
const target = modelCapability(name, aliases, knownModels, settingsPath);
|
|
193
|
-
if (!availableModels.has(target)) unknownModel(name, target, settingsPath);
|
|
194
|
-
} catch (error) { throw annotateModelAliasError(error, name); }
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
export function resolveAgentResourcePolicy(cwd: string, projectTrusted: boolean, globalSettingsPath = workflowSettingsPath()): AgentResourcePolicy {
|
|
198
|
-
const resolved = resolveWorkflowSettings(cwd, projectTrusted, globalSettingsPath);
|
|
199
|
-
const global = selectorSet(selectorsFromSettings(resolved.global));
|
|
200
|
-
const project = selectorSet(selectorsFromSettings(resolved.project));
|
|
201
|
-
const effective = selectorSet(selectorsFromSettings(resolved.effective));
|
|
202
|
-
return { globalSettingsPath: resolved.globalSettingsPath, projectSettingsPath: resolved.projectSettingsPath, projectTrusted, global, project, effective, unmatchedSkills: [], unmatchedExtensions: [], unmatchedTools: [], selectorSources: { global: selectorsFromSettings(resolved.global), project: selectorsFromSettings(resolved.project) } };
|
|
203
|
-
}
|
|
204
|
-
export function saveModelAliases(path = workflowSettingsPath(), aliases: Readonly<Record<string, string>> = {}): void {
|
|
205
|
-
const normalized = validateModelAliases(aliases, path);
|
|
206
|
-
let parsed: unknown = {};
|
|
207
|
-
try {
|
|
208
|
-
loadSettings(path);
|
|
209
|
-
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
210
|
-
} catch (error) {
|
|
211
|
-
if (!isNodeError(error, "ENOENT")) throw error;
|
|
212
|
-
}
|
|
213
|
-
if (!object(parsed)) fail("INVALID_SETTINGS", `Workflow settings at ${path} must be an object`);
|
|
214
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
215
|
-
atomicWriteFile(path, `${JSON.stringify({ ...parsed, modelAliases: normalized }, null, 2)}\n`, true);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export function parseRoleMarkdown(content: string, strict = false, rolePath?: string): AgentDefinition {
|
|
219
|
-
if (!strict) {
|
|
220
|
-
if (!content.startsWith("---\n")) return { prompt: content };
|
|
221
|
-
const end = content.indexOf("\n---", 4);
|
|
222
|
-
if (end < 0) return { prompt: content };
|
|
223
|
-
const meta: Record<string, string> = {};
|
|
224
|
-
for (const line of content.slice(4, end).split("\n")) {
|
|
225
|
-
const match = /^(model|tools|skills|extensions|description|overrideSystemPrompt|override_system_prompt|is_system_prompt|contextFiles|extensionSettings|disabledAgentResources|thinking)\s*:\s*(.+)$/.exec(line.trim());
|
|
226
|
-
if (match?.[1] === "disabledAgentResources") fail("INVALID_METADATA", "disabledAgentResources is no longer supported; use skills, extensions, and tools selectors");
|
|
227
|
-
if (match?.[1] === "thinking") fail("INVALID_METADATA", "Role thinking is not supported; put it on model as provider/model:thinking");
|
|
228
|
-
if (match?.[1] && match[2]) meta[match[1]] = match[2].trim();
|
|
229
|
-
}
|
|
230
|
-
const unquote = (v: string) => v.replace(/^['"]|['"]$/g, "");
|
|
231
|
-
const parseList = (value: string | undefined): string[] | undefined => value === undefined ? undefined : value.replace(/^\[|\]$/g, "").split(",").map((entry) => unquote(entry.trim())).filter(Boolean);
|
|
232
|
-
const tools = parseList(meta.tools);
|
|
233
|
-
const skills = parseList(meta.skills);
|
|
234
|
-
const extensions = parseList(meta.extensions);
|
|
235
|
-
let extensionSettings: unknown;
|
|
236
|
-
if (meta.extensionSettings !== undefined) {
|
|
237
|
-
try { extensionSettings = JSON.parse(meta.extensionSettings) as unknown; }
|
|
238
|
-
catch (error) { fail("INVALID_METADATA", `Invalid role extensionSettings: ${errorText(error)}`); }
|
|
239
|
-
}
|
|
240
|
-
const rolePathValue = rolePath ?? "<role>";
|
|
241
|
-
const normalizedExtensionSettings = validateWorkflowExtensionSettings(extensionSettings, rolePathValue, "INVALID_METADATA", false);
|
|
242
|
-
const definition: AgentDefinition = { prompt: content.slice(end + 4).replace(/^\n/, "") };
|
|
243
|
-
if (meta.model) {
|
|
244
|
-
const model = unquote(meta.model);
|
|
245
|
-
assertModelThinking(model, "Role model");
|
|
246
|
-
definition.model = model;
|
|
247
|
-
}
|
|
248
|
-
if (meta.description) definition.description = unquote(meta.description);
|
|
249
|
-
if (tools) definition.tools = tools;
|
|
250
|
-
if (skills) definition.skills = skills;
|
|
251
|
-
if (extensions) definition.extensions = extensions;
|
|
252
|
-
if (normalizedExtensionSettings !== undefined) definition.extensionSettings = normalizedExtensionSettings;
|
|
253
|
-
const overrideSystemPrompt = meta.overrideSystemPrompt ?? meta.override_system_prompt ?? meta.is_system_prompt;
|
|
254
|
-
const contextFiles = meta.contextFiles ? meta.contextFiles.replace(/^\[|\]$/g, "").split(",").map((scope) => unquote(scope.trim())).filter(Boolean) : undefined;
|
|
255
|
-
const normalizedContextFiles = validateContextFileScopes(contextFiles, "role");
|
|
256
|
-
if (overrideSystemPrompt) definition.overrideSystemPrompt = overrideSystemPrompt === "true";
|
|
257
|
-
if (normalizedContextFiles) definition.contextFiles = normalizedContextFiles;
|
|
258
|
-
return definition;
|
|
259
|
-
}
|
|
260
|
-
const normalized = content.replace(/\r\n?/g, "\n");
|
|
261
|
-
if (normalized.startsWith("---\n") && normalized.indexOf("\n---", 3) < 0) fail("INVALID_METADATA", "Role frontmatter is missing its closing delimiter");
|
|
262
|
-
let parsed: ReturnType<typeof parseFrontmatter>;
|
|
263
|
-
try { parsed = parseFrontmatter(content); }
|
|
264
|
-
catch (error) { fail("INVALID_METADATA", `Invalid role frontmatter: ${errorText(error)}`); }
|
|
265
|
-
if (!object(parsed.frontmatter)) fail("INVALID_METADATA", "Role frontmatter must be an object");
|
|
266
|
-
const { model, tools, skills, extensions, extensionSettings, description, contextFiles } = parsed.frontmatter;
|
|
267
|
-
if (Object.prototype.hasOwnProperty.call(parsed.frontmatter, "disabledAgentResources")) fail("INVALID_METADATA", "disabledAgentResources is no longer supported; use skills, extensions, and tools selectors");
|
|
268
|
-
if (Object.prototype.hasOwnProperty.call(parsed.frontmatter, "thinking")) fail("INVALID_METADATA", "Role thinking is not supported; put it on model as provider/model:thinking");
|
|
269
|
-
const overrideSystemPrompt = parsed.frontmatter.overrideSystemPrompt ?? parsed.frontmatter.override_system_prompt ?? parsed.frontmatter.is_system_prompt;
|
|
270
|
-
if (model !== undefined && (typeof model !== "string" || model.trim() === "")) fail("INVALID_METADATA", "Role model must be a non-empty string");
|
|
271
|
-
if (description !== undefined && (typeof description !== "string" || description.trim() === "" || description.length > 1024 || /[\r\n]/.test(description))) fail("INVALID_METADATA", "Role description must be a non-empty single-line string of at most 1024 characters");
|
|
272
|
-
if (overrideSystemPrompt !== undefined && typeof overrideSystemPrompt !== "boolean") fail("INVALID_METADATA", "Role overrideSystemPrompt must be a boolean");
|
|
273
|
-
const normalizedContextFiles = validateContextFileScopes(contextFiles, rolePath ?? "<role>");
|
|
274
|
-
const rolePathValue = rolePath ?? "<role>";
|
|
275
|
-
const normalizedTools = validateSelectorList(tools, rolePathValue, "tools", "INVALID_METADATA");
|
|
276
|
-
const normalizedSkills = validateSelectorList(skills, rolePathValue, "skills", "INVALID_METADATA");
|
|
277
|
-
const normalizedExtensions = validateSelectorList(extensions, rolePathValue, "extensions", "INVALID_METADATA");
|
|
278
|
-
const normalizedExtensionSettings = validateWorkflowExtensionSettings(extensionSettings, rolePathValue, "INVALID_METADATA", false);
|
|
279
|
-
const normalizedDescription = typeof description === "string" ? description.trim() : undefined;
|
|
280
|
-
const normalizedModel = typeof model === "string" ? model.trim() : undefined;
|
|
281
|
-
if (normalizedModel !== undefined) assertModelThinking(normalizedModel, "Role model");
|
|
282
|
-
const definition: AgentDefinition = { prompt: parsed.body };
|
|
283
|
-
if (normalizedDescription !== undefined) definition.description = normalizedDescription;
|
|
284
|
-
if (normalizedModel !== undefined) definition.model = normalizedModel;
|
|
285
|
-
if (normalizedTools !== undefined) definition.tools = normalizedTools;
|
|
286
|
-
if (normalizedSkills !== undefined) definition.skills = normalizedSkills;
|
|
287
|
-
if (normalizedExtensions !== undefined) definition.extensions = normalizedExtensions;
|
|
288
|
-
if (normalizedExtensionSettings !== undefined) definition.extensionSettings = normalizedExtensionSettings;
|
|
289
|
-
if (typeof overrideSystemPrompt === "boolean") definition.overrideSystemPrompt = overrideSystemPrompt;
|
|
290
|
-
if (normalizedContextFiles !== undefined) definition.contextFiles = normalizedContextFiles;
|
|
291
|
-
return definition;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const ROLE_DIRECTORY = "pi-extensible-workflows";
|
|
295
|
-
|
|
296
|
-
export function workflowRoleDirectories(agentDir = getAgentDir()): readonly string[] {
|
|
297
|
-
return [join(agentDir, ROLE_DIRECTORY, "roles")];
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function projectRoleDirectories(root: string): readonly string[] {
|
|
301
|
-
return [join(root, ROLE_DIRECTORY, "roles")];
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
type RoleDirectorySource = { path: string; extension?: WorkflowExtensionMetadata; builtin?: true };
|
|
305
|
-
export type WorkflowRoleDirectoryInput = string | WorkflowRoleDirectoryRegistration;
|
|
306
|
-
type RoleFile = { name: string; path: string; source: RoleDirectorySource };
|
|
307
|
-
function roleDirectorySources(dirs: readonly WorkflowRoleDirectoryInput[]): RoleDirectorySource[] {
|
|
308
|
-
const seen = new Set<string>();
|
|
309
|
-
return dirs.flatMap((value) => {
|
|
310
|
-
const source = typeof value === "string" ? { path: value } : { path: value.path, extension: value.extension, ...(value.builtin === true ? { builtin: true as const } : {}) };
|
|
311
|
-
if (seen.has(source.path)) return [];
|
|
312
|
-
seen.add(source.path);
|
|
313
|
-
return [source];
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
function roleDirectoryLabel(source: RoleDirectorySource, extension = true): string {
|
|
317
|
-
return source.extension ? `Extension "${source.extension.headline}" (${source.extension.version})` : extension ? "Registered workflow extension" : "Standard workflow";
|
|
318
|
-
}
|
|
319
|
-
function isRoleFile(dir: string, entry: import("node:fs").Dirent): boolean {
|
|
320
|
-
if (extname(entry.name) !== ".md") return false;
|
|
321
|
-
if (entry.isFile()) return true;
|
|
322
|
-
if (!entry.isSymbolicLink()) return false;
|
|
323
|
-
try { return statSync(join(dir, entry.name)).isFile(); }
|
|
324
|
-
catch (error) { if (isNodeError(error, "ENOENT")) return false; throw error; }
|
|
325
|
-
}
|
|
326
|
-
function scanRoleFiles(dirs: readonly WorkflowRoleDirectoryInput[], extension: boolean): RoleFile[] {
|
|
327
|
-
const files: RoleFile[] = [];
|
|
328
|
-
for (const source of roleDirectorySources(dirs)) {
|
|
329
|
-
let entries: import("node:fs").Dirent[];
|
|
330
|
-
try { entries = readdirSync(source.path, { withFileTypes: true }); }
|
|
331
|
-
catch (error) {
|
|
332
|
-
if (!extension && isNodeError(error, "ENOENT")) continue;
|
|
333
|
-
fail("INVALID_METADATA", `${roleDirectoryLabel(source, extension)} role directory "${source.path}" could not be scanned: ${errorText(error)}`);
|
|
334
|
-
}
|
|
335
|
-
for (const entry of entries) if (isRoleFile(source.path, entry)) files.push({ name: basename(entry.name, ".md"), path: join(source.path, entry.name), source });
|
|
336
|
-
}
|
|
337
|
-
return files.sort((left, right) => left.name.localeCompare(right.name) || left.path.localeCompare(right.path));
|
|
338
|
-
}
|
|
339
|
-
function readRoleDefinitions(dirs: readonly WorkflowRoleDirectoryInput[], extension = false): Record<string, AgentDefinition> {
|
|
340
|
-
const files = scanRoleFiles(dirs, extension);
|
|
341
|
-
const starterFiles = files.filter(({ source }) => source.builtin === true);
|
|
342
|
-
const regularFiles = files.filter(({ source }) => source.builtin !== true);
|
|
343
|
-
if (extension) {
|
|
344
|
-
const byName = new Map<string, RoleFile[]>();
|
|
345
|
-
for (const file of regularFiles) byName.set(file.name, [...(byName.get(file.name) ?? []), file]);
|
|
346
|
-
for (const [name, matches] of byName) if (matches.length > 1) fail("INVALID_METADATA", `Duplicate extension role "${name}": ${matches.map(({ path, source }) => `${roleDirectoryLabel(source)} role directory "${source.path}" (${path})`).join("; ")}`);
|
|
347
|
-
}
|
|
348
|
-
const read = (roleFiles: readonly RoleFile[]): Record<string, AgentDefinition> => Object.fromEntries(roleFiles.map(({ name, path, source }) => {
|
|
349
|
-
try { return [name, parseRoleMarkdown(readFileSync(path, "utf8"), true, path)]; }
|
|
350
|
-
catch (error) {
|
|
351
|
-
if (extension) fail("INVALID_METADATA", `${roleDirectoryLabel(source)} role directory "${source.path}" contains invalid role "${name}" at "${path}": ${errorText(error)}`);
|
|
352
|
-
throw error;
|
|
353
|
-
}
|
|
354
|
-
}));
|
|
355
|
-
return extension ? { ...read(starterFiles), ...read(regularFiles) } : read(files);
|
|
356
|
-
}
|
|
357
|
-
export function loadProjectAgentDefinitions(cwd: string): Readonly<Record<string, AgentDefinition>> { return readRoleDefinitions(projectRoleDirectories(join(cwd, ".pi"))); }
|
|
358
|
-
export function loadAgentDefinitions(cwd: string, agentDir = getAgentDir(), projectTrusted = true, extensionRoleDirectories: readonly WorkflowRoleDirectoryInput[] = registeredWorkflowRoleDirectoryRegistrations()): Readonly<Record<string, AgentDefinition>> {
|
|
359
|
-
return deepFreeze({ ...readRoleDefinitions(extensionRoleDirectories, true), ...readRoleDefinitions(workflowRoleDirectories(agentDir)), ...(projectTrusted ? readRoleDefinitions(projectRoleDirectories(join(cwd, ".pi"))) : {}) });
|
|
360
|
-
}
|
|
361
24
|
function validateRolePolicies(definitions: Readonly<Record<string, AgentDefinition>>, roles: readonly string[], availableModels: ReadonlySet<string>, aliases: Readonly<Record<string, string>> = {}, knownModels = availableModels, settingsPath?: string): void {
|
|
362
25
|
for (const role of roles) {
|
|
363
26
|
const definition = definitions[role];
|
|
@@ -628,13 +291,6 @@ export function workflowPrompt(template: string, values: Readonly<Record<string,
|
|
|
628
291
|
});
|
|
629
292
|
}
|
|
630
293
|
|
|
631
|
-
export function validateSchema(schema: unknown, at = "schema"): asserts schema is JsonSchema {
|
|
632
|
-
if (!object(schema) || Object.getPrototypeOf(schema) !== Object.prototype || !jsonValue(schema)) fail("INVALID_SCHEMA", `${at} must be a plain JSON-compatible Schema object`);
|
|
633
|
-
if (typeof schema.type !== "string" && !Array.isArray(schema.type) && schema.$ref === undefined && schema.anyOf === undefined && schema.oneOf === undefined && schema.allOf === undefined && schema.const === undefined && schema.enum === undefined) fail("INVALID_SCHEMA", `${at} has no JSON Schema shape`);
|
|
634
|
-
if (schema.required !== undefined && (!Array.isArray(schema.required) || schema.required.some((key) => typeof key !== "string"))) fail("INVALID_SCHEMA", `${at}.required must be an array of strings`);
|
|
635
|
-
if (schema.properties !== undefined && !object(schema.properties)) fail("INVALID_SCHEMA", `${at}.properties must be an object`);
|
|
636
|
-
}
|
|
637
|
-
|
|
638
294
|
const AGENT_OPTION_KEYS = new Set(["label", "model", "tools", "skills", "extensions", "contextFiles", "role", "outputSchema", "retries", "timeoutMs"]);
|
|
639
295
|
function validateAgentOption(key: string, value: unknown, aliases?: Readonly<Record<string, string>>, knownModels?: ReadonlySet<string>, settingsPath?: string): void {
|
|
640
296
|
switch (key) {
|
package/subagents/README.md
CHANGED
|
@@ -44,7 +44,7 @@ subagents_run({ prompt: "Summarize README.md.", mode: "foreground" })
|
|
|
44
44
|
|
|
45
45
|
## Interactive inspection
|
|
46
46
|
|
|
47
|
-
In Pi's TUI, `/subagents` opens
|
|
47
|
+
In Pi's TUI, `/subagents` opens the `/workflow` dashboard layout for the durable standalone runs of the current session: the run list sits beside the selected run's details at 80 columns or wider, and narrower terminals drill down from the list to the details. The details show the run ID and the same activity, stall warning, state, model, role, tools, attempts, duration, cumulative token accounting, cost, and error fields as a `/workflow` agent; a running run shows its live activity, which `subagents_inspect` leaves out. Requests without a role show `Role: (none)`. While a run is active the view refreshes the active runs and the selection every second; runs launched while it is open appear after an action or when it reopens. `a` or `enter` opens registered standalone agent actions, editor controls for the prompt, the live system prompt, and the result, `Steer` and `Stop` while running, `Retry` for failed or stopped runs, and the `/workflow` run actions that apply: `Delete`, `Delete all completed`, `Delete all failed`, `Copy run path`, and `Copy agent ID`. Stop and every deletion ask for confirmation, and a run whose worktree still awaits cleanup cannot be deleted. Outside the TUI, `/subagents` opens a picker with the same run summary and bulk deletions as the `/workflow` picker. Background runs appear below the editor in the workflow widget's frame, one row per run with its model, tokens, cost, elapsed time, and quiet or stalled warning, and each leaves a receipt in the transcript when it settles. Inspection does not launch a new LLM call; lifecycle actions may steer, stop, retry, or delete a run.
|
|
48
48
|
|
|
49
49
|
## Tools
|
|
50
50
|
|
|
@@ -114,6 +114,8 @@ export interface SubagentManagerContext {
|
|
|
114
114
|
readonly extensionContext: ExtensionContext;
|
|
115
115
|
readonly waitForForeground?: boolean;
|
|
116
116
|
readonly includeAttemptMetadata?: boolean;
|
|
117
|
+
/** Adds the live progress and activity of an active run; tool results leave streamed activity out. */
|
|
118
|
+
readonly includeActivity?: boolean;
|
|
117
119
|
}
|
|
118
120
|
export interface SubagentAttemptActionData {
|
|
119
121
|
readonly attempt: AgentAttemptSummary;
|
|
@@ -160,6 +162,8 @@ export interface SubagentManager {
|
|
|
160
162
|
steer(request: Readonly<SubagentSteerRequest>, context: Readonly<SubagentManagerContext>): Promise<unknown>;
|
|
161
163
|
stop(request: Readonly<SubagentIdRequest>, context: Readonly<SubagentManagerContext>): Promise<unknown>;
|
|
162
164
|
retry(request: Readonly<SubagentIdRequest>, context: Readonly<SubagentManagerContext>): Promise<unknown>;
|
|
165
|
+
/** Removes a settled run record; used by the /subagents navigator only. */
|
|
166
|
+
delete?(request: Readonly<SubagentIdRequest>, context: Readonly<SubagentManagerContext>): Promise<unknown>;
|
|
163
167
|
getAttemptActionData?(id: string): Readonly<SubagentAttemptActionData> | undefined;
|
|
164
168
|
dispose?(): Promise<void>;
|
|
165
169
|
}
|