agentxchain 2.155.24 → 2.155.26
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/dashboard/app.js +3 -0
- package/dashboard/components/watch.js +131 -0
- package/dashboard/index.html +1 -0
- package/package.json +1 -1
- package/src/lib/dashboard/bridge-server.js +7 -0
- package/src/lib/dashboard/file-watcher.js +4 -0
- package/src/lib/dashboard/state-reader.js +4 -0
- package/src/lib/dashboard/watch-results-reader.js +86 -0
package/dashboard/app.js
CHANGED
|
@@ -21,6 +21,7 @@ import { render as renderChain } from './components/chain.js';
|
|
|
21
21
|
import { render as renderRunHistory } from './components/run-history.js';
|
|
22
22
|
import { render as renderTimeouts } from './components/timeouts.js';
|
|
23
23
|
import { render as renderCoordinatorTimeouts } from './components/coordinator-timeouts.js';
|
|
24
|
+
import { render as renderWatch } from './components/watch.js';
|
|
24
25
|
import {
|
|
25
26
|
buildLiveMeta,
|
|
26
27
|
createLiveEventFromMessage,
|
|
@@ -42,6 +43,7 @@ const VIEWS = {
|
|
|
42
43
|
mission: { fetch: ['missions', 'plans'], render: renderMission },
|
|
43
44
|
chain: { fetch: ['chainReports'], render: renderChain },
|
|
44
45
|
'run-history': { fetch: ['runHistory'], render: renderRunHistory },
|
|
46
|
+
watch: { fetch: ['watchResults'], render: renderWatch },
|
|
45
47
|
timeouts: { fetch: ['timeouts'], render: renderTimeouts },
|
|
46
48
|
'coordinator-timeouts': { fetch: ['coordinatorTimeouts'], render: renderCoordinatorTimeouts },
|
|
47
49
|
};
|
|
@@ -70,6 +72,7 @@ const API_MAP = {
|
|
|
70
72
|
chainReports: '/api/chain-reports',
|
|
71
73
|
connectors: '/api/connectors',
|
|
72
74
|
runHistory: '/api/run-history',
|
|
75
|
+
watchResults: '/api/watch-results',
|
|
73
76
|
timeouts: '/api/timeouts',
|
|
74
77
|
coordinatorTimeouts: '/api/coordinator/timeouts',
|
|
75
78
|
gateActions: '/api/gate-actions',
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
function esc(str) {
|
|
2
|
+
if (str == null) return '';
|
|
3
|
+
return String(str)
|
|
4
|
+
.replace(/&/g, '&')
|
|
5
|
+
.replace(/</g, '<')
|
|
6
|
+
.replace(/>/g, '>')
|
|
7
|
+
.replace(/"/g, '"')
|
|
8
|
+
.replace(/'/g, ''');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function badge(label, color = 'var(--text-dim)') {
|
|
12
|
+
return `<span class="badge" style="color:${color};border-color:${color}">${esc(label)}</span>`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function classify(record) {
|
|
16
|
+
if (Array.isArray(record?.errors) && record.errors.length > 0) return 'error';
|
|
17
|
+
if (record?.deduplicated === true) return 'deduplicated';
|
|
18
|
+
if (record?.route?.started === true) return 'started';
|
|
19
|
+
if (record?.route?.planned === true) return 'planned';
|
|
20
|
+
if (record?.route?.approved === true) return 'approved';
|
|
21
|
+
if (record?.route?.triaged === true) return 'triaged';
|
|
22
|
+
if (record?.route?.matched === false) return 'unrouted';
|
|
23
|
+
return 'detected';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function statusBadge(record) {
|
|
27
|
+
const status = classify(record);
|
|
28
|
+
const colors = {
|
|
29
|
+
error: 'var(--red)',
|
|
30
|
+
deduplicated: 'var(--yellow)',
|
|
31
|
+
started: 'var(--green)',
|
|
32
|
+
planned: 'var(--accent)',
|
|
33
|
+
approved: 'var(--accent)',
|
|
34
|
+
triaged: 'var(--text)',
|
|
35
|
+
unrouted: 'var(--text-dim)',
|
|
36
|
+
detected: 'var(--text-dim)',
|
|
37
|
+
};
|
|
38
|
+
return badge(status, colors[status] || 'var(--text-dim)');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function formatTime(value) {
|
|
42
|
+
if (!value) return '—';
|
|
43
|
+
const date = new Date(value);
|
|
44
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
45
|
+
return date.toLocaleString();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function renderResultRow(record) {
|
|
49
|
+
const payload = record.payload || {};
|
|
50
|
+
const route = record.route || {};
|
|
51
|
+
const errors = Array.isArray(record.errors) ? record.errors : [];
|
|
52
|
+
const routeDetail = route.matched === false
|
|
53
|
+
? 'No route'
|
|
54
|
+
: [
|
|
55
|
+
route.run_id ? `run ${route.run_id}` : null,
|
|
56
|
+
route.role ? `role ${route.role}` : null,
|
|
57
|
+
route.preferred_role ? `hint ${route.preferred_role}` : null,
|
|
58
|
+
].filter(Boolean).join(' · ') || 'Route matched';
|
|
59
|
+
|
|
60
|
+
return `<tr${errors.length > 0 ? ' style="border-left:3px solid var(--red)"' : ''}>
|
|
61
|
+
<td>
|
|
62
|
+
<div class="mono">${esc(record.result_id || '—')}</div>
|
|
63
|
+
<div class="turn-status">${esc(formatTime(record.timestamp))}</div>
|
|
64
|
+
</td>
|
|
65
|
+
<td>${statusBadge(record)}</td>
|
|
66
|
+
<td>
|
|
67
|
+
<div class="mono">${esc(payload.category || 'unknown')}</div>
|
|
68
|
+
<div class="turn-status">${esc(payload.repo || '—')}${payload.ref ? ` · ${esc(payload.ref)}` : ''}</div>
|
|
69
|
+
</td>
|
|
70
|
+
<td>
|
|
71
|
+
<div>${esc(routeDetail)}</div>
|
|
72
|
+
<div class="turn-status">Intent ${esc(record.intent_id || '—')} · Event ${esc(record.event_id || '—')}</div>
|
|
73
|
+
</td>
|
|
74
|
+
<td class="mono">${esc(record.delivery_id || '—')}</td>
|
|
75
|
+
<td>${errors.length > 0 ? esc(errors.join(' | ')) : '—'}</td>
|
|
76
|
+
</tr>`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function render({ watchResults }) {
|
|
80
|
+
if (!watchResults) {
|
|
81
|
+
return `<div class="placeholder"><h2>Watch</h2><p>No watch result data available.</p></div>`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (watchResults.ok === false) {
|
|
85
|
+
return `<div class="placeholder"><h2>Watch</h2><p>${esc(watchResults.error || 'Failed to load watch results.')}</p></div>`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const recent = Array.isArray(watchResults.recent) ? watchResults.recent : [];
|
|
89
|
+
const summary = watchResults.summary || {};
|
|
90
|
+
const byStatus = summary.by_status || {};
|
|
91
|
+
|
|
92
|
+
if ((watchResults.total || 0) === 0 && (watchResults.corrupt || 0) === 0) {
|
|
93
|
+
return `<div class="placeholder"><h2>Watch</h2><p>No watch intake results yet. Use <code>agentxchain watch --listen</code>, <code>--event-file</code>, or <code>--event-dir</code> to ingest events.</p></div>`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let html = `<div class="watch-view"><div class="run-header"><div class="run-meta">`;
|
|
97
|
+
html += badge(`${watchResults.total || 0} results`, 'var(--accent)');
|
|
98
|
+
html += badge(`${summary.routed || 0} routed`, 'var(--green)');
|
|
99
|
+
html += badge(`${summary.unrouted || 0} unrouted`, summary.unrouted ? 'var(--yellow)' : 'var(--text-dim)');
|
|
100
|
+
html += badge(`${summary.deduplicated || 0} deduped`, summary.deduplicated ? 'var(--yellow)' : 'var(--text-dim)');
|
|
101
|
+
html += badge(`${summary.errored || 0} errors`, summary.errored ? 'var(--red)' : 'var(--text-dim)');
|
|
102
|
+
if (watchResults.corrupt) html += badge(`${watchResults.corrupt} corrupt`, 'var(--red)');
|
|
103
|
+
html += `</div></div>`;
|
|
104
|
+
|
|
105
|
+
html += `<div class="section"><h3>Intake Summary</h3>
|
|
106
|
+
<p class="section-subtitle">Last result: ${esc(formatTime(summary.last_timestamp))}</p>
|
|
107
|
+
<table class="data-table">
|
|
108
|
+
<thead><tr><th>Status</th><th>Count</th></tr></thead>
|
|
109
|
+
<tbody>${Object.keys(byStatus).sort().map((status) => `<tr><td>${esc(status)}</td><td>${esc(byStatus[status])}</td></tr>`).join('')}</tbody>
|
|
110
|
+
</table>
|
|
111
|
+
</div>`;
|
|
112
|
+
|
|
113
|
+
html += `<div class="section"><h3>Recent Watch Results</h3>
|
|
114
|
+
<table class="data-table">
|
|
115
|
+
<thead>
|
|
116
|
+
<tr>
|
|
117
|
+
<th>Result</th>
|
|
118
|
+
<th>Status</th>
|
|
119
|
+
<th>Payload</th>
|
|
120
|
+
<th>Route</th>
|
|
121
|
+
<th>Delivery</th>
|
|
122
|
+
<th>Errors</th>
|
|
123
|
+
</tr>
|
|
124
|
+
</thead>
|
|
125
|
+
<tbody>${recent.map(renderResultRow).join('')}</tbody>
|
|
126
|
+
</table>
|
|
127
|
+
</div>`;
|
|
128
|
+
|
|
129
|
+
html += `</div>`;
|
|
130
|
+
return html;
|
|
131
|
+
}
|
package/dashboard/index.html
CHANGED
package/package.json
CHANGED
|
@@ -33,6 +33,7 @@ import { readGateActionSnapshot } from './gate-action-reader.js';
|
|
|
33
33
|
import { readChainReportSnapshot } from './chain-report-reader.js';
|
|
34
34
|
import { readMissionSnapshot } from './mission-reader.js';
|
|
35
35
|
import { readPlanSnapshot } from './plan-reader.js';
|
|
36
|
+
import { readWatchResultsSnapshot } from './watch-results-reader.js';
|
|
36
37
|
|
|
37
38
|
const MIME_TYPES = {
|
|
38
39
|
'.html': 'text/html; charset=utf-8',
|
|
@@ -504,6 +505,12 @@ export function createBridgeServer({ agentxchainDir, dashboardDir, port = 3847,
|
|
|
504
505
|
return;
|
|
505
506
|
}
|
|
506
507
|
|
|
508
|
+
if (pathname === '/api/watch-results') {
|
|
509
|
+
const limit = url.searchParams.get('limit') ?? undefined;
|
|
510
|
+
writeJson(res, 200, readWatchResultsSnapshot(workspacePath, { limit }));
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
507
514
|
// API routes
|
|
508
515
|
if (pathname.startsWith('/api/')) {
|
|
509
516
|
const result = readResource(agentxchainDir, pathname);
|
|
@@ -47,6 +47,10 @@ export class FileWatcher extends EventEmitter {
|
|
|
47
47
|
if (!relativeDir && fileSegment === 'multirepo') {
|
|
48
48
|
this.#watchPath('multirepo');
|
|
49
49
|
}
|
|
50
|
+
if (!relativeDir && fileSegment === 'watch-results') {
|
|
51
|
+
this.#watchPath('watch-results');
|
|
52
|
+
this.emit('invalidate', { resource: '/api/watch-results' });
|
|
53
|
+
}
|
|
50
54
|
return;
|
|
51
55
|
}
|
|
52
56
|
|
|
@@ -66,6 +66,7 @@ export const WATCH_DIRECTORIES = [
|
|
|
66
66
|
MULTIREPO_DIR,
|
|
67
67
|
'missions',
|
|
68
68
|
'reports',
|
|
69
|
+
'watch-results',
|
|
69
70
|
];
|
|
70
71
|
|
|
71
72
|
/**
|
|
@@ -94,6 +95,9 @@ export function resourcesForRelativePath(filePath) {
|
|
|
94
95
|
if (normalized.startsWith('reports/chain-') && normalized.endsWith('.json')) {
|
|
95
96
|
return ['/api/chain-reports', '/api/missions'];
|
|
96
97
|
}
|
|
98
|
+
if (normalized === 'watch-results' || (normalized.startsWith('watch-results/') && normalized.endsWith('.json'))) {
|
|
99
|
+
return ['/api/watch-results'];
|
|
100
|
+
}
|
|
97
101
|
return FILE_TO_RESOURCE[normalized] ? [FILE_TO_RESOURCE[normalized]] : [];
|
|
98
102
|
}
|
|
99
103
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_LIMIT = 25;
|
|
5
|
+
|
|
6
|
+
export function readWatchResultsSnapshot(workspacePath, { limit = DEFAULT_LIMIT } = {}) {
|
|
7
|
+
const resultsDir = join(workspacePath, '.agentxchain', 'watch-results');
|
|
8
|
+
const parsedLimit = parseLimit(limit);
|
|
9
|
+
const records = [];
|
|
10
|
+
let corrupt = 0;
|
|
11
|
+
|
|
12
|
+
if (existsSync(resultsDir)) {
|
|
13
|
+
const entries = readdirSync(resultsDir, { withFileTypes: true })
|
|
14
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'));
|
|
15
|
+
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
try {
|
|
18
|
+
const record = JSON.parse(readFileSync(join(resultsDir, entry.name), 'utf8'));
|
|
19
|
+
if (!record || typeof record !== 'object' || Array.isArray(record)) {
|
|
20
|
+
corrupt += 1;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
records.push(record);
|
|
24
|
+
} catch {
|
|
25
|
+
corrupt += 1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
records.sort((a, b) => String(b.timestamp || '').localeCompare(String(a.timestamp || '')));
|
|
31
|
+
const recent = parsedLimit === 0 ? records : records.slice(0, parsedLimit);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
ok: true,
|
|
35
|
+
total: records.length,
|
|
36
|
+
corrupt,
|
|
37
|
+
recent,
|
|
38
|
+
summary: summarize(records, corrupt),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseLimit(value) {
|
|
43
|
+
const parsed = Number.parseInt(value, 10);
|
|
44
|
+
if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_LIMIT;
|
|
45
|
+
return parsed;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function summarize(records, corrupt) {
|
|
49
|
+
const byStatus = {};
|
|
50
|
+
let errored = 0;
|
|
51
|
+
let deduplicated = 0;
|
|
52
|
+
let routed = 0;
|
|
53
|
+
let unrouted = 0;
|
|
54
|
+
let lastTimestamp = null;
|
|
55
|
+
|
|
56
|
+
for (const record of records) {
|
|
57
|
+
const status = classifyWatchResult(record);
|
|
58
|
+
byStatus[status] = (byStatus[status] || 0) + 1;
|
|
59
|
+
if (status === 'error') errored += 1;
|
|
60
|
+
if (record.deduplicated === true) deduplicated += 1;
|
|
61
|
+
if (record.route?.matched === false) unrouted += 1;
|
|
62
|
+
else if (record.route?.matched === true) routed += 1;
|
|
63
|
+
if (!lastTimestamp && record.timestamp) lastTimestamp = record.timestamp;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
by_status: byStatus,
|
|
68
|
+
errored,
|
|
69
|
+
deduplicated,
|
|
70
|
+
routed,
|
|
71
|
+
unrouted,
|
|
72
|
+
corrupt,
|
|
73
|
+
last_timestamp: lastTimestamp,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function classifyWatchResult(record) {
|
|
78
|
+
if (Array.isArray(record?.errors) && record.errors.length > 0) return 'error';
|
|
79
|
+
if (record?.deduplicated === true) return 'deduplicated';
|
|
80
|
+
if (record?.route?.started === true) return 'started';
|
|
81
|
+
if (record?.route?.planned === true) return 'planned';
|
|
82
|
+
if (record?.route?.approved === true) return 'approved';
|
|
83
|
+
if (record?.route?.triaged === true) return 'triaged';
|
|
84
|
+
if (record?.route?.matched === false) return 'unrouted';
|
|
85
|
+
return 'detected';
|
|
86
|
+
}
|