agentxchain 2.77.0 → 2.78.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/agentxchain.js +8 -0
- package/package.json +1 -1
- package/src/commands/diff.js +121 -0
- package/src/lib/run-diff.js +194 -0
package/bin/agentxchain.js
CHANGED
|
@@ -111,6 +111,7 @@ import { intakeResolveCommand } from '../src/commands/intake-resolve.js';
|
|
|
111
111
|
import { intakeStatusCommand } from '../src/commands/intake-status.js';
|
|
112
112
|
import { demoCommand } from '../src/commands/demo.js';
|
|
113
113
|
import { historyCommand } from '../src/commands/history.js';
|
|
114
|
+
import { diffCommand } from '../src/commands/diff.js';
|
|
114
115
|
import { eventsCommand } from '../src/commands/events.js';
|
|
115
116
|
import { connectorCheckCommand } from '../src/commands/connector.js';
|
|
116
117
|
import { scheduleDaemonCommand, scheduleListCommand, scheduleRunDueCommand, scheduleStatusCommand } from '../src/commands/schedule.js';
|
|
@@ -330,6 +331,13 @@ program
|
|
|
330
331
|
.option('-d, --dir <path>', 'Project directory')
|
|
331
332
|
.action(historyCommand);
|
|
332
333
|
|
|
334
|
+
program
|
|
335
|
+
.command('diff <left_run_id> <right_run_id>')
|
|
336
|
+
.description('Compare two recorded governed runs from run-history')
|
|
337
|
+
.option('-j, --json', 'Output as JSON')
|
|
338
|
+
.option('-d, --dir <path>', 'Project directory')
|
|
339
|
+
.action(diffCommand);
|
|
340
|
+
|
|
333
341
|
program
|
|
334
342
|
.command('events')
|
|
335
343
|
.description('Show repo-local run lifecycle events')
|
package/package.json
CHANGED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
import { findProjectRoot } from '../lib/config.js';
|
|
4
|
+
import { buildRunDiff, resolveRunHistoryReference } from '../lib/run-diff.js';
|
|
5
|
+
|
|
6
|
+
export async function diffCommand(leftRef, rightRef, opts) {
|
|
7
|
+
const root = findProjectRoot(opts.dir || process.cwd());
|
|
8
|
+
if (!root) {
|
|
9
|
+
console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const leftResult = resolveRunHistoryReference(root, leftRef);
|
|
14
|
+
if (!leftResult.ok) {
|
|
15
|
+
console.error(chalk.red(leftResult.error));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const rightResult = resolveRunHistoryReference(root, rightRef);
|
|
20
|
+
if (!rightResult.ok) {
|
|
21
|
+
console.error(chalk.red(rightResult.error));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const diff = buildRunDiff(leftResult.entry, rightResult.entry);
|
|
26
|
+
if (opts.json) {
|
|
27
|
+
console.log(JSON.stringify(diff, null, 2));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(formatRunDiffText(diff));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatRunDiffText(diff) {
|
|
35
|
+
const lines = [];
|
|
36
|
+
lines.push(chalk.bold('Run Diff'));
|
|
37
|
+
lines.push(`${chalk.dim('Left:')} ${formatRunHeader(diff.left)}`);
|
|
38
|
+
lines.push(`${chalk.dim('Right:')} ${formatRunHeader(diff.right)}`);
|
|
39
|
+
|
|
40
|
+
if (!diff.changed) {
|
|
41
|
+
lines.push('');
|
|
42
|
+
lines.push(chalk.green('No differences.'));
|
|
43
|
+
return lines.join('\n');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
appendChangedSection(lines, 'Changed fields', Object.values(diff.scalar_changes)
|
|
47
|
+
.filter((entry) => entry.changed)
|
|
48
|
+
.map((entry) => `${entry.label}: ${formatValue(entry.left, entry.label)} -> ${formatValue(entry.right, entry.label)}`));
|
|
49
|
+
|
|
50
|
+
appendChangedSection(lines, 'Numeric deltas', Object.values(diff.numeric_changes)
|
|
51
|
+
.filter((entry) => entry.changed)
|
|
52
|
+
.map((entry) => `${entry.label}: ${formatValue(entry.left, entry.label)} -> ${formatValue(entry.right, entry.label)}${formatDelta(entry.delta, entry.label)}`));
|
|
53
|
+
|
|
54
|
+
appendChangedSection(lines, 'List changes', Object.values(diff.list_changes)
|
|
55
|
+
.filter((entry) => entry.changed)
|
|
56
|
+
.flatMap((entry) => {
|
|
57
|
+
const items = [];
|
|
58
|
+
if (entry.added.length > 0) {
|
|
59
|
+
items.push(`${entry.label} added: ${entry.added.join(', ')}`);
|
|
60
|
+
}
|
|
61
|
+
if (entry.removed.length > 0) {
|
|
62
|
+
items.push(`${entry.label} removed: ${entry.removed.join(', ')}`);
|
|
63
|
+
}
|
|
64
|
+
return items;
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
appendChangedSection(lines, 'Gate changes', diff.gate_changes
|
|
68
|
+
.filter((entry) => entry.changed)
|
|
69
|
+
.map((entry) => `${entry.gate_id}: ${formatValue(entry.left)} -> ${formatValue(entry.right)}`));
|
|
70
|
+
|
|
71
|
+
return lines.join('\n');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function appendChangedSection(lines, heading, items) {
|
|
75
|
+
if (items.length === 0) return;
|
|
76
|
+
lines.push('');
|
|
77
|
+
lines.push(chalk.bold(heading));
|
|
78
|
+
for (const item of items) {
|
|
79
|
+
lines.push(`- ${item}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function formatRunHeader(entry) {
|
|
84
|
+
const status = entry.status || '—';
|
|
85
|
+
const trigger = entry.trigger || 'legacy';
|
|
86
|
+
const recordedAt = entry.recorded_at
|
|
87
|
+
? new Date(entry.recorded_at).toLocaleString()
|
|
88
|
+
: 'unknown date';
|
|
89
|
+
return `${entry.run_id} (${status}, ${trigger}, ${recordedAt})`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function formatValue(value, label = '') {
|
|
93
|
+
if (value == null) return '—';
|
|
94
|
+
if (typeof value === 'boolean') return value ? 'yes' : 'no';
|
|
95
|
+
if (label === 'Cost' || label === 'Budget') return `$${value.toFixed(4)}`;
|
|
96
|
+
if (label === 'Duration') return formatDuration(value);
|
|
97
|
+
return String(value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function formatDelta(delta, label) {
|
|
101
|
+
if (delta == null || delta === 0) return '';
|
|
102
|
+
if (label === 'Cost' || label === 'Budget') {
|
|
103
|
+
return ` (${delta > 0 ? '+' : ''}$${delta.toFixed(4)})`;
|
|
104
|
+
}
|
|
105
|
+
if (label === 'Duration') {
|
|
106
|
+
return ` (${delta > 0 ? '+' : ''}${formatDuration(delta)})`;
|
|
107
|
+
}
|
|
108
|
+
return ` (${delta > 0 ? '+' : ''}${delta})`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function formatDuration(ms) {
|
|
112
|
+
if (ms < 1000) return `${ms}ms`;
|
|
113
|
+
const secs = Math.floor(ms / 1000);
|
|
114
|
+
if (secs < 60) return `${secs}s`;
|
|
115
|
+
const mins = Math.floor(secs / 60);
|
|
116
|
+
const remainSecs = secs % 60;
|
|
117
|
+
if (mins < 60) return `${mins}m ${remainSecs}s`;
|
|
118
|
+
const hrs = Math.floor(mins / 60);
|
|
119
|
+
const remainMins = mins % 60;
|
|
120
|
+
return `${hrs}h ${remainMins}m`;
|
|
121
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { isInheritable, queryRunHistory } from './run-history.js';
|
|
2
|
+
import { getRunTriggerLabel } from './run-provenance.js';
|
|
3
|
+
|
|
4
|
+
const SCALAR_FIELDS = [
|
|
5
|
+
['status', 'Status'],
|
|
6
|
+
['trigger', 'Trigger'],
|
|
7
|
+
['template', 'Template'],
|
|
8
|
+
['connector_used', 'Connector'],
|
|
9
|
+
['model_used', 'Model'],
|
|
10
|
+
['blocked_reason', 'Blocked reason'],
|
|
11
|
+
['headline', 'Headline'],
|
|
12
|
+
['inheritable', 'Inheritance snapshot'],
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const NUMERIC_FIELDS = [
|
|
16
|
+
['total_turns', 'Turns'],
|
|
17
|
+
['decisions_count', 'Decisions'],
|
|
18
|
+
['total_cost_usd', 'Cost'],
|
|
19
|
+
['budget_limit_usd', 'Budget'],
|
|
20
|
+
['duration_ms', 'Duration'],
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export function resolveRunHistoryReference(root, ref) {
|
|
24
|
+
const entries = queryRunHistory(root, {});
|
|
25
|
+
|
|
26
|
+
if (entries.length === 0) {
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
error: 'No run history found. Run at least one governed run first.',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const exact = entries.find((entry) => entry.run_id === ref);
|
|
34
|
+
if (exact) {
|
|
35
|
+
return { ok: true, entry: exact, resolved_ref: exact.run_id, match_kind: 'exact' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const prefixMatches = entries.filter((entry) => typeof entry.run_id === 'string' && entry.run_id.startsWith(ref));
|
|
39
|
+
if (prefixMatches.length === 1) {
|
|
40
|
+
return {
|
|
41
|
+
ok: true,
|
|
42
|
+
entry: prefixMatches[0],
|
|
43
|
+
resolved_ref: prefixMatches[0].run_id,
|
|
44
|
+
match_kind: 'prefix',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (prefixMatches.length > 1) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
error: `Run reference "${ref}" is ambiguous. Matches: ${prefixMatches.map((entry) => entry.run_id).join(', ')}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
error: `Run ${ref} not found in run history.`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function buildRunDiff(leftEntry, rightEntry) {
|
|
62
|
+
const left = normalizeRunEntry(leftEntry);
|
|
63
|
+
const right = normalizeRunEntry(rightEntry);
|
|
64
|
+
|
|
65
|
+
const scalar_changes = Object.fromEntries(
|
|
66
|
+
SCALAR_FIELDS.map(([field, label]) => {
|
|
67
|
+
const leftValue = left[field];
|
|
68
|
+
const rightValue = right[field];
|
|
69
|
+
return [field, {
|
|
70
|
+
label,
|
|
71
|
+
left: leftValue,
|
|
72
|
+
right: rightValue,
|
|
73
|
+
changed: !isEqual(leftValue, rightValue),
|
|
74
|
+
}];
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const numeric_changes = Object.fromEntries(
|
|
79
|
+
NUMERIC_FIELDS.map(([field, label]) => {
|
|
80
|
+
const leftValue = left[field];
|
|
81
|
+
const rightValue = right[field];
|
|
82
|
+
return [field, {
|
|
83
|
+
label,
|
|
84
|
+
left: leftValue,
|
|
85
|
+
right: rightValue,
|
|
86
|
+
changed: !isEqual(leftValue, rightValue),
|
|
87
|
+
delta: typeof leftValue === 'number' && typeof rightValue === 'number'
|
|
88
|
+
? rightValue - leftValue
|
|
89
|
+
: null,
|
|
90
|
+
}];
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const list_changes = {
|
|
95
|
+
phases_completed: buildListChange('Phases', left.phases_completed, right.phases_completed),
|
|
96
|
+
roles_used: buildListChange('Roles', left.roles_used, right.roles_used),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const gate_changes = buildGateChanges(left.gate_results, right.gate_results);
|
|
100
|
+
|
|
101
|
+
const changed = [
|
|
102
|
+
...Object.values(scalar_changes),
|
|
103
|
+
...Object.values(numeric_changes),
|
|
104
|
+
].some((entry) => entry.changed)
|
|
105
|
+
|| Object.values(list_changes).some((entry) => entry.changed)
|
|
106
|
+
|| gate_changes.some((entry) => entry.changed);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
changed,
|
|
110
|
+
left,
|
|
111
|
+
right,
|
|
112
|
+
scalar_changes,
|
|
113
|
+
numeric_changes,
|
|
114
|
+
list_changes,
|
|
115
|
+
gate_changes,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalizeRunEntry(entry) {
|
|
120
|
+
return {
|
|
121
|
+
run_id: entry.run_id || null,
|
|
122
|
+
status: entry.status || null,
|
|
123
|
+
trigger: getRunTriggerLabel(entry.provenance),
|
|
124
|
+
template: entry.template || null,
|
|
125
|
+
connector_used: entry.connector_used || null,
|
|
126
|
+
model_used: entry.model_used || null,
|
|
127
|
+
blocked_reason: entry.blocked_reason || null,
|
|
128
|
+
headline: entry.retrospective?.headline || null,
|
|
129
|
+
inheritable: isInheritable(entry),
|
|
130
|
+
total_turns: typeof entry.total_turns === 'number' ? entry.total_turns : null,
|
|
131
|
+
decisions_count: typeof entry.decisions_count === 'number' ? entry.decisions_count : null,
|
|
132
|
+
total_cost_usd: typeof entry.total_cost_usd === 'number' ? entry.total_cost_usd : null,
|
|
133
|
+
budget_limit_usd: typeof entry.budget_limit_usd === 'number' ? entry.budget_limit_usd : null,
|
|
134
|
+
duration_ms: typeof entry.duration_ms === 'number' ? entry.duration_ms : null,
|
|
135
|
+
phases_completed: normalizeStringArray(entry.phases_completed),
|
|
136
|
+
roles_used: normalizeStringArray(entry.roles_used),
|
|
137
|
+
gate_results: normalizeGateResults(entry.gate_results),
|
|
138
|
+
recorded_at: entry.recorded_at || null,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function buildListChange(label, left, right) {
|
|
143
|
+
const leftSet = new Set(left);
|
|
144
|
+
const rightSet = new Set(right);
|
|
145
|
+
const added = right.filter((value) => !leftSet.has(value));
|
|
146
|
+
const removed = left.filter((value) => !rightSet.has(value));
|
|
147
|
+
return {
|
|
148
|
+
label,
|
|
149
|
+
left,
|
|
150
|
+
right,
|
|
151
|
+
added,
|
|
152
|
+
removed,
|
|
153
|
+
changed: added.length > 0 || removed.length > 0,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildGateChanges(leftGateResults, rightGateResults) {
|
|
158
|
+
const gateIds = [...new Set([
|
|
159
|
+
...Object.keys(leftGateResults),
|
|
160
|
+
...Object.keys(rightGateResults),
|
|
161
|
+
])].sort((a, b) => a.localeCompare(b, 'en'));
|
|
162
|
+
|
|
163
|
+
return gateIds.map((gateId) => {
|
|
164
|
+
const left = gateId in leftGateResults ? leftGateResults[gateId] : null;
|
|
165
|
+
const right = gateId in rightGateResults ? rightGateResults[gateId] : null;
|
|
166
|
+
return {
|
|
167
|
+
gate_id: gateId,
|
|
168
|
+
left,
|
|
169
|
+
right,
|
|
170
|
+
changed: !isEqual(left, right),
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function normalizeStringArray(value) {
|
|
176
|
+
if (!Array.isArray(value)) return [];
|
|
177
|
+
return [...new Set(value
|
|
178
|
+
.filter((item) => typeof item === 'string' && item.trim().length > 0)
|
|
179
|
+
.map((item) => item.trim()))]
|
|
180
|
+
.sort((a, b) => a.localeCompare(b, 'en'));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function normalizeGateResults(value) {
|
|
184
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
185
|
+
return Object.fromEntries(
|
|
186
|
+
Object.entries(value)
|
|
187
|
+
.filter(([gateId]) => typeof gateId === 'string' && gateId.trim().length > 0)
|
|
188
|
+
.map(([gateId, result]) => [gateId.trim(), result ?? null]),
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function isEqual(left, right) {
|
|
193
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
194
|
+
}
|