pi-extensible-workflows 2.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +18 -11
  2. package/dist/src/agent-execution.d.ts +4 -94
  3. package/dist/src/agent-execution.js +34 -208
  4. package/dist/src/budget.d.ts +38 -0
  5. package/dist/src/budget.js +160 -0
  6. package/dist/src/cli.d.ts +2 -0
  7. package/dist/src/cli.js +60 -8
  8. package/dist/src/doctor-cleanup.d.ts +41 -0
  9. package/dist/src/doctor-cleanup.js +597 -0
  10. package/dist/src/doctor.d.ts +7 -2
  11. package/dist/src/doctor.js +127 -22
  12. package/dist/src/execution.d.ts +17 -0
  13. package/dist/src/execution.js +630 -0
  14. package/dist/src/host.d.ts +101 -0
  15. package/dist/src/host.js +3084 -0
  16. package/dist/src/index.d.ts +13 -634
  17. package/dist/src/index.js +10 -4432
  18. package/dist/src/persistence.d.ts +9 -19
  19. package/dist/src/persistence.js +175 -61
  20. package/dist/src/registry.d.ts +43 -0
  21. package/dist/src/registry.js +279 -0
  22. package/dist/src/session-inspector.js +5 -3
  23. package/dist/src/types.d.ts +692 -0
  24. package/dist/src/types.js +27 -0
  25. package/dist/src/utils.d.ts +32 -0
  26. package/dist/src/utils.js +168 -0
  27. package/dist/src/validation.d.ts +28 -0
  28. package/dist/src/validation.js +832 -0
  29. package/package.json +3 -2
  30. package/skills/pi-extensible-workflows/SKILL.md +69 -34
  31. package/src/agent-execution.ts +37 -189
  32. package/src/budget.ts +75 -0
  33. package/src/cli.ts +47 -7
  34. package/src/doctor-cleanup.ts +337 -0
  35. package/src/doctor.ts +117 -24
  36. package/src/execution.ts +540 -0
  37. package/src/host.ts +2598 -0
  38. package/src/index.ts +14 -3856
  39. package/src/persistence.ts +122 -37
  40. package/src/registry.ts +240 -0
  41. package/src/session-inspector.ts +5 -3
  42. package/src/types.ts +158 -0
  43. package/src/utils.ts +142 -0
  44. package/src/validation.ts +688 -0
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  Turn multi-agent tasks into deterministic jobs that fan out in parallel, pause for approval, and resume without rerunning completed work.
8
8
 
9
- [Documentation](https://vekexasia.github.io/pi-extensible-workflows/) | [Developer guide](https://vekexasia.github.io/pi-extensible-workflows/developers.html) | [Agent guide](https://vekexasia.github.io/pi-extensible-workflows/agents.html)
9
+ [Documentation](https://vekexasia.github.io/pi-extensible-workflows/) | [Developer guide](https://vekexasia.github.io/pi-extensible-workflows/developers.html) | [Agent guide](https://vekexasia.github.io/pi-extensible-workflows/agents.html) | [Extension authoring](https://vekexasia.github.io/pi-extensible-workflows/extensions.html)
10
10
 
11
11
  Requires Node.js 22.19 or newer. This is a trusted Pi extension with the same filesystem and process access as Pi.
12
12
 
@@ -22,19 +22,25 @@ For source installs and local development, see the [installation guide](https://
22
22
 
23
23
  The main Pi agent acts as the orchestrator: it writes workflow scripts on the fly for each task. Pi extensions can add reusable functions and variables to those scripts; every registered function is also directly runnable as a top-level workflow.
24
24
 
25
+ Inline workflow launches require a non-empty `name`; registered function launches reject `name` and use their registered function name as the run name. Workflow worktree scopes always use the explicit `withWorktree(name, callback)` form.
26
+
25
27
  A workflow can fan out across specialized agents, combine their results, and resume without rerunning completed work.
26
28
 
27
29
  ```js
28
30
  const reviews = await parallel("review", {
29
- correctness: () => agent("Review the current changes for correctness issues."),
30
- security: () => agent("Review the current changes for security risks.", {
31
- role: "security-specialist",
32
- }),
31
+ correctness: () =>
32
+ agent("Review the current changes for correctness issues."),
33
+ security: () =>
34
+ agent("Review the current changes for security risks.", {
35
+ role: "security-specialist",
36
+ }),
33
37
  tests: () => agent("Review the current changes for missing test coverage."),
34
38
  });
35
39
 
36
40
  const summary = await agent(
37
- prompt("Deduplicate and prioritize these findings:\n\n{reviews}", { reviews }),
41
+ prompt("Deduplicate and prioritize these findings:\n\n{reviews}", {
42
+ reviews,
43
+ }),
38
44
  );
39
45
 
40
46
  return summary;
@@ -46,7 +52,7 @@ Learn more about roles, workflow contracts, and extension APIs in the documentat
46
52
  - [Global and project settings](https://vekexasia.github.io/pi-extensible-workflows/developers.html#settings)
47
53
  - [Aggregate run budgets](https://vekexasia.github.io/pi-extensible-workflows/developers.html#budgets)
48
54
  - [Workflow DSL and worktrees](https://vekexasia.github.io/pi-extensible-workflows/developers.html#dsl)
49
- - [Reusable extension primitives](https://vekexasia.github.io/pi-extensible-workflows/developers.html#extensions)
55
+ - [Extension authoring guide](https://vekexasia.github.io/pi-extensible-workflows/extensions.html)
50
56
  - [Run artifacts and lifecycle events](https://vekexasia.github.io/pi-extensible-workflows/developers.html#lifecycle)
51
57
  - [Run inspection and recovery](https://vekexasia.github.io/pi-extensible-workflows/developers.html#operations)
52
58
  - [Agent patterns and model selection](https://vekexasia.github.io/pi-extensible-workflows/agents.html#patterns)
@@ -54,21 +60,22 @@ Learn more about roles, workflow contracts, and extension APIs in the documentat
54
60
 
55
61
  ## Configuration
56
62
 
57
- Global workflow settings live at `~/.pi/agent/pi-extensible-workflows/settings.json` by default and configure concurrency, model aliases, and workflow-agent skill or extension exclusions. Trusted projects can add resource exclusions at `<project>/.pi/pi-extensible-workflows/settings.json`; they cannot override global aliases or concurrency. See [global and project settings](https://vekexasia.github.io/pi-extensible-workflows/developers.html#settings) for the schema and merge rules.
63
+ Global workflow settings live at `~/.pi/agent/pi-extensible-workflows/settings.json` by default. Trusted projects can add partial overrides at `<project>/.pi/pi-extensible-workflows/settings.json`; precedence is defaults < global < trusted project < per-run options. Project collections replace global collections, so `{}` and empty arrays clear inherited aliases or resource exclusions. Untrusted project settings are ignored. See [global and project settings](https://vekexasia.github.io/pi-extensible-workflows/developers.html#settings) for the schema, trust gate, source reporting, and merge rules.
58
64
 
59
65
  ## CLI
60
66
 
61
67
  ```sh
62
68
  npx pi-extensible-workflows doctor
69
+ npx pi-extensible-workflows doctor cleanup [--older-than-days <days>] [--yes]
63
70
  npx pi-extensible-workflows inspect [session-id]
64
71
  npx pi-extensible-workflows transcript <session-file>
65
72
  npx pi-extensible-workflows run <workflow-name> [workflow arguments]
66
73
  npx pi-extensible-workflows export <workflow-name> [--name <command>] [--output <path>] [--force]
67
74
  ```
68
75
 
69
- `doctor` validates the installation and active Pi resources. `inspect` opens a read-only terminal view of persisted workflow runs. `transcript` renders a session transcript to stdout. `run` derives flat CLI arguments and help from a registered function's input schema. Use `--input '<json>'` for nested or otherwise complex inputs. It executes in the current working directory, writes the final JSON result to stdout, and writes progress and errors to stderr. `export` creates an executable POSIX launcher in `~/.local/bin` by default.
70
- `run` and `export` accept the trust overrides `--approve` and `--no-approve`; the generated launcher forwards its arguments to `run`. `--` ends launcher option parsing, and later tokens are passed to workflow input instead of being interpreted as launcher options.
71
- Launch snapshots use identity version 4. A cold resume intentionally rejects persisted v3 runs with `RESUME_INCOMPATIBLE`; relaunch the workflow instead.
76
+ `doctor` validates the installation and active Pi resources and remains read-only. `doctor cleanup` is an explicit, dry-run-first maintenance command: it previews terminal workflow runs older than 90 days across the current project's stored sessions and deletes them only with `--yes`; use `--older-than-days` to change the positive age threshold. It protects active, corrupt, leased, and dependency-linked runs. `inspect` opens a read-only terminal view of persisted workflow runs. `transcript` renders a session transcript to stdout. `run` derives flat CLI arguments and help from a registered function's input schema. Use `--input '<json>'` for nested or otherwise complex inputs. It executes in the current working directory, writes the final JSON result to stdout, and writes progress and errors to stderr. `export` creates an executable POSIX launcher in `~/.local/bin` by default.
77
+ `run` and `export` accept the trust overrides `--approve` and `--no-approve`; the generated launcher forwards its arguments to `run`. `--` ends launcher option parsing, and later tokens are passed to workflow input instead of being interpreted as launcher options. Headless `run` and generated `export` launchers cannot execute workflows containing checkpoints; use the Pi workflow tool/UI path for checkpointed workflows.
78
+ Launch snapshots use identity version 5. Cold resume rejects older snapshots, including v4 snapshots created with the previous worktree or registered-function naming contracts, with `RESUME_INCOMPATIBLE`; relaunch the workflow instead.
72
79
 
73
80
  ## Development
74
81
 
@@ -1,22 +1,8 @@
1
- import { type AgentSessionEvent, type InlineExtension, type SessionStats, type ToolDefinition } from "@earendil-works/pi-coding-agent";
1
+ import { type ToolDefinition } from "@earendil-works/pi-coding-agent";
2
2
  type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
3
- type AgentMessage = {
4
- role: string;
5
- content?: unknown;
6
- stopReason?: string;
7
- errorMessage?: string;
8
- usage?: {
9
- input: number;
10
- output: number;
11
- cacheRead: number;
12
- cacheWrite: number;
13
- cost: {
14
- total: number;
15
- };
16
- };
17
- };
18
- import type { AgentIdentity, AgentResourceExclusions, AgentResourcePolicy, AgentSetupSummary, JsonSchema, JsonValue, ModelSpec, WorkflowRunContext } from "./index.js";
3
+ import type { AgentIdentity, AgentResourceExclusions, AgentResourcePolicy, AgentSetupSummary, JsonSchema, JsonValue, ModelSpec, NativeSession, RegisteredAgentSetupHook, SessionFactory, SessionInput, WorkflowRunContext } from "./types.js";
19
4
  import type { RunStore } from "./persistence.js";
5
+ export type { AgentSetup, AgentSetupContext, AgentSetupHook, NativeSession, RegisteredAgentSetupHook, SessionFactory, SessionInput } from "./types.js";
20
6
  export interface AgentBudgetHooks {
21
7
  beforeAttempt(): void;
22
8
  beforeTurn(): void;
@@ -63,10 +49,6 @@ export interface AgentExecutionOptions {
63
49
  budget?: AgentBudgetHooks;
64
50
  agentOptions?: Readonly<Record<string, JsonValue>>;
65
51
  agentIdentity?: AgentIdentity;
66
- conversation?: {
67
- id: string;
68
- turn: number;
69
- };
70
52
  }
71
53
  export interface AgentExecutionRoot {
72
54
  cwd: string;
@@ -125,74 +107,6 @@ export interface AgentExecutionResult {
125
107
  attempts: readonly AgentAttempt[];
126
108
  cwd: string;
127
109
  }
128
- export interface AgentSetup {
129
- prompt: string;
130
- options: Record<string, JsonValue>;
131
- sessionInput: SessionInput;
132
- createSession: SessionFactory;
133
- }
134
- export interface AgentSetupContext {
135
- readonly run: Readonly<WorkflowRunContext>;
136
- readonly identity: Readonly<AgentIdentity>;
137
- readonly attempt: number;
138
- readonly signal: AbortSignal;
139
- }
140
- export interface AgentSetupHook {
141
- priority?: number;
142
- setup: (agent: AgentSetup, context: Readonly<AgentSetupContext>) => void | Promise<void>;
143
- }
144
- export interface RegisteredAgentSetupHook {
145
- name: string;
146
- priority: number;
147
- setup: AgentSetupHook["setup"];
148
- }
149
- type NativeSessionStats = Pick<SessionStats, "tokens" | "cost">;
150
- export interface NativeSession {
151
- readonly sessionId: string;
152
- readonly sessionFile: string | undefined;
153
- readonly messages: readonly AgentMessage[];
154
- getSessionStats(): NativeSessionStats;
155
- readonly systemPrompt?: string;
156
- readonly model?: {
157
- provider: string;
158
- model?: string;
159
- id?: string;
160
- };
161
- readonly agent?: {
162
- state: {
163
- tools: readonly {
164
- name: string;
165
- }[];
166
- };
167
- };
168
- getLeafId?: () => string | null;
169
- getToolDefinitions?: () => unknown;
170
- subscribe?(listener: (event: AgentSessionEvent) => void): () => void;
171
- prompt(text: string): Promise<void>;
172
- steer?(text: string): Promise<void>;
173
- abort?(): Promise<void>;
174
- dispose(): void;
175
- }
176
- export interface SessionInput {
177
- cwd: string;
178
- model: ModelSpec;
179
- tools: string[];
180
- sessionLabel: string;
181
- agentDir?: string;
182
- customTools?: ToolDefinition[];
183
- resultTool?: ToolDefinition;
184
- systemPromptAppend?: string;
185
- extensionFactories?: InlineExtension[];
186
- resourcePolicy?: AgentResourcePolicy;
187
- options?: Record<string, JsonValue>;
188
- continuation?: {
189
- sessionId: string;
190
- sessionFile: string;
191
- leafId: string;
192
- };
193
- allowModelChange?: boolean;
194
- }
195
- export type SessionFactory = (input: SessionInput) => Promise<NativeSession>;
196
110
  export declare function createNativeAgentSession(input: SessionInput): Promise<NativeSession>;
197
111
  export declare class WorkflowAgentExecutor {
198
112
  private readonly root;
@@ -222,10 +136,6 @@ export interface ScheduledAgentOptions {
222
136
  timeoutMs?: number | null;
223
137
  agentOptions?: Readonly<Record<string, JsonValue>>;
224
138
  agentIdentity?: AgentIdentity;
225
- conversation?: {
226
- id: string;
227
- turn: number;
228
- };
229
139
  }
230
140
  export type ScheduledAgentResult = {
231
141
  id: string;
@@ -279,6 +189,7 @@ export declare class FairAgentScheduler {
279
189
  private readonly writeOwnership?;
280
190
  constructor(runner: ScheduledAgentRunner, sessionLimit?: number, writeOwnership?: OwnershipWriter | undefined);
281
191
  addRun(runId: string, limit?: number, beforeLaunch?: () => void): void;
192
+ updateRunLimit(runId: string, limit: number): void;
282
193
  spawn(runId: string, prompt: string, options: ScheduledAgentOptions, parentId?: string): {
283
194
  id: string;
284
195
  result: Promise<ScheduledAgentResult>;
@@ -296,4 +207,3 @@ export declare class FairAgentScheduler {
296
207
  restoreRun(runId: string, limit: number, ownership: readonly OwnershipRecord[], beforeLaunch?: () => void): void;
297
208
  flush(): Promise<void>;
298
209
  }
299
- export {};
@@ -1,17 +1,16 @@
1
- import { createHash } from "node:crypto";
2
1
  import { realpathSync } from "node:fs";
3
2
  import { join, resolve } from "node:path";
4
3
  import { Type } from "@earendil-works/pi-ai";
5
4
  import { Value } from "typebox/value";
6
5
  import { createAgentSession, DefaultPackageManager, DefaultResourceLoader, getAgentDir, ModelRuntime, SessionManager, SettingsManager } from "@earendil-works/pi-coding-agent";
7
- import { mergeAgentResourceExclusions, resolveModelReference, WorkflowError } from "./index.js";
6
+ import { jsonObject, disabledResources, mergeAgentResourceExclusions, modelAliasName, modelCapability, resolveModelReference, unmatchedResourcePatterns } from "./utils.js";
7
+ import { WorkflowError } from "./types.js";
8
8
  function parseModel(value, fallback, thinking, aliases = {}, knownModels, settingsPath) {
9
9
  if (!value)
10
10
  return { ...fallback, ...(thinking ? { thinking } : {}) };
11
11
  const parsed = resolveModelReference(value, aliases, knownModels, settingsPath);
12
12
  return { ...parsed, ...(thinking ? { thinking } : !parsed.thinking && fallback.thinking ? { thinking: fallback.thinking } : {}) };
13
13
  }
14
- function modelCapability(model) { return `${model.provider}/${model.model}`; }
15
14
  function text(messages) {
16
15
  const message = [...messages].reverse().find((item) => item.role === "assistant");
17
16
  if (!message || !Array.isArray(message.content))
@@ -54,33 +53,8 @@ catch {
54
53
  } }
55
54
  export async function createNativeAgentSession(input) {
56
55
  const agentDir = input.agentDir ?? getAgentDir();
57
- let manager;
58
- if (input.continuation) {
59
- try {
60
- manager = SessionManager.open(input.continuation.sessionFile, input.agentDir ? join(agentDir, "sessions") : undefined, input.cwd);
61
- const header = manager.getHeader();
62
- if (!header || canonicalSourcePath(header.cwd) !== canonicalSourcePath(input.cwd) || manager.getSessionId() !== input.continuation.sessionId || !manager.getEntry(input.continuation.leafId))
63
- throw new Error("Persisted transcript identity does not match the conversation head");
64
- manager.branch(input.continuation.leafId);
65
- const context = manager.buildSessionContext();
66
- if (context.model && (context.model.provider !== input.model.provider || context.model.modelId !== input.model.model)) {
67
- if (!input.allowModelChange)
68
- throw new Error("Persisted transcript model does not match the conversation execution policy");
69
- manager.appendModelChange(input.model.provider, input.model.model);
70
- }
71
- if (input.model.thinking && context.thinkingLevel !== input.model.thinking)
72
- throw new Error("Persisted transcript thinking level does not match the conversation execution policy");
73
- }
74
- catch (error) {
75
- if (error instanceof WorkflowError && error.code === "RESUME_INCOMPATIBLE")
76
- throw error;
77
- throw new WorkflowError("RESUME_INCOMPATIBLE", `Cannot reopen conversation transcript: ${error instanceof Error ? error.message : String(error)}`);
78
- }
79
- }
80
- else {
81
- manager = input.agentDir ? SessionManager.create(input.cwd, join(agentDir, "sessions")) : SessionManager.create(input.cwd);
82
- manager.appendSessionInfo(input.sessionLabel);
83
- }
56
+ const manager = input.agentDir ? SessionManager.create(input.cwd, join(agentDir, "sessions")) : SessionManager.create(input.cwd);
57
+ manager.appendSessionInfo(input.sessionLabel);
84
58
  const modelRuntime = await ModelRuntime.create({ authPath: join(agentDir, "auth.json"), modelsPath: join(agentDir, "models.json") });
85
59
  const model = modelRuntime.getModel(input.model.provider, input.model.model);
86
60
  if (!model)
@@ -95,14 +69,17 @@ export async function createNativeAgentSession(input) {
95
69
  settingsManager.setProjectTrusted(policy.projectTrusted);
96
70
  const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager });
97
71
  const resolved = await packageManager.resolve();
98
- const disabledExtensions = new Set(policy.effective.extensions);
99
- const extensionPaths = [...new Set(resolved.extensions.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => canonicalSourcePath(path)).filter((path) => !disabledExtensions.has(canonicalSourcePath(path))))];
72
+ const discoveredExtensions = [...new Set(resolved.extensions.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => canonicalSourcePath(path)))];
73
+ const excludedExtensions = new Set(disabledResources(policy.effective.extensions, discoveredExtensions));
74
+ const extensionPaths = discoveredExtensions.filter((path) => !excludedExtensions.has(path));
75
+ Object.assign(policy, { excludedExtensions: [...excludedExtensions], unmatchedExtensions: unmatchedResourcePatterns(policy.effective.extensions, discoveredExtensions) });
100
76
  const skillPaths = [...new Set(resolved.skills.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => path))];
101
77
  const updateSkillMatches = (skills) => {
102
- const names = new Set(skills.map(({ name }) => name));
103
- Object.assign(policy, { unmatchedSkills: policy.effective.skills.filter((name) => !names.has(name)) });
78
+ const names = [...new Set(skills.map(({ name }) => name))];
79
+ const excludedSkills = disabledResources(policy.effective.skills, names);
80
+ Object.assign(policy, { excludedSkills, unmatchedSkills: unmatchedResourcePatterns(policy.effective.skills, names) });
81
+ return new Set(excludedSkills);
104
82
  };
105
- const disabledSkills = new Set(policy.effective.skills);
106
83
  resourceLoader = new DefaultResourceLoader({
107
84
  cwd: input.cwd,
108
85
  agentDir,
@@ -113,22 +90,18 @@ export async function createNativeAgentSession(input) {
113
90
  additionalSkillPaths: skillPaths,
114
91
  ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}),
115
92
  skillsOverride: (base) => {
116
- updateSkillMatches(base.skills);
93
+ const disabledSkills = updateSkillMatches(base.skills);
117
94
  return { ...base, skills: base.skills.filter(({ name }) => !disabledSkills.has(name)) };
118
95
  },
119
96
  ...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}),
120
97
  });
121
98
  await resourceLoader.reload();
122
- const discoveredExtensions = new Set(resolved.extensions.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => canonicalSourcePath(path)));
123
- Object.assign(policy, { unmatchedExtensions: policy.effective.extensions.filter((path) => !discoveredExtensions.has(canonicalSourcePath(path))) });
124
99
  }
125
100
  else if (input.systemPromptAppend || input.extensionFactories?.length) {
126
101
  resourceLoader = new DefaultResourceLoader({ cwd: input.cwd, agentDir, ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}), ...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}) });
127
102
  await resourceLoader.reload();
128
103
  }
129
- const { session, modelFallbackMessage } = await createAgentSession({ ...(input.options ?? {}), cwd: input.cwd, agentDir, modelRuntime, model, ...(settingsManager ? { settingsManager } : {}), ...(input.model.thinking ? { thinkingLevel: input.model.thinking } : {}), tools, ...(customTools.length ? { customTools } : {}), ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}), ...(resourceLoader ? { resourceLoader } : {}), sessionManager: manager });
130
- if (input.continuation && modelFallbackMessage)
131
- throw new WorkflowError("RESUME_INCOMPATIBLE", modelFallbackMessage);
104
+ const { session } = await createAgentSession({ ...(input.options ?? {}), cwd: input.cwd, agentDir, modelRuntime, model, ...(settingsManager ? { settingsManager } : {}), ...(input.model.thinking ? { thinkingLevel: input.model.thinking } : {}), tools, ...(customTools.length ? { customTools } : {}), ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}), ...(resourceLoader ? { resourceLoader } : {}), sessionManager: manager });
132
105
  return Object.assign(session, {
133
106
  getLeafId: () => manager.getLeafId(),
134
107
  getToolDefinitions: () => session.getAllTools().map(({ name, description, parameters, promptGuidelines }) => ({ name, description, parameters, ...(promptGuidelines ? { promptGuidelines } : {}) })),
@@ -136,24 +109,6 @@ export async function createNativeAgentSession(input) {
136
109
  }
137
110
  function changedOption(options, baseline, key) { return JSON.stringify(options[key]) !== JSON.stringify(baseline[key]); }
138
111
  function validThinking(value) { return typeof value === "string" && ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(value); }
139
- function jsonValue(value, seen = new Set()) {
140
- if (value === null || typeof value === "boolean" || typeof value === "string")
141
- return true;
142
- if (typeof value === "number")
143
- return Number.isFinite(value);
144
- if (typeof value !== "object" || seen.has(value))
145
- return false;
146
- if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
147
- return false;
148
- const keys = Reflect.ownKeys(value);
149
- if (keys.some((key) => typeof key !== "string"))
150
- return false;
151
- seen.add(value);
152
- const valid = (Array.isArray(value) ? Array.from(value) : keys.map((key) => value[key])).every((item) => jsonValue(item, seen));
153
- seen.delete(value);
154
- return valid;
155
- }
156
- function jsonObject(value) { return jsonValue(value) && typeof value === "object" && value !== null && !Array.isArray(value); }
157
112
  function isChildAgentToolParams(value) {
158
113
  if (!jsonObject(value) || typeof value.prompt !== "string" || typeof value.label !== "string")
159
114
  return false;
@@ -179,58 +134,9 @@ function fallbackSetupContext(root, options, signal) {
179
134
  return { run, identity: Object.freeze({ ...identity, structuralPath: Object.freeze([...identity.structuralPath]) }) };
180
135
  }
181
136
  function resourcePolicySummary(policy) {
182
- return { skills: [...policy.effective.skills], extensions: [...policy.effective.extensions], unmatchedSkills: [...policy.unmatchedSkills], unmatchedExtensions: [...policy.unmatchedExtensions] };
183
- }
184
- function canonicalJson(value) {
185
- if (Array.isArray(value))
186
- return value.map((item) => canonicalJson(item));
187
- if (value && typeof value === "object")
188
- return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, canonicalJson(item)]));
189
- return value;
190
- }
191
- function fingerprint(value) { return createHash("sha256").update(JSON.stringify(canonicalJson(value))).digest("hex"); }
192
- function promptFingerprint(value) { return createHash("sha256").update(value).digest("hex"); }
193
- function fixedConversationOptions(options) {
194
- const fixedOptions = structuredClone(options);
195
- delete fixedOptions.timeoutMs;
196
- delete fixedOptions.retries;
197
- return fixedOptions;
198
- }
199
- function conversationExecutionPolicy(options, setup) {
200
- return structuredClone({
201
- model: setup.sessionInput.model,
202
- tools: [...setup.sessionInput.tools],
203
- cwd: setup.sessionInput.cwd,
204
- role: options.role ?? null,
205
- worktreeOwner: options.worktreeOwner ?? null,
206
- parent: options.parent ?? null,
207
- systemPromptAppend: setup.sessionInput.systemPromptAppend ?? "",
208
- options: fixedConversationOptions(setup.options),
209
- resourcePolicy: setup.sessionInput.resourcePolicy ? resourcePolicySummary(setup.sessionInput.resourcePolicy) : null,
210
- });
211
- }
212
- function conversationPolicyMatches(expected, current, allowModelChange) {
213
- if (fingerprint(expected) === fingerprint(current))
214
- return true;
215
- if (!allowModelChange || !expected || typeof expected !== "object" || Array.isArray(expected) || !current || typeof current !== "object" || Array.isArray(current))
216
- return false;
217
- const expectedModel = conversationPolicyModel(expected);
218
- const currentModel = conversationPolicyModel(current);
219
- if (!expectedModel || !currentModel)
220
- return false;
221
- return fingerprint(expected) === fingerprint({ ...current, model: { ...currentModel, provider: expectedModel.provider, model: expectedModel.model } });
222
- }
223
- function conversationPolicyModel(policy) {
224
- if (!policy || typeof policy !== "object" || Array.isArray(policy))
225
- return undefined;
226
- const model = policy.model;
227
- if (!model || typeof model !== "object" || Array.isArray(model) || typeof model.provider !== "string" || typeof model.model !== "string")
228
- return undefined;
229
- const thinking = typeof model.thinking === "string" && ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(model.thinking) ? model.thinking : undefined;
230
- return thinking === undefined ? { provider: model.provider, model: model.model } : { provider: model.provider, model: model.model, thinking };
137
+ return { skills: [...policy.effective.skills], extensions: [...policy.effective.extensions], excludedSkills: [...(policy.excludedSkills ?? [])], excludedExtensions: [...(policy.excludedExtensions ?? [])], unmatchedSkills: [...policy.unmatchedSkills], unmatchedExtensions: [...policy.unmatchedExtensions] };
231
138
  }
232
- function conversationFailure(message) { return new WorkflowError("RESUME_INCOMPATIBLE", message); }
233
- async function prepareAgentSetup(root, createSession, task, options, resolved, cwd, attempt, signal, customTools, resultTool, continuation) {
139
+ async function prepareAgentSetup(root, createSession, task, options, resolved, cwd, attempt, signal, customTools, resultTool) {
234
140
  const setupSignal = signal ?? root.runContext?.signal ?? new AbortController().signal;
235
141
  const baselineOptions = structuredClone(options.agentOptions ?? {});
236
142
  const baseResourcePolicy = await root.agentResourcePolicy?.();
@@ -265,8 +171,6 @@ async function prepareAgentSetup(root, createSession, task, options, resolved, c
265
171
  setup.sessionInput.tools = [...setup.options.tools];
266
172
  if (changedOption(setup.options, baselineOptions, "cwd") && typeof setup.options.cwd === "string")
267
173
  setup.sessionInput.cwd = setup.options.cwd;
268
- if (continuation)
269
- setup.sessionInput.continuation = { sessionId: continuation.sessionId, sessionFile: continuation.sessionFile, leafId: continuation.leafId };
270
174
  const model = setup.sessionInput.model;
271
175
  const summary = { hookNames: [...hookNames], model: { provider: model.provider, model: model.model, ...(model.thinking ? { thinking: model.thinking } : {}) }, tools: [...setup.sessionInput.tools], cwd: setup.sessionInput.cwd, ...(setup.sessionInput.resourcePolicy ? { disabledAgentResources: resourcePolicySummary(setup.sessionInput.resourcePolicy) } : {}) };
272
176
  return { setup, summary };
@@ -291,17 +195,18 @@ export class WorkflowAgentExecutor {
291
195
  if (forbidden)
292
196
  throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the launching session boundary: ${forbidden}`);
293
197
  const requestedModel = options.model ?? definition?.model;
294
- const hasAlias = requestedModel !== undefined && Object.prototype.hasOwnProperty.call(this.root.modelAliases ?? {}, requestedModel);
295
- if (requestedModel !== undefined && this.root.blockedAliases?.has(requestedModel) && !hasAlias) {
296
- const target = this.root.blockedAliasTargets?.[requestedModel];
198
+ const alias = requestedModel === undefined ? undefined : modelAliasName(requestedModel, this.root.modelAliases ?? {});
199
+ const blockedAlias = requestedModel?.split(":", 1)[0];
200
+ if (requestedModel !== undefined && blockedAlias && this.root.blockedAliases?.has(blockedAlias) && !alias) {
201
+ const target = this.root.blockedAliasTargets?.[blockedAlias];
297
202
  throw new WorkflowError("UNKNOWN_MODEL", `Unknown model alias ${requestedModel}${target ? ` resolved to ${target}` : ""}${this.root.settingsPath ? ` (settings: ${this.root.settingsPath})` : ""}`);
298
203
  }
299
- const aliasThinking = requestedModel !== undefined && hasAlias ? resolveModelReference(requestedModel, this.root.modelAliases, this.root.knownModels ?? this.root.availableModels, this.root.settingsPath).thinking : undefined;
204
+ const aliasThinking = requestedModel !== undefined && alias ? resolveModelReference(requestedModel, this.root.modelAliases, this.root.knownModels ?? this.root.availableModels, this.root.settingsPath).thinking : undefined;
300
205
  const model = options.modelOverride ?? parseModel(requestedModel, this.root.model, options.thinking ?? (aliasThinking === undefined ? definition?.thinking : undefined), this.root.modelAliases, this.root.knownModels ?? this.root.availableModels, this.root.settingsPath);
301
206
  const availableModels = this.root.knownModels ?? this.root.availableModels ?? new Set([modelCapability(this.root.model)]);
302
207
  if (!availableModels.has(modelCapability(model)))
303
208
  throw new WorkflowError("UNKNOWN_MODEL", `Unknown model${requestedModel ? ` ${requestedModel} resolved to ${modelCapability(model)}` : ""}${this.root.settingsPath ? ` (settings: ${this.root.settingsPath})` : ""}`);
304
- return { model, ...(hasAlias ? { requestedModel } : {}), tools: [...requested], systemPromptAppend: definition?.prompt ?? "" };
209
+ return { model, ...(alias && requestedModel ? { requestedModel } : {}), tools: [...requested], systemPromptAppend: definition?.prompt ?? "" };
305
210
  }
306
211
  async execute(task, options, signal, customTools = [], setSteer, beforeRetry) {
307
212
  const executionSignal = signal ?? this.root.runContext?.signal;
@@ -339,29 +244,7 @@ export class WorkflowAgentExecutor {
339
244
  throw new WorkflowError("INVALID_METADATA", "Only child agents or worktree scopes may provide a cwd");
340
245
  cwd = this.root.cwd;
341
246
  }
342
- let conversationRecord;
343
- if (options.conversation) {
344
- const store = this.root.runStore;
345
- if (!store)
346
- throw conversationFailure("Conversation persistence is unavailable");
347
- try {
348
- conversationRecord = await store.conversation(options.conversation.id);
349
- }
350
- catch (error) {
351
- throw conversationFailure(`Cannot load conversation state: ${error instanceof Error ? error.message : String(error)}`);
352
- }
353
- if (conversationRecord) {
354
- const model = conversationPolicyModel(conversationRecord.policy);
355
- if (model)
356
- resolved = this.resolve({ ...options, modelOverride: model });
357
- }
358
- if (!Number.isInteger(options.conversation.turn) || options.conversation.turn < 1)
359
- throw conversationFailure("Conversation turn must be a positive integer");
360
- if (conversationRecord ? conversationRecord.head.turn + 1 !== options.conversation.turn : options.conversation.turn !== 1)
361
- throw conversationFailure(`Conversation turn ${String(options.conversation.turn)} does not continue its persisted head`);
362
- }
363
247
  const attempts = [];
364
- let conversationBaseline;
365
248
  let maxAttempts = (options.retries ?? 0) + 1;
366
249
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
367
250
  if (recoveryModel)
@@ -374,10 +257,6 @@ export class WorkflowAgentExecutor {
374
257
  let setupFailed = false;
375
258
  let budgetError;
376
259
  let turnStarted = false;
377
- let conversationSystemPrompt = "";
378
- let conversationToolDefinitionsSha256 = "";
379
- let conversationMismatch;
380
- const conversationMismatchError = () => conversationMismatch ? new WorkflowError("RESUME_INCOMPATIBLE", conversationMismatch.message) : undefined;
381
260
  const hasSchemaResult = () => schemaResult !== undefined;
382
261
  const resultTool = options.schema ? {
383
262
  name: "workflow_result", label: "Workflow Result", description: "Submit the terminal structured workflow result", parameters: Type.Unsafe(options.schema),
@@ -411,70 +290,21 @@ export class WorkflowAgentExecutor {
411
290
  };
412
291
  try {
413
292
  setupFailed = true;
414
- const prepared = await prepareAgentSetup(this.root, this.createSession, task, options, resolved, cwd, attempt, executionSignal, customTools, resultTool, conversationRecord?.head);
293
+ const prepared = await prepareAgentSetup(this.root, this.createSession, task, options, resolved, cwd, attempt, executionSignal, customTools, resultTool);
415
294
  setup = prepared.setup;
416
295
  setupSummary = prepared.summary;
417
296
  setupFailed = false;
418
297
  if (executionSignal?.aborted)
419
298
  throw new WorkflowError("CANCELLED", "Agent cancelled");
420
- if (recoveryModel && conversationRecord)
421
- setup.sessionInput.allowModelChange = true;
422
299
  const started = Date.now();
423
300
  session = await setup.createSession(setup.sessionInput);
424
301
  if (setup.sessionInput.resourcePolicy)
425
302
  setupSummary = { ...setupSummary, disabledAgentResources: resourcePolicySummary(setup.sessionInput.resourcePolicy) };
426
- if (options.conversation) {
427
- conversationSystemPrompt = session.systemPrompt ?? "";
428
- conversationToolDefinitionsSha256 = fingerprint(session.getToolDefinitions?.() ?? session.agent?.state.tools ?? []);
429
- const currentExecutionPolicy = conversationExecutionPolicy(options, setup);
430
- if (conversationRecord) {
431
- if (session.sessionId !== conversationRecord.head.sessionId || requiredFile(session.sessionFile) !== conversationRecord.head.sessionFile)
432
- throw conversationFailure("Conversation transcript identity changed");
433
- if (!session.getLeafId || (!recoveryModel && session.getLeafId() !== conversationRecord.head.leafId))
434
- throw conversationFailure("Conversation transcript leaf identity changed");
435
- if (!conversationPolicyMatches(conversationRecord.policy, currentExecutionPolicy, Boolean(recoveryModel)))
436
- throw conversationFailure("Conversation execution policy changed");
437
- if (!session.subscribe && (promptFingerprint(conversationSystemPrompt) !== conversationRecord.head.systemPromptSha256 || conversationSystemPrompt !== conversationRecord.head.systemPrompt))
438
- throw conversationFailure("Conversation system prompt changed");
439
- if (conversationToolDefinitionsSha256 !== conversationRecord.head.toolDefinitionsSha256)
440
- throw conversationFailure("Conversation tool definitions changed");
441
- }
442
- else if (conversationBaseline) {
443
- if (!conversationPolicyMatches(conversationBaseline.executionPolicy, currentExecutionPolicy, Boolean(recoveryModel)))
444
- throw conversationFailure("Conversation execution policy changed");
445
- if (conversationToolDefinitionsSha256 !== conversationBaseline.toolDefinitionsSha256)
446
- throw conversationFailure("Conversation tool definitions changed");
447
- }
448
- else {
449
- conversationBaseline = { executionPolicy: currentExecutionPolicy, toolDefinitionsSha256: conversationToolDefinitionsSha256 };
450
- }
451
- if (!session.subscribe) {
452
- const expectedPrompt = conversationRecord?.head.systemPrompt ?? conversationBaseline?.systemPrompt;
453
- const expectedDigest = conversationRecord?.head.systemPromptSha256 ?? conversationBaseline?.systemPromptSha256;
454
- if (expectedPrompt !== undefined && expectedDigest !== undefined && (promptFingerprint(conversationSystemPrompt) !== expectedDigest || expectedPrompt !== conversationSystemPrompt))
455
- throw conversationFailure("Conversation system prompt changed");
456
- if (!conversationRecord && conversationBaseline && conversationBaseline.systemPrompt === undefined)
457
- conversationBaseline = { ...conversationBaseline, systemPrompt: conversationSystemPrompt, systemPromptSha256: promptFingerprint(conversationSystemPrompt) };
458
- }
459
- if (conversationRecord && (!session.model || session.model.provider !== setup.sessionInput.model.provider || (session.model.model ?? session.model.id) !== setup.sessionInput.model.model))
460
- throw conversationFailure("Conversation model changed");
461
- }
462
303
  const includeAttemptSetup = Boolean(this.root.agentSetupHooks?.length || setup.sessionInput.resourcePolicy);
463
304
  await options.onAttempt?.({ attempt, sessionId: session.sessionId, sessionFile: requiredFile(session.sessionFile), ...(includeAttemptSetup ? { setup: setupSummary } : {}) });
464
305
  const activeSession = session;
465
306
  unsubscribe = activeSession.subscribe?.((event) => {
466
307
  if (event.type === "agent_start" && session?.systemPrompt !== undefined) {
467
- if (options.conversation) {
468
- conversationSystemPrompt = session.systemPrompt;
469
- const expectedPrompt = conversationRecord?.head.systemPrompt ?? conversationBaseline?.systemPrompt;
470
- const expectedDigest = conversationRecord?.head.systemPromptSha256 ?? conversationBaseline?.systemPromptSha256;
471
- if (expectedPrompt !== undefined && expectedDigest !== undefined && (promptFingerprint(conversationSystemPrompt) !== expectedDigest || expectedPrompt !== conversationSystemPrompt)) {
472
- conversationMismatch = conversationFailure("Conversation system prompt changed");
473
- void session.abort?.();
474
- }
475
- if (!conversationRecord && conversationBaseline && conversationBaseline.systemPrompt === undefined)
476
- conversationBaseline = { ...conversationBaseline, systemPrompt: conversationSystemPrompt, systemPromptSha256: promptFingerprint(conversationSystemPrompt) };
477
- }
478
308
  if (this.root.runStore) {
479
309
  systemPromptTurn += 1;
480
310
  const entry = { sessionId: session.sessionId, attempt, turn: systemPromptTurn, prompt: session.systemPrompt };
@@ -528,6 +358,7 @@ export class WorkflowAgentExecutor {
528
358
  if (activity?.kind === "tool" && activity.text === event.toolName)
529
359
  activity = undefined;
530
360
  report(false);
361
+ toolCalls.delete(event.toolCallId);
531
362
  }
532
363
  });
533
364
  report(false);
@@ -548,8 +379,6 @@ export class WorkflowAgentExecutor {
548
379
  if (!hasSchemaResult())
549
380
  throw error;
550
381
  }
551
- if (conversationMismatch)
552
- throw conversationMismatch;
553
382
  throwIfTerminalAssistantError(session, setup.sessionInput.model);
554
383
  {
555
384
  const completedAccounting = accounting(session.getSessionStats());
@@ -596,9 +425,6 @@ export class WorkflowAgentExecutor {
596
425
  if (schemaResult === undefined)
597
426
  throw new WorkflowError("RESULT_INVALID", "Agent did not submit a valid workflow_result after one repair");
598
427
  }
599
- const mismatch = conversationMismatchError();
600
- if (mismatch)
601
- throw mismatch;
602
428
  const value = options.schema ? schemaResult : text(session.messages);
603
429
  if (options.worktreeOwner)
604
430
  await this.root.runStore?.snapshotWorktree(options.worktreeOwner);
@@ -607,22 +433,13 @@ export class WorkflowAgentExecutor {
607
433
  await flushSystemPrompts();
608
434
  unsubscribe?.();
609
435
  const attemptAccounting = accounting(session.getSessionStats());
610
- const leafId = session.getLeafId?.() ?? undefined;
611
- if (options.conversation) {
612
- if (!leafId)
613
- throw conversationFailure("Conversation transcript has no persisted leaf");
614
- const store = this.root.runStore;
615
- if (!store)
616
- throw conversationFailure("Conversation persistence is unavailable");
617
- await store.saveConversation({ id: options.conversation.id, policy: conversationExecutionPolicy(options, setup), head: { turn: options.conversation.turn, sessionId: session.sessionId, sessionFile: requiredFile(session.sessionFile), leafId, systemPrompt: conversationSystemPrompt, systemPromptSha256: promptFingerprint(conversationSystemPrompt), toolDefinitionsSha256: conversationToolDefinitionsSha256 } });
618
- }
619
436
  const includeCompletedSetup = Boolean(this.root.agentSetupHooks?.length || setup.sessionInput.resourcePolicy);
620
437
  attempts.push({ attempt, sessionId: session.sessionId, sessionFile: requiredFile(session.sessionFile), result: value, accounting: attemptAccounting, ...(includeCompletedSetup ? { setup: setupSummary } : {}) });
621
438
  session.dispose();
622
439
  return { value, attempts, cwd: setupSummary.cwd };
623
440
  }
624
441
  catch (error) {
625
- const typed = budgetError ?? conversationMismatch ?? (error instanceof WorkflowError ? error : new WorkflowError(executionSignal?.aborted && setupFailed ? "CANCELLED" : "AGENT_FAILED", error instanceof Error ? error.message : String(error)));
442
+ const typed = budgetError ?? (error instanceof WorkflowError ? error : new WorkflowError(executionSignal?.aborted && setupFailed ? "CANCELLED" : "AGENT_FAILED", error instanceof Error ? error.message : String(error)));
626
443
  if (session) {
627
444
  report(true);
628
445
  await progress;
@@ -704,6 +521,15 @@ export class FairAgentScheduler {
704
521
  this.#runs.set(runId, { limit, ...(beforeLaunch ? { beforeLaunch } : {}), logical: 0, active: 0, queue: [] });
705
522
  this.#runOrder.push(runId);
706
523
  }
524
+ updateRunLimit(runId, limit) {
525
+ const run = this.#runs.get(runId);
526
+ if (!run)
527
+ throw new WorkflowError("INTERNAL_ERROR", `Unknown scheduler run: ${runId}`);
528
+ if (!Number.isInteger(limit) || limit < 1 || limit > this.sessionLimit)
529
+ throw new WorkflowError("INVALID_SETTINGS", "Invalid run concurrency");
530
+ run.limit = limit;
531
+ this.#dispatch();
532
+ }
707
533
  spawn(runId, prompt, options, parentId) {
708
534
  const run = this.#runs.get(runId);
709
535
  if (!run)