@quintinshaw/pi-dynamic-workflows 1.0.0 → 1.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 +2 -2
- package/dist/agent.d.ts +15 -0
- package/dist/agent.js +17 -0
- package/dist/display.d.ts +1 -0
- package/dist/display.js +4 -2
- package/dist/workflow-tool.js +4 -2
- package/dist/workflow.d.ts +2 -0
- package/dist/workflow.js +21 -8
- package/package.json +1 -1
- package/src/agent.ts +32 -0
- package/src/display.ts +5 -2
- package/src/workflow-tool.ts +6 -2
- package/src/workflow.ts +26 -10
package/README.md
CHANGED
|
@@ -116,8 +116,9 @@ Scripts run inside a Node `vm` sandbox. Intentionally unavailable: `Date.now()`,
|
|
|
116
116
|
|
|
117
117
|
- **Core runtime** — `agent` / `parallel` / `pipeline` / `phase` / `log` / `budget` in a sandboxed script
|
|
118
118
|
- **Structured output** — JSON-Schema-validated subagent results
|
|
119
|
+
- **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
|
|
119
120
|
- **Safety limits** — 1000-agent cap (`maxAgents`), per-agent timeout (`agentTimeoutMs`), recoverable-vs-fatal error classification
|
|
120
|
-
- **Live progress + token display**, `Esc` to abort
|
|
121
|
+
- **Live progress + token/cost display**, `Esc` to abort
|
|
121
122
|
- **Log persistence** to `.pi/workflows/runs/`
|
|
122
123
|
|
|
123
124
|
## Roadmap
|
|
@@ -125,7 +126,6 @@ Scripts run inside a Node `vm` sandbox. Intentionally unavailable: `Date.now()`,
|
|
|
125
126
|
Tracked toward closer parity with Claude Code dynamic workflows:
|
|
126
127
|
|
|
127
128
|
- **Real per-agent / per-phase model routing** (`opts.model`, `meta.phases[].model`)
|
|
128
|
-
- **Real token accounting** via the SDK's session stats (today's display uses an estimate)
|
|
129
129
|
- **Command surface** — `/workflows` (list / status / stop) and reachable background runs
|
|
130
130
|
- **Resume** — journaled results, replay the unchanged prefix, run the rest live
|
|
131
131
|
- **Worktree isolation** for parallel edits, and **bundled `/deep-research`**
|
package/dist/agent.d.ts
CHANGED
|
@@ -9,12 +9,27 @@ export interface WorkflowAgentOptions {
|
|
|
9
9
|
/** Extra system guidance prepended to every subagent task. */
|
|
10
10
|
instructions?: string;
|
|
11
11
|
}
|
|
12
|
+
/** Real token/cost usage for a single subagent run, read from the SDK session. */
|
|
13
|
+
export interface AgentUsage {
|
|
14
|
+
input: number;
|
|
15
|
+
output: number;
|
|
16
|
+
cacheRead: number;
|
|
17
|
+
cacheWrite: number;
|
|
18
|
+
total: number;
|
|
19
|
+
cost: number;
|
|
20
|
+
}
|
|
12
21
|
export interface AgentRunOptions<TSchemaDef extends TSchema | undefined = undefined> {
|
|
13
22
|
label?: string;
|
|
14
23
|
schema?: TSchemaDef;
|
|
15
24
|
tools?: ToolDefinition[];
|
|
16
25
|
instructions?: string;
|
|
17
26
|
signal?: AbortSignal;
|
|
27
|
+
/**
|
|
28
|
+
* Called once with this subagent's real usage, read from the session right
|
|
29
|
+
* before disposal. Fires on both the success and error paths so partial
|
|
30
|
+
* usage is never lost. `total === 0` means the provider reported no usage.
|
|
31
|
+
*/
|
|
32
|
+
onUsage?: (usage: AgentUsage) => void;
|
|
18
33
|
}
|
|
19
34
|
export type AgentRunResult<TSchemaDef extends TSchema | undefined> = TSchemaDef extends TSchema ? Static<TSchemaDef> : string;
|
|
20
35
|
export declare class WorkflowAgent {
|
package/dist/agent.js
CHANGED
|
@@ -52,6 +52,23 @@ export class WorkflowAgent {
|
|
|
52
52
|
}
|
|
53
53
|
finally {
|
|
54
54
|
removeAbortListener?.();
|
|
55
|
+
// Read real usage before disposing — dispose tears down the session state.
|
|
56
|
+
if (options.onUsage) {
|
|
57
|
+
try {
|
|
58
|
+
const { tokens, cost } = session.getSessionStats();
|
|
59
|
+
options.onUsage({
|
|
60
|
+
input: tokens.input,
|
|
61
|
+
output: tokens.output,
|
|
62
|
+
cacheRead: tokens.cacheRead,
|
|
63
|
+
cacheWrite: tokens.cacheWrite,
|
|
64
|
+
total: tokens.total,
|
|
65
|
+
cost,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Usage is best-effort; never let stats failure mask the real result/error.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
55
72
|
session.dispose();
|
|
56
73
|
}
|
|
57
74
|
}
|
package/dist/display.d.ts
CHANGED
package/dist/display.js
CHANGED
|
@@ -80,8 +80,10 @@ export function renderWorkflowLines(snapshot, options = {}) {
|
|
|
80
80
|
: snapshot.runningCount > 0
|
|
81
81
|
? `, ${snapshot.runningCount} running`
|
|
82
82
|
: "";
|
|
83
|
-
// Build header with token info
|
|
84
|
-
const
|
|
83
|
+
// Build header with token info (and cost when the provider reports it)
|
|
84
|
+
const usage = snapshot.tokenUsage;
|
|
85
|
+
const costInfo = usage?.cost ? ` · $${usage.cost.toFixed(4)}` : "";
|
|
86
|
+
const tokenInfo = usage ? ` · ${usage.total.toLocaleString()} tokens${costInfo}` : "";
|
|
85
87
|
const lines = [
|
|
86
88
|
`◆ Workflow: ${snapshot.name} (${snapshot.doneCount}/${snapshot.agentCount} done${state}${tokenInfo})`,
|
|
87
89
|
];
|
package/dist/workflow-tool.js
CHANGED
|
@@ -160,8 +160,10 @@ export function createWorkflowTool(options = {}) {
|
|
|
160
160
|
snapshot.durationMs = result.durationMs;
|
|
161
161
|
snapshot = recomputeWorkflowSnapshot(snapshot);
|
|
162
162
|
display.complete(snapshot);
|
|
163
|
-
// Format token usage
|
|
164
|
-
const tokenInfo = result.tokenUsage
|
|
163
|
+
// Format token usage (include cost when the provider reports it)
|
|
164
|
+
const tokenInfo = result.tokenUsage
|
|
165
|
+
? `\n\nToken usage: ${result.tokenUsage.total.toLocaleString()} tokens${result.tokenUsage.cost ? ` ($${result.tokenUsage.cost.toFixed(4)})` : ""}`
|
|
166
|
+
: "";
|
|
165
167
|
return {
|
|
166
168
|
content: [
|
|
167
169
|
{
|
package/dist/workflow.d.ts
CHANGED
|
@@ -42,6 +42,7 @@ export interface WorkflowRunOptions extends WorkflowAgentOptions {
|
|
|
42
42
|
input: number;
|
|
43
43
|
output: number;
|
|
44
44
|
total: number;
|
|
45
|
+
cost: number;
|
|
45
46
|
}) => void;
|
|
46
47
|
}
|
|
47
48
|
export interface WorkflowRunResult<T = unknown> {
|
|
@@ -56,6 +57,7 @@ export interface WorkflowRunResult<T = unknown> {
|
|
|
56
57
|
input: number;
|
|
57
58
|
output: number;
|
|
58
59
|
total: number;
|
|
60
|
+
cost: number;
|
|
59
61
|
};
|
|
60
62
|
}
|
|
61
63
|
export interface AgentOptions<TSchemaDef extends TSchema | undefined = TSchema | undefined> {
|
package/dist/workflow.js
CHANGED
|
@@ -23,7 +23,7 @@ export async function runWorkflow(script, options = {}) {
|
|
|
23
23
|
phases: [],
|
|
24
24
|
agentCount: 0,
|
|
25
25
|
spent: 0,
|
|
26
|
-
tokenUsage: { input: 0, output: 0, total: 0 },
|
|
26
|
+
tokenUsage: { input: 0, output: 0, total: 0, cost: 0 },
|
|
27
27
|
};
|
|
28
28
|
const agentRunner = options.agent ?? new WorkflowAgent(options);
|
|
29
29
|
const concurrency = Math.max(1, Math.min(options.concurrency ?? Math.max(1, (globalThis.navigator?.hardwareConcurrency ?? 8) - 2), MAX_CONCURRENCY));
|
|
@@ -67,6 +67,20 @@ export async function runWorkflow(script, options = {}) {
|
|
|
67
67
|
const label = requestedLabel || defaultAgentLabel(assignedPhase, state.agentCount);
|
|
68
68
|
const timeout = agentOptions.timeoutMs ?? agentTimeoutMs;
|
|
69
69
|
options.onAgentStart?.({ label, phase: assignedPhase, prompt });
|
|
70
|
+
// Captured from the subagent's real session usage; falls back to an
|
|
71
|
+
// estimate when the provider reports no usage (total === 0).
|
|
72
|
+
let usage;
|
|
73
|
+
const recordTokens = (result) => {
|
|
74
|
+
const tokens = usage && usage.total > 0 ? usage.total : estimateTokens(result) + estimateTokens(prompt);
|
|
75
|
+
if (usage) {
|
|
76
|
+
state.tokenUsage.input += usage.input;
|
|
77
|
+
state.tokenUsage.output += usage.output;
|
|
78
|
+
state.tokenUsage.cost += usage.cost;
|
|
79
|
+
}
|
|
80
|
+
state.tokenUsage.total += tokens;
|
|
81
|
+
state.spent += tokens;
|
|
82
|
+
return tokens;
|
|
83
|
+
};
|
|
70
84
|
try {
|
|
71
85
|
throwIfAborted();
|
|
72
86
|
// Run agent with timeout
|
|
@@ -75,12 +89,12 @@ export async function runWorkflow(script, options = {}) {
|
|
|
75
89
|
schema: agentOptions.schema,
|
|
76
90
|
signal: options.signal,
|
|
77
91
|
instructions: buildAgentInstructions(assignedPhase, agentOptions),
|
|
92
|
+
onUsage: (u) => {
|
|
93
|
+
usage = u;
|
|
94
|
+
},
|
|
78
95
|
}), timeout, `Agent "${label}" timed out after ${timeout}ms`);
|
|
79
96
|
throwIfAborted();
|
|
80
|
-
|
|
81
|
-
const tokens = estimateTokens(result) + estimateTokens(prompt);
|
|
82
|
-
state.spent += tokens;
|
|
83
|
-
state.tokenUsage.total += tokens;
|
|
97
|
+
const tokens = recordTokens(result);
|
|
84
98
|
options.onAgentEnd?.({ label, phase: assignedPhase, result, tokens });
|
|
85
99
|
return result;
|
|
86
100
|
}
|
|
@@ -89,9 +103,8 @@ export async function runWorkflow(script, options = {}) {
|
|
|
89
103
|
throw error;
|
|
90
104
|
const workflowError = wrapError(error, { agentLabel: label });
|
|
91
105
|
logger.error(`agent ${label} failed: ${workflowError.message}`);
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
options.onAgentEnd?.({ label, phase: assignedPhase, result: null, tokens: errorTokens });
|
|
106
|
+
const tokens = recordTokens(null);
|
|
107
|
+
options.onAgentEnd?.({ label, phase: assignedPhase, result: null, tokens });
|
|
95
108
|
// Return null for recoverable errors
|
|
96
109
|
if (workflowError.recoverable) {
|
|
97
110
|
return null;
|
package/package.json
CHANGED
package/src/agent.ts
CHANGED
|
@@ -21,12 +21,28 @@ export interface WorkflowAgentOptions {
|
|
|
21
21
|
instructions?: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/** Real token/cost usage for a single subagent run, read from the SDK session. */
|
|
25
|
+
export interface AgentUsage {
|
|
26
|
+
input: number;
|
|
27
|
+
output: number;
|
|
28
|
+
cacheRead: number;
|
|
29
|
+
cacheWrite: number;
|
|
30
|
+
total: number;
|
|
31
|
+
cost: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
24
34
|
export interface AgentRunOptions<TSchemaDef extends TSchema | undefined = undefined> {
|
|
25
35
|
label?: string;
|
|
26
36
|
schema?: TSchemaDef;
|
|
27
37
|
tools?: ToolDefinition[];
|
|
28
38
|
instructions?: string;
|
|
29
39
|
signal?: AbortSignal;
|
|
40
|
+
/**
|
|
41
|
+
* Called once with this subagent's real usage, read from the session right
|
|
42
|
+
* before disposal. Fires on both the success and error paths so partial
|
|
43
|
+
* usage is never lost. `total === 0` means the provider reported no usage.
|
|
44
|
+
*/
|
|
45
|
+
onUsage?: (usage: AgentUsage) => void;
|
|
30
46
|
}
|
|
31
47
|
|
|
32
48
|
export type AgentRunResult<TSchemaDef extends TSchema | undefined> = TSchemaDef extends TSchema
|
|
@@ -93,6 +109,22 @@ export class WorkflowAgent {
|
|
|
93
109
|
return this.lastAssistantText(session.messages) as AgentRunResult<TSchemaDef>;
|
|
94
110
|
} finally {
|
|
95
111
|
removeAbortListener?.();
|
|
112
|
+
// Read real usage before disposing — dispose tears down the session state.
|
|
113
|
+
if (options.onUsage) {
|
|
114
|
+
try {
|
|
115
|
+
const { tokens, cost } = session.getSessionStats();
|
|
116
|
+
options.onUsage({
|
|
117
|
+
input: tokens.input,
|
|
118
|
+
output: tokens.output,
|
|
119
|
+
cacheRead: tokens.cacheRead,
|
|
120
|
+
cacheWrite: tokens.cacheWrite,
|
|
121
|
+
total: tokens.total,
|
|
122
|
+
cost,
|
|
123
|
+
});
|
|
124
|
+
} catch {
|
|
125
|
+
// Usage is best-effort; never let stats failure mask the real result/error.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
96
128
|
session.dispose();
|
|
97
129
|
}
|
|
98
130
|
}
|
package/src/display.ts
CHANGED
|
@@ -32,6 +32,7 @@ export interface WorkflowSnapshot {
|
|
|
32
32
|
input: number;
|
|
33
33
|
output: number;
|
|
34
34
|
total: number;
|
|
35
|
+
cost?: number;
|
|
35
36
|
};
|
|
36
37
|
runId?: string;
|
|
37
38
|
}
|
|
@@ -143,8 +144,10 @@ export function renderWorkflowLines(snapshot: WorkflowSnapshot, options: Workflo
|
|
|
143
144
|
: snapshot.runningCount > 0
|
|
144
145
|
? `, ${snapshot.runningCount} running`
|
|
145
146
|
: "";
|
|
146
|
-
// Build header with token info
|
|
147
|
-
const
|
|
147
|
+
// Build header with token info (and cost when the provider reports it)
|
|
148
|
+
const usage = snapshot.tokenUsage;
|
|
149
|
+
const costInfo = usage?.cost ? ` · $${usage.cost.toFixed(4)}` : "";
|
|
150
|
+
const tokenInfo = usage ? ` · ${usage.total.toLocaleString()} tokens${costInfo}` : "";
|
|
148
151
|
const lines = [
|
|
149
152
|
`◆ Workflow: ${snapshot.name} (${snapshot.doneCount}/${snapshot.agentCount} done${state}${tokenInfo})`,
|
|
150
153
|
];
|
package/src/workflow-tool.ts
CHANGED
|
@@ -198,8 +198,12 @@ export function createWorkflowTool(options: WorkflowToolOptions = {}): ToolDefin
|
|
|
198
198
|
snapshot = recomputeWorkflowSnapshot(snapshot);
|
|
199
199
|
display.complete(snapshot);
|
|
200
200
|
|
|
201
|
-
// Format token usage
|
|
202
|
-
const tokenInfo = result.tokenUsage
|
|
201
|
+
// Format token usage (include cost when the provider reports it)
|
|
202
|
+
const tokenInfo = result.tokenUsage
|
|
203
|
+
? `\n\nToken usage: ${result.tokenUsage.total.toLocaleString()} tokens${
|
|
204
|
+
result.tokenUsage.cost ? ` ($${result.tokenUsage.cost.toFixed(4)})` : ""
|
|
205
|
+
}`
|
|
206
|
+
: "";
|
|
203
207
|
|
|
204
208
|
return {
|
|
205
209
|
content: [
|
package/src/workflow.ts
CHANGED
|
@@ -2,6 +2,7 @@ import vm from "node:vm";
|
|
|
2
2
|
import type { Node } from "acorn";
|
|
3
3
|
import { parse } from "acorn";
|
|
4
4
|
import type { TSchema } from "typebox";
|
|
5
|
+
import type { AgentUsage } from "./agent.js";
|
|
5
6
|
import { WorkflowAgent, type WorkflowAgentOptions } from "./agent.js";
|
|
6
7
|
import { DEFAULT_AGENT_TIMEOUT_MS, MAX_AGENTS_PER_RUN, MAX_CONCURRENCY } from "./config.js";
|
|
7
8
|
import { WorkflowError, WorkflowErrorCode, wrapError } from "./errors.js";
|
|
@@ -38,7 +39,7 @@ export interface WorkflowRunOptions extends WorkflowAgentOptions {
|
|
|
38
39
|
onPhase?: (title: string) => void;
|
|
39
40
|
onAgentStart?: (event: { label: string; phase?: string; prompt: string }) => void;
|
|
40
41
|
onAgentEnd?: (event: { label: string; phase?: string; result: unknown; tokens?: number }) => void;
|
|
41
|
-
onTokenUsage?: (usage: { input: number; output: number; total: number }) => void;
|
|
42
|
+
onTokenUsage?: (usage: { input: number; output: number; total: number; cost: number }) => void;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
export interface WorkflowRunResult<T = unknown> {
|
|
@@ -53,6 +54,7 @@ export interface WorkflowRunResult<T = unknown> {
|
|
|
53
54
|
input: number;
|
|
54
55
|
output: number;
|
|
55
56
|
total: number;
|
|
57
|
+
cost: number;
|
|
56
58
|
};
|
|
57
59
|
}
|
|
58
60
|
|
|
@@ -77,6 +79,7 @@ interface RuntimeState {
|
|
|
77
79
|
input: number;
|
|
78
80
|
output: number;
|
|
79
81
|
total: number;
|
|
82
|
+
cost: number;
|
|
80
83
|
};
|
|
81
84
|
}
|
|
82
85
|
|
|
@@ -107,7 +110,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
107
110
|
phases: [],
|
|
108
111
|
agentCount: 0,
|
|
109
112
|
spent: 0,
|
|
110
|
-
tokenUsage: { input: 0, output: 0, total: 0 },
|
|
113
|
+
tokenUsage: { input: 0, output: 0, total: 0, cost: 0 },
|
|
111
114
|
};
|
|
112
115
|
|
|
113
116
|
const agentRunner = options.agent ?? new WorkflowAgent(options);
|
|
@@ -169,6 +172,21 @@ export async function runWorkflow<T = unknown>(
|
|
|
169
172
|
|
|
170
173
|
options.onAgentStart?.({ label, phase: assignedPhase, prompt });
|
|
171
174
|
|
|
175
|
+
// Captured from the subagent's real session usage; falls back to an
|
|
176
|
+
// estimate when the provider reports no usage (total === 0).
|
|
177
|
+
let usage: AgentUsage | undefined;
|
|
178
|
+
const recordTokens = (result: unknown): number => {
|
|
179
|
+
const tokens = usage && usage.total > 0 ? usage.total : estimateTokens(result) + estimateTokens(prompt);
|
|
180
|
+
if (usage) {
|
|
181
|
+
state.tokenUsage.input += usage.input;
|
|
182
|
+
state.tokenUsage.output += usage.output;
|
|
183
|
+
state.tokenUsage.cost += usage.cost;
|
|
184
|
+
}
|
|
185
|
+
state.tokenUsage.total += tokens;
|
|
186
|
+
state.spent += tokens;
|
|
187
|
+
return tokens;
|
|
188
|
+
};
|
|
189
|
+
|
|
172
190
|
try {
|
|
173
191
|
throwIfAborted();
|
|
174
192
|
|
|
@@ -179,6 +197,9 @@ export async function runWorkflow<T = unknown>(
|
|
|
179
197
|
schema: agentOptions.schema,
|
|
180
198
|
signal: options.signal,
|
|
181
199
|
instructions: buildAgentInstructions(assignedPhase, agentOptions),
|
|
200
|
+
onUsage: (u: AgentUsage) => {
|
|
201
|
+
usage = u;
|
|
202
|
+
},
|
|
182
203
|
} as any),
|
|
183
204
|
timeout,
|
|
184
205
|
`Agent "${label}" timed out after ${timeout}ms`,
|
|
@@ -186,11 +207,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
186
207
|
|
|
187
208
|
throwIfAborted();
|
|
188
209
|
|
|
189
|
-
|
|
190
|
-
const tokens = estimateTokens(result) + estimateTokens(prompt);
|
|
191
|
-
state.spent += tokens;
|
|
192
|
-
state.tokenUsage.total += tokens;
|
|
193
|
-
|
|
210
|
+
const tokens = recordTokens(result);
|
|
194
211
|
options.onAgentEnd?.({ label, phase: assignedPhase, result, tokens });
|
|
195
212
|
return result;
|
|
196
213
|
} catch (error) {
|
|
@@ -198,9 +215,8 @@ export async function runWorkflow<T = unknown>(
|
|
|
198
215
|
|
|
199
216
|
const workflowError = wrapError(error, { agentLabel: label });
|
|
200
217
|
logger.error(`agent ${label} failed: ${workflowError.message}`);
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
options.onAgentEnd?.({ label, phase: assignedPhase, result: null, tokens: errorTokens });
|
|
218
|
+
const tokens = recordTokens(null);
|
|
219
|
+
options.onAgentEnd?.({ label, phase: assignedPhase, result: null, tokens });
|
|
204
220
|
|
|
205
221
|
// Return null for recoverable errors
|
|
206
222
|
if (workflowError.recoverable) {
|