@orchagent/cli 0.3.87 → 0.3.88
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/dist/commands/index.js +6 -0
- package/dist/commands/metrics.js +137 -0
- package/dist/commands/replay.js +198 -0
- package/dist/commands/trace.js +311 -0
- package/package.json +1 -1
package/dist/commands/index.js
CHANGED
|
@@ -40,6 +40,9 @@ const diff_1 = require("./diff");
|
|
|
40
40
|
const health_1 = require("./health");
|
|
41
41
|
const dev_1 = require("./dev");
|
|
42
42
|
const estimate_1 = require("./estimate");
|
|
43
|
+
const replay_1 = require("./replay");
|
|
44
|
+
const trace_1 = require("./trace");
|
|
45
|
+
const metrics_1 = require("./metrics");
|
|
43
46
|
function registerCommands(program) {
|
|
44
47
|
(0, login_1.registerLoginCommand)(program);
|
|
45
48
|
(0, logout_1.registerLogoutCommand)(program);
|
|
@@ -80,4 +83,7 @@ function registerCommands(program) {
|
|
|
80
83
|
(0, health_1.registerHealthCommand)(program);
|
|
81
84
|
(0, dev_1.registerDevCommand)(program);
|
|
82
85
|
(0, estimate_1.registerEstimateCommand)(program);
|
|
86
|
+
(0, replay_1.registerReplayCommand)(program);
|
|
87
|
+
(0, trace_1.registerTraceCommand)(program);
|
|
88
|
+
(0, metrics_1.registerMetricsCommand)(program);
|
|
83
89
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerMetricsCommand = registerMetricsCommand;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const config_1 = require("../lib/config");
|
|
9
|
+
const api_1 = require("../lib/api");
|
|
10
|
+
const errors_1 = require("../lib/errors");
|
|
11
|
+
const output_1 = require("../lib/output");
|
|
12
|
+
// ============================================
|
|
13
|
+
// HELPERS
|
|
14
|
+
// ============================================
|
|
15
|
+
async function resolveWorkspaceId(config, slug) {
|
|
16
|
+
const configFile = await (0, config_1.loadConfig)();
|
|
17
|
+
const targetSlug = slug ?? configFile.workspace;
|
|
18
|
+
if (!targetSlug) {
|
|
19
|
+
throw new errors_1.CliError('No workspace specified. Use --workspace <slug> or run `orch workspace use <slug>` first.');
|
|
20
|
+
}
|
|
21
|
+
const response = await (0, api_1.request)(config, 'GET', '/workspaces');
|
|
22
|
+
const workspace = response.workspaces.find((w) => w.slug === targetSlug);
|
|
23
|
+
if (!workspace) {
|
|
24
|
+
throw new errors_1.CliError(`Workspace '${targetSlug}' not found.`);
|
|
25
|
+
}
|
|
26
|
+
return workspace.id;
|
|
27
|
+
}
|
|
28
|
+
function formatDuration(ms) {
|
|
29
|
+
if (ms === 0)
|
|
30
|
+
return '-';
|
|
31
|
+
if (ms < 1000)
|
|
32
|
+
return `${ms}ms`;
|
|
33
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
34
|
+
}
|
|
35
|
+
function rateColor(rate) {
|
|
36
|
+
if (rate >= 95)
|
|
37
|
+
return chalk_1.default.green(`${rate}%`);
|
|
38
|
+
if (rate >= 80)
|
|
39
|
+
return chalk_1.default.yellow(`${rate}%`);
|
|
40
|
+
return chalk_1.default.red(`${rate}%`);
|
|
41
|
+
}
|
|
42
|
+
function padRight(str, len) {
|
|
43
|
+
return str.length >= len ? str.slice(0, len) : str + ' '.repeat(len - str.length);
|
|
44
|
+
}
|
|
45
|
+
function padLeft(str, len) {
|
|
46
|
+
return str.length >= len ? str.slice(0, len) : ' '.repeat(len - str.length) + str;
|
|
47
|
+
}
|
|
48
|
+
// ============================================
|
|
49
|
+
// COMMAND
|
|
50
|
+
// ============================================
|
|
51
|
+
function registerMetricsCommand(program) {
|
|
52
|
+
program
|
|
53
|
+
.command('metrics')
|
|
54
|
+
.description('Show agent performance metrics for a workspace')
|
|
55
|
+
.option('--workspace <slug>', 'Workspace slug (default: active workspace)')
|
|
56
|
+
.option('--days <n>', 'Number of days to analyze', '30')
|
|
57
|
+
.option('--agent <name>', 'Filter to a specific agent')
|
|
58
|
+
.option('--json', 'Output as JSON')
|
|
59
|
+
.action(async (options) => {
|
|
60
|
+
const config = await (0, config_1.getResolvedConfig)();
|
|
61
|
+
const workspaceId = await resolveWorkspaceId(config, options.workspace);
|
|
62
|
+
const days = parseInt(options.days || '30', 10);
|
|
63
|
+
if (isNaN(days) || days < 1 || days > 365) {
|
|
64
|
+
process.stderr.write(chalk_1.default.red('Error: --days must be between 1 and 365\n'));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
const params = new URLSearchParams({ days: String(days) });
|
|
68
|
+
if (options.agent)
|
|
69
|
+
params.set('agent_name', options.agent);
|
|
70
|
+
let data;
|
|
71
|
+
try {
|
|
72
|
+
data = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/metrics/dashboard?${params}`, { headers: { 'X-Workspace-Id': workspaceId } });
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (err instanceof api_1.ApiError && err.status === 403) {
|
|
76
|
+
process.stderr.write(chalk_1.default.red('Error: Not a member of this workspace\n'));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
if (options.json) {
|
|
82
|
+
(0, output_1.printJson)(data);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const { overview, agents } = data;
|
|
86
|
+
// Header
|
|
87
|
+
process.stdout.write('\n');
|
|
88
|
+
process.stdout.write(chalk_1.default.bold('Agent Metrics') + chalk_1.default.gray(` (last ${days}d)\n`));
|
|
89
|
+
process.stdout.write('='.repeat(50) + '\n\n');
|
|
90
|
+
// Overview stats
|
|
91
|
+
if (overview.total_runs === 0) {
|
|
92
|
+
process.stdout.write(chalk_1.default.yellow('No runs in this period.\n\n'));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
process.stdout.write(` Total Runs: ${chalk_1.default.bold(String(overview.total_runs))}\n`);
|
|
96
|
+
process.stdout.write(` Success Rate: ${rateColor(overview.success_rate)}\n`);
|
|
97
|
+
process.stdout.write(` Error Rate: ${overview.error_rate}% ${chalk_1.default.gray(`(${overview.failed} failed, ${overview.timeout} timeout)`)}\n`);
|
|
98
|
+
process.stdout.write(` p50 Latency: ${chalk_1.default.cyan(formatDuration(overview.p50_latency_ms))}\n`);
|
|
99
|
+
process.stdout.write(` p95 Latency: ${chalk_1.default.yellow(formatDuration(overview.p95_latency_ms))}\n`);
|
|
100
|
+
process.stdout.write(` Avg Latency: ${formatDuration(overview.avg_latency_ms)}\n`);
|
|
101
|
+
process.stdout.write(` Runs/Day: ${overview.runs_per_day}\n`);
|
|
102
|
+
// Per-agent table
|
|
103
|
+
if (agents.length > 0) {
|
|
104
|
+
process.stdout.write(`\n${chalk_1.default.bold('Per Agent')}\n`);
|
|
105
|
+
process.stdout.write('-'.repeat(90) + '\n');
|
|
106
|
+
// Header
|
|
107
|
+
process.stdout.write(padRight('Agent', 25) +
|
|
108
|
+
padLeft('Runs', 8) +
|
|
109
|
+
padLeft('Success', 10) +
|
|
110
|
+
padLeft('p50', 10) +
|
|
111
|
+
padLeft('p95', 10) +
|
|
112
|
+
padLeft('Errors', 8) +
|
|
113
|
+
' Top Error\n');
|
|
114
|
+
process.stdout.write('-'.repeat(90) + '\n');
|
|
115
|
+
for (const agent of agents) {
|
|
116
|
+
const nameStr = agent.agent_name.length > 22
|
|
117
|
+
? agent.agent_name.slice(0, 22) + '..'
|
|
118
|
+
: agent.agent_name;
|
|
119
|
+
const errorStr = agent.top_error
|
|
120
|
+
? agent.top_error.slice(0, 20)
|
|
121
|
+
: '-';
|
|
122
|
+
process.stdout.write(padRight(nameStr, 25) +
|
|
123
|
+
padLeft(String(agent.total_runs), 8) +
|
|
124
|
+
padLeft(agent.success_rate >= 95 ? chalk_1.default.green(`${agent.success_rate}%`) :
|
|
125
|
+
agent.success_rate >= 80 ? chalk_1.default.yellow(`${agent.success_rate}%`) :
|
|
126
|
+
chalk_1.default.red(`${agent.success_rate}%`),
|
|
127
|
+
// chalk adds escape chars — pad the raw value, then colorize
|
|
128
|
+
10) +
|
|
129
|
+
padLeft(formatDuration(agent.p50_latency_ms), 10) +
|
|
130
|
+
padLeft(formatDuration(agent.p95_latency_ms), 10) +
|
|
131
|
+
padLeft(String(agent.failed + agent.timeout), 8) +
|
|
132
|
+
' ' + chalk_1.default.gray(errorStr) + '\n');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
process.stdout.write('\n');
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerReplayCommand = registerReplayCommand;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const config_1 = require("../lib/config");
|
|
9
|
+
const api_1 = require("../lib/api");
|
|
10
|
+
const errors_1 = require("../lib/errors");
|
|
11
|
+
const output_1 = require("../lib/output");
|
|
12
|
+
// ============================================
|
|
13
|
+
// HELPERS
|
|
14
|
+
// ============================================
|
|
15
|
+
async function resolveWorkspaceId(config, slug) {
|
|
16
|
+
const configFile = await (0, config_1.loadConfig)();
|
|
17
|
+
const targetSlug = slug ?? configFile.workspace;
|
|
18
|
+
if (!targetSlug) {
|
|
19
|
+
throw new errors_1.CliError('No workspace specified. Use --workspace <slug> or run `orch workspace use <slug>` first.');
|
|
20
|
+
}
|
|
21
|
+
const response = await (0, api_1.request)(config, 'GET', '/workspaces');
|
|
22
|
+
const workspace = response.workspaces.find((w) => w.slug === targetSlug);
|
|
23
|
+
if (!workspace) {
|
|
24
|
+
throw new errors_1.CliError(`Workspace '${targetSlug}' not found.`);
|
|
25
|
+
}
|
|
26
|
+
return workspace.id;
|
|
27
|
+
}
|
|
28
|
+
function isUuid(value) {
|
|
29
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
30
|
+
}
|
|
31
|
+
function isShortUuid(value) {
|
|
32
|
+
return /^[0-9a-f]{7,}$/i.test(value) && !value.includes('/');
|
|
33
|
+
}
|
|
34
|
+
async function resolveShortRunId(config, workspaceId, shortId) {
|
|
35
|
+
const result = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/runs?limit=200&run_id_prefix=${encodeURIComponent(shortId)}`);
|
|
36
|
+
if (result.runs.length === 0) {
|
|
37
|
+
throw new errors_1.CliError(`No run found matching '${shortId}'.`);
|
|
38
|
+
}
|
|
39
|
+
if (result.runs.length > 1) {
|
|
40
|
+
throw new errors_1.CliError(`Ambiguous run ID '${shortId}' — matches ${result.runs.length} runs. Use more characters to narrow it down.`);
|
|
41
|
+
}
|
|
42
|
+
return result.runs[0].id;
|
|
43
|
+
}
|
|
44
|
+
function statusColor(status) {
|
|
45
|
+
if (!status)
|
|
46
|
+
return '-';
|
|
47
|
+
switch (status) {
|
|
48
|
+
case 'completed':
|
|
49
|
+
return chalk_1.default.green(status);
|
|
50
|
+
case 'failed':
|
|
51
|
+
return chalk_1.default.red(status);
|
|
52
|
+
case 'running':
|
|
53
|
+
case 'queued':
|
|
54
|
+
case 'claimed':
|
|
55
|
+
return chalk_1.default.yellow(status);
|
|
56
|
+
case 'timeout':
|
|
57
|
+
return chalk_1.default.red(status);
|
|
58
|
+
default:
|
|
59
|
+
return status;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function formatDuration(ms) {
|
|
63
|
+
if (ms == null)
|
|
64
|
+
return '-';
|
|
65
|
+
if (ms < 1000)
|
|
66
|
+
return `${ms}ms`;
|
|
67
|
+
if (ms < 60000)
|
|
68
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
69
|
+
return `${(ms / 60000).toFixed(1)}m`;
|
|
70
|
+
}
|
|
71
|
+
const POLL_INTERVAL_MS = 2000;
|
|
72
|
+
const MAX_POLL_MS = 600000; // 10 minutes
|
|
73
|
+
// ============================================
|
|
74
|
+
// COMMAND REGISTRATION
|
|
75
|
+
// ============================================
|
|
76
|
+
function registerReplayCommand(program) {
|
|
77
|
+
program
|
|
78
|
+
.command('replay <run-id>')
|
|
79
|
+
.description('Replay a previous run. Re-executes with the same input and config from the original snapshot.')
|
|
80
|
+
.option('--workspace <slug>', 'Workspace slug (default: current workspace)')
|
|
81
|
+
.option('--reason <text>', 'Reason for replay (stored in audit log)')
|
|
82
|
+
.option('--override-policy <id>', 'Override provider policy ID for this replay')
|
|
83
|
+
.option('--no-wait', 'Queue the replay and return immediately without waiting for results')
|
|
84
|
+
.option('--json', 'Output as JSON')
|
|
85
|
+
.action(async (runId, options) => {
|
|
86
|
+
const config = await (0, config_1.getResolvedConfig)();
|
|
87
|
+
if (!config.apiKey) {
|
|
88
|
+
throw new errors_1.CliError('Missing API key. Run `orch login` first.');
|
|
89
|
+
}
|
|
90
|
+
const workspaceId = await resolveWorkspaceId(config, options.workspace);
|
|
91
|
+
// Resolve short run IDs
|
|
92
|
+
let resolvedRunId = runId;
|
|
93
|
+
if (isUuid(runId)) {
|
|
94
|
+
resolvedRunId = runId;
|
|
95
|
+
}
|
|
96
|
+
else if (isShortUuid(runId)) {
|
|
97
|
+
resolvedRunId = await resolveShortRunId(config, workspaceId, runId);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
throw new errors_1.CliError(`Invalid run ID '${runId}'. Provide a full UUID or a short hex prefix (7+ characters).`);
|
|
101
|
+
}
|
|
102
|
+
// Submit replay request
|
|
103
|
+
const body = {};
|
|
104
|
+
if (options.reason)
|
|
105
|
+
body.reason = options.reason;
|
|
106
|
+
if (options.overridePolicy)
|
|
107
|
+
body.override_provider_policy_id = options.overridePolicy;
|
|
108
|
+
const replay = await (0, api_1.request)(config, 'POST', `/workspaces/${workspaceId}/runs/${resolvedRunId}/replay`, {
|
|
109
|
+
body: JSON.stringify(body),
|
|
110
|
+
headers: { 'Content-Type': 'application/json' },
|
|
111
|
+
});
|
|
112
|
+
if (options.json && options.wait === false) {
|
|
113
|
+
(0, output_1.printJson)(replay);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (options.wait === false) {
|
|
117
|
+
process.stdout.write(chalk_1.default.green('Replay queued.\n') +
|
|
118
|
+
` Run ID: ${replay.run_id}\n` +
|
|
119
|
+
` Job ID: ${replay.job_id}\n` +
|
|
120
|
+
` Replaying: ${replay.replay_of_run_id}\n` +
|
|
121
|
+
chalk_1.default.gray('\nCheck status: orch logs ' + replay.run_id.slice(0, 8) + '\n'));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// Poll for completion
|
|
125
|
+
if (!options.json) {
|
|
126
|
+
process.stderr.write(chalk_1.default.gray(`Replaying run ${resolvedRunId.slice(0, 8)}... `));
|
|
127
|
+
}
|
|
128
|
+
const startTime = Date.now();
|
|
129
|
+
let lastStatus = 'queued';
|
|
130
|
+
let pollResult = null;
|
|
131
|
+
while (Date.now() - startTime < MAX_POLL_MS) {
|
|
132
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
133
|
+
try {
|
|
134
|
+
pollResult = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/runs/${replay.run_id}`);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Run may not exist yet if job worker hasn't picked it up
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
lastStatus = pollResult.status;
|
|
141
|
+
if (!options.json) {
|
|
142
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
|
|
143
|
+
process.stderr.write(`\r${chalk_1.default.gray(`Replaying run ${resolvedRunId.slice(0, 8)}... ${statusColor(lastStatus)} (${elapsed}s)`)}`);
|
|
144
|
+
}
|
|
145
|
+
if (['completed', 'failed', 'timeout', 'dead_letter', 'cancelled'].includes(lastStatus)) {
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (!options.json) {
|
|
150
|
+
process.stderr.write('\n');
|
|
151
|
+
}
|
|
152
|
+
// Fetch final logs
|
|
153
|
+
const logs = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/runs/${replay.run_id}/logs`);
|
|
154
|
+
if (options.json) {
|
|
155
|
+
(0, output_1.printJson)({
|
|
156
|
+
replay: {
|
|
157
|
+
run_id: replay.run_id,
|
|
158
|
+
replay_of_run_id: replay.replay_of_run_id,
|
|
159
|
+
job_id: replay.job_id,
|
|
160
|
+
},
|
|
161
|
+
result: logs,
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// Render results
|
|
166
|
+
renderReplayResult(replay, logs);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
// ============================================
|
|
170
|
+
// RENDER REPLAY RESULT
|
|
171
|
+
// ============================================
|
|
172
|
+
function renderReplayResult(replay, logs) {
|
|
173
|
+
process.stdout.write(chalk_1.default.bold(`\nReplay ${replay.run_id}\n`) +
|
|
174
|
+
` Original: ${replay.replay_of_run_id.slice(0, 8)}\n` +
|
|
175
|
+
` Agent: ${logs.agent_name ?? '-'}@${logs.agent_version ?? '-'}\n` +
|
|
176
|
+
` Status: ${statusColor(logs.run_status)}\n` +
|
|
177
|
+
` Duration: ${formatDuration(logs.execution_time_ms)}\n`);
|
|
178
|
+
if (logs.exit_code != null) {
|
|
179
|
+
const exitLabel = logs.exit_code === 0 ? chalk_1.default.green(String(logs.exit_code)) : chalk_1.default.red(String(logs.exit_code));
|
|
180
|
+
process.stdout.write(` Exit code: ${exitLabel}\n`);
|
|
181
|
+
}
|
|
182
|
+
if (logs.output_data != null && Object.keys(logs.output_data).length > 0) {
|
|
183
|
+
process.stdout.write('\n' + chalk_1.default.bold.green('--- output ---') + '\n' +
|
|
184
|
+
JSON.stringify(logs.output_data, null, 2) + '\n');
|
|
185
|
+
}
|
|
186
|
+
if (logs.error_message) {
|
|
187
|
+
process.stdout.write('\n' + chalk_1.default.red.bold('Error:\n') + chalk_1.default.red(logs.error_message) + '\n');
|
|
188
|
+
}
|
|
189
|
+
if (logs.stdout) {
|
|
190
|
+
process.stdout.write('\n' + chalk_1.default.bold.cyan('--- stdout ---') + '\n' + logs.stdout + '\n');
|
|
191
|
+
}
|
|
192
|
+
if (logs.stderr) {
|
|
193
|
+
process.stdout.write('\n' + chalk_1.default.bold.yellow('--- stderr ---') + '\n' + logs.stderr + '\n');
|
|
194
|
+
}
|
|
195
|
+
process.stdout.write('\n');
|
|
196
|
+
// Footer hint
|
|
197
|
+
process.stdout.write(chalk_1.default.gray(`View trace: orch trace ${replay.run_id.slice(0, 8)}\n`));
|
|
198
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerTraceCommand = registerTraceCommand;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const config_1 = require("../lib/config");
|
|
9
|
+
const api_1 = require("../lib/api");
|
|
10
|
+
const errors_1 = require("../lib/errors");
|
|
11
|
+
const output_1 = require("../lib/output");
|
|
12
|
+
// ============================================
|
|
13
|
+
// HELPERS
|
|
14
|
+
// ============================================
|
|
15
|
+
async function resolveWorkspaceId(config, slug) {
|
|
16
|
+
const configFile = await (0, config_1.loadConfig)();
|
|
17
|
+
const targetSlug = slug ?? configFile.workspace;
|
|
18
|
+
if (!targetSlug) {
|
|
19
|
+
throw new errors_1.CliError('No workspace specified. Use --workspace <slug> or run `orch workspace use <slug>` first.');
|
|
20
|
+
}
|
|
21
|
+
const response = await (0, api_1.request)(config, 'GET', '/workspaces');
|
|
22
|
+
const workspace = response.workspaces.find((w) => w.slug === targetSlug);
|
|
23
|
+
if (!workspace) {
|
|
24
|
+
throw new errors_1.CliError(`Workspace '${targetSlug}' not found.`);
|
|
25
|
+
}
|
|
26
|
+
return workspace.id;
|
|
27
|
+
}
|
|
28
|
+
function isUuid(value) {
|
|
29
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
30
|
+
}
|
|
31
|
+
function isShortUuid(value) {
|
|
32
|
+
return /^[0-9a-f]{7,}$/i.test(value) && !value.includes('/');
|
|
33
|
+
}
|
|
34
|
+
async function resolveShortRunId(config, workspaceId, shortId) {
|
|
35
|
+
const result = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/runs?limit=200&run_id_prefix=${encodeURIComponent(shortId)}`);
|
|
36
|
+
if (result.runs.length === 0) {
|
|
37
|
+
throw new errors_1.CliError(`No run found matching '${shortId}'.`);
|
|
38
|
+
}
|
|
39
|
+
if (result.runs.length > 1) {
|
|
40
|
+
throw new errors_1.CliError(`Ambiguous run ID '${shortId}' — matches ${result.runs.length} runs. Use more characters to narrow it down.`);
|
|
41
|
+
}
|
|
42
|
+
return result.runs[0].id;
|
|
43
|
+
}
|
|
44
|
+
function statusColor(status) {
|
|
45
|
+
if (!status)
|
|
46
|
+
return '-';
|
|
47
|
+
switch (status) {
|
|
48
|
+
case 'completed':
|
|
49
|
+
return chalk_1.default.green(status);
|
|
50
|
+
case 'failed':
|
|
51
|
+
return chalk_1.default.red(status);
|
|
52
|
+
case 'running':
|
|
53
|
+
return chalk_1.default.yellow(status);
|
|
54
|
+
case 'timeout':
|
|
55
|
+
return chalk_1.default.red(status);
|
|
56
|
+
default:
|
|
57
|
+
return status;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function formatDuration(ms) {
|
|
61
|
+
if (ms == null)
|
|
62
|
+
return '-';
|
|
63
|
+
if (ms < 1000)
|
|
64
|
+
return `${ms}ms`;
|
|
65
|
+
if (ms < 60000)
|
|
66
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
67
|
+
return `${(ms / 60000).toFixed(1)}m`;
|
|
68
|
+
}
|
|
69
|
+
function formatCost(usd) {
|
|
70
|
+
if (usd == null || usd === 0)
|
|
71
|
+
return '-';
|
|
72
|
+
if (usd < 0.01)
|
|
73
|
+
return `$${usd.toFixed(6)}`;
|
|
74
|
+
return `$${usd.toFixed(4)}`;
|
|
75
|
+
}
|
|
76
|
+
function formatTokens(input, output) {
|
|
77
|
+
const parts = [];
|
|
78
|
+
if (input)
|
|
79
|
+
parts.push(`${input.toLocaleString()} in`);
|
|
80
|
+
if (output)
|
|
81
|
+
parts.push(`${output.toLocaleString()} out`);
|
|
82
|
+
return parts.length > 0 ? parts.join(', ') : '-';
|
|
83
|
+
}
|
|
84
|
+
// ============================================
|
|
85
|
+
// COMMAND REGISTRATION
|
|
86
|
+
// ============================================
|
|
87
|
+
function registerTraceCommand(program) {
|
|
88
|
+
program
|
|
89
|
+
.command('trace <run-id>')
|
|
90
|
+
.description('View the execution trace for a run. Shows LLM calls, tool calls, decisions, and errors in timeline order.')
|
|
91
|
+
.option('--workspace <slug>', 'Workspace slug (default: current workspace)')
|
|
92
|
+
.option('--json', 'Output as JSON')
|
|
93
|
+
.action(async (runId, options) => {
|
|
94
|
+
const config = await (0, config_1.getResolvedConfig)();
|
|
95
|
+
if (!config.apiKey) {
|
|
96
|
+
throw new errors_1.CliError('Missing API key. Run `orch login` first.');
|
|
97
|
+
}
|
|
98
|
+
const workspaceId = await resolveWorkspaceId(config, options.workspace);
|
|
99
|
+
// Resolve short run IDs
|
|
100
|
+
let resolvedRunId = runId;
|
|
101
|
+
if (isUuid(runId)) {
|
|
102
|
+
resolvedRunId = runId;
|
|
103
|
+
}
|
|
104
|
+
else if (isShortUuid(runId)) {
|
|
105
|
+
resolvedRunId = await resolveShortRunId(config, workspaceId, runId);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
throw new errors_1.CliError(`Invalid run ID '${runId}'. Provide a full UUID or a short hex prefix (7+ characters).`);
|
|
109
|
+
}
|
|
110
|
+
// Fetch run detail for context
|
|
111
|
+
const run = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/runs/${resolvedRunId}`);
|
|
112
|
+
// Fetch trace header
|
|
113
|
+
let trace;
|
|
114
|
+
try {
|
|
115
|
+
const traceResp = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/runs/${resolvedRunId}/trace`);
|
|
116
|
+
trace = traceResp.trace;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
throw new errors_1.CliError(`No trace available for run ${resolvedRunId.slice(0, 8)}. Traces are captured for cloud runs only.`);
|
|
120
|
+
}
|
|
121
|
+
// Fetch all trace events (paginate if needed)
|
|
122
|
+
const allEvents = [];
|
|
123
|
+
let offset = 0;
|
|
124
|
+
const pageSize = 500;
|
|
125
|
+
while (true) {
|
|
126
|
+
const eventsResp = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/traces/${trace.id}/events?limit=${pageSize}&offset=${offset}`);
|
|
127
|
+
allEvents.push(...eventsResp.events);
|
|
128
|
+
if (eventsResp.next_cursor === null || allEvents.length >= eventsResp.total) {
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
offset = parseInt(eventsResp.next_cursor, 10);
|
|
132
|
+
}
|
|
133
|
+
if (options.json) {
|
|
134
|
+
(0, output_1.printJson)({ run, trace, events: allEvents });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
renderTrace(run, trace, allEvents);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
// ============================================
|
|
141
|
+
// TRACE RENDERING
|
|
142
|
+
// ============================================
|
|
143
|
+
function renderTrace(run, trace, events) {
|
|
144
|
+
// Header
|
|
145
|
+
process.stdout.write(chalk_1.default.bold(`\nTrace for run ${run.id}\n`) +
|
|
146
|
+
` Agent: ${run.agent_name ?? '-'}@${run.agent_version ?? '-'}\n` +
|
|
147
|
+
` Status: ${statusColor(run.status)}\n` +
|
|
148
|
+
` Duration: ${formatDuration(run.duration_ms)}\n` +
|
|
149
|
+
` Source: ${run.trigger_source ?? '-'}\n`);
|
|
150
|
+
// Aggregate stats
|
|
151
|
+
const stats = computeStats(events);
|
|
152
|
+
if (stats.totalLlmCalls > 0 || stats.totalToolCalls > 0) {
|
|
153
|
+
process.stdout.write('\n' + chalk_1.default.bold(' Summary\n'));
|
|
154
|
+
if (stats.totalLlmCalls > 0) {
|
|
155
|
+
process.stdout.write(` LLM calls: ${stats.totalLlmCalls}` +
|
|
156
|
+
` (${formatTokens(stats.totalTokenInput, stats.totalTokenOutput)})` +
|
|
157
|
+
` cost: ${formatCost(stats.totalCostUsd)}\n`);
|
|
158
|
+
}
|
|
159
|
+
if (stats.totalToolCalls > 0) {
|
|
160
|
+
process.stdout.write(` Tool calls: ${stats.totalToolCalls}\n`);
|
|
161
|
+
}
|
|
162
|
+
if (stats.providers.length > 0) {
|
|
163
|
+
process.stdout.write(` Providers: ${stats.providers.join(', ')}\n`);
|
|
164
|
+
}
|
|
165
|
+
if (stats.totalErrors > 0) {
|
|
166
|
+
process.stdout.write(` Errors: ${chalk_1.default.red(String(stats.totalErrors))}\n`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (events.length === 0) {
|
|
170
|
+
process.stdout.write(chalk_1.default.gray('\n No trace events recorded.\n\n'));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
// Timeline
|
|
174
|
+
process.stdout.write('\n' + chalk_1.default.bold(' Timeline\n'));
|
|
175
|
+
for (const event of events) {
|
|
176
|
+
renderEvent(event);
|
|
177
|
+
}
|
|
178
|
+
process.stdout.write('\n');
|
|
179
|
+
// Footer hint
|
|
180
|
+
process.stdout.write(chalk_1.default.gray(`View logs: orch logs ${run.id.slice(0, 8)}\n`));
|
|
181
|
+
process.stdout.write(chalk_1.default.gray(`Replay: orch replay ${run.id.slice(0, 8)}\n`));
|
|
182
|
+
}
|
|
183
|
+
function computeStats(events) {
|
|
184
|
+
const stats = {
|
|
185
|
+
totalLlmCalls: 0,
|
|
186
|
+
totalToolCalls: 0,
|
|
187
|
+
totalErrors: 0,
|
|
188
|
+
totalTokenInput: 0,
|
|
189
|
+
totalTokenOutput: 0,
|
|
190
|
+
totalCostUsd: 0,
|
|
191
|
+
providers: [],
|
|
192
|
+
};
|
|
193
|
+
const providerSet = new Set();
|
|
194
|
+
for (const e of events) {
|
|
195
|
+
if (e.event_type === 'llm_call_succeeded') {
|
|
196
|
+
stats.totalLlmCalls++;
|
|
197
|
+
stats.totalTokenInput += e.token_input ?? 0;
|
|
198
|
+
stats.totalTokenOutput += e.token_output ?? 0;
|
|
199
|
+
stats.totalCostUsd += e.cost_usd ?? 0;
|
|
200
|
+
if (e.provider)
|
|
201
|
+
providerSet.add(e.provider);
|
|
202
|
+
}
|
|
203
|
+
else if (e.event_type === 'llm_call_failed') {
|
|
204
|
+
stats.totalLlmCalls++;
|
|
205
|
+
stats.totalErrors++;
|
|
206
|
+
if (e.provider)
|
|
207
|
+
providerSet.add(e.provider);
|
|
208
|
+
}
|
|
209
|
+
else if (e.event_type === 'tool_call_succeeded') {
|
|
210
|
+
stats.totalToolCalls++;
|
|
211
|
+
}
|
|
212
|
+
else if (e.event_type === 'tool_call_failed') {
|
|
213
|
+
stats.totalToolCalls++;
|
|
214
|
+
stats.totalErrors++;
|
|
215
|
+
}
|
|
216
|
+
else if (e.event_type === 'error') {
|
|
217
|
+
stats.totalErrors++;
|
|
218
|
+
}
|
|
219
|
+
else if (e.event_type === 'policy_violation') {
|
|
220
|
+
stats.totalErrors++;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
stats.providers = Array.from(providerSet);
|
|
224
|
+
return stats;
|
|
225
|
+
}
|
|
226
|
+
function renderEvent(event) {
|
|
227
|
+
const seq = chalk_1.default.gray(` #${String(event.sequence_no).padStart(2, ' ')} `);
|
|
228
|
+
switch (event.event_type) {
|
|
229
|
+
case 'llm_call_started': {
|
|
230
|
+
const provider = event.provider ?? '?';
|
|
231
|
+
const model = event.model ?? '?';
|
|
232
|
+
process.stdout.write(seq + chalk_1.default.blue('LLM ') + chalk_1.default.gray(`${provider}/${model} started\n`));
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
case 'llm_call_succeeded': {
|
|
236
|
+
const provider = event.provider ?? '?';
|
|
237
|
+
const model = event.model ?? '?';
|
|
238
|
+
const tokens = formatTokens(event.token_input, event.token_output);
|
|
239
|
+
const cost = formatCost(event.cost_usd);
|
|
240
|
+
const dur = formatDuration(event.duration_ms);
|
|
241
|
+
process.stdout.write(seq + chalk_1.default.green('LLM ') +
|
|
242
|
+
`${provider}/${model}` +
|
|
243
|
+
chalk_1.default.gray(` | ${tokens} | ${cost} | ${dur}`) + '\n');
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
case 'llm_call_failed': {
|
|
247
|
+
const provider = event.provider ?? '?';
|
|
248
|
+
const model = event.model ?? '?';
|
|
249
|
+
const errMsg = event.error_message || event.error_type || 'unknown error';
|
|
250
|
+
const dur = formatDuration(event.duration_ms);
|
|
251
|
+
process.stdout.write(seq + chalk_1.default.red('LLM ') +
|
|
252
|
+
`${provider}/${model} ` +
|
|
253
|
+
chalk_1.default.red(errMsg) +
|
|
254
|
+
chalk_1.default.gray(` | ${dur}`) + '\n');
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
case 'tool_call_started': {
|
|
258
|
+
const toolName = event.payload?.tool_name || '?';
|
|
259
|
+
process.stdout.write(seq + chalk_1.default.cyan('TOOL ') + chalk_1.default.gray(`${toolName} started\n`));
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
case 'tool_call_succeeded': {
|
|
263
|
+
const toolName = event.payload?.tool_name || '?';
|
|
264
|
+
const dur = formatDuration(event.duration_ms);
|
|
265
|
+
process.stdout.write(seq + chalk_1.default.green('TOOL ') + `${toolName}` +
|
|
266
|
+
chalk_1.default.gray(` | ${dur}`) + '\n');
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
case 'tool_call_failed': {
|
|
270
|
+
const toolName = event.payload?.tool_name || '?';
|
|
271
|
+
const errMsg = event.error_message || event.error_type || 'unknown error';
|
|
272
|
+
const dur = formatDuration(event.duration_ms);
|
|
273
|
+
process.stdout.write(seq + chalk_1.default.red('TOOL ') + `${toolName} ` +
|
|
274
|
+
chalk_1.default.red(errMsg) +
|
|
275
|
+
chalk_1.default.gray(` | ${dur}`) + '\n');
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
case 'decision': {
|
|
279
|
+
const desc = event.payload?.description || 'decision';
|
|
280
|
+
process.stdout.write(seq + chalk_1.default.magenta('DECIDE ') + chalk_1.default.gray(desc) + '\n');
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
case 'fallback_transition': {
|
|
284
|
+
const from = event.payload?.from_provider || '?';
|
|
285
|
+
const to = event.payload?.to_provider || '?';
|
|
286
|
+
const reason = event.payload?.reason || '';
|
|
287
|
+
process.stdout.write(seq + chalk_1.default.yellow('FALLBACK ') +
|
|
288
|
+
`${from} -> ${to}` +
|
|
289
|
+
(reason ? chalk_1.default.gray(` (${reason})`) : '') + '\n');
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
case 'policy_violation': {
|
|
293
|
+
const vType = event.payload?.violation_type || 'violation';
|
|
294
|
+
const detail = event.payload?.detail || '';
|
|
295
|
+
process.stdout.write(seq + chalk_1.default.red('POLICY ') + chalk_1.default.red(vType) +
|
|
296
|
+
(detail ? chalk_1.default.gray(` — ${detail}`) : '') + '\n');
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
case 'error': {
|
|
300
|
+
const errType = event.error_type || 'error';
|
|
301
|
+
const errMsg = event.error_message || '';
|
|
302
|
+
process.stdout.write(seq + chalk_1.default.red('ERROR ') + chalk_1.default.red(errType) +
|
|
303
|
+
(errMsg ? chalk_1.default.gray(` — ${errMsg}`) : '') + '\n');
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
default: {
|
|
307
|
+
// Unknown event type — show raw
|
|
308
|
+
process.stdout.write(seq + chalk_1.default.gray(event.event_type) + '\n');
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
package/package.json
CHANGED