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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-extensible-workflows",
|
|
3
|
-
"version": "
|
|
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
|
-
##
|
|
10
|
+
## Example
|
|
11
|
+
|
|
10
12
|
```js
|
|
11
|
-
const reportSchema = {
|
|
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: () =>
|
|
15
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
);
|
|
36
|
+
return agent(prompt("Review these reports:\n\n{reports}", { reports }), {
|
|
37
|
+
role: "reviewer",
|
|
38
|
+
outputSchema: reportSchema,
|
|
39
|
+
});
|
|
22
40
|
```
|
|
23
41
|
|
|
24
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
53
|
+
Example use of `shell`:
|
|
54
|
+
|
|
34
55
|
```js
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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;
|
|
50
|
-
|
|
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
|
-
##
|
|
59
|
-
|
|
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
|
|
63
|
-
const
|
|
64
|
-
|
|
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
|
-
|
|
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.
|
package/src/agent-execution.ts
CHANGED
|
@@ -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
|
|
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 "./
|
|
10
|
-
import { mergeAgentResourceExclusions, resolveModelReference,
|
|
11
|
-
import
|
|
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:
|
|
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
|
-
|
|
143
|
-
|
|
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
|
|
177
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
343
|
-
|
|
344
|
-
|
|
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, ...(
|
|
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
|
|
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 ??
|
|
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);
|