@quintinshaw/pi-dynamic-workflows 1.1.0 → 1.3.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 +17 -2
- package/dist/agent.d.ts +17 -0
- package/dist/agent.js +41 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/workflow-commands.d.ts +8 -0
- package/dist/workflow-commands.js +111 -0
- package/dist/workflow-tool.d.ts +6 -0
- package/dist/workflow-tool.js +4 -4
- package/dist/workflow.d.ts +1 -0
- package/dist/workflow.js +8 -3
- package/extensions/workflow.ts +9 -2
- package/package.json +1 -1
- package/src/agent.ts +53 -1
- package/src/index.ts +1 -0
- package/src/workflow-commands.ts +117 -0
- package/src/workflow-tool.ts +9 -5
- package/src/workflow.ts +9 -3
package/README.md
CHANGED
|
@@ -46,6 +46,18 @@ The model writes a workflow script and calls the `workflow` tool. Live progress
|
|
|
46
46
|
|
|
47
47
|
Press `Esc` to cancel a running run; active subagents are aborted and surfaced as skipped.
|
|
48
48
|
|
|
49
|
+
### Background runs & `/workflows`
|
|
50
|
+
|
|
51
|
+
Ask for a background workflow (the model passes `background: true`) and it runs without blocking your session. Manage it with the `/workflows` command:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
/workflows # list runs (default)
|
|
55
|
+
/workflows status <id> # show a run's progress
|
|
56
|
+
/workflows stop <id> # abort a running run
|
|
57
|
+
/workflows pause <id> # pause a running run
|
|
58
|
+
/workflows rm <id> # remove a run from the list
|
|
59
|
+
```
|
|
60
|
+
|
|
49
61
|
## Workflow script shape
|
|
50
62
|
|
|
51
63
|
A workflow is plain JavaScript. The first statement must export literal metadata:
|
|
@@ -86,8 +98,11 @@ return { inventory, summary }
|
|
|
86
98
|
| `label` | string | Human-readable label for progress display |
|
|
87
99
|
| `phase` | string | Override the current phase for this agent |
|
|
88
100
|
| `schema` | object | JSON Schema for structured output |
|
|
101
|
+
| `model` | string | Run this agent on a specific model — `provider/modelId` or a bare `modelId` |
|
|
89
102
|
| `timeoutMs` | number | Override the default 5-minute agent timeout |
|
|
90
103
|
|
|
104
|
+
Models can also be set per phase via `meta.phases[].model`. Precedence is `opts.model` > phase model > session default; an unknown model logs a warning and falls back to the default.
|
|
105
|
+
|
|
91
106
|
### Structured output
|
|
92
107
|
|
|
93
108
|
Pass a JSON Schema via `opts.schema` and the subagent returns a validated object:
|
|
@@ -117,6 +132,8 @@ Scripts run inside a Node `vm` sandbox. Intentionally unavailable: `Date.now()`,
|
|
|
117
132
|
- **Core runtime** — `agent` / `parallel` / `pipeline` / `phase` / `log` / `budget` in a sandboxed script
|
|
118
133
|
- **Structured output** — JSON-Schema-validated subagent results
|
|
119
134
|
- **Real token & cost accounting** — read from each subagent's SDK session (input / output / total / cost), with a character estimate only as fallback when a provider reports no usage; `budget` gates on the real total
|
|
135
|
+
- **Real per-agent / per-phase model routing** — `opts.model` and `meta.phases[].model` actually select the model (resolved against your authed model registry), with graceful fallback
|
|
136
|
+
- **`/workflows` command** — list, inspect, stop, pause, and remove background runs; runs started with `background: true` are reachable from the command
|
|
120
137
|
- **Safety limits** — 1000-agent cap (`maxAgents`), per-agent timeout (`agentTimeoutMs`), recoverable-vs-fatal error classification
|
|
121
138
|
- **Live progress + token/cost display**, `Esc` to abort
|
|
122
139
|
- **Log persistence** to `.pi/workflows/runs/`
|
|
@@ -125,8 +142,6 @@ Scripts run inside a Node `vm` sandbox. Intentionally unavailable: `Date.now()`,
|
|
|
125
142
|
|
|
126
143
|
Tracked toward closer parity with Claude Code dynamic workflows:
|
|
127
144
|
|
|
128
|
-
- **Real per-agent / per-phase model routing** (`opts.model`, `meta.phases[].model`)
|
|
129
|
-
- **Command surface** — `/workflows` (list / status / stop) and reachable background runs
|
|
130
145
|
- **Resume** — journaled results, replay the unchanged prefix, run the rest live
|
|
131
146
|
- **Worktree isolation** for parallel edits, and **bundled `/deep-research`**
|
|
132
147
|
- **Saved workflows** as `/<name>` slash commands
|
package/dist/agent.d.ts
CHANGED
|
@@ -30,6 +30,14 @@ export interface AgentRunOptions<TSchemaDef extends TSchema | undefined = undefi
|
|
|
30
30
|
* usage is never lost. `total === 0` means the provider reported no usage.
|
|
31
31
|
*/
|
|
32
32
|
onUsage?: (usage: AgentUsage) => void;
|
|
33
|
+
/**
|
|
34
|
+
* Model spec for this subagent: either `provider/modelId` (unambiguous) or a
|
|
35
|
+
* bare `modelId`. When it can't be resolved, the session default is used and
|
|
36
|
+
* a warning is logged. When omitted, the session default applies.
|
|
37
|
+
*/
|
|
38
|
+
model?: string;
|
|
39
|
+
/** Called with the resolved model id once known (for display/telemetry). */
|
|
40
|
+
onModelResolved?: (modelId: string) => void;
|
|
33
41
|
}
|
|
34
42
|
export type AgentRunResult<TSchemaDef extends TSchema | undefined> = TSchemaDef extends TSchema ? Static<TSchemaDef> : string;
|
|
35
43
|
export declare class WorkflowAgent {
|
|
@@ -37,7 +45,16 @@ export declare class WorkflowAgent {
|
|
|
37
45
|
private readonly baseTools;
|
|
38
46
|
private readonly sessionOptions;
|
|
39
47
|
private readonly instructions?;
|
|
48
|
+
/** Lazily built once; shares the SDK's agentDir/auth so resolved models are authed. */
|
|
49
|
+
private registry?;
|
|
40
50
|
constructor(options?: WorkflowAgentOptions);
|
|
51
|
+
private getRegistry;
|
|
52
|
+
/**
|
|
53
|
+
* Resolve a model spec to a Model. Accepts `provider/modelId` (unambiguous)
|
|
54
|
+
* or a bare `modelId` (prefers auth-configured models, then any known model).
|
|
55
|
+
* Returns undefined when nothing matches.
|
|
56
|
+
*/
|
|
57
|
+
private resolveModel;
|
|
41
58
|
run<TSchemaDef extends TSchema | undefined = undefined>(prompt: string, options?: AgentRunOptions<TSchemaDef>): Promise<AgentRunResult<TSchemaDef>>;
|
|
42
59
|
private buildPrompt;
|
|
43
60
|
private lastAssistantText;
|
package/dist/agent.js
CHANGED
|
@@ -1,22 +1,60 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { AuthStorage, createAgentSession, createCodingTools, getAgentDir, ModelRegistry, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { createStructuredOutputTool } from "./structured-output.js";
|
|
3
4
|
export class WorkflowAgent {
|
|
4
5
|
cwd;
|
|
5
6
|
baseTools;
|
|
6
7
|
sessionOptions;
|
|
7
8
|
instructions;
|
|
9
|
+
/** Lazily built once; shares the SDK's agentDir/auth so resolved models are authed. */
|
|
10
|
+
registry;
|
|
8
11
|
constructor(options = {}) {
|
|
9
12
|
this.cwd = options.cwd ?? process.cwd();
|
|
10
13
|
this.baseTools = options.tools ?? createCodingTools(this.cwd);
|
|
11
14
|
this.sessionOptions = options.session ?? {};
|
|
12
15
|
this.instructions = options.instructions;
|
|
13
16
|
}
|
|
17
|
+
getRegistry() {
|
|
18
|
+
if (!this.registry) {
|
|
19
|
+
const dir = getAgentDir();
|
|
20
|
+
// Same agentDir/auth files createAgentSession uses by default, so a model
|
|
21
|
+
// resolved here carries valid credentials.
|
|
22
|
+
const auth = AuthStorage.create(join(dir, "auth.json"));
|
|
23
|
+
this.registry = ModelRegistry.create(auth, join(dir, "models.json"));
|
|
24
|
+
}
|
|
25
|
+
return this.registry;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolve a model spec to a Model. Accepts `provider/modelId` (unambiguous)
|
|
29
|
+
* or a bare `modelId` (prefers auth-configured models, then any known model).
|
|
30
|
+
* Returns undefined when nothing matches.
|
|
31
|
+
*/
|
|
32
|
+
resolveModel(spec) {
|
|
33
|
+
const registry = this.getRegistry();
|
|
34
|
+
const slash = spec.indexOf("/");
|
|
35
|
+
if (slash > 0) {
|
|
36
|
+
return registry.find(spec.slice(0, slash), spec.slice(slash + 1));
|
|
37
|
+
}
|
|
38
|
+
return registry.getAvailable().find((m) => m.id === spec) ?? registry.getAll().find((m) => m.id === spec);
|
|
39
|
+
}
|
|
14
40
|
async run(prompt, options = {}) {
|
|
15
41
|
const capture = { called: false, value: undefined };
|
|
16
42
|
const customTools = [...this.baseTools, ...(options.tools ?? [])];
|
|
17
43
|
if (options.schema) {
|
|
18
44
|
customTools.push(createStructuredOutputTool({ schema: options.schema, capture }));
|
|
19
45
|
}
|
|
46
|
+
// Resolve a requested model spec to a Model object. A given-but-unresolved
|
|
47
|
+
// spec falls back to the session default (with a warning) rather than failing.
|
|
48
|
+
let resolvedModel;
|
|
49
|
+
if (options.model) {
|
|
50
|
+
resolvedModel = this.resolveModel(options.model);
|
|
51
|
+
if (resolvedModel) {
|
|
52
|
+
options.onModelResolved?.(`${resolvedModel.provider}/${resolvedModel.id}`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
console.warn(`[workflow] model "${options.model}" not found; using session default`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
20
58
|
const agentDir = getAgentDir();
|
|
21
59
|
const { session } = await createAgentSession({
|
|
22
60
|
cwd: this.cwd,
|
|
@@ -29,6 +67,8 @@ export class WorkflowAgent {
|
|
|
29
67
|
settingsManager: SettingsManager.create(this.cwd, agentDir),
|
|
30
68
|
customTools,
|
|
31
69
|
...this.sessionOptions,
|
|
70
|
+
// Per-call model wins over any sessionOptions.model.
|
|
71
|
+
...(resolvedModel ? { model: resolvedModel } : {}),
|
|
32
72
|
});
|
|
33
73
|
let removeAbortListener;
|
|
34
74
|
try {
|
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export type { StructuredOutputCapture, StructuredOutputToolOptions } from "./str
|
|
|
20
20
|
export { createStructuredOutputTool } from "./structured-output.js";
|
|
21
21
|
export type { AgentOptions, WorkflowMeta, WorkflowMetaPhase, WorkflowRunOptions, WorkflowRunResult, } from "./workflow.js";
|
|
22
22
|
export { parseWorkflowScript, runWorkflow } from "./workflow.js";
|
|
23
|
+
export { registerWorkflowCommands } from "./workflow-commands.js";
|
|
23
24
|
export type { ManagedRun, WorkflowManagerOptions } from "./workflow-manager.js";
|
|
24
25
|
export { WorkflowManager } from "./workflow-manager.js";
|
|
25
26
|
export type { SavedWorkflow, WorkflowStorage } from "./workflow-saved.js";
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export { buildModelRoutingInstructions, parseModelRoutingFromMeta, resolveModelF
|
|
|
10
10
|
export { createRunPersistence, generateRunId } from "./run-persistence.js";
|
|
11
11
|
export { createStructuredOutputTool } from "./structured-output.js";
|
|
12
12
|
export { parseWorkflowScript, runWorkflow } from "./workflow.js";
|
|
13
|
+
export { registerWorkflowCommands } from "./workflow-commands.js";
|
|
13
14
|
export { WorkflowManager } from "./workflow-manager.js";
|
|
14
15
|
export { createWorkflowStorage } from "./workflow-saved.js";
|
|
15
16
|
export { createWorkflowTool } from "./workflow-tool.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/workflows` slash command: list, inspect, and control background workflow runs.
|
|
3
|
+
* Shares the extension's single WorkflowManager so background runs are reachable.
|
|
4
|
+
*/
|
|
5
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import type { WorkflowManager } from "./workflow-manager.js";
|
|
7
|
+
/** Register the `/workflows` command against the shared manager. Idempotent. */
|
|
8
|
+
export declare function registerWorkflowCommands(pi: ExtensionAPI, manager: WorkflowManager): void;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/workflows` slash command: list, inspect, and control background workflow runs.
|
|
3
|
+
* Shares the extension's single WorkflowManager so background runs are reachable.
|
|
4
|
+
*/
|
|
5
|
+
import { renderWorkflowText } from "./display.js";
|
|
6
|
+
const STATUS_ICON = {
|
|
7
|
+
pending: "·",
|
|
8
|
+
running: "◆",
|
|
9
|
+
paused: "⏸",
|
|
10
|
+
completed: "✓",
|
|
11
|
+
failed: "✗",
|
|
12
|
+
aborted: "⊘",
|
|
13
|
+
};
|
|
14
|
+
const USAGE = "Usage: /workflows [list] | status <id> | stop <id> | pause <id> | resume <id> | rm <id>";
|
|
15
|
+
function summarizeRun(run) {
|
|
16
|
+
const icon = STATUS_ICON[run.status] ?? "?";
|
|
17
|
+
const done = run.agents.filter((a) => a.status === "done").length;
|
|
18
|
+
const total = run.agents.length;
|
|
19
|
+
const tokens = run.tokenUsage ? ` · ${run.tokenUsage.total.toLocaleString()} tok` : "";
|
|
20
|
+
return `${icon} ${run.runId} ${run.workflowName} [${run.status}] ${done}/${total} agents${tokens}`;
|
|
21
|
+
}
|
|
22
|
+
function renderPersistedStatus(run) {
|
|
23
|
+
const lines = [`${STATUS_ICON[run.status] ?? "?"} ${run.workflowName} (${run.runId}) — ${run.status}`];
|
|
24
|
+
if (run.currentPhase)
|
|
25
|
+
lines.push(` phase: ${run.currentPhase}`);
|
|
26
|
+
for (const agent of run.agents) {
|
|
27
|
+
const icon = agent.status === "done" ? "✓" : agent.status === "error" ? "✗" : agent.status === "running" ? "◆" : "·";
|
|
28
|
+
lines.push(` ${icon} ${agent.label}`);
|
|
29
|
+
}
|
|
30
|
+
if (run.tokenUsage)
|
|
31
|
+
lines.push(` tokens: ${run.tokenUsage.total.toLocaleString()}`);
|
|
32
|
+
if (run.durationMs)
|
|
33
|
+
lines.push(` duration: ${(run.durationMs / 1000).toFixed(1)}s`);
|
|
34
|
+
return lines.join("\n");
|
|
35
|
+
}
|
|
36
|
+
/** Register the `/workflows` command against the shared manager. Idempotent. */
|
|
37
|
+
export function registerWorkflowCommands(pi, manager) {
|
|
38
|
+
try {
|
|
39
|
+
const taken = (pi.getCommands?.() ?? []).some((c) => c.name === "workflows");
|
|
40
|
+
if (taken)
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// getCommands may be unavailable in some hosts; fall through and try to register.
|
|
45
|
+
}
|
|
46
|
+
pi.registerCommand("workflows", {
|
|
47
|
+
description: "List and control background workflow runs",
|
|
48
|
+
async handler(args, ctx) {
|
|
49
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
50
|
+
const sub = (parts[0] ?? "list").toLowerCase();
|
|
51
|
+
const id = parts[1];
|
|
52
|
+
const print = (text) => pi.sendMessage({ customType: "workflows", content: text, display: true });
|
|
53
|
+
switch (sub) {
|
|
54
|
+
case "list": {
|
|
55
|
+
const runs = manager.listRuns();
|
|
56
|
+
if (!runs.length) {
|
|
57
|
+
await print("No workflow runs yet. Start one with a background workflow (background: true).");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
await print(["Workflow runs:", ...runs.map(summarizeRun), "", USAGE].join("\n"));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
case "status": {
|
|
64
|
+
if (!id) {
|
|
65
|
+
ctx.ui.notify(USAGE, "warning");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const live = manager.getSnapshot(id);
|
|
69
|
+
if (live) {
|
|
70
|
+
await print(renderWorkflowText(live, false));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const run = manager.listRuns().find((r) => r.runId === id);
|
|
74
|
+
if (!run) {
|
|
75
|
+
ctx.ui.notify(`No workflow run "${id}"`, "error");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
await print(renderPersistedStatus(run));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
case "stop": {
|
|
82
|
+
if (!id)
|
|
83
|
+
return ctx.ui.notify(USAGE, "warning");
|
|
84
|
+
ctx.ui.notify(manager.stop(id) ? `Stopped ${id}` : `Cannot stop ${id} (not running)`, manager.getRun(id) ? "info" : "warning");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
case "pause": {
|
|
88
|
+
if (!id)
|
|
89
|
+
return ctx.ui.notify(USAGE, "warning");
|
|
90
|
+
ctx.ui.notify(manager.pause(id) ? `Paused ${id}` : `Cannot pause ${id} (not running)`, "info");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
case "resume": {
|
|
94
|
+
if (!id)
|
|
95
|
+
return ctx.ui.notify(USAGE, "warning");
|
|
96
|
+
const ok = await manager.resume(id);
|
|
97
|
+
ctx.ui.notify(ok ? `Resumed ${id}` : `Resume not available for ${id} yet`, ok ? "info" : "warning");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
case "rm": {
|
|
101
|
+
if (!id)
|
|
102
|
+
return ctx.ui.notify(USAGE, "warning");
|
|
103
|
+
ctx.ui.notify(manager.deleteRun(id) ? `Removed ${id}` : `No run ${id}`, "info");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
default:
|
|
107
|
+
ctx.ui.notify(`Unknown subcommand "${sub}". ${USAGE}`, "warning");
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
package/dist/workflow-tool.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
+
import { WorkflowManager } from "./workflow-manager.js";
|
|
4
|
+
import { type WorkflowStorage } from "./workflow-saved.js";
|
|
3
5
|
declare const workflowToolSchema: Type.TObject<{
|
|
4
6
|
script: Type.TString;
|
|
5
7
|
args: Type.TOptional<Type.TAny>;
|
|
@@ -17,6 +19,10 @@ export type WorkflowToolInput = {
|
|
|
17
19
|
export interface WorkflowToolOptions {
|
|
18
20
|
cwd?: string;
|
|
19
21
|
concurrency?: number;
|
|
22
|
+
/** Shared manager so background runs are reachable from the `/workflows` command. */
|
|
23
|
+
manager?: WorkflowManager;
|
|
24
|
+
/** Shared saved-workflow storage. */
|
|
25
|
+
storage?: WorkflowStorage;
|
|
20
26
|
}
|
|
21
27
|
export declare function createWorkflowTool(options?: WorkflowToolOptions): ToolDefinition<typeof workflowToolSchema, any>;
|
|
22
28
|
export {};
|
package/dist/workflow-tool.js
CHANGED
|
@@ -27,8 +27,8 @@ const workflowToolSchema = Type.Object({
|
|
|
27
27
|
})),
|
|
28
28
|
});
|
|
29
29
|
export function createWorkflowTool(options = {}) {
|
|
30
|
-
const manager = new WorkflowManager({ cwd: options.cwd, concurrency: options.concurrency });
|
|
31
|
-
const _storage = createWorkflowStorage(options.cwd ?? process.cwd());
|
|
30
|
+
const manager = options.manager ?? new WorkflowManager({ cwd: options.cwd, concurrency: options.concurrency });
|
|
31
|
+
const _storage = options.storage ?? createWorkflowStorage(options.cwd ?? process.cwd());
|
|
32
32
|
return defineTool({
|
|
33
33
|
name: "workflow",
|
|
34
34
|
label: "Workflow",
|
|
@@ -70,8 +70,8 @@ export function createWorkflowTool(options = {}) {
|
|
|
70
70
|
text: [
|
|
71
71
|
`Workflow "${parsed.meta.name}" started in background.`,
|
|
72
72
|
`Run ID: ${runId}`,
|
|
73
|
-
`Use /
|
|
74
|
-
`Use /
|
|
73
|
+
`Use /workflows status ${runId} to check progress.`,
|
|
74
|
+
`Use /workflows stop ${runId} to cancel.`,
|
|
75
75
|
].join("\n"),
|
|
76
76
|
},
|
|
77
77
|
],
|
package/dist/workflow.d.ts
CHANGED
package/dist/workflow.js
CHANGED
|
@@ -4,10 +4,13 @@ import { WorkflowAgent } from "./agent.js";
|
|
|
4
4
|
import { DEFAULT_AGENT_TIMEOUT_MS, MAX_AGENTS_PER_RUN, MAX_CONCURRENCY } from "./config.js";
|
|
5
5
|
import { WorkflowError, WorkflowErrorCode, wrapError } from "./errors.js";
|
|
6
6
|
import { createWorkflowLogger } from "./logger.js";
|
|
7
|
+
import { parseModelRoutingFromMeta, resolveModelForPhase } from "./model-routing.js";
|
|
7
8
|
const DETERMINISM_BLOCKLIST = /\bDate\s*\.\s*now\b|\bMath\s*\.\s*random\b|\bnew\s+Date\s*\(\s*\)/;
|
|
8
9
|
export async function runWorkflow(script, options = {}) {
|
|
9
10
|
const started = Date.now();
|
|
10
11
|
const { meta, body } = parseWorkflowScript(script);
|
|
12
|
+
// Per-phase model routing from meta.phases[].model (empty when none declared).
|
|
13
|
+
const routingConfig = parseModelRoutingFromMeta(meta.phases);
|
|
11
14
|
const maxAgents = options.maxAgents ?? MAX_AGENTS_PER_RUN;
|
|
12
15
|
const agentTimeoutMs = options.agentTimeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS;
|
|
13
16
|
const runId = options.runId ?? `run-${started.toString(36)}`;
|
|
@@ -62,11 +65,13 @@ export async function runWorkflow(script, options = {}) {
|
|
|
62
65
|
}
|
|
63
66
|
const assignedPhase = agentOptions.phase ?? state.currentPhase;
|
|
64
67
|
const requestedLabel = agentOptions.label?.trim();
|
|
68
|
+
// Precedence: explicit agentOptions.model > phase model (meta.phases[].model).
|
|
69
|
+
const modelSpec = agentOptions.model ?? resolveModelForPhase(assignedPhase, routingConfig);
|
|
65
70
|
return limiter(async () => {
|
|
66
71
|
state.agentCount++;
|
|
67
72
|
const label = requestedLabel || defaultAgentLabel(assignedPhase, state.agentCount);
|
|
68
73
|
const timeout = agentOptions.timeoutMs ?? agentTimeoutMs;
|
|
69
|
-
options.onAgentStart?.({ label, phase: assignedPhase, prompt });
|
|
74
|
+
options.onAgentStart?.({ label, phase: assignedPhase, prompt, model: modelSpec });
|
|
70
75
|
// Captured from the subagent's real session usage; falls back to an
|
|
71
76
|
// estimate when the provider reports no usage (total === 0).
|
|
72
77
|
let usage;
|
|
@@ -89,6 +94,7 @@ export async function runWorkflow(script, options = {}) {
|
|
|
89
94
|
schema: agentOptions.schema,
|
|
90
95
|
signal: options.signal,
|
|
91
96
|
instructions: buildAgentInstructions(assignedPhase, agentOptions),
|
|
97
|
+
model: modelSpec,
|
|
92
98
|
onUsage: (u) => {
|
|
93
99
|
usage = u;
|
|
94
100
|
},
|
|
@@ -350,8 +356,7 @@ function buildAgentInstructions(phase, options) {
|
|
|
350
356
|
lines.push(`Act as workflow subagent type: ${options.agentType}`);
|
|
351
357
|
if (options.isolation)
|
|
352
358
|
lines.push(`Requested isolation: ${options.isolation}`);
|
|
353
|
-
|
|
354
|
-
lines.push(`Requested model: ${options.model}`);
|
|
359
|
+
// Note: options.model is applied for real via the session, not injected as prose.
|
|
355
360
|
return lines.length ? lines.join("\n") : undefined;
|
|
356
361
|
}
|
|
357
362
|
function estimateTokens(value) {
|
package/extensions/workflow.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { createWorkflowTool } from "../src/index.js";
|
|
2
|
+
import { createWorkflowStorage, createWorkflowTool, registerWorkflowCommands, WorkflowManager } from "../src/index.js";
|
|
3
3
|
|
|
4
4
|
export default function extension(pi: ExtensionAPI) {
|
|
5
|
-
|
|
5
|
+
// Single manager/storage shared by the workflow tool and the /workflows command,
|
|
6
|
+
// so background runs started by the tool are reachable from the command.
|
|
7
|
+
const cwd = process.cwd();
|
|
8
|
+
const manager = new WorkflowManager({ cwd });
|
|
9
|
+
const storage = createWorkflowStorage(cwd);
|
|
10
|
+
|
|
11
|
+
const workflowTool = createWorkflowTool({ cwd, manager, storage });
|
|
6
12
|
pi.registerTool(workflowTool);
|
|
13
|
+
registerWorkflowCommands(pi, manager);
|
|
7
14
|
|
|
8
15
|
pi.on("session_start", () => {
|
|
9
16
|
const active = pi.getActiveTools();
|
package/package.json
CHANGED
package/src/agent.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import type { AssistantMessage, Model, TextContent } from "@earendil-works/pi-ai";
|
|
2
3
|
import {
|
|
4
|
+
AuthStorage,
|
|
3
5
|
type CreateAgentSessionOptions,
|
|
4
6
|
createAgentSession,
|
|
5
7
|
createCodingTools,
|
|
6
8
|
getAgentDir,
|
|
9
|
+
ModelRegistry,
|
|
7
10
|
SessionManager,
|
|
8
11
|
SettingsManager,
|
|
9
12
|
type ToolDefinition,
|
|
@@ -43,6 +46,14 @@ export interface AgentRunOptions<TSchemaDef extends TSchema | undefined = undefi
|
|
|
43
46
|
* usage is never lost. `total === 0` means the provider reported no usage.
|
|
44
47
|
*/
|
|
45
48
|
onUsage?: (usage: AgentUsage) => void;
|
|
49
|
+
/**
|
|
50
|
+
* Model spec for this subagent: either `provider/modelId` (unambiguous) or a
|
|
51
|
+
* bare `modelId`. When it can't be resolved, the session default is used and
|
|
52
|
+
* a warning is logged. When omitted, the session default applies.
|
|
53
|
+
*/
|
|
54
|
+
model?: string;
|
|
55
|
+
/** Called with the resolved model id once known (for display/telemetry). */
|
|
56
|
+
onModelResolved?: (modelId: string) => void;
|
|
46
57
|
}
|
|
47
58
|
|
|
48
59
|
export type AgentRunResult<TSchemaDef extends TSchema | undefined> = TSchemaDef extends TSchema
|
|
@@ -54,6 +65,8 @@ export class WorkflowAgent {
|
|
|
54
65
|
private readonly baseTools: ToolDefinition[];
|
|
55
66
|
private readonly sessionOptions: Partial<CreateAgentSessionOptions>;
|
|
56
67
|
private readonly instructions?: string;
|
|
68
|
+
/** Lazily built once; shares the SDK's agentDir/auth so resolved models are authed. */
|
|
69
|
+
private registry?: ModelRegistry;
|
|
57
70
|
|
|
58
71
|
constructor(options: WorkflowAgentOptions = {}) {
|
|
59
72
|
this.cwd = options.cwd ?? process.cwd();
|
|
@@ -62,6 +75,31 @@ export class WorkflowAgent {
|
|
|
62
75
|
this.instructions = options.instructions;
|
|
63
76
|
}
|
|
64
77
|
|
|
78
|
+
private getRegistry(): ModelRegistry {
|
|
79
|
+
if (!this.registry) {
|
|
80
|
+
const dir = getAgentDir();
|
|
81
|
+
// Same agentDir/auth files createAgentSession uses by default, so a model
|
|
82
|
+
// resolved here carries valid credentials.
|
|
83
|
+
const auth = AuthStorage.create(join(dir, "auth.json"));
|
|
84
|
+
this.registry = ModelRegistry.create(auth, join(dir, "models.json"));
|
|
85
|
+
}
|
|
86
|
+
return this.registry;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolve a model spec to a Model. Accepts `provider/modelId` (unambiguous)
|
|
91
|
+
* or a bare `modelId` (prefers auth-configured models, then any known model).
|
|
92
|
+
* Returns undefined when nothing matches.
|
|
93
|
+
*/
|
|
94
|
+
private resolveModel(spec: string): Model<any> | undefined {
|
|
95
|
+
const registry = this.getRegistry();
|
|
96
|
+
const slash = spec.indexOf("/");
|
|
97
|
+
if (slash > 0) {
|
|
98
|
+
return registry.find(spec.slice(0, slash), spec.slice(slash + 1));
|
|
99
|
+
}
|
|
100
|
+
return registry.getAvailable().find((m) => m.id === spec) ?? registry.getAll().find((m) => m.id === spec);
|
|
101
|
+
}
|
|
102
|
+
|
|
65
103
|
async run<TSchemaDef extends TSchema | undefined = undefined>(
|
|
66
104
|
prompt: string,
|
|
67
105
|
options: AgentRunOptions<TSchemaDef> = {},
|
|
@@ -73,6 +111,18 @@ export class WorkflowAgent {
|
|
|
73
111
|
customTools.push(createStructuredOutputTool({ schema: options.schema, capture }) as unknown as ToolDefinition);
|
|
74
112
|
}
|
|
75
113
|
|
|
114
|
+
// Resolve a requested model spec to a Model object. A given-but-unresolved
|
|
115
|
+
// spec falls back to the session default (with a warning) rather than failing.
|
|
116
|
+
let resolvedModel: Model<any> | undefined;
|
|
117
|
+
if (options.model) {
|
|
118
|
+
resolvedModel = this.resolveModel(options.model);
|
|
119
|
+
if (resolvedModel) {
|
|
120
|
+
options.onModelResolved?.(`${resolvedModel.provider}/${resolvedModel.id}`);
|
|
121
|
+
} else {
|
|
122
|
+
console.warn(`[workflow] model "${options.model}" not found; using session default`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
76
126
|
const agentDir = getAgentDir();
|
|
77
127
|
const { session } = await createAgentSession({
|
|
78
128
|
cwd: this.cwd,
|
|
@@ -85,6 +135,8 @@ export class WorkflowAgent {
|
|
|
85
135
|
settingsManager: SettingsManager.create(this.cwd, agentDir),
|
|
86
136
|
customTools,
|
|
87
137
|
...this.sessionOptions,
|
|
138
|
+
// Per-call model wins over any sessionOptions.model.
|
|
139
|
+
...(resolvedModel ? { model: resolvedModel } : {}),
|
|
88
140
|
});
|
|
89
141
|
|
|
90
142
|
let removeAbortListener: (() => void) | undefined;
|
package/src/index.ts
CHANGED
|
@@ -47,6 +47,7 @@ export type {
|
|
|
47
47
|
WorkflowRunResult,
|
|
48
48
|
} from "./workflow.js";
|
|
49
49
|
export { parseWorkflowScript, runWorkflow } from "./workflow.js";
|
|
50
|
+
export { registerWorkflowCommands } from "./workflow-commands.js";
|
|
50
51
|
export type { ManagedRun, WorkflowManagerOptions } from "./workflow-manager.js";
|
|
51
52
|
export { WorkflowManager } from "./workflow-manager.js";
|
|
52
53
|
export type { SavedWorkflow, WorkflowStorage } from "./workflow-saved.js";
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/workflows` slash command: list, inspect, and control background workflow runs.
|
|
3
|
+
* Shares the extension's single WorkflowManager so background runs are reachable.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { renderWorkflowText } from "./display.js";
|
|
8
|
+
import type { PersistedRunState } from "./run-persistence.js";
|
|
9
|
+
import type { WorkflowManager } from "./workflow-manager.js";
|
|
10
|
+
|
|
11
|
+
const STATUS_ICON: Record<string, string> = {
|
|
12
|
+
pending: "·",
|
|
13
|
+
running: "◆",
|
|
14
|
+
paused: "⏸",
|
|
15
|
+
completed: "✓",
|
|
16
|
+
failed: "✗",
|
|
17
|
+
aborted: "⊘",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const USAGE = "Usage: /workflows [list] | status <id> | stop <id> | pause <id> | resume <id> | rm <id>";
|
|
21
|
+
|
|
22
|
+
function summarizeRun(run: PersistedRunState): string {
|
|
23
|
+
const icon = STATUS_ICON[run.status] ?? "?";
|
|
24
|
+
const done = run.agents.filter((a) => a.status === "done").length;
|
|
25
|
+
const total = run.agents.length;
|
|
26
|
+
const tokens = run.tokenUsage ? ` · ${run.tokenUsage.total.toLocaleString()} tok` : "";
|
|
27
|
+
return `${icon} ${run.runId} ${run.workflowName} [${run.status}] ${done}/${total} agents${tokens}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function renderPersistedStatus(run: PersistedRunState): string {
|
|
31
|
+
const lines = [`${STATUS_ICON[run.status] ?? "?"} ${run.workflowName} (${run.runId}) — ${run.status}`];
|
|
32
|
+
if (run.currentPhase) lines.push(` phase: ${run.currentPhase}`);
|
|
33
|
+
for (const agent of run.agents) {
|
|
34
|
+
const icon =
|
|
35
|
+
agent.status === "done" ? "✓" : agent.status === "error" ? "✗" : agent.status === "running" ? "◆" : "·";
|
|
36
|
+
lines.push(` ${icon} ${agent.label}`);
|
|
37
|
+
}
|
|
38
|
+
if (run.tokenUsage) lines.push(` tokens: ${run.tokenUsage.total.toLocaleString()}`);
|
|
39
|
+
if (run.durationMs) lines.push(` duration: ${(run.durationMs / 1000).toFixed(1)}s`);
|
|
40
|
+
return lines.join("\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Register the `/workflows` command against the shared manager. Idempotent. */
|
|
44
|
+
export function registerWorkflowCommands(pi: ExtensionAPI, manager: WorkflowManager): void {
|
|
45
|
+
try {
|
|
46
|
+
const taken = (pi.getCommands?.() ?? []).some((c: { name: string }) => c.name === "workflows");
|
|
47
|
+
if (taken) return;
|
|
48
|
+
} catch {
|
|
49
|
+
// getCommands may be unavailable in some hosts; fall through and try to register.
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
pi.registerCommand("workflows", {
|
|
53
|
+
description: "List and control background workflow runs",
|
|
54
|
+
async handler(args: string, ctx: ExtensionCommandContext) {
|
|
55
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
56
|
+
const sub = (parts[0] ?? "list").toLowerCase();
|
|
57
|
+
const id = parts[1];
|
|
58
|
+
const print = (text: string) => pi.sendMessage({ customType: "workflows", content: text, display: true });
|
|
59
|
+
|
|
60
|
+
switch (sub) {
|
|
61
|
+
case "list": {
|
|
62
|
+
const runs = manager.listRuns();
|
|
63
|
+
if (!runs.length) {
|
|
64
|
+
await print("No workflow runs yet. Start one with a background workflow (background: true).");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
await print(["Workflow runs:", ...runs.map(summarizeRun), "", USAGE].join("\n"));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
case "status": {
|
|
71
|
+
if (!id) {
|
|
72
|
+
ctx.ui.notify(USAGE, "warning");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const live = manager.getSnapshot(id);
|
|
76
|
+
if (live) {
|
|
77
|
+
await print(renderWorkflowText(live, false));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const run = manager.listRuns().find((r) => r.runId === id);
|
|
81
|
+
if (!run) {
|
|
82
|
+
ctx.ui.notify(`No workflow run "${id}"`, "error");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
await print(renderPersistedStatus(run));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
case "stop": {
|
|
89
|
+
if (!id) return ctx.ui.notify(USAGE, "warning");
|
|
90
|
+
ctx.ui.notify(
|
|
91
|
+
manager.stop(id) ? `Stopped ${id}` : `Cannot stop ${id} (not running)`,
|
|
92
|
+
manager.getRun(id) ? "info" : "warning",
|
|
93
|
+
);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
case "pause": {
|
|
97
|
+
if (!id) return ctx.ui.notify(USAGE, "warning");
|
|
98
|
+
ctx.ui.notify(manager.pause(id) ? `Paused ${id}` : `Cannot pause ${id} (not running)`, "info");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
case "resume": {
|
|
102
|
+
if (!id) return ctx.ui.notify(USAGE, "warning");
|
|
103
|
+
const ok = await manager.resume(id);
|
|
104
|
+
ctx.ui.notify(ok ? `Resumed ${id}` : `Resume not available for ${id} yet`, ok ? "info" : "warning");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
case "rm": {
|
|
108
|
+
if (!id) return ctx.ui.notify(USAGE, "warning");
|
|
109
|
+
ctx.ui.notify(manager.deleteRun(id) ? `Removed ${id}` : `No run ${id}`, "info");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
default:
|
|
113
|
+
ctx.ui.notify(`Unknown subcommand "${sub}". ${USAGE}`, "warning");
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
package/src/workflow-tool.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
import { WorkflowError, WorkflowErrorCode } from "./errors.js";
|
|
13
13
|
import { parseWorkflowScript, runWorkflow, type WorkflowRunResult } from "./workflow.js";
|
|
14
14
|
import { WorkflowManager } from "./workflow-manager.js";
|
|
15
|
-
import { createWorkflowStorage } from "./workflow-saved.js";
|
|
15
|
+
import { createWorkflowStorage, type WorkflowStorage } from "./workflow-saved.js";
|
|
16
16
|
|
|
17
17
|
const workflowToolSchema = Type.Object({
|
|
18
18
|
script: Type.String({
|
|
@@ -54,11 +54,15 @@ export type WorkflowToolInput = {
|
|
|
54
54
|
export interface WorkflowToolOptions {
|
|
55
55
|
cwd?: string;
|
|
56
56
|
concurrency?: number;
|
|
57
|
+
/** Shared manager so background runs are reachable from the `/workflows` command. */
|
|
58
|
+
manager?: WorkflowManager;
|
|
59
|
+
/** Shared saved-workflow storage. */
|
|
60
|
+
storage?: WorkflowStorage;
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
export function createWorkflowTool(options: WorkflowToolOptions = {}): ToolDefinition<typeof workflowToolSchema, any> {
|
|
60
|
-
const manager = new WorkflowManager({ cwd: options.cwd, concurrency: options.concurrency });
|
|
61
|
-
const _storage = createWorkflowStorage(options.cwd ?? process.cwd());
|
|
64
|
+
const manager = options.manager ?? new WorkflowManager({ cwd: options.cwd, concurrency: options.concurrency });
|
|
65
|
+
const _storage = options.storage ?? createWorkflowStorage(options.cwd ?? process.cwd());
|
|
62
66
|
|
|
63
67
|
return defineTool({
|
|
64
68
|
name: "workflow",
|
|
@@ -103,8 +107,8 @@ export function createWorkflowTool(options: WorkflowToolOptions = {}): ToolDefin
|
|
|
103
107
|
text: [
|
|
104
108
|
`Workflow "${parsed.meta.name}" started in background.`,
|
|
105
109
|
`Run ID: ${runId}`,
|
|
106
|
-
`Use /
|
|
107
|
-
`Use /
|
|
110
|
+
`Use /workflows status ${runId} to check progress.`,
|
|
111
|
+
`Use /workflows stop ${runId} to cancel.`,
|
|
108
112
|
].join("\n"),
|
|
109
113
|
},
|
|
110
114
|
],
|
package/src/workflow.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { WorkflowAgent, type WorkflowAgentOptions } from "./agent.js";
|
|
|
7
7
|
import { DEFAULT_AGENT_TIMEOUT_MS, MAX_AGENTS_PER_RUN, MAX_CONCURRENCY } from "./config.js";
|
|
8
8
|
import { WorkflowError, WorkflowErrorCode, wrapError } from "./errors.js";
|
|
9
9
|
import { createWorkflowLogger } from "./logger.js";
|
|
10
|
+
import { parseModelRoutingFromMeta, resolveModelForPhase } from "./model-routing.js";
|
|
10
11
|
|
|
11
12
|
export interface WorkflowMetaPhase {
|
|
12
13
|
title: string;
|
|
@@ -37,7 +38,7 @@ export interface WorkflowRunOptions extends WorkflowAgentOptions {
|
|
|
37
38
|
runId?: string;
|
|
38
39
|
onLog?: (message: string) => void;
|
|
39
40
|
onPhase?: (title: string) => void;
|
|
40
|
-
onAgentStart?: (event: { label: string; phase?: string; prompt: string }) => void;
|
|
41
|
+
onAgentStart?: (event: { label: string; phase?: string; prompt: string; model?: string }) => void;
|
|
41
42
|
onAgentEnd?: (event: { label: string; phase?: string; result: unknown; tokens?: number }) => void;
|
|
42
43
|
onTokenUsage?: (usage: { input: number; output: number; total: number; cost: number }) => void;
|
|
43
44
|
}
|
|
@@ -93,6 +94,8 @@ export async function runWorkflow<T = unknown>(
|
|
|
93
94
|
): Promise<WorkflowRunResult<T>> {
|
|
94
95
|
const started = Date.now();
|
|
95
96
|
const { meta, body } = parseWorkflowScript(script);
|
|
97
|
+
// Per-phase model routing from meta.phases[].model (empty when none declared).
|
|
98
|
+
const routingConfig = parseModelRoutingFromMeta(meta.phases);
|
|
96
99
|
const maxAgents = options.maxAgents ?? MAX_AGENTS_PER_RUN;
|
|
97
100
|
const agentTimeoutMs = options.agentTimeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS;
|
|
98
101
|
const runId = options.runId ?? `run-${started.toString(36)}`;
|
|
@@ -164,13 +167,15 @@ export async function runWorkflow<T = unknown>(
|
|
|
164
167
|
|
|
165
168
|
const assignedPhase = agentOptions.phase ?? state.currentPhase;
|
|
166
169
|
const requestedLabel = agentOptions.label?.trim();
|
|
170
|
+
// Precedence: explicit agentOptions.model > phase model (meta.phases[].model).
|
|
171
|
+
const modelSpec = agentOptions.model ?? resolveModelForPhase(assignedPhase, routingConfig);
|
|
167
172
|
|
|
168
173
|
return limiter(async () => {
|
|
169
174
|
state.agentCount++;
|
|
170
175
|
const label = requestedLabel || defaultAgentLabel(assignedPhase, state.agentCount);
|
|
171
176
|
const timeout = agentOptions.timeoutMs ?? agentTimeoutMs;
|
|
172
177
|
|
|
173
|
-
options.onAgentStart?.({ label, phase: assignedPhase, prompt });
|
|
178
|
+
options.onAgentStart?.({ label, phase: assignedPhase, prompt, model: modelSpec });
|
|
174
179
|
|
|
175
180
|
// Captured from the subagent's real session usage; falls back to an
|
|
176
181
|
// estimate when the provider reports no usage (total === 0).
|
|
@@ -197,6 +202,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
197
202
|
schema: agentOptions.schema,
|
|
198
203
|
signal: options.signal,
|
|
199
204
|
instructions: buildAgentInstructions(assignedPhase, agentOptions),
|
|
205
|
+
model: modelSpec,
|
|
200
206
|
onUsage: (u: AgentUsage) => {
|
|
201
207
|
usage = u;
|
|
202
208
|
},
|
|
@@ -480,7 +486,7 @@ function buildAgentInstructions(phase: string | undefined, options: AgentOptions
|
|
|
480
486
|
if (phase) lines.push(`Workflow phase: ${phase}`);
|
|
481
487
|
if (options.agentType) lines.push(`Act as workflow subagent type: ${options.agentType}`);
|
|
482
488
|
if (options.isolation) lines.push(`Requested isolation: ${options.isolation}`);
|
|
483
|
-
|
|
489
|
+
// Note: options.model is applied for real via the session, not injected as prose.
|
|
484
490
|
return lines.length ? lines.join("\n") : undefined;
|
|
485
491
|
}
|
|
486
492
|
|