@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 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
@@ -29,6 +29,7 @@ export interface WorkflowSnapshot {
29
29
  input: number;
30
30
  output: number;
31
31
  total: number;
32
+ cost?: number;
32
33
  };
33
34
  runId?: string;
34
35
  }
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 tokenInfo = snapshot.tokenUsage ? ` · ${snapshot.tokenUsage.total.toLocaleString()} tokens` : "";
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
  ];
@@ -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 ? `\n\nToken usage: ${result.tokenUsage.total.toLocaleString()} tokens` : "";
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
  {
@@ -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
- // Estimate token usage
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 errorTokens = estimateTokens(prompt);
93
- state.tokenUsage.total += errorTokens;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quintinshaw/pi-dynamic-workflows",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Claude-Code-style dynamic workflow orchestration for Pi.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
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 tokenInfo = snapshot.tokenUsage ? ` · ${snapshot.tokenUsage.total.toLocaleString()} tokens` : "";
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
  ];
@@ -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 ? `\n\nToken usage: ${result.tokenUsage.total.toLocaleString()} tokens` : "";
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
- // Estimate token usage
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 errorTokens = estimateTokens(prompt);
202
- state.tokenUsage.total += errorTokens;
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) {