agent-state-machine 2.2.2 → 2.4.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/bin/cli.js CHANGED
@@ -99,6 +99,7 @@ Options:
99
99
  --new, -n Generate a new remote follow path
100
100
  --full-auto, -a Auto-select first option for choice interactions (no blocking)
101
101
  --delay, -d Seconds to wait before auto-select in full-auto mode (default: 20)
102
+ --non-verbose, -q Suppress per-agent token usage display (show only final summary)
102
103
  -reset Reset workflow state before running
103
104
  -reset-hard Hard reset workflow before running
104
105
  --help, -h Show help
@@ -108,7 +109,7 @@ Environment Variables:
108
109
  STATE_MACHINE_REMOTE_URL Override the default remote server URL (for local dev testing)
109
110
 
110
111
  Workflow Structure:
111
- workflows/<name>/
112
+ .workflows/<name>/
112
113
  ├── workflow.js # Native JS workflow (async/await)
113
114
  ├── config.js # Model/API key configuration
114
115
  ├── package.json # Sets "type": "module" for this workflow folder
@@ -142,7 +143,7 @@ async function confirmHardReset(workflowName) {
142
143
  }
143
144
 
144
145
  function workflowsRoot() {
145
- return path.join(process.cwd(), 'workflows');
146
+ return path.join(process.cwd(), '.workflows');
146
147
  }
147
148
 
148
149
  function resolveWorkflowDir(workflowName) {
@@ -190,11 +191,55 @@ function summarizeStatus(state) {
190
191
  return state.status ? ` [${state.status}]` : '';
191
192
  }
192
193
 
194
+ /**
195
+ * Display usage summary after workflow completion
196
+ */
197
+ function displayUsageSummary(runtime) {
198
+ const u = runtime._usageTotals;
199
+ if (!u || (!u.totalInputTokens && !u.totalOutputTokens)) return;
200
+
201
+ const C = {
202
+ bold: '\x1b[1m',
203
+ dim: '\x1b[2m',
204
+ cyan: '\x1b[36m',
205
+ reset: '\x1b[0m'
206
+ };
207
+
208
+ const formatTokens = (count) => {
209
+ if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
210
+ if (count >= 10000) return `${Math.round(count / 1000)}k`;
211
+ if (count >= 1000) return `${(count / 1000).toFixed(1)}k`;
212
+ return count.toString();
213
+ };
214
+
215
+ console.log(`\n${C.bold}Token Usage Summary${C.reset}`);
216
+ console.log(`${C.dim}${'─'.repeat(40)}${C.reset}`);
217
+ console.log(` Input: ${formatTokens(u.totalInputTokens)}`);
218
+ console.log(` Output: ${formatTokens(u.totalOutputTokens)}`);
219
+ if (u.totalCachedTokens > 0) {
220
+ console.log(` Cached: ${formatTokens(u.totalCachedTokens)}`);
221
+ }
222
+ console.log(` ${C.bold}Total: ${formatTokens(u.totalInputTokens + u.totalOutputTokens)}${C.reset}`);
223
+ if (u.totalCost > 0) {
224
+ console.log(` ${C.cyan}Cost: $${u.totalCost.toFixed(4)}${C.reset}`);
225
+ }
226
+
227
+ // Show per-model breakdown if multiple models used
228
+ const models = Object.keys(u.modelUsage || {});
229
+ if (models.length > 1) {
230
+ console.log(`\n${C.dim}By Model:${C.reset}`);
231
+ for (const model of models) {
232
+ const m = u.modelUsage[model];
233
+ console.log(` ${model}: ${formatTokens(m.inputTokens)} in / ${formatTokens(m.outputTokens)} out`);
234
+ }
235
+ }
236
+ }
237
+
193
238
  function listWorkflows() {
194
239
  const root = workflowsRoot();
195
240
 
196
241
  if (!fs.existsSync(root)) {
197
- console.log('No workflows directory found.');
242
+ console.log('No .workflows directory found.');
198
243
  console.log('Run `state-machine --setup <name>` to create your first workflow.');
199
244
  return;
200
245
  }
@@ -242,7 +287,8 @@ async function runOrResume(
242
287
  preReset = false,
243
288
  preResetHard = false,
244
289
  fullAuto = false,
245
- autoSelectDelay = null
290
+ autoSelectDelay = null,
291
+ nonVerbose = false
246
292
  } = {}
247
293
  ) {
248
294
  const workflowDir = resolveWorkflowDir(workflowName);
@@ -308,6 +354,11 @@ async function runOrResume(
308
354
  console.log(`\n\x1b[36m\x1b[1m⚡ Full-auto mode enabled\x1b[0m - Agent will auto-select recommended options after ${delay}s countdown`);
309
355
  }
310
356
 
357
+ // Set non-verbose mode from CLI flag
358
+ if (nonVerbose) {
359
+ runtime.workflowConfig.nonVerbose = true;
360
+ }
361
+
311
362
  // Prevent system sleep while workflow runs (macOS only)
312
363
  // Display can still sleep, but system stays awake for remote follow
313
364
  const stopCaffeinate = preventSleep();
@@ -317,6 +368,9 @@ async function runOrResume(
317
368
 
318
369
  try {
319
370
  await runtime.runWorkflow(workflowUrl);
371
+
372
+ // Display usage summary after workflow completion
373
+ displayUsageSummary(runtime);
320
374
  } finally {
321
375
  // Allow sleep again
322
376
  if (stopCaffeinate) {
@@ -385,6 +439,7 @@ async function main() {
385
439
  const preReset = args.includes('-reset');
386
440
  const preResetHard = args.includes('-reset-hard');
387
441
  const fullAuto = args.includes('--full-auto') || args.includes('-a');
442
+ const nonVerbose = args.includes('--non-verbose') || args.includes('-q') || args.includes('--quiet');
388
443
  const remoteEnabled = !useLocalServer; // Use Vercel if not local
389
444
 
390
445
  // Parse --delay or -d flag
@@ -405,7 +460,8 @@ async function main() {
405
460
  preReset,
406
461
  preResetHard,
407
462
  fullAuto,
408
- autoSelectDelay
463
+ autoSelectDelay,
464
+ nonVerbose
409
465
  });
410
466
  } catch (err) {
411
467
  console.error('Error:', err.message || String(err));
package/lib/file-tree.js CHANGED
@@ -21,7 +21,7 @@ export const DEFAULT_IGNORE = [
21
21
  'coverage/**',
22
22
  '.next/**',
23
23
  '.cache/**',
24
- 'workflows/**',
24
+ '.workflows/**',
25
25
  '*.log',
26
26
  '.DS_Store'
27
27
  ];
package/lib/llm.js CHANGED
@@ -12,6 +12,111 @@ import { resolveUnknownModel } from './runtime/model-resolution.js';
12
12
 
13
13
  const require = createRequire(import.meta.url);
14
14
 
15
+ /**
16
+ * Parse Claude CLI JSON output
17
+ * @param {string} output - Raw JSON output from claude --output-format json
18
+ * @returns {{ text: string, model: string|null, usage: object|null }}
19
+ */
20
+ function parseClaudeOutput(output) {
21
+ try {
22
+ const json = JSON.parse(output);
23
+ const modelUsage = json.modelUsage || {};
24
+ const modelName = Object.keys(modelUsage)[0] || null;
25
+
26
+ const usage = json.usage ? {
27
+ inputTokens: json.usage.input_tokens || 0,
28
+ outputTokens: json.usage.output_tokens || 0,
29
+ cacheReadInputTokens: json.usage.cache_read_input_tokens || 0,
30
+ cacheCreationInputTokens: json.usage.cache_creation_input_tokens || 0,
31
+ cost: json.total_cost_usd || null
32
+ } : null;
33
+
34
+ return {
35
+ text: json.result || output,
36
+ model: modelName,
37
+ usage
38
+ };
39
+ } catch {
40
+ return { text: output, model: null, usage: null };
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Parse Gemini CLI JSON output
46
+ * @param {string} output - Raw JSON output from gemini --output-format json
47
+ * @returns {{ text: string, model: string|null, usage: object|null }}
48
+ */
49
+ function parseGeminiOutput(output) {
50
+ try {
51
+ const json = JSON.parse(output);
52
+ const stats = json.stats?.models || {};
53
+ const modelName = Object.keys(stats)[0] || null;
54
+ const tokens = modelName ? stats[modelName]?.tokens || {} : {};
55
+
56
+ const usage = {
57
+ inputTokens: tokens.input || tokens.prompt || 0,
58
+ outputTokens: tokens.candidates || 0,
59
+ cachedTokens: tokens.cached || 0,
60
+ thoughtTokens: tokens.thoughts || 0
61
+ };
62
+
63
+ return {
64
+ text: json.response || output,
65
+ model: modelName,
66
+ usage
67
+ };
68
+ } catch {
69
+ return { text: output, model: null, usage: null };
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Parse Codex CLI JSON output (NDJSON format)
75
+ * @param {string} output - Raw NDJSON output from codex --json
76
+ * @returns {{ text: string, model: string|null, usage: object|null }}
77
+ */
78
+ function parseCodexOutput(output) {
79
+ const lines = output.trim().split('\n');
80
+ let text = '';
81
+ let usage = null;
82
+
83
+ for (const line of lines) {
84
+ try {
85
+ const json = JSON.parse(line);
86
+ // Extract text from agent_message items
87
+ if (json.type === 'item.completed' && json.item?.type === 'agent_message') {
88
+ text = json.item.text || text;
89
+ }
90
+ // Extract usage from turn.completed event
91
+ if (json.type === 'turn.completed' && json.usage) {
92
+ usage = {
93
+ inputTokens: json.usage.input_tokens || 0,
94
+ outputTokens: json.usage.output_tokens || 0,
95
+ cachedInputTokens: json.usage.cached_input_tokens || 0
96
+ };
97
+ }
98
+ } catch {
99
+ // Non-JSON line - might be the actual response text
100
+ if (!text && line.trim()) text = line;
101
+ }
102
+ }
103
+
104
+ return { text, model: null, usage };
105
+ }
106
+
107
+ /**
108
+ * Parse CLI output based on tool type
109
+ * @param {string} output - Raw CLI output
110
+ * @param {string} baseCmd - Base command (claude, gemini, codex)
111
+ * @returns {{ text: string, model: string|null, usage: object|null }}
112
+ */
113
+ function parseCLIOutput(output, baseCmd) {
114
+ if (baseCmd === 'claude') return parseClaudeOutput(output);
115
+ if (baseCmd === 'gemini') return parseGeminiOutput(output);
116
+ if (baseCmd === 'codex') return parseCodexOutput(output);
117
+ return { text: output.trim(), model: null, usage: null };
118
+ }
119
+
15
120
  /**
16
121
  * LLM Helper Module
17
122
  *
@@ -250,17 +355,15 @@ async function executeCLI(command, promptText, options = {}, apiKeys = {}) {
250
355
  if (baseCmd === 'claude') {
251
356
  args.push('--print');
252
357
  args.push('--permission-mode', 'acceptEdits');
358
+ args.push('--output-format', 'json');
253
359
  // Input via stdin
254
360
  } else if (baseCmd === 'gemini') {
255
361
  args.push('--approval-mode', 'auto_edit');
362
+ args.push('--output-format', 'json');
256
363
  // Input via stdin
257
364
  } else if (baseCmd === 'codex') {
258
365
  ensureCodexExec();
259
- const lastMessageFile = path.join(
260
- os.tmpdir(),
261
- `codex-last-message-${process.pid}-${Date.now()}.txt`
262
- );
263
- args.push('--output-last-message', lastMessageFile);
366
+ args.push('--json');
264
367
  args.push('-'); // Explicitly read from stdin
265
368
  } else {
266
369
  // Generic CLI: Fallback to temp file if not a known stdin consumer
@@ -310,24 +413,23 @@ async function executeCLI(command, promptText, options = {}, apiKeys = {}) {
310
413
  }
311
414
 
312
415
  if (code === 0) {
313
- if (baseCmd === 'codex') {
314
- const outputFlagIndex = args.findIndex(a => a === '--output-last-message' || a === '-o');
315
- const outputFile = outputFlagIndex >= 0 ? args[outputFlagIndex + 1] : null;
316
- if (outputFile && fs.existsSync(outputFile)) {
317
- try {
318
- stdout = fs.readFileSync(outputFile, 'utf-8');
319
- } finally {
320
- try { fs.unlinkSync(outputFile); } catch {}
321
- }
322
- }
416
+ // Parse JSON output for standard CLI tools
417
+ if (isStandardCLI) {
418
+ const parsed = parseCLIOutput(stdout, baseCmd);
419
+ resolve({
420
+ text: parsed.text,
421
+ model: parsed.model || command,
422
+ provider: 'cli',
423
+ usage: parsed.usage
424
+ });
425
+ } else {
426
+ resolve({
427
+ text: stdout.trim(),
428
+ model: command,
429
+ provider: 'cli',
430
+ usage: null
431
+ });
323
432
  }
324
-
325
- resolve({
326
- text: stdout.trim(),
327
- model: command,
328
- provider: 'cli',
329
- usage: null
330
- });
331
433
  } else {
332
434
  reject(new Error(`CLI command failed (exit ${code}): ${stderr || stdout}`));
333
435
  }
@@ -482,6 +584,16 @@ export async function llm(context, options) {
482
584
  result = await executeCLI(modelConfig, fullPrompt, options, apiKeys);
483
585
  }
484
586
 
587
+ // Record usage in agent tracker (if active)
588
+ if (result.usage) {
589
+ try {
590
+ const { recordLLMUsage } = await import('./runtime/agent.js');
591
+ recordLLMUsage(result.usage, result.model, result.provider);
592
+ } catch {
593
+ // Agent tracking not available (outside agent context)
594
+ }
595
+ }
596
+
485
597
  return { ...result, fullPrompt };
486
598
  }
487
599
 
@@ -16,6 +16,74 @@ import { withChangeTracking } from './track-changes.js';
16
16
 
17
17
  const require = createRequire(import.meta.url);
18
18
 
19
+ /**
20
+ * Token Usage Tracking
21
+ *
22
+ * Tracks LLM token usage across all calls within a single agent execution.
23
+ * The tracker is cleared before each agent runs and aggregated after completion.
24
+ */
25
+ const AGENT_USAGE_KEY = Symbol.for('agent-state-machine.agent-usage');
26
+
27
+ function getAgentUsageTracker() {
28
+ return globalThis[AGENT_USAGE_KEY] || (globalThis[AGENT_USAGE_KEY] = []);
29
+ }
30
+
31
+ export function clearAgentUsageTracker() {
32
+ globalThis[AGENT_USAGE_KEY] = [];
33
+ }
34
+
35
+ /**
36
+ * Record usage from an LLM call (called from llm.js)
37
+ */
38
+ export function recordLLMUsage(usage, model, provider) {
39
+ if (!usage) return;
40
+ const tracker = getAgentUsageTracker();
41
+ tracker.push({ usage, model, provider, timestamp: new Date().toISOString() });
42
+ }
43
+
44
+ /**
45
+ * Aggregate all recorded usage into a summary
46
+ */
47
+ export function aggregateUsage() {
48
+ const tracker = getAgentUsageTracker();
49
+ if (tracker.length === 0) return null;
50
+
51
+ const agg = {
52
+ inputTokens: 0,
53
+ outputTokens: 0,
54
+ cachedTokens: 0,
55
+ cost: 0,
56
+ calls: tracker.length,
57
+ models: {}
58
+ };
59
+
60
+ for (const { usage, model } of tracker) {
61
+ agg.inputTokens += usage.inputTokens || 0;
62
+ agg.outputTokens += usage.outputTokens || 0;
63
+ agg.cachedTokens += usage.cachedTokens || usage.cacheReadInputTokens || usage.cachedInputTokens || 0;
64
+ if (usage.cost) agg.cost += usage.cost;
65
+
66
+ const m = model || 'unknown';
67
+ if (!agg.models[m]) {
68
+ agg.models[m] = { inputTokens: 0, outputTokens: 0 };
69
+ }
70
+ agg.models[m].inputTokens += usage.inputTokens || 0;
71
+ agg.models[m].outputTokens += usage.outputTokens || 0;
72
+ }
73
+
74
+ return agg;
75
+ }
76
+
77
+ /**
78
+ * Format token count for display
79
+ */
80
+ function formatTokens(count) {
81
+ if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
82
+ if (count >= 10000) return `${Math.round(count / 1000)}k`;
83
+ if (count >= 1000) return `${(count / 1000).toFixed(1)}k`;
84
+ return count.toString();
85
+ }
86
+
19
87
  /**
20
88
  * Run an agent with context
21
89
  * @param {string} name - Agent name (file basename)
@@ -43,20 +111,49 @@ export async function agent(name, params = {}, options = {}) {
43
111
  console.log(` [Agent: ${name}] Starting...`);
44
112
  }
45
113
 
114
+ // Clear usage tracker before each attempt
115
+ clearAgentUsageTracker();
116
+
46
117
  const result = await executeAgent(runtime, name, params, options);
47
118
 
48
119
  if (result && typeof result === 'object' && result._debug_prompt) {
49
120
  delete result._debug_prompt;
50
121
  }
51
122
 
123
+ // Aggregate token usage from all LLM calls in this agent
124
+ const usage = aggregateUsage();
125
+
52
126
  console.log(` [Agent: ${name}] Completed`);
127
+
128
+ // Display token usage (unless non-verbose mode)
129
+ if (usage && !runtime.workflowConfig?.nonVerbose) {
130
+ let usageLine = ` Tokens: ${formatTokens(usage.inputTokens)} in / ${formatTokens(usage.outputTokens)} out`;
131
+ if (usage.cachedTokens > 0) {
132
+ usageLine += ` (${formatTokens(usage.cachedTokens)} cached)`;
133
+ }
134
+ if (usage.cost) {
135
+ usageLine += ` $${usage.cost.toFixed(4)}`;
136
+ }
137
+ console.log(usageLine);
138
+ }
139
+
140
+ // Get primary model from usage
141
+ const primaryModel = usage?.models ? Object.keys(usage.models)[0] : null;
142
+
53
143
  await runtime.prependHistory({
54
144
  event: 'AGENT_COMPLETED',
55
145
  agent: name,
56
146
  output: result,
57
- attempts: attempt + 1
147
+ attempts: attempt + 1,
148
+ usage: usage,
149
+ model: primaryModel
58
150
  });
59
151
 
152
+ // Update running totals
153
+ if (usage && runtime.updateUsageTotals) {
154
+ runtime.updateUsageTotals(name, usage);
155
+ }
156
+
60
157
  return result;
61
158
  } catch (error) {
62
159
  lastError = error;
@@ -68,6 +68,9 @@ export class WorkflowRuntime {
68
68
  this.status = savedState.status || 'IDLE';
69
69
  this.startedAt = savedState.startedAt || null;
70
70
 
71
+ // Load usage totals
72
+ this._usageTotals = savedState.usage || null;
73
+
71
74
  // Create memory proxy for auto-persistence
72
75
  this.memory = createMemoryProxy(this._rawMemory, () => this.persist());
73
76
 
@@ -197,12 +200,67 @@ export class WorkflowRuntime {
197
200
  memory: this._rawMemory,
198
201
  _error: this._error,
199
202
  startedAt: this.startedAt,
200
- lastUpdatedAt: new Date().toISOString()
203
+ lastUpdatedAt: new Date().toISOString(),
204
+ usage: this._usageTotals
201
205
  };
202
206
 
203
207
  fs.writeFileSync(this.stateFile, JSON.stringify(state, null, 2));
204
208
  }
205
209
 
210
+ /**
211
+ * Update running token usage totals
212
+ * @param {string} agentName - Name of the agent that generated the usage
213
+ * @param {object} usage - Usage object with inputTokens, outputTokens, etc.
214
+ */
215
+ updateUsageTotals(agentName, usage) {
216
+ if (!usage) return;
217
+
218
+ if (!this._usageTotals) {
219
+ this._usageTotals = {
220
+ totalInputTokens: 0,
221
+ totalOutputTokens: 0,
222
+ totalCachedTokens: 0,
223
+ totalCost: 0,
224
+ agentUsage: {},
225
+ modelUsage: {}
226
+ };
227
+ }
228
+
229
+ // Update totals
230
+ this._usageTotals.totalInputTokens += usage.inputTokens || 0;
231
+ this._usageTotals.totalOutputTokens += usage.outputTokens || 0;
232
+ this._usageTotals.totalCachedTokens += usage.cachedTokens || 0;
233
+ this._usageTotals.totalCost += usage.cost || 0;
234
+
235
+ // Per-agent tracking
236
+ if (!this._usageTotals.agentUsage[agentName]) {
237
+ this._usageTotals.agentUsage[agentName] = { inputTokens: 0, outputTokens: 0, calls: 0 };
238
+ }
239
+ this._usageTotals.agentUsage[agentName].inputTokens += usage.inputTokens || 0;
240
+ this._usageTotals.agentUsage[agentName].outputTokens += usage.outputTokens || 0;
241
+ this._usageTotals.agentUsage[agentName].calls += usage.calls || 1;
242
+
243
+ // Per-model tracking
244
+ if (usage.models) {
245
+ for (const [model, modelUsage] of Object.entries(usage.models)) {
246
+ if (!this._usageTotals.modelUsage[model]) {
247
+ this._usageTotals.modelUsage[model] = { inputTokens: 0, outputTokens: 0 };
248
+ }
249
+ this._usageTotals.modelUsage[model].inputTokens += modelUsage.inputTokens || 0;
250
+ this._usageTotals.modelUsage[model].outputTokens += modelUsage.outputTokens || 0;
251
+ }
252
+ }
253
+
254
+ this.persist();
255
+ }
256
+
257
+ /**
258
+ * Reset usage totals
259
+ */
260
+ resetUsageTotals() {
261
+ this._usageTotals = null;
262
+ }
263
+
206
264
  /**
207
265
  * Prepend an event to history.jsonl (newest first)
208
266
  */
@@ -562,14 +620,14 @@ export class WorkflowRuntime {
562
620
  showStatus() {
563
621
  console.log(`\n${C.bold}Workflow: ${C.cyan}${this.workflowName}${C.reset}`);
564
622
  console.log(`${C.dim}${'─'.repeat(40)}${C.reset}`);
565
-
623
+
566
624
  let statusColor = C.reset;
567
625
  if (this.status === 'COMPLETED') statusColor = C.green;
568
626
  if (this.status === 'FAILED') statusColor = C.red;
569
627
  if (this.status === 'STOPPED') statusColor = C.yellow;
570
628
  if (this.status === 'RUNNING') statusColor = C.blue;
571
629
  if (this.status === 'IDLE') statusColor = C.gray;
572
-
630
+
573
631
  console.log(`Status: ${statusColor}${this.status}${C.reset}`);
574
632
 
575
633
  if (this.startedAt) {
@@ -580,6 +638,36 @@ export class WorkflowRuntime {
580
638
  console.log(`Error: ${C.red}${this._error}${C.reset}`);
581
639
  }
582
640
 
641
+ // Display token usage if available
642
+ if (this._usageTotals && (this._usageTotals.totalInputTokens > 0 || this._usageTotals.totalOutputTokens > 0)) {
643
+ console.log(`\n${C.bold}Token Usage:${C.reset}`);
644
+ const formatTokens = (count) => {
645
+ if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
646
+ if (count >= 10000) return `${Math.round(count / 1000)}k`;
647
+ if (count >= 1000) return `${(count / 1000).toFixed(1)}k`;
648
+ return count.toString();
649
+ };
650
+ console.log(` Input: ${formatTokens(this._usageTotals.totalInputTokens)}`);
651
+ console.log(` Output: ${formatTokens(this._usageTotals.totalOutputTokens)}`);
652
+ if (this._usageTotals.totalCachedTokens > 0) {
653
+ console.log(` Cached: ${formatTokens(this._usageTotals.totalCachedTokens)}`);
654
+ }
655
+ console.log(` ${C.bold}Total: ${formatTokens(this._usageTotals.totalInputTokens + this._usageTotals.totalOutputTokens)}${C.reset}`);
656
+ if (this._usageTotals.totalCost > 0) {
657
+ console.log(` ${C.cyan}Cost: $${this._usageTotals.totalCost.toFixed(4)}${C.reset}`);
658
+ }
659
+
660
+ // Show per-model breakdown if multiple models used
661
+ const models = Object.keys(this._usageTotals.modelUsage || {});
662
+ if (models.length > 1) {
663
+ console.log(`\n${C.dim}By Model:${C.reset}`);
664
+ for (const model of models) {
665
+ const m = this._usageTotals.modelUsage[model];
666
+ console.log(` ${model}: ${formatTokens(m.inputTokens)} in / ${formatTokens(m.outputTokens)} out`);
667
+ }
668
+ }
669
+ }
670
+
583
671
  const memoryKeys = Object.keys(this._rawMemory).filter((k) => !k.startsWith('_'));
584
672
  console.log(`\nMemory Keys: ${C.yellow}${memoryKeys.length}${C.reset}`);
585
673
  if (memoryKeys.length > 0) {
@@ -633,6 +721,7 @@ export class WorkflowRuntime {
633
721
  this._error = null;
634
722
  this.status = 'IDLE';
635
723
  this.startedAt = null;
724
+ this.resetUsageTotals();
636
725
 
637
726
  // Recreate memory proxy
638
727
  this.memory = createMemoryProxy(this._rawMemory, () => this.persist());
@@ -663,6 +752,7 @@ export class WorkflowRuntime {
663
752
  this._error = null;
664
753
  this.status = 'IDLE';
665
754
  this.startedAt = null;
755
+ this.resetUsageTotals();
666
756
 
667
757
  // Recreate memory proxy
668
758
  this.memory = createMemoryProxy(this._rawMemory, () => this.persist());
package/lib/setup.js CHANGED
@@ -70,7 +70,7 @@ function copyTemplateDir(srcDir, destDir, replacements, createdPaths) {
70
70
  * Setup a new workflow with directory structure
71
71
  */
72
72
  async function setup(workflowName, options = {}) {
73
- const workflowsDir = path.join(process.cwd(), 'workflows');
73
+ const workflowsDir = path.join(process.cwd(), '.workflows');
74
74
  const workflowDir = path.join(workflowsDir, workflowName);
75
75
  const templateName = options.template || DEFAULT_TEMPLATE;
76
76
 
@@ -111,9 +111,9 @@ async function setup(workflowName, options = {}) {
111
111
  console.log('─'.repeat(40));
112
112
  console.log(`\n✓ Workflow '${workflowName}' created successfully!\n`);
113
113
  console.log('Next steps:');
114
- console.log(` 1. Edit workflows/${workflowName}/workflow.js to implement your flow`);
115
- console.log(` 2. Edit workflows/${workflowName}/config.js to set models/API keys`);
116
- console.log(` 3. Add custom agents in workflows/${workflowName}/agents/`);
114
+ console.log(` 1. Edit .workflows/${workflowName}/workflow.js to implement your flow`);
115
+ console.log(` 2. Edit .workflows/${workflowName}/config.js to set models/API keys`);
116
+ console.log(` 3. Add custom agents in .workflows/${workflowName}/agents/`);
117
117
  console.log(` 4. Run: state-machine run ${workflowName}\n`);
118
118
  }
119
119
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-state-machine",
3
- "version": "2.2.2",
3
+ "version": "2.4.0",
4
4
  "type": "module",
5
5
  "description": "A workflow orchestrator for running agents and scripts in sequence with state management",
6
6
  "main": "lib/index.js",
@@ -1,9 +1,9 @@
1
1
  export const config = {
2
2
  models: {
3
- fast: "gemini -m gemini-2.5-flash-lite",
4
- low: "gemini -m gemini-2.5-flash-lite",
5
- med: "gemini -m gemini-2.5-flash-lite",
6
- high: "gemini -m gemini-2.5-flash-lite",
3
+ fast: "gemini -m gemini-2.5-pro",
4
+ low: "gemini -m gemini-2.5-pro",
5
+ med: "gemini -m gemini-2.5-pro",
6
+ high: "gemini -m gemini-2.5-pro",
7
7
  },
8
8
  apiKeys: {
9
9
  gemini: process.env.GEMINI_API_KEY,