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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-extensible-workflows",
3
- "version": "2.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Deterministic multi-agent workflow orchestration for Pi",
5
5
  "homepage": "https://vekexasia.github.io/pi-extensible-workflows/",
6
6
  "repository": {
@@ -64,6 +64,7 @@
64
64
  },
65
65
  "license": "MIT",
66
66
  "dependencies": {
67
- "acorn": "^8.17.0"
67
+ "acorn": "^8.17.0",
68
+ "minimatch": "^10.2.5"
68
69
  }
69
70
  }
@@ -4,50 +4,78 @@ description: Use when the task is complex enough to require multiple subagents o
4
4
  ---
5
5
 
6
6
  # pi-extensible-workflows
7
+
7
8
  Use `workflow` only for genuinely multi-agent orchestration; one agent uses ordinary tools or `Agent` directly. Give phases distinct responsibilities and keep result flow explicit.
8
9
 
9
- ## Pattern
10
+ ## Example
11
+
10
12
  ```js
11
- const reportSchema = { type: "object", properties: { summary: { type: "string" }, findings: { type: "array", items: { type: "string" } } }, required: ["summary", "findings"], additionalProperties: false };
13
+ const reportSchema = {
14
+ type: "object",
15
+ properties: {
16
+ summary: { type: "string" },
17
+ findings: { type: "array", items: { type: "string" } },
18
+ },
19
+ required: ["summary", "findings"],
20
+ additionalProperties: false,
21
+ };
12
22
 
13
23
  const reports = await parallel("research", {
14
- first: () => agent("Research the first target.", { role: "scout", outputSchema: reportSchema }),
15
- second: () => agent("Research the second target.", { role: "scout", outputSchema: reportSchema }),
24
+ first: () =>
25
+ agent("Research the first target.", {
26
+ role: "scout",
27
+ outputSchema: reportSchema,
28
+ }),
29
+ second: () =>
30
+ agent("Research the second target.", {
31
+ role: "scout",
32
+ outputSchema: reportSchema,
33
+ }),
16
34
  });
17
35
 
18
- return agent(
19
- prompt("Review these reports:\n\n{reports}", { reports }),
20
- { role: "reviewer", outputSchema: reportSchema },
21
- );
36
+ return agent(prompt("Review these reports:\n\n{reports}", { reports }), {
37
+ role: "reviewer",
38
+ outputSchema: reportSchema,
39
+ });
22
40
  ```
23
41
 
24
- Pass structured input from the main agent with `args`:
42
+ Inline launches require a non-empty `name`. Registered function launches must omit `name`; they use `workflow` as the run name:
43
+
25
44
  ```json
26
45
  { "workflow": "workflowName", "args": { "issue": 42 } }
27
46
  ```
28
- Inside the workflow, read `args.issue` (`args` is `null` when omitted). `workflow_stop` requires the exact run ID; foreground results retain their value and completed `runId`, while background launches return `runId` immediately. A terminal `parentRunId` reuses matching named `withWorktree` scopes; unnamed or missing names create new worktrees.
29
- If `workflow_catalog` is available, call it once before creating the first workflow for a task. The name-less result is a compact index with launch-ready input schemas, descriptions, variables, and configured model aliases. Use those inputs to launch registered functions directly with `{ "workflow": "name", "args": { ... } }`; their input and output schemas are enforced. Request full detail with `{ "name": "name" }` only when composing a function programmatically or inspecting its output contract and extension metadata; do not load full definitions unconditionally. Alias targets are catalog metadata, not an availability probe. Do not try to reinvent already exposed functions.
47
+
48
+ Inside the workflow, read `args.issue` (`args` is `null` when omitted). `workflow_stop` requires the exact run ID; foreground results retain their value and completed `runId`, while background launches return `runId` immediately. A terminal `parentRunId` reuses matching named `withWorktree` scopes but does not replay journal results. For an explicitly failed run, use the exact `runId` from failure diagnostics with `workflow_retry({ runId })`: diagnostics list replayable and incomplete paths, artifacts, and valid named worktrees; the tool creates a linked child, replays completed agent, shell, function, and checkpoint operations, and executes incomplete work. Retry versus per-agent `retries` and `workflow_resume` is always explicit; external side effects before failure are not guaranteed exactly once.
49
+ Inspect tool `workflow_catalog` result at least once before creating the first workflow for a task.
30
50
 
31
51
  Workflow JavaScript has no imports, filesystem, network, process, or timers. Delegate that work to agents. `shell(command, options)` is the trusted host RPC for deterministic gates: it inherits the workflow or active-worktree cwd, merges string `env` overrides, and returns `{ exitCode, stdout, stderr }`; nonzero exits are results, but launch failures and timeouts fail with `SHELL_FAILED`.
32
52
 
33
- Use a bounded verification loop when command output controls the gate:
53
+ Example use of `shell`:
54
+
34
55
  ```js
35
- return withWorktree("fix-tests", async ({ path, branch }) => {
36
- for (let attempt = 1; attempt <= 5; attempt += 1) {
37
- const tests = await shell("yarn test", { env: { CI: "1" } });
38
- if (tests.exitCode === 0) return { path, branch, tests };
39
- await agent(prompt("Fix these failures:\n\n{output}", { output: tests.stderr || tests.stdout }));
40
- }
41
- return { path, branch, tests: await shell("yarn test", { env: { CI: "1" } }) };
42
- });
56
+ // ... other code
57
+ const testRes = await shell("yarn test", { env: { CI: "1" } });
58
+ if (testRes.exitCode === 0) {
59
+ // success path
60
+ return { ok: true };
61
+ }
43
62
  ```
44
- Shell results are journaled only after process exit and RPC validation. A host crash after side effects but before journaling can rerun the command on resume; use `shell()` mainly for verification and bounded gates, not exactly-once mutations.
63
+
64
+ Most of the times using `shell()` to perform mutations is an antipattern. Use it mainly for verifications or idempotent actions.
45
65
 
46
66
  ## `agent()` options
67
+
47
68
  ```typescript
48
- interface AgentOptions {
49
- label?: string; model?: string; role?: string; tools?: string[];
50
- thinking?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; outputSchema?: JsonSchema; retries?: number; timeoutMs?: number | null;
69
+ export interface AgentOptions {
70
+ label?: string;
71
+ model?: string;
72
+ role?: string;
73
+ tools?: string[];
74
+ thinking?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
75
+ outputSchema?: JsonSchema;
76
+ retries?: number;
77
+ timeoutMs?: number | null;
78
+ [key: string]: JsonValue;
51
79
  }
52
80
  ```
53
81
 
@@ -55,29 +83,35 @@ Extensions may add JSON-compatible agent options such as `advisor: true`; core k
55
83
 
56
84
  Agent calls are unnamed. Direct calls receive hidden source call-site identity; aliases are unsupported, and calls from one source site must not race outside `parallel` or `pipeline`, whose structural keys make replay deterministic.
57
85
 
58
- ## Persistent conversations
59
- Use `conversation(name, options)` when several agent turns should share a persisted transcript and build on one another. Unlike independent `agent(prompt, options)` calls, the returned handle continues from the last successful turn:
86
+ ## Passing agent results
87
+
88
+ Use independent `agent(prompt, options)` calls and pass each completed result explicitly to the next prompt. This keeps workflow execution deterministic and makes replay state local to each call:
60
89
 
61
90
  ```js
62
- const handle = conversation("developer", { role: "developer" });
63
- const findings = await handle.run("Inspect the implementation.");
64
- const fix = await handle.run("Now propose the smallest fix.");
91
+ const findings = await agent("Inspect the implementation.");
92
+ const fix = await agent(
93
+ prompt("Propose the smallest fix from these findings:\n\n{findings}", {
94
+ findings,
95
+ }),
96
+ );
65
97
  return { findings, fix };
66
98
  ```
67
99
 
68
- Await each `handle.run(prompt, turnOptions)` call before starting the next one; conversation turns must be sequential and cannot overlap. Conversation creation accepts the same execution-policy options as `agent()`. `timeoutMs` and `retries` passed to `run()` are turn-local, so a failed turn does not advance the persisted conversation head.
69
-
70
100
  ## Worktrees
71
- Use `withWorktree(callback)` or `withWorktree(name, callback)` for top-level agents that collaborate in one worktree:
101
+
102
+ Use `withWorktree(name, callback)` for top-level agents that collaborate in one explicitly named worktree scope:
103
+
72
104
  ```js
73
105
  const result = await withWorktree("issue", async ({ path, branch }) => {
74
106
  const report = await agent("Implement the issue");
75
107
  return { path, branch, report };
76
108
  });
77
109
  ```
110
+
78
111
  Entering the scope materializes its worktree before the callback. The callback receives a frozen reference containing only the real string `path` and `branch`; callbacks may ignore the argument, and their bare return value is preserved. Concurrent agents share mutable files, so assign non-conflicting work or coordinate explicitly.
79
112
 
80
113
  Branches may call any workflow function, not only `agent()`. Use separate named scopes when parallel branches need isolated worktrees:
114
+
81
115
  ```js
82
116
  const results = await parallel("implementation", {
83
117
  api: () => withWorktree("api", () => agent("Implement the API")),
@@ -88,14 +122,15 @@ const results = await parallel("implementation", {
88
122
  Registered extension functions receive `withWorktree` in context and can compose other registered functions with `context.invoke("reviewRepository", { focus: "security" })`. Their public inputs and outputs remain JSON; callbacks cannot cross the extension boundary.
89
123
 
90
124
  ## Rules
125
+
91
126
  - Use `log(messageString)` for brief operator status.
92
127
  - A role owns execution policy: with `role`, do not set `model`, `thinking`, or `tools`; only task options such as `outputSchema`, retries, timeout, or a `withWorktree` scope may accompany it.
93
128
  - Use `parallel()` for independent tasks with different flows and `pipeline()` when every keyed item follows the same ordered stages; do not duplicate identical chains in `parallel()`. Signatures are `parallel(operationName, tasksRecord)` and `pipeline(operationName, itemsRecord, stagesRecord)`; keys are stable task, item, and stage names.
94
129
  - Preserve item metadata in workflow code between pipeline stages instead of making agents echo it through `outputSchema`.
95
130
  - Use a JavaScript loop for repeated work; each direct `agent(...)` call gets deterministic call-site and occurrence identity.
96
131
  - Runs default to background; set tool-call `foreground: true` when asked to wait.
97
- - Add `budget` only for aggregate limits. Valid dimensions are exactly `tokens`, `costUsd`, `durationMs`, and `agentLaunches`; each is `{ soft?: number, hard?: number }` with `soft < hard`.
98
- - `budget_exhausted` runs resume through `workflow_resume`: omitted patch values stay unchanged, `null` removes a limit, and tightening resumes directly. Relaxation stores the exact proposal and returns `{ state: "awaiting_approval", proposalId }`; `workflow_respond` must answer that ID. Rejection leaves the run exhausted; approval applies the budget and cold-resumes it.
132
+ - Add `budget` only for aggregate limits. Do not invent limits, omit if user do not ask explicitly. Valid dimensions are exactly `tokens`, `costUsd`, `durationMs`, and `agentLaunches`; each is `{ soft?: number, hard?: number }` with `soft < hard`.
133
+ - `budget_exhausted` runs resume through `workflow_resume`: omitted patch values stay unchanged, `null` removes a limit, and tightening resumes directly. Relaxation stores the exact proposal and returns `{ state: "awaiting_approval", proposalId }`; `workflow_respond` must answer that ID. Rejection leaves the run exhausted; approval applies the budget and cold-resumes it. `workflow_retry` is only for persisted `failed` runs and inherits cumulative usage; replay itself consumes no budget.
99
134
  - `parallel()` and `pipeline()` return keyed bare values; await them before use. Interpolate results with `prompt("...{value}", { value })`; placeholders in plain strings remain literal.
100
135
  - Use `outputSchema` only when another phase compares, aggregates, or validates a result, never for final prose. Keep only consumer-needed fields and avoid repeated evidence. Agents with it must call `workflow_result`; one repair prompt is built in. Omit `retries` unless an extra retry is justified and work is idempotent.
101
136
  - Do not add persona specifications to agent prompts; define the task directly.
@@ -1,15 +1,15 @@
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
- import { createAgentSession, DefaultPackageManager, DefaultResourceLoader, getAgentDir, ModelRuntime, SessionManager, SettingsManager, type AgentSessionEvent, type InlineExtension, type SessionStats, type ToolDefinition } from "@earendil-works/pi-coding-agent";
5
+ import { createAgentSession, DefaultPackageManager, DefaultResourceLoader, getAgentDir, ModelRuntime, SessionManager, SettingsManager, type ToolDefinition } from "@earendil-works/pi-coding-agent";
7
6
  type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
8
7
  type AgentMessage = { role: string; content?: unknown; stopReason?: string; errorMessage?: string; usage?: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: { total: number } } };
9
- import type { AgentIdentity, AgentResourceExclusions, AgentResourcePolicy, AgentSetupSummary, JsonSchema, JsonValue, ModelSpec, WorkflowRunContext } from "./index.js";
10
- import { mergeAgentResourceExclusions, resolveModelReference, WorkflowError } from "./index.js";
11
- import type { ConversationHead, PersistedConversation, RunStore } from "./persistence.js";
12
-
8
+ import type { AgentIdentity, AgentResourceExclusions, AgentResourcePolicy, AgentSetup, AgentSetupSummary, JsonSchema, JsonValue, ModelSpec, NativeSession, RegisteredAgentSetupHook, SessionFactory, SessionInput, WorkflowRunContext } from "./types.js";
9
+ import { jsonObject, disabledResources, mergeAgentResourceExclusions, modelAliasName, modelCapability, resolveModelReference, unmatchedResourcePatterns } from "./utils.js";
10
+ import { WorkflowError } from "./types.js";
11
+ import type { RunStore } from "./persistence.js";
12
+ export type { AgentSetup, AgentSetupContext, AgentSetupHook, NativeSession, RegisteredAgentSetupHook, SessionFactory, SessionInput } from "./types.js";
13
13
  export interface AgentBudgetHooks {
14
14
  beforeAttempt(): void;
15
15
  beforeTurn(): void;
@@ -42,7 +42,6 @@ export interface AgentExecutionOptions {
42
42
  budget?: AgentBudgetHooks;
43
43
  agentOptions?: Readonly<Record<string, JsonValue>>;
44
44
  agentIdentity?: AgentIdentity;
45
- conversation?: { id: string; turn: number };
46
45
  }
47
46
  export interface AgentExecutionRoot {
48
47
  cwd: string;
@@ -68,36 +67,12 @@ export interface AgentActivity { kind: "reasoning" | "tool" | "text"; text: stri
68
67
  export interface AgentProgress { accounting: AgentAccounting; toolCalls: readonly AgentToolCallProgress[]; activity?: AgentActivity; persist: boolean }
69
68
  export interface AgentAttempt { attempt: number; sessionId: string; sessionFile: string; result?: JsonValue; error?: { code: string; message: string }; accounting: AgentAccounting; setup?: AgentSetupSummary }
70
69
  export interface AgentExecutionResult { value: JsonValue; attempts: readonly AgentAttempt[]; cwd: string }
71
- export interface AgentSetup { prompt: string; options: Record<string, JsonValue>; sessionInput: SessionInput; createSession: SessionFactory }
72
- export interface AgentSetupContext { readonly run: Readonly<WorkflowRunContext>; readonly identity: Readonly<AgentIdentity>; readonly attempt: number; readonly signal: AbortSignal }
73
- export interface AgentSetupHook { priority?: number; setup: (agent: AgentSetup, context: Readonly<AgentSetupContext>) => void | Promise<void> }
74
- export interface RegisteredAgentSetupHook { name: string; priority: number; setup: AgentSetupHook["setup"] }
75
- type NativeSessionStats = Pick<SessionStats, "tokens" | "cost">;
76
- export interface NativeSession {
77
- readonly sessionId: string;
78
- readonly sessionFile: string | undefined;
79
- readonly messages: readonly AgentMessage[];
80
- getSessionStats(): NativeSessionStats;
81
- readonly systemPrompt?: string;
82
- readonly model?: { provider: string; model?: string; id?: string };
83
- readonly agent?: { state: { tools: readonly { name: string }[] } };
84
- getLeafId?: () => string | null;
85
- getToolDefinitions?: () => unknown;
86
- subscribe?(listener: (event: AgentSessionEvent) => void): () => void;
87
- prompt(text: string): Promise<void>;
88
- steer?(text: string): Promise<void>;
89
- abort?(): Promise<void>;
90
- dispose(): void;
91
- }
92
- export interface SessionInput { cwd: string; model: ModelSpec; tools: string[]; sessionLabel: string; agentDir?: string; customTools?: ToolDefinition[]; resultTool?: ToolDefinition; systemPromptAppend?: string; extensionFactories?: InlineExtension[]; resourcePolicy?: AgentResourcePolicy; options?: Record<string, JsonValue>; continuation?: { sessionId: string; sessionFile: string; leafId: string }; allowModelChange?: boolean }
93
- export type SessionFactory = (input: SessionInput) => Promise<NativeSession>;
94
70
 
95
71
  function parseModel(value: string | undefined, fallback: ModelSpec, thinking?: ThinkingLevel, aliases: Readonly<Record<string, string>> = {}, knownModels?: ReadonlySet<string>, settingsPath?: string): ModelSpec {
96
72
  if (!value) return { ...fallback, ...(thinking ? { thinking } : {}) };
97
73
  const parsed = resolveModelReference(value, aliases, knownModels, settingsPath);
98
74
  return { ...parsed, ...(thinking ? { thinking } : !parsed.thinking && fallback.thinking ? { thinking: fallback.thinking } : {}) };
99
75
  }
100
- function modelCapability(model: ModelSpec): string { return `${model.provider}/${model.model}`; }
101
76
 
102
77
  function text(messages: readonly AgentMessage[]): string {
103
78
  const message = [...messages].reverse().find((item) => item.role === "assistant");
@@ -132,34 +107,15 @@ function terminalProviderError(error: WorkflowError): TerminalProviderError | un
132
107
  return typeof candidate.provider === "string" && typeof candidate.model === "string" && typeof candidate.error === "string" ? { provider: candidate.provider, model: candidate.model, error: candidate.error } : undefined;
133
108
  }
134
109
 
135
- function accounting(stats: NativeSessionStats): AgentAccounting {
110
+ function accounting(stats: ReturnType<NativeSession["getSessionStats"]>): AgentAccounting {
136
111
  return { input: stats.tokens.input, output: stats.tokens.output, cacheRead: stats.tokens.cacheRead, cacheWrite: stats.tokens.cacheWrite, cost: stats.cost };
137
112
  }
138
113
  function canonicalSourcePath(path: string): string { try { return realpathSync(path); } catch { return resolve(path); } }
139
114
 
140
115
  export async function createNativeAgentSession(input: SessionInput): Promise<NativeSession> {
141
116
  const agentDir = input.agentDir ?? getAgentDir();
142
- let manager: SessionManager;
143
- if (input.continuation) {
144
- try {
145
- manager = SessionManager.open(input.continuation.sessionFile, input.agentDir ? join(agentDir, "sessions") : undefined, input.cwd);
146
- const header = manager.getHeader();
147
- if (!header || canonicalSourcePath(header.cwd) !== canonicalSourcePath(input.cwd) || manager.getSessionId() !== input.continuation.sessionId || !manager.getEntry(input.continuation.leafId)) throw new Error("Persisted transcript identity does not match the conversation head");
148
- manager.branch(input.continuation.leafId);
149
- const context = manager.buildSessionContext();
150
- if (context.model && (context.model.provider !== input.model.provider || context.model.modelId !== input.model.model)) {
151
- if (!input.allowModelChange) throw new Error("Persisted transcript model does not match the conversation execution policy");
152
- manager.appendModelChange(input.model.provider, input.model.model);
153
- }
154
- if (input.model.thinking && context.thinkingLevel !== input.model.thinking) throw new Error("Persisted transcript thinking level does not match the conversation execution policy");
155
- } catch (error) {
156
- if (error instanceof WorkflowError && error.code === "RESUME_INCOMPATIBLE") throw error;
157
- throw new WorkflowError("RESUME_INCOMPATIBLE", `Cannot reopen conversation transcript: ${error instanceof Error ? error.message : String(error)}`);
158
- }
159
- } else {
160
- manager = input.agentDir ? SessionManager.create(input.cwd, join(agentDir, "sessions")) : SessionManager.create(input.cwd);
161
- manager.appendSessionInfo(input.sessionLabel);
162
- }
117
+ const manager = input.agentDir ? SessionManager.create(input.cwd, join(agentDir, "sessions")) : SessionManager.create(input.cwd);
118
+ manager.appendSessionInfo(input.sessionLabel);
163
119
  const modelRuntime = await ModelRuntime.create({ authPath: join(agentDir, "auth.json"), modelsPath: join(agentDir, "models.json") });
164
120
  const model = modelRuntime.getModel(input.model.provider, input.model.model);
165
121
  if (!model) throw new WorkflowError("UNKNOWN_MODEL", `Unknown model: ${input.model.provider}/${input.model.model}`);
@@ -173,14 +129,17 @@ export async function createNativeAgentSession(input: SessionInput): Promise<Nat
173
129
  settingsManager.setProjectTrusted(policy.projectTrusted);
174
130
  const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager });
175
131
  const resolved = await packageManager.resolve();
176
- const disabledExtensions = new Set(policy.effective.extensions);
177
- 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))))];
132
+ const discoveredExtensions = [...new Set(resolved.extensions.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => canonicalSourcePath(path)))];
133
+ const excludedExtensions = new Set(disabledResources(policy.effective.extensions, discoveredExtensions));
134
+ const extensionPaths = discoveredExtensions.filter((path) => !excludedExtensions.has(path));
135
+ Object.assign(policy, { excludedExtensions: [...excludedExtensions], unmatchedExtensions: unmatchedResourcePatterns(policy.effective.extensions, discoveredExtensions) });
178
136
  const skillPaths = [...new Set(resolved.skills.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => path))];
179
- const updateSkillMatches = (skills: readonly { name: string }[]) => {
180
- const names = new Set(skills.map(({ name }) => name));
181
- Object.assign(policy, { unmatchedSkills: policy.effective.skills.filter((name) => !names.has(name)) });
137
+ const updateSkillMatches = (skills: readonly { name: string }[]): Set<string> => {
138
+ const names = [...new Set(skills.map(({ name }) => name))];
139
+ const excludedSkills = disabledResources(policy.effective.skills, names);
140
+ Object.assign(policy, { excludedSkills, unmatchedSkills: unmatchedResourcePatterns(policy.effective.skills, names) });
141
+ return new Set(excludedSkills);
182
142
  };
183
- const disabledSkills = new Set(policy.effective.skills);
184
143
  resourceLoader = new DefaultResourceLoader({
185
144
  cwd: input.cwd,
186
145
  agentDir,
@@ -191,20 +150,17 @@ export async function createNativeAgentSession(input: SessionInput): Promise<Nat
191
150
  additionalSkillPaths: skillPaths,
192
151
  ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}),
193
152
  skillsOverride: (base) => {
194
- updateSkillMatches(base.skills);
153
+ const disabledSkills = updateSkillMatches(base.skills);
195
154
  return { ...base, skills: base.skills.filter(({ name }) => !disabledSkills.has(name)) };
196
155
  },
197
156
  ...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}),
198
157
  });
199
158
  await resourceLoader.reload();
200
- const discoveredExtensions = new Set(resolved.extensions.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => canonicalSourcePath(path)));
201
- Object.assign(policy, { unmatchedExtensions: policy.effective.extensions.filter((path) => !discoveredExtensions.has(canonicalSourcePath(path))) });
202
159
  } else if (input.systemPromptAppend || input.extensionFactories?.length) {
203
160
  resourceLoader = new DefaultResourceLoader({ cwd: input.cwd, agentDir, ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}), ...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}) });
204
161
  await resourceLoader.reload();
205
162
  }
206
- 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 });
207
- if (input.continuation && modelFallbackMessage) throw new WorkflowError("RESUME_INCOMPATIBLE", modelFallbackMessage);
163
+ 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 });
208
164
  return Object.assign(session, {
209
165
  getLeafId: () => manager.getLeafId(),
210
166
  getToolDefinitions: () => session.getAllTools().map(({ name, description, parameters, promptGuidelines }) => ({ name, description, parameters, ...(promptGuidelines ? { promptGuidelines } : {}) })),
@@ -212,19 +168,6 @@ export async function createNativeAgentSession(input: SessionInput): Promise<Nat
212
168
  }
213
169
  function changedOption(options: Readonly<Record<string, JsonValue>>, baseline: Readonly<Record<string, JsonValue>>, key: string): boolean { return JSON.stringify(options[key]) !== JSON.stringify(baseline[key]); }
214
170
  function validThinking(value: unknown): value is ThinkingLevel { return typeof value === "string" && ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(value); }
215
- function jsonValue(value: unknown, seen = new Set<object>()): value is JsonValue {
216
- if (value === null || typeof value === "boolean" || typeof value === "string") return true;
217
- if (typeof value === "number") return Number.isFinite(value);
218
- if (typeof value !== "object" || seen.has(value)) return false;
219
- if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) return false;
220
- const keys = Reflect.ownKeys(value);
221
- if (keys.some((key) => typeof key !== "string")) return false;
222
- seen.add(value);
223
- const valid = (Array.isArray(value) ? Array.from(value) : keys.map((key) => (value as Record<string, unknown>)[key as string])).every((item) => jsonValue(item, seen));
224
- seen.delete(value);
225
- return valid;
226
- }
227
- function jsonObject(value: unknown): value is Record<string, JsonValue> { return jsonValue(value) && typeof value === "object" && value !== null && !Array.isArray(value); }
228
171
  interface ChildAgentToolParams {
229
172
  prompt: string;
230
173
  label: string;
@@ -254,51 +197,9 @@ function fallbackSetupContext(root: AgentExecutionRoot, options: AgentExecutionO
254
197
  return { run, identity: Object.freeze({ ...identity, structuralPath: Object.freeze([...identity.structuralPath]) }) };
255
198
  }
256
199
  function resourcePolicySummary(policy: AgentResourcePolicy): NonNullable<AgentSetupSummary["disabledAgentResources"]> {
257
- return { skills: [...policy.effective.skills], extensions: [...policy.effective.extensions], unmatchedSkills: [...policy.unmatchedSkills], unmatchedExtensions: [...policy.unmatchedExtensions] };
200
+ return { skills: [...policy.effective.skills], extensions: [...policy.effective.extensions], excludedSkills: [...(policy.excludedSkills ?? [])], excludedExtensions: [...(policy.excludedExtensions ?? [])], unmatchedSkills: [...policy.unmatchedSkills], unmatchedExtensions: [...policy.unmatchedExtensions] };
258
201
  }
259
- function canonicalJson(value: unknown): unknown {
260
- if (Array.isArray(value)) return value.map((item) => canonicalJson(item));
261
- if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, canonicalJson(item)]));
262
- return value;
263
- }
264
- function fingerprint(value: unknown): string { return createHash("sha256").update(JSON.stringify(canonicalJson(value))).digest("hex"); }
265
- function promptFingerprint(value: string): string { return createHash("sha256").update(value).digest("hex"); }
266
- function fixedConversationOptions(options: Readonly<Record<string, JsonValue>>): JsonValue {
267
- const fixedOptions = structuredClone(options) as Record<string, JsonValue>;
268
- delete fixedOptions.timeoutMs;
269
- delete fixedOptions.retries;
270
- return fixedOptions;
271
- }
272
- function conversationExecutionPolicy(options: AgentExecutionOptions, setup: AgentSetup): JsonValue {
273
- return structuredClone({
274
- model: setup.sessionInput.model,
275
- tools: [...setup.sessionInput.tools],
276
- cwd: setup.sessionInput.cwd,
277
- role: options.role ?? null,
278
- worktreeOwner: options.worktreeOwner ?? null,
279
- parent: options.parent ?? null,
280
- systemPromptAppend: setup.sessionInput.systemPromptAppend ?? "",
281
- options: fixedConversationOptions(setup.options),
282
- resourcePolicy: setup.sessionInput.resourcePolicy ? resourcePolicySummary(setup.sessionInput.resourcePolicy) : null,
283
- }) as unknown as JsonValue;
284
- }
285
- function conversationPolicyMatches(expected: JsonValue, current: JsonValue, allowModelChange: boolean): boolean {
286
- if (fingerprint(expected) === fingerprint(current)) return true;
287
- if (!allowModelChange || !expected || typeof expected !== "object" || Array.isArray(expected) || !current || typeof current !== "object" || Array.isArray(current)) return false;
288
- const expectedModel = conversationPolicyModel(expected);
289
- const currentModel = conversationPolicyModel(current);
290
- if (!expectedModel || !currentModel) return false;
291
- return fingerprint(expected) === fingerprint({ ...current, model: { ...currentModel, provider: expectedModel.provider, model: expectedModel.model } });
292
- }
293
- function conversationPolicyModel(policy: JsonValue): ModelSpec | undefined {
294
- if (!policy || typeof policy !== "object" || Array.isArray(policy)) return undefined;
295
- const model = policy.model;
296
- if (!model || typeof model !== "object" || Array.isArray(model) || typeof model.provider !== "string" || typeof model.model !== "string") return undefined;
297
- const thinking = typeof model.thinking === "string" && ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(model.thinking) ? model.thinking as ModelSpec["thinking"] : undefined;
298
- return thinking === undefined ? { provider: model.provider, model: model.model } : { provider: model.provider, model: model.model, thinking };
299
- }
300
- function conversationFailure(message: string): WorkflowError { return new WorkflowError("RESUME_INCOMPATIBLE", message); }
301
- async function prepareAgentSetup(root: AgentExecutionRoot, createSession: SessionFactory, task: string, options: AgentExecutionOptions, resolved: { model: ModelSpec; tools: readonly string[]; systemPromptAppend: string }, cwd: string, attempt: number, signal: AbortSignal | undefined, customTools: readonly ToolDefinition[], resultTool: ToolDefinition | undefined, continuation?: ConversationHead): Promise<{ setup: AgentSetup; summary: AgentSetupSummary }> {
202
+ async function prepareAgentSetup(root: AgentExecutionRoot, createSession: SessionFactory, task: string, options: AgentExecutionOptions, resolved: { model: ModelSpec; tools: readonly string[]; systemPromptAppend: string }, cwd: string, attempt: number, signal: AbortSignal | undefined, customTools: readonly ToolDefinition[], resultTool: ToolDefinition | undefined): Promise<{ setup: AgentSetup; summary: AgentSetupSummary }> {
302
203
  const setupSignal = signal ?? root.runContext?.signal ?? new AbortController().signal;
303
204
  const baselineOptions = structuredClone(options.agentOptions ?? {});
304
205
  const baseResourcePolicy = await root.agentResourcePolicy?.();
@@ -320,7 +221,6 @@ async function prepareAgentSetup(root: AgentExecutionRoot, createSession: Sessio
320
221
  if (changedOption(setup.options, baselineOptions, "thinking") && validThinking(setup.options.thinking)) setup.sessionInput.model = { ...setup.sessionInput.model, thinking: setup.options.thinking };
321
222
  if (changedOption(setup.options, baselineOptions, "tools") && Array.isArray(setup.options.tools) && setup.options.tools.every((tool) => typeof tool === "string")) setup.sessionInput.tools = [...setup.options.tools];
322
223
  if (changedOption(setup.options, baselineOptions, "cwd") && typeof setup.options.cwd === "string") setup.sessionInput.cwd = setup.options.cwd;
323
- if (continuation) setup.sessionInput.continuation = { sessionId: continuation.sessionId, sessionFile: continuation.sessionFile, leafId: continuation.leafId };
324
224
  const model = setup.sessionInput.model;
325
225
  const summary: AgentSetupSummary = { 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) } : {}) };
326
226
  return { setup, summary };
@@ -339,13 +239,14 @@ export class WorkflowAgentExecutor {
339
239
  const forbidden = requested.find((tool) => !this.root.tools.has(tool));
340
240
  if (forbidden) throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the launching session boundary: ${forbidden}`);
341
241
  const requestedModel = options.model ?? definition?.model;
342
- const hasAlias = requestedModel !== undefined && Object.prototype.hasOwnProperty.call(this.root.modelAliases ?? {}, requestedModel);
343
- if (requestedModel !== undefined && this.root.blockedAliases?.has(requestedModel) && !hasAlias) { const target = this.root.blockedAliasTargets?.[requestedModel]; throw new WorkflowError("UNKNOWN_MODEL", `Unknown model alias ${requestedModel}${target ? ` resolved to ${target}` : ""}${this.root.settingsPath ? ` (settings: ${this.root.settingsPath})` : ""}`); }
344
- const aliasThinking = requestedModel !== undefined && hasAlias ? resolveModelReference(requestedModel, this.root.modelAliases, this.root.knownModels ?? this.root.availableModels, this.root.settingsPath).thinking : undefined;
242
+ const alias = requestedModel === undefined ? undefined : modelAliasName(requestedModel, this.root.modelAliases ?? {});
243
+ const blockedAlias = requestedModel?.split(":", 1)[0];
244
+ if (requestedModel !== undefined && blockedAlias && this.root.blockedAliases?.has(blockedAlias) && !alias) { const target = this.root.blockedAliasTargets?.[blockedAlias]; throw new WorkflowError("UNKNOWN_MODEL", `Unknown model alias ${requestedModel}${target ? ` resolved to ${target}` : ""}${this.root.settingsPath ? ` (settings: ${this.root.settingsPath})` : ""}`); }
245
+ const aliasThinking = requestedModel !== undefined && alias ? resolveModelReference(requestedModel, this.root.modelAliases, this.root.knownModels ?? this.root.availableModels, this.root.settingsPath).thinking : undefined;
345
246
  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);
346
247
  const availableModels = this.root.knownModels ?? this.root.availableModels ?? new Set([modelCapability(this.root.model)]);
347
248
  if (!availableModels.has(modelCapability(model))) throw new WorkflowError("UNKNOWN_MODEL", `Unknown model${requestedModel ? ` ${requestedModel} resolved to ${modelCapability(model)}` : ""}${this.root.settingsPath ? ` (settings: ${this.root.settingsPath})` : ""}`);
348
- return { model, ...(hasAlias ? { requestedModel } : {}), tools: [...requested], systemPromptAppend: definition?.prompt ?? "" };
249
+ return { model, ...(alias && requestedModel ? { requestedModel } : {}), tools: [...requested], systemPromptAppend: definition?.prompt ?? "" };
349
250
  }
350
251
 
351
252
  async execute(task: string, options: AgentExecutionOptions, signal?: AbortSignal, customTools: readonly ToolDefinition[] = [], setSteer?: (handler: (message: string) => void | Promise<void>) => void, beforeRetry?: () => void): Promise<AgentExecutionResult> {
@@ -373,20 +274,7 @@ export class WorkflowAgentExecutor {
373
274
  if (options.cwd) throw new WorkflowError("INVALID_METADATA", "Only child agents or worktree scopes may provide a cwd");
374
275
  cwd = this.root.cwd;
375
276
  }
376
- let conversationRecord: PersistedConversation | undefined;
377
- if (options.conversation) {
378
- const store = this.root.runStore;
379
- if (!store) throw conversationFailure("Conversation persistence is unavailable");
380
- try { conversationRecord = await store.conversation(options.conversation.id); } catch (error) { throw conversationFailure(`Cannot load conversation state: ${error instanceof Error ? error.message : String(error)}`); }
381
- if (conversationRecord) {
382
- const model = conversationPolicyModel(conversationRecord.policy);
383
- if (model) resolved = this.resolve({ ...options, modelOverride: model });
384
- }
385
- if (!Number.isInteger(options.conversation.turn) || options.conversation.turn < 1) throw conversationFailure("Conversation turn must be a positive integer");
386
- if (conversationRecord ? conversationRecord.head.turn + 1 !== options.conversation.turn : options.conversation.turn !== 1) throw conversationFailure(`Conversation turn ${String(options.conversation.turn)} does not continue its persisted head`);
387
- }
388
277
  const attempts: AgentAttempt[] = [];
389
- let conversationBaseline: { executionPolicy: JsonValue; toolDefinitionsSha256: string; systemPrompt?: string; systemPromptSha256?: string } | undefined;
390
278
  let maxAttempts = (options.retries ?? 0) + 1;
391
279
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
392
280
  if (recoveryModel) resolved = this.resolve({ ...options, modelOverride: recoveryModel });
@@ -398,10 +286,6 @@ export class WorkflowAgentExecutor {
398
286
  let setupFailed = false;
399
287
  let budgetError: WorkflowError | undefined;
400
288
  let turnStarted = false;
401
- let conversationSystemPrompt = "";
402
- let conversationToolDefinitionsSha256 = "";
403
- let conversationMismatch: WorkflowError | undefined;
404
- const conversationMismatchError = () => conversationMismatch ? new WorkflowError("RESUME_INCOMPATIBLE", conversationMismatch.message) : undefined;
405
289
  const hasSchemaResult = () => schemaResult !== undefined;
406
290
  const resultTool = options.schema ? {
407
291
  name: "workflow_result", label: "Workflow Result", description: "Submit the terminal structured workflow result", parameters: Type.Unsafe(options.schema),
@@ -431,51 +315,19 @@ export class WorkflowAgentExecutor {
431
315
  };
432
316
  try {
433
317
  setupFailed = true;
434
- const prepared = await prepareAgentSetup(this.root, this.createSession, task, options, resolved, cwd, attempt, executionSignal, customTools, resultTool, conversationRecord?.head);
318
+ const prepared = await prepareAgentSetup(this.root, this.createSession, task, options, resolved, cwd, attempt, executionSignal, customTools, resultTool);
435
319
  setup = prepared.setup;
436
320
  setupSummary = prepared.summary;
437
321
  setupFailed = false;
438
322
  if (executionSignal?.aborted) throw new WorkflowError("CANCELLED", "Agent cancelled");
439
- if (recoveryModel && conversationRecord) setup.sessionInput.allowModelChange = true;
440
323
  const started = Date.now();
441
324
  session = await setup.createSession(setup.sessionInput);
442
325
  if (setup.sessionInput.resourcePolicy) setupSummary = { ...setupSummary, disabledAgentResources: resourcePolicySummary(setup.sessionInput.resourcePolicy) };
443
- if (options.conversation) {
444
- conversationSystemPrompt = session.systemPrompt ?? "";
445
- conversationToolDefinitionsSha256 = fingerprint(session.getToolDefinitions?.() ?? session.agent?.state.tools ?? []);
446
- const currentExecutionPolicy = conversationExecutionPolicy(options, setup);
447
- if (conversationRecord) {
448
- if (session.sessionId !== conversationRecord.head.sessionId || requiredFile(session.sessionFile) !== conversationRecord.head.sessionFile) throw conversationFailure("Conversation transcript identity changed");
449
- if (!session.getLeafId || (!recoveryModel && session.getLeafId() !== conversationRecord.head.leafId)) throw conversationFailure("Conversation transcript leaf identity changed");
450
- if (!conversationPolicyMatches(conversationRecord.policy, currentExecutionPolicy, Boolean(recoveryModel))) throw conversationFailure("Conversation execution policy changed");
451
- if (!session.subscribe && (promptFingerprint(conversationSystemPrompt) !== conversationRecord.head.systemPromptSha256 || conversationSystemPrompt !== conversationRecord.head.systemPrompt)) throw conversationFailure("Conversation system prompt changed");
452
- if (conversationToolDefinitionsSha256 !== conversationRecord.head.toolDefinitionsSha256) throw conversationFailure("Conversation tool definitions changed");
453
- } else if (conversationBaseline) {
454
- if (!conversationPolicyMatches(conversationBaseline.executionPolicy, currentExecutionPolicy, Boolean(recoveryModel))) throw conversationFailure("Conversation execution policy changed");
455
- if (conversationToolDefinitionsSha256 !== conversationBaseline.toolDefinitionsSha256) throw conversationFailure("Conversation tool definitions changed");
456
- } else {
457
- conversationBaseline = { executionPolicy: currentExecutionPolicy, toolDefinitionsSha256: conversationToolDefinitionsSha256 };
458
- }
459
- if (!session.subscribe) {
460
- const expectedPrompt = conversationRecord?.head.systemPrompt ?? conversationBaseline?.systemPrompt;
461
- const expectedDigest = conversationRecord?.head.systemPromptSha256 ?? conversationBaseline?.systemPromptSha256;
462
- if (expectedPrompt !== undefined && expectedDigest !== undefined && (promptFingerprint(conversationSystemPrompt) !== expectedDigest || expectedPrompt !== conversationSystemPrompt)) throw conversationFailure("Conversation system prompt changed");
463
- if (!conversationRecord && conversationBaseline && conversationBaseline.systemPrompt === undefined) conversationBaseline = { ...conversationBaseline, systemPrompt: conversationSystemPrompt, systemPromptSha256: promptFingerprint(conversationSystemPrompt) };
464
- }
465
- if (conversationRecord && (!session.model || session.model.provider !== setup.sessionInput.model.provider || (session.model.model ?? session.model.id) !== setup.sessionInput.model.model)) throw conversationFailure("Conversation model changed");
466
- }
467
326
  const includeAttemptSetup = Boolean(this.root.agentSetupHooks?.length || setup.sessionInput.resourcePolicy);
468
327
  await options.onAttempt?.({ attempt, sessionId: session.sessionId, sessionFile: requiredFile(session.sessionFile), ...(includeAttemptSetup ? { setup: setupSummary } : {}) });
469
328
  const activeSession = session;
470
329
  unsubscribe = activeSession.subscribe?.((event) => {
471
330
  if (event.type === "agent_start" && session?.systemPrompt !== undefined) {
472
- if (options.conversation) {
473
- conversationSystemPrompt = session.systemPrompt;
474
- const expectedPrompt = conversationRecord?.head.systemPrompt ?? conversationBaseline?.systemPrompt;
475
- const expectedDigest = conversationRecord?.head.systemPromptSha256 ?? conversationBaseline?.systemPromptSha256;
476
- if (expectedPrompt !== undefined && expectedDigest !== undefined && (promptFingerprint(conversationSystemPrompt) !== expectedDigest || expectedPrompt !== conversationSystemPrompt)) { conversationMismatch = conversationFailure("Conversation system prompt changed"); void session.abort?.(); }
477
- if (!conversationRecord && conversationBaseline && conversationBaseline.systemPrompt === undefined) conversationBaseline = { ...conversationBaseline, systemPrompt: conversationSystemPrompt, systemPromptSha256: promptFingerprint(conversationSystemPrompt) };
478
- }
479
331
  if (this.root.runStore) {
480
332
  systemPromptTurn += 1;
481
333
  const entry = { sessionId: session.sessionId, attempt, turn: systemPromptTurn, prompt: session.systemPrompt };
@@ -497,7 +349,7 @@ export class WorkflowAgentExecutor {
497
349
  }
498
350
  }
499
351
  if (event.type === "tool_execution_start") { toolCalls.set(event.toolCallId, { id: event.toolCallId, name: event.toolName, state: "running" }); activity = { kind: "tool", text: event.toolName }; report(false); }
500
- if (event.type === "tool_execution_end") { toolCalls.set(event.toolCallId, { id: event.toolCallId, name: event.toolName, state: event.isError ? "failed" : "completed" }); if (activity?.kind === "tool" && activity.text === event.toolName) activity = undefined; report(false); }
352
+ if (event.type === "tool_execution_end") { toolCalls.set(event.toolCallId, { id: event.toolCallId, name: event.toolName, state: event.isError ? "failed" : "completed" }); if (activity?.kind === "tool" && activity.text === event.toolName) activity = undefined; report(false); toolCalls.delete(event.toolCallId); }
501
353
  });
502
354
  report(false);
503
355
  if (setSteer) {
@@ -510,7 +362,6 @@ export class WorkflowAgentExecutor {
510
362
  options.budget?.beforeTurn();
511
363
  turnStarted = true;
512
364
  try { await promptWithProviderPause(session, promptText, remaining(options.timeoutMs, started), executionSignal, this.root.providerPause); } catch (error) { if (!hasSchemaResult()) throw error; }
513
- if (conversationMismatch) throw conversationMismatch;
514
365
  throwIfTerminalAssistantError(session, setup.sessionInput.model);
515
366
  { const completedAccounting = accounting(session.getSessionStats()); options.budget?.afterTurn(completedAccounting, options.schema !== undefined ? hasSchemaResult() : !latestAssistantHasToolCall(session.messages)); turnStarted = false; }
516
367
  if (budgetError) throw budgetError;
@@ -525,8 +376,6 @@ export class WorkflowAgentExecutor {
525
376
  }
526
377
  if (schemaResult === undefined) throw new WorkflowError("RESULT_INVALID", "Agent did not submit a valid workflow_result after one repair");
527
378
  }
528
- const mismatch = conversationMismatchError();
529
- if (mismatch) throw mismatch;
530
379
  const value = options.schema ? schemaResult as JsonValue : text(session.messages);
531
380
  if (options.worktreeOwner) await this.root.runStore?.snapshotWorktree(options.worktreeOwner);
532
381
  report(true);
@@ -534,19 +383,12 @@ export class WorkflowAgentExecutor {
534
383
  await flushSystemPrompts();
535
384
  unsubscribe?.();
536
385
  const attemptAccounting = accounting(session.getSessionStats());
537
- const leafId = session.getLeafId?.() ?? undefined;
538
- if (options.conversation) {
539
- if (!leafId) throw conversationFailure("Conversation transcript has no persisted leaf");
540
- const store = this.root.runStore;
541
- if (!store) throw conversationFailure("Conversation persistence is unavailable");
542
- 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 } });
543
- }
544
386
  const includeCompletedSetup = Boolean(this.root.agentSetupHooks?.length || setup.sessionInput.resourcePolicy);
545
387
  attempts.push({ attempt, sessionId: session.sessionId, sessionFile: requiredFile(session.sessionFile), result: value, accounting: attemptAccounting, ...(includeCompletedSetup ? { setup: setupSummary } : {}) });
546
388
  session.dispose();
547
389
  return { value, attempts, cwd: setupSummary.cwd };
548
390
  } catch (error) {
549
- const typed = budgetError ?? conversationMismatch ?? (error instanceof WorkflowError ? error : new WorkflowError(executionSignal?.aborted && setupFailed ? "CANCELLED" : "AGENT_FAILED", error instanceof Error ? error.message : String(error)));
391
+ const typed = budgetError ?? (error instanceof WorkflowError ? error : new WorkflowError(executionSignal?.aborted && setupFailed ? "CANCELLED" : "AGENT_FAILED", error instanceof Error ? error.message : String(error)));
550
392
  if (session) {
551
393
  report(true);
552
394
  await progress;
@@ -598,7 +440,6 @@ export interface ScheduledAgentOptions {
598
440
  timeoutMs?: number | null;
599
441
  agentOptions?: Readonly<Record<string, JsonValue>>;
600
442
  agentIdentity?: AgentIdentity;
601
- conversation?: { id: string; turn: number };
602
443
  }
603
444
 
604
445
  export type ScheduledAgentResult =
@@ -656,6 +497,13 @@ export class FairAgentScheduler {
656
497
  this.#runs.set(runId, { limit, ...(beforeLaunch ? { beforeLaunch } : {}), logical: 0, active: 0, queue: [] });
657
498
  this.#runOrder.push(runId);
658
499
  }
500
+ updateRunLimit(runId: string, limit: number): void {
501
+ const run = this.#runs.get(runId);
502
+ if (!run) throw new WorkflowError("INTERNAL_ERROR", `Unknown scheduler run: ${runId}`);
503
+ if (!Number.isInteger(limit) || limit < 1 || limit > this.sessionLimit) throw new WorkflowError("INVALID_SETTINGS", "Invalid run concurrency");
504
+ run.limit = limit;
505
+ this.#dispatch();
506
+ }
659
507
 
660
508
  spawn(runId: string, prompt: string, options: ScheduledAgentOptions, parentId?: string): { id: string; result: Promise<ScheduledAgentResult> } {
661
509
  const run = this.#runs.get(runId);