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.
- package/README.md +18 -11
- package/dist/src/agent-execution.d.ts +4 -94
- package/dist/src/agent-execution.js +34 -208
- package/dist/src/budget.d.ts +38 -0
- package/dist/src/budget.js +160 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +60 -8
- package/dist/src/doctor-cleanup.d.ts +41 -0
- package/dist/src/doctor-cleanup.js +597 -0
- package/dist/src/doctor.d.ts +7 -2
- package/dist/src/doctor.js +127 -22
- package/dist/src/execution.d.ts +17 -0
- package/dist/src/execution.js +630 -0
- package/dist/src/host.d.ts +101 -0
- package/dist/src/host.js +3084 -0
- package/dist/src/index.d.ts +13 -634
- package/dist/src/index.js +10 -4432
- package/dist/src/persistence.d.ts +9 -19
- package/dist/src/persistence.js +175 -61
- package/dist/src/registry.d.ts +43 -0
- package/dist/src/registry.js +279 -0
- package/dist/src/session-inspector.js +5 -3
- package/dist/src/types.d.ts +692 -0
- package/dist/src/types.js +27 -0
- package/dist/src/utils.d.ts +32 -0
- package/dist/src/utils.js +168 -0
- package/dist/src/validation.d.ts +28 -0
- package/dist/src/validation.js +832 -0
- package/package.json +3 -2
- package/skills/pi-extensible-workflows/SKILL.md +69 -34
- package/src/agent-execution.ts +37 -189
- package/src/budget.ts +75 -0
- package/src/cli.ts +47 -7
- package/src/doctor-cleanup.ts +337 -0
- package/src/doctor.ts +117 -24
- package/src/execution.ts +540 -0
- package/src/host.ts +2598 -0
- package/src/index.ts +14 -3856
- package/src/persistence.ts +122 -37
- package/src/registry.ts +240 -0
- package/src/session-inspector.ts +5 -3
- package/src/types.ts +158 -0
- package/src/utils.ts +142 -0
- 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: () =>
|
|
30
|
-
|
|
31
|
-
|
|
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}", {
|
|
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
|
-
- [
|
|
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
|
|
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
|
|
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
|
|
1
|
+
import { type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
3
|
-
type
|
|
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,
|
|
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
|
-
|
|
58
|
-
|
|
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
|
|
99
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
295
|
-
|
|
296
|
-
|
|
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 &&
|
|
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, ...(
|
|
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
|
|
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 ??
|
|
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)
|