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 +61 -5
- package/lib/file-tree.js +1 -1
- package/lib/llm.js +134 -22
- package/lib/runtime/agent.js +98 -1
- package/lib/runtime/runtime.js +93 -3
- package/lib/setup.js +4 -4
- package/package.json +1 -1
- package/templates/project-builder/config.js +4 -4
- package/vercel-server/public/remote/assets/index-Bnvi3AUu.js +173 -0
- package/vercel-server/public/remote/assets/index-DH2uv4Ll.css +1 -0
- package/vercel-server/public/remote/index.html +2 -2
- package/vercel-server/ui/src/App.jsx +1 -16
- package/vercel-server/ui/src/components/ContentCard.jsx +177 -16
- package/vercel-server/ui/src/components/Footer.jsx +1 -6
- package/vercel-server/ui/src/index.css +53 -0
- package/vercel-server/public/remote/assets/index-BsJsLDKc.css +0 -1
- package/vercel-server/public/remote/assets/index-CmtT6ADh.js +0 -168
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
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
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
|
package/lib/runtime/agent.js
CHANGED
|
@@ -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;
|
package/lib/runtime/runtime.js
CHANGED
|
@@ -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,9 +1,9 @@
|
|
|
1
1
|
export const config = {
|
|
2
2
|
models: {
|
|
3
|
-
fast: "gemini -m gemini-2.5-
|
|
4
|
-
low: "gemini -m gemini-2.5-
|
|
5
|
-
med: "gemini -m gemini-2.5-
|
|
6
|
-
high: "gemini -m gemini-2.5-
|
|
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,
|