a2acalling 0.5.0 → 0.5.3
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/README.md +29 -1
- package/SKILL.md +18 -0
- package/bin/cli.js +123 -0
- package/docs/protocol.md +19 -0
- package/package.json +1 -1
- package/scripts/install-openclaw.js +26 -4
- package/src/dashboard/public/app.js +195 -7
- package/src/dashboard/public/index.html +51 -0
- package/src/dashboard/public/style.css +27 -0
- package/src/index.js +5 -0
- package/src/lib/call-monitor.js +68 -6
- package/src/lib/config.js +11 -1
- package/src/lib/conversations.js +13 -1
- package/src/lib/disclosure.js +11 -1
- package/src/lib/invite-host.js +17 -7
- package/src/lib/logger.js +566 -0
- package/src/lib/openclaw-integration.js +53 -6
- package/src/lib/runtime-adapter.js +133 -39
- package/src/lib/summarizer.js +21 -2
- package/src/lib/tokens.js +12 -1
- package/src/routes/a2a.js +170 -11
- package/src/routes/dashboard.js +41 -0
- package/src/server.js +169 -32
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
Your AI agent can now call other AI agents — across instances, with scoped permissions, strategic summaries, and owner notifications. Think of it as a
|
|
10
|
+
Your AI agent can now call other AI agents — across instances, with scoped permissions, strategic summaries, and owner notifications. Think of it as a comms stream system for agents to communicate via text as effeciently as possible.
|
|
11
11
|
|
|
12
12
|
## ✨ Features
|
|
13
13
|
|
|
@@ -21,6 +21,7 @@ Your AI agent can now call other AI agents — across instances, with scoped per
|
|
|
21
21
|
- 🧭 **Adaptive collaboration mode** — dynamic phase changes based on overlap and depth
|
|
22
22
|
- 🗂️ **Minimal dashboard** — contacts, calls, tier settings, and invite generation
|
|
23
23
|
- 💾 **Conversation history** — SQLite storage with context retrieval
|
|
24
|
+
- 🧾 **Traceable logs** — DB-backed structured logs with `trace_id`, `error_code`, and hints
|
|
24
25
|
|
|
25
26
|
## 🚀 Quick Start
|
|
26
27
|
|
|
@@ -195,6 +196,31 @@ Dashboard paths:
|
|
|
195
196
|
- Standalone A2A server: `http://<host>:<port>/dashboard`
|
|
196
197
|
- OpenClaw gateway mode: `http://<gateway>/a2a`
|
|
197
198
|
|
|
199
|
+
### Traceability and Logs
|
|
200
|
+
|
|
201
|
+
All runtime logs are persisted in SQLite and also emitted to stdout:
|
|
202
|
+
|
|
203
|
+
- Log DB: `~/.config/openclaw/a2a-logs.db` (or `$A2A_CONFIG_DIR/a2a-logs.db`)
|
|
204
|
+
- Trace fields: `trace_id`, `conversation_id`, `token_id`, `error_code`, `status_code`, `hint`
|
|
205
|
+
|
|
206
|
+
Dashboard/API log routes:
|
|
207
|
+
|
|
208
|
+
- `GET /api/a2a/dashboard/logs`
|
|
209
|
+
- `GET /api/a2a/dashboard/logs/trace/:traceId`
|
|
210
|
+
- `GET /api/a2a/dashboard/logs/stats`
|
|
211
|
+
|
|
212
|
+
Useful filters for `/api/a2a/dashboard/logs`:
|
|
213
|
+
|
|
214
|
+
- `trace_id`, `conversation_id`, `token_id`
|
|
215
|
+
- `error_code`, `status_code`
|
|
216
|
+
- `component`, `event`, `level`, `search`, `from`, `to`, `limit`
|
|
217
|
+
|
|
218
|
+
Example:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
curl "http://localhost:3001/api/a2a/dashboard/logs?trace_id=trace_abc123&error_code=TOKEN_INVALID_OR_EXPIRED"
|
|
222
|
+
```
|
|
223
|
+
|
|
198
224
|
## 📡 Protocol
|
|
199
225
|
|
|
200
226
|
Tokens use the `a2a://` URI scheme:
|
|
@@ -338,6 +364,8 @@ app.listen(3001);
|
|
|
338
364
|
| `A2A_OWNER_NAME` | Override owner display name |
|
|
339
365
|
| `A2A_COLLAB_MODE` | Conversation style: `adaptive` (default) or `deep_dive` |
|
|
340
366
|
| `A2A_ADMIN_TOKEN` | Protect dashboard/conversation admin routes for non-local access |
|
|
367
|
+
| `A2A_LOG_LEVEL` | Minimum persisted/stdout log level: `trace`, `debug`, `info`, `warn`, `error` (default: `info`) |
|
|
368
|
+
| `A2A_LOG_STACKS` | Include stack traces in log DB error payloads (`true` by default outside production) |
|
|
341
369
|
|
|
342
370
|
## 🤝 Philosophy
|
|
343
371
|
|
package/SKILL.md
CHANGED
|
@@ -106,6 +106,24 @@ done — Save manifest and finish
|
|
|
106
106
|
|
|
107
107
|
4. Manifest saved to `~/.config/openclaw/a2a-disclosure.json`
|
|
108
108
|
|
|
109
|
+
### Open GUI (Dashboard)
|
|
110
|
+
|
|
111
|
+
User says: `/a2a gui`, `/a2a dashboard`, "open the GUI", "open the dashboard", "show me A2A logs"
|
|
112
|
+
|
|
113
|
+
This opens the local dashboard UI in the default browser (or prints the URL if auto-open is not possible).
|
|
114
|
+
|
|
115
|
+
Notes:
|
|
116
|
+
- This command is safe and **does not require onboarding**.
|
|
117
|
+
- Optional: open a specific tab via `--tab`.
|
|
118
|
+
|
|
119
|
+
Examples:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
a2a gui
|
|
123
|
+
a2a gui --tab logs
|
|
124
|
+
a2a dashboard --tab calls
|
|
125
|
+
```
|
|
126
|
+
|
|
109
127
|
### Invite (Create & Share Token)
|
|
110
128
|
|
|
111
129
|
User says: `/a2a invite`, `/a2a invite public`, `/a2a invite friends`, `/a2a invite family`, "create an invite", "generate an A2A invite"
|
package/bin/cli.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* a2a remotes List remote agents
|
|
11
11
|
* a2a call <url> <msg> Call a remote agent
|
|
12
12
|
* a2a ping <url> Ping a remote agent
|
|
13
|
+
* a2a gui Open the local dashboard GUI in a browser
|
|
13
14
|
* a2a setup Auto setup (gateway-aware dashboard install)
|
|
14
15
|
*/
|
|
15
16
|
|
|
@@ -69,6 +70,77 @@ function formatTimeAgo(date) {
|
|
|
69
70
|
return date.toLocaleDateString();
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
function openInBrowser(url) {
|
|
74
|
+
const { spawn } = require('child_process');
|
|
75
|
+
|
|
76
|
+
const platform = process.platform;
|
|
77
|
+
let cmd = null;
|
|
78
|
+
let args = [];
|
|
79
|
+
|
|
80
|
+
if (platform === 'darwin') {
|
|
81
|
+
cmd = 'open';
|
|
82
|
+
args = [url];
|
|
83
|
+
} else if (platform === 'win32') {
|
|
84
|
+
cmd = 'cmd';
|
|
85
|
+
args = ['/c', 'start', '', url];
|
|
86
|
+
} else {
|
|
87
|
+
cmd = 'xdg-open';
|
|
88
|
+
args = [url];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
|
|
93
|
+
child.unref();
|
|
94
|
+
return { attempted: true, command: [cmd, ...args].join(' ') };
|
|
95
|
+
} catch (err) {
|
|
96
|
+
return { attempted: false, error: err.message };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function findLocalServerPort(preferredPorts = []) {
|
|
101
|
+
const http = require('http');
|
|
102
|
+
|
|
103
|
+
const candidates = [];
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
for (const port of preferredPorts) {
|
|
106
|
+
const n = Number.parseInt(String(port), 10);
|
|
107
|
+
if (!Number.isFinite(n) || n <= 0 || n > 65535) continue;
|
|
108
|
+
if (seen.has(n)) continue;
|
|
109
|
+
seen.add(n);
|
|
110
|
+
candidates.push(n);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const defaultPorts = [3001, 80, 8080, 8443, 9001];
|
|
114
|
+
for (const port of defaultPorts) {
|
|
115
|
+
if (seen.has(port)) continue;
|
|
116
|
+
seen.add(port);
|
|
117
|
+
candidates.push(port);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const probe = (port) => new Promise(resolve => {
|
|
121
|
+
const req = http.request({
|
|
122
|
+
hostname: '127.0.0.1',
|
|
123
|
+
port,
|
|
124
|
+
path: '/api/a2a/ping',
|
|
125
|
+
method: 'GET',
|
|
126
|
+
timeout: 800
|
|
127
|
+
}, (res) => {
|
|
128
|
+
res.resume();
|
|
129
|
+
resolve(res.statusCode === 200);
|
|
130
|
+
});
|
|
131
|
+
req.on('error', () => resolve(false));
|
|
132
|
+
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
133
|
+
req.end();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
for (const port of candidates) {
|
|
137
|
+
// eslint-disable-next-line no-await-in-loop
|
|
138
|
+
const ok = await probe(port);
|
|
139
|
+
if (ok) return port;
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
72
144
|
// Parse arguments
|
|
73
145
|
function parseArgs(argv) {
|
|
74
146
|
const args = { _: [], flags: {} };
|
|
@@ -747,6 +819,55 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
747
819
|
}
|
|
748
820
|
},
|
|
749
821
|
|
|
822
|
+
gui: async (args) => {
|
|
823
|
+
// GUI is always safe to open even before onboarding.
|
|
824
|
+
const tab = (args.flags.tab || args.flags.t || '').trim().toLowerCase();
|
|
825
|
+
const allowedTabs = new Set(['contacts', 'calls', 'logs', 'settings', 'invites']);
|
|
826
|
+
const hash = allowedTabs.has(tab) ? `#${tab}` : '';
|
|
827
|
+
|
|
828
|
+
const urlFlag = args.flags.url;
|
|
829
|
+
if (urlFlag) {
|
|
830
|
+
const url = String(urlFlag);
|
|
831
|
+
console.log(`Dashboard URL: ${url}`);
|
|
832
|
+
const opened = openInBrowser(url);
|
|
833
|
+
if (opened.attempted) {
|
|
834
|
+
console.log(`Opening browser via: ${opened.command}`);
|
|
835
|
+
} else {
|
|
836
|
+
console.log('Could not auto-open browser.');
|
|
837
|
+
}
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const preferred = [];
|
|
842
|
+
if (args.flags.port || args.flags.p) preferred.push(args.flags.port || args.flags.p);
|
|
843
|
+
if (process.env.A2A_PORT) preferred.push(process.env.A2A_PORT);
|
|
844
|
+
if (process.env.PORT) preferred.push(process.env.PORT);
|
|
845
|
+
|
|
846
|
+
const port = await findLocalServerPort(preferred);
|
|
847
|
+
if (!port) {
|
|
848
|
+
console.log('Dashboard is not reachable on common ports.');
|
|
849
|
+
console.log('Start the server (example):');
|
|
850
|
+
console.log(' A2A_HOSTNAME="localhost:3001" a2a server --port 3001');
|
|
851
|
+
console.log('Then open:');
|
|
852
|
+
console.log(' http://127.0.0.1:3001/dashboard/');
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const url = `http://127.0.0.1:${port}/dashboard/${hash}`;
|
|
857
|
+
console.log(`Dashboard URL: ${url}`);
|
|
858
|
+
const opened = openInBrowser(url);
|
|
859
|
+
if (opened.attempted) {
|
|
860
|
+
console.log(`Opening browser via: ${opened.command}`);
|
|
861
|
+
} else {
|
|
862
|
+
console.log('Could not auto-open browser; open the URL above manually.');
|
|
863
|
+
}
|
|
864
|
+
},
|
|
865
|
+
|
|
866
|
+
dashboard: (args) => {
|
|
867
|
+
// Alias for gui
|
|
868
|
+
return commands.gui(args);
|
|
869
|
+
},
|
|
870
|
+
|
|
750
871
|
status: async (args) => {
|
|
751
872
|
const url = args._[1];
|
|
752
873
|
if (!url) {
|
|
@@ -1099,6 +1220,8 @@ Calling:
|
|
|
1099
1220
|
call <contact|url> <msg> Call a remote agent
|
|
1100
1221
|
ping <url> Check if agent is reachable
|
|
1101
1222
|
status <url> Get A2A status
|
|
1223
|
+
gui Open the local dashboard GUI in a browser
|
|
1224
|
+
--tab, -t Optional: contacts|calls|logs|settings|invites
|
|
1102
1225
|
|
|
1103
1226
|
Server:
|
|
1104
1227
|
server Start the A2A server
|
package/docs/protocol.md
CHANGED
|
@@ -127,6 +127,25 @@ Error responses:
|
|
|
127
127
|
{"success": false, "error": "internal_error", "message": "..."}
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
+
## Traceability and Log APIs
|
|
131
|
+
|
|
132
|
+
A2A persists structured runtime logs to `~/.config/openclaw/a2a-logs.db` (or `$A2A_CONFIG_DIR/a2a-logs.db`).
|
|
133
|
+
|
|
134
|
+
Primary log fields:
|
|
135
|
+
- `trace_id`
|
|
136
|
+
- `conversation_id`
|
|
137
|
+
- `token_id`
|
|
138
|
+
- `error_code`
|
|
139
|
+
- `status_code`
|
|
140
|
+
- `hint`
|
|
141
|
+
|
|
142
|
+
Dashboard API endpoints:
|
|
143
|
+
- `GET /api/a2a/dashboard/logs`
|
|
144
|
+
- `GET /api/a2a/dashboard/logs/trace/:traceId`
|
|
145
|
+
- `GET /api/a2a/dashboard/logs/stats`
|
|
146
|
+
|
|
147
|
+
Example filters for `/logs`: `trace_id`, `conversation_id`, `token_id`, `error_code`, `status_code`, `component`, `event`, `level`, `search`, `from`, `to`.
|
|
148
|
+
|
|
130
149
|
## Permission Scopes
|
|
131
150
|
|
|
132
151
|
| Scope | Tools | Files | Memory | Actions |
|
package/package.json
CHANGED
|
@@ -278,12 +278,25 @@ function sendHtml(res: ServerResponse, status: number, html: string) {
|
|
|
278
278
|
|
|
279
279
|
function resolveBackendUrl(api: OpenClawPluginApi): URL {
|
|
280
280
|
const fallback = process.env.A2A_DASHBOARD_BACKEND_URL || "http://127.0.0.1:3001";
|
|
281
|
+
const pluginConfigRaw = (api as OpenClawPluginApi & { pluginConfig?: unknown }).pluginConfig;
|
|
282
|
+
const pluginConfig = (pluginConfigRaw && typeof pluginConfigRaw === "object")
|
|
283
|
+
? (pluginConfigRaw as Record<string, unknown>)
|
|
284
|
+
: {};
|
|
285
|
+
const configuredUrl = typeof pluginConfig.backendUrl === "string" && pluginConfig.backendUrl
|
|
286
|
+
? pluginConfig.backendUrl
|
|
287
|
+
: "";
|
|
288
|
+
if (configuredUrl) {
|
|
289
|
+
return new URL(configuredUrl);
|
|
290
|
+
}
|
|
281
291
|
try {
|
|
282
292
|
const cfg = api.runtime.config.loadConfig() as Record<string, unknown>;
|
|
283
293
|
const plugins = (cfg.plugins || {}) as Record<string, unknown>;
|
|
284
294
|
const entries = (plugins.entries || {}) as Record<string, unknown>;
|
|
285
295
|
const pluginEntry = (entries[PLUGIN_ID] || {}) as Record<string, unknown>;
|
|
286
|
-
const
|
|
296
|
+
const entryConfig = (pluginEntry.config || {}) as Record<string, unknown>;
|
|
297
|
+
const candidate = typeof entryConfig.backendUrl === "string" && entryConfig.backendUrl
|
|
298
|
+
? entryConfig.backendUrl
|
|
299
|
+
: typeof pluginEntry.backendUrl === "string" && pluginEntry.backendUrl
|
|
287
300
|
? pluginEntry.backendUrl
|
|
288
301
|
: fallback;
|
|
289
302
|
return new URL(candidate);
|
|
@@ -559,11 +572,20 @@ async function install() {
|
|
|
559
572
|
|
|
560
573
|
config.plugins = config.plugins || {};
|
|
561
574
|
config.plugins.entries = config.plugins.entries || {};
|
|
562
|
-
const
|
|
575
|
+
const rawEntry = config.plugins.entries[DASHBOARD_PLUGIN_ID];
|
|
576
|
+
const existingEntry = (rawEntry && typeof rawEntry === 'object') ? rawEntry : {};
|
|
577
|
+
const existingConfig = (existingEntry.config && typeof existingEntry.config === 'object')
|
|
578
|
+
? existingEntry.config
|
|
579
|
+
: {};
|
|
580
|
+
if (typeof existingEntry.backendUrl === 'string' && existingEntry.backendUrl) {
|
|
581
|
+
log(`Migrated legacy plugin key plugins.entries.${DASHBOARD_PLUGIN_ID}.backendUrl -> plugins.entries.${DASHBOARD_PLUGIN_ID}.config.backendUrl`);
|
|
582
|
+
}
|
|
563
583
|
config.plugins.entries[DASHBOARD_PLUGIN_ID] = {
|
|
564
|
-
...existingEntry,
|
|
565
584
|
enabled: true,
|
|
566
|
-
|
|
585
|
+
config: {
|
|
586
|
+
...existingConfig,
|
|
587
|
+
backendUrl
|
|
588
|
+
}
|
|
567
589
|
};
|
|
568
590
|
configUpdated = true;
|
|
569
591
|
log(`Configured gateway plugin entry: ${DASHBOARD_PLUGIN_ID}`);
|
|
@@ -2,7 +2,10 @@ const state = {
|
|
|
2
2
|
settings: null,
|
|
3
3
|
contacts: [],
|
|
4
4
|
calls: [],
|
|
5
|
-
invites: []
|
|
5
|
+
invites: [],
|
|
6
|
+
logs: [],
|
|
7
|
+
logStats: null,
|
|
8
|
+
trace: null
|
|
6
9
|
};
|
|
7
10
|
|
|
8
11
|
function showNotice(message) {
|
|
@@ -46,16 +49,46 @@ function fmtDate(value) {
|
|
|
46
49
|
}
|
|
47
50
|
}
|
|
48
51
|
|
|
52
|
+
function esc(text) {
|
|
53
|
+
return String(text ?? '')
|
|
54
|
+
.replaceAll('&', '&')
|
|
55
|
+
.replaceAll('<', '<')
|
|
56
|
+
.replaceAll('>', '>')
|
|
57
|
+
.replaceAll('"', '"')
|
|
58
|
+
.replaceAll("'", ''');
|
|
59
|
+
}
|
|
60
|
+
|
|
49
61
|
function bindTabs() {
|
|
62
|
+
const activateTab = (tab, options = {}) => {
|
|
63
|
+
const target = String(tab || '').replace(/^#/, '').trim();
|
|
64
|
+
if (!target) return false;
|
|
65
|
+
const btn = Array.from(document.querySelectorAll('.tab')).find(b => b.dataset.tab === target);
|
|
66
|
+
const panel = document.getElementById(`tab-${target}`);
|
|
67
|
+
if (!btn || !panel) return false;
|
|
68
|
+
|
|
69
|
+
document.querySelectorAll('.tab').forEach(b => b.classList.remove('is-active'));
|
|
70
|
+
document.querySelectorAll('.panel').forEach(p => p.classList.remove('is-active'));
|
|
71
|
+
btn.classList.add('is-active');
|
|
72
|
+
panel.classList.add('is-active');
|
|
73
|
+
|
|
74
|
+
if (options.updateHash) {
|
|
75
|
+
try { window.location.hash = target; } catch (err) {}
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
};
|
|
79
|
+
|
|
50
80
|
document.querySelectorAll('.tab').forEach(btn => {
|
|
51
81
|
btn.addEventListener('click', () => {
|
|
52
|
-
|
|
53
|
-
document.querySelectorAll('.tab').forEach(b => b.classList.remove('is-active'));
|
|
54
|
-
document.querySelectorAll('.panel').forEach(p => p.classList.remove('is-active'));
|
|
55
|
-
btn.classList.add('is-active');
|
|
56
|
-
document.getElementById(`tab-${tab}`).classList.add('is-active');
|
|
82
|
+
activateTab(btn.dataset.tab, { updateHash: true });
|
|
57
83
|
});
|
|
58
84
|
});
|
|
85
|
+
|
|
86
|
+
window.addEventListener('hashchange', () => {
|
|
87
|
+
activateTab(window.location.hash);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Deep-link into a tab with /dashboard/#logs, etc.
|
|
91
|
+
activateTab(window.location.hash);
|
|
59
92
|
}
|
|
60
93
|
|
|
61
94
|
function renderContacts() {
|
|
@@ -144,6 +177,137 @@ async function loadCallsForContact(contactId, contactName) {
|
|
|
144
177
|
`;
|
|
145
178
|
}
|
|
146
179
|
|
|
180
|
+
function readLogFilters() {
|
|
181
|
+
const level = document.getElementById('logs-level').value.trim();
|
|
182
|
+
const component = document.getElementById('logs-component').value.trim();
|
|
183
|
+
const event = document.getElementById('logs-event').value.trim();
|
|
184
|
+
const traceId = document.getElementById('logs-trace').value.trim();
|
|
185
|
+
const conversationId = document.getElementById('logs-conversation').value.trim();
|
|
186
|
+
const tokenId = document.getElementById('logs-token').value.trim();
|
|
187
|
+
const search = document.getElementById('logs-search').value.trim();
|
|
188
|
+
const limit = Number.parseInt(document.getElementById('logs-limit').value, 10) || 200;
|
|
189
|
+
|
|
190
|
+
const params = new URLSearchParams();
|
|
191
|
+
params.set('limit', String(Math.min(1000, Math.max(1, limit))));
|
|
192
|
+
if (level) params.set('level', level);
|
|
193
|
+
if (component) params.set('component', component);
|
|
194
|
+
if (event) params.set('event', event);
|
|
195
|
+
if (traceId) params.set('trace_id', traceId);
|
|
196
|
+
if (conversationId) params.set('conversation_id', conversationId);
|
|
197
|
+
if (tokenId) params.set('token_id', tokenId);
|
|
198
|
+
if (search) params.set('search', search);
|
|
199
|
+
|
|
200
|
+
return params;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function renderLogStats() {
|
|
204
|
+
const el = document.getElementById('log-stats');
|
|
205
|
+
if (!state.logStats) {
|
|
206
|
+
el.textContent = '';
|
|
207
|
+
el.style.display = 'none';
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const stats = state.logStats;
|
|
211
|
+
const levels = Object.entries(stats.by_level || {}).sort((a, b) => a[0].localeCompare(b[0]));
|
|
212
|
+
const components = Object.entries(stats.by_component || {})
|
|
213
|
+
.sort((a, b) => b[1] - a[1])
|
|
214
|
+
.slice(0, 12);
|
|
215
|
+
|
|
216
|
+
el.style.display = 'block';
|
|
217
|
+
el.innerHTML = `
|
|
218
|
+
<div class="row">
|
|
219
|
+
<strong>Total:</strong> ${stats.total || 0}
|
|
220
|
+
</div>
|
|
221
|
+
<div class="row">
|
|
222
|
+
<strong>By level:</strong> ${levels.map(([k, v]) => `${esc(k)}=${v}`).join(' · ') || '(none)'}
|
|
223
|
+
</div>
|
|
224
|
+
<div class="row">
|
|
225
|
+
<strong>Top components:</strong> ${components.map(([k, v]) => `${esc(k)}=${v}`).join(' · ') || '(none)'}
|
|
226
|
+
</div>
|
|
227
|
+
`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function renderTraceDetail() {
|
|
231
|
+
const el = document.getElementById('trace-detail');
|
|
232
|
+
if (!state.trace || !Array.isArray(state.trace.logs)) {
|
|
233
|
+
el.textContent = '';
|
|
234
|
+
el.style.display = 'none';
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
el.style.display = 'block';
|
|
238
|
+
const logs = state.trace.logs || [];
|
|
239
|
+
const lines = logs.map(row => {
|
|
240
|
+
const msg = row.message || '';
|
|
241
|
+
const meta = [
|
|
242
|
+
row.component ? row.component : null,
|
|
243
|
+
row.event ? row.event : null,
|
|
244
|
+
row.error_code ? `code=${row.error_code}` : null,
|
|
245
|
+
row.status_code ? `status=${row.status_code}` : null
|
|
246
|
+
].filter(Boolean).join(' ');
|
|
247
|
+
return `[${fmtDate(row.timestamp)}] ${row.level?.toUpperCase() || ''} ${meta}\n${msg}${row.hint ? `\nHint: ${row.hint}` : ''}`;
|
|
248
|
+
}).join('\n\n');
|
|
249
|
+
|
|
250
|
+
el.innerHTML = `
|
|
251
|
+
<div class="row">
|
|
252
|
+
<h3 style="margin:0;">Trace: <span class="mono">${esc(state.trace.trace_id || '')}</span></h3>
|
|
253
|
+
<button id="clear-trace">Clear</button>
|
|
254
|
+
</div>
|
|
255
|
+
<pre class="summary mono">${esc(lines || 'No trace logs.')}</pre>
|
|
256
|
+
`;
|
|
257
|
+
const clearBtn = document.getElementById('clear-trace');
|
|
258
|
+
if (clearBtn) {
|
|
259
|
+
clearBtn.addEventListener('click', () => {
|
|
260
|
+
state.trace = null;
|
|
261
|
+
renderTraceDetail();
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function renderLogs() {
|
|
267
|
+
const tbody = document.querySelector('#logs-table tbody');
|
|
268
|
+
tbody.innerHTML = '';
|
|
269
|
+
|
|
270
|
+
state.logs.forEach(row => {
|
|
271
|
+
const tr = document.createElement('tr');
|
|
272
|
+
const trace = row.trace_id || '';
|
|
273
|
+
tr.innerHTML = `
|
|
274
|
+
<td>${esc(fmtDate(row.timestamp))}</td>
|
|
275
|
+
<td>${esc(row.level || '-')}</td>
|
|
276
|
+
<td>${esc(row.component || '-')}</td>
|
|
277
|
+
<td>${esc(row.event || '-')}</td>
|
|
278
|
+
<td title="${esc(row.message || '')}">${esc(String(row.message || '').slice(0, 120) || '-')}</td>
|
|
279
|
+
<td class="mono">${esc(trace ? trace.slice(0, 14) + '…' : '-')}</td>
|
|
280
|
+
<td class="mono">${esc(row.conversation_id ? row.conversation_id.slice(0, 14) + '…' : '-')}</td>
|
|
281
|
+
<td class="mono">${esc(row.token_id || '-')}</td>
|
|
282
|
+
<td>${esc(row.error_code || '-')}</td>
|
|
283
|
+
<td>${esc(row.status_code ?? '-')}</td>
|
|
284
|
+
`;
|
|
285
|
+
if (trace) {
|
|
286
|
+
tr.addEventListener('click', () => loadTrace(trace).catch(err => showNotice(err.message)));
|
|
287
|
+
}
|
|
288
|
+
tbody.appendChild(tr);
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function loadLogs() {
|
|
293
|
+
const qs = readLogFilters().toString();
|
|
294
|
+
const payload = await request(`/logs?${qs}`);
|
|
295
|
+
state.logs = payload.logs || [];
|
|
296
|
+
renderLogs();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function loadLogStats() {
|
|
300
|
+
const payload = await request('/logs/stats');
|
|
301
|
+
state.logStats = payload.stats || null;
|
|
302
|
+
renderLogStats();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function loadTrace(traceId) {
|
|
306
|
+
const payload = await request(`/logs/trace/${encodeURIComponent(traceId)}?limit=500`);
|
|
307
|
+
state.trace = payload;
|
|
308
|
+
renderTraceDetail();
|
|
309
|
+
}
|
|
310
|
+
|
|
147
311
|
function fillTierSelects() {
|
|
148
312
|
const tiers = (state.settings?.tiers || []).slice().sort((a, b) => a.id.localeCompare(b.id));
|
|
149
313
|
const tierSelect = document.getElementById('tier-select');
|
|
@@ -326,6 +490,30 @@ function bindRefreshButtons() {
|
|
|
326
490
|
document.getElementById('refresh-contacts').addEventListener('click', () => loadContacts().catch(err => showNotice(err.message)));
|
|
327
491
|
document.getElementById('refresh-calls').addEventListener('click', () => loadCalls().catch(err => showNotice(err.message)));
|
|
328
492
|
document.getElementById('refresh-invites').addEventListener('click', () => loadInvites().catch(err => showNotice(err.message)));
|
|
493
|
+
document.getElementById('refresh-logs').addEventListener('click', () => loadLogs().catch(err => showNotice(err.message)));
|
|
494
|
+
document.getElementById('refresh-log-stats').addEventListener('click', () => loadLogStats().catch(err => showNotice(err.message)));
|
|
495
|
+
|
|
496
|
+
// Auto-refresh logs as filters change (debounced).
|
|
497
|
+
let debounce = null;
|
|
498
|
+
const schedule = () => {
|
|
499
|
+
clearTimeout(debounce);
|
|
500
|
+
debounce = setTimeout(() => loadLogs().catch(err => showNotice(err.message)), 250);
|
|
501
|
+
};
|
|
502
|
+
[
|
|
503
|
+
'logs-level',
|
|
504
|
+
'logs-component',
|
|
505
|
+
'logs-event',
|
|
506
|
+
'logs-trace',
|
|
507
|
+
'logs-conversation',
|
|
508
|
+
'logs-token',
|
|
509
|
+
'logs-search',
|
|
510
|
+
'logs-limit'
|
|
511
|
+
].forEach(id => {
|
|
512
|
+
const el = document.getElementById(id);
|
|
513
|
+
if (!el) return;
|
|
514
|
+
el.addEventListener('input', schedule);
|
|
515
|
+
el.addEventListener('change', schedule);
|
|
516
|
+
});
|
|
329
517
|
}
|
|
330
518
|
|
|
331
519
|
async function bootstrap() {
|
|
@@ -335,7 +523,7 @@ async function bootstrap() {
|
|
|
335
523
|
bindRefreshButtons();
|
|
336
524
|
|
|
337
525
|
try {
|
|
338
|
-
await Promise.all([loadSettings(), loadContacts(), loadCalls(), loadInvites()]);
|
|
526
|
+
await Promise.all([loadSettings(), loadContacts(), loadCalls(), loadInvites(), loadLogStats(), loadLogs()]);
|
|
339
527
|
showNotice('Dashboard loaded');
|
|
340
528
|
} catch (err) {
|
|
341
529
|
showNotice(err.message);
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
<nav>
|
|
16
16
|
<button class="tab is-active" data-tab="contacts">Contacts</button>
|
|
17
17
|
<button class="tab" data-tab="calls">Calls</button>
|
|
18
|
+
<button class="tab" data-tab="logs">Logs</button>
|
|
18
19
|
<button class="tab" data-tab="settings">Settings</button>
|
|
19
20
|
<button class="tab" data-tab="invites">Invites</button>
|
|
20
21
|
</nav>
|
|
@@ -61,6 +62,56 @@
|
|
|
61
62
|
<div id="call-detail"></div>
|
|
62
63
|
</section>
|
|
63
64
|
|
|
65
|
+
<section id="tab-logs" class="panel">
|
|
66
|
+
<div class="row">
|
|
67
|
+
<h2>Logs</h2>
|
|
68
|
+
<button id="refresh-logs">Refresh</button>
|
|
69
|
+
<button id="refresh-log-stats">Stats</button>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div class="filters">
|
|
73
|
+
<label>Level
|
|
74
|
+
<select id="logs-level">
|
|
75
|
+
<option value="">(any)</option>
|
|
76
|
+
<option value="trace">trace</option>
|
|
77
|
+
<option value="debug">debug</option>
|
|
78
|
+
<option value="info">info</option>
|
|
79
|
+
<option value="warn">warn</option>
|
|
80
|
+
<option value="error">error</option>
|
|
81
|
+
</select>
|
|
82
|
+
</label>
|
|
83
|
+
<label>Component <input id="logs-component" type="text" placeholder="a2a.routes"></label>
|
|
84
|
+
<label>Event <input id="logs-event" type="text" placeholder="invoke"></label>
|
|
85
|
+
<label>Trace ID <input id="logs-trace" type="text" placeholder="a2a_..."></label>
|
|
86
|
+
<label>Conversation ID <input id="logs-conversation" type="text" placeholder="conv_..."></label>
|
|
87
|
+
<label>Token ID <input id="logs-token" type="text" placeholder="tok_..."></label>
|
|
88
|
+
<label>Search <input id="logs-search" type="text" placeholder="text"></label>
|
|
89
|
+
<label>Limit <input id="logs-limit" type="number" min="1" max="1000" value="200"></label>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div id="log-stats" class="card"></div>
|
|
93
|
+
|
|
94
|
+
<table id="logs-table">
|
|
95
|
+
<thead>
|
|
96
|
+
<tr>
|
|
97
|
+
<th>Time</th>
|
|
98
|
+
<th>Level</th>
|
|
99
|
+
<th>Component</th>
|
|
100
|
+
<th>Event</th>
|
|
101
|
+
<th>Message</th>
|
|
102
|
+
<th>Trace</th>
|
|
103
|
+
<th>Conv</th>
|
|
104
|
+
<th>Tok</th>
|
|
105
|
+
<th>Code</th>
|
|
106
|
+
<th>Status</th>
|
|
107
|
+
</tr>
|
|
108
|
+
</thead>
|
|
109
|
+
<tbody></tbody>
|
|
110
|
+
</table>
|
|
111
|
+
|
|
112
|
+
<div id="trace-detail" class="card"></div>
|
|
113
|
+
</section>
|
|
114
|
+
|
|
64
115
|
<section id="tab-settings" class="panel">
|
|
65
116
|
<h2>Tier Settings</h2>
|
|
66
117
|
<div class="row">
|
|
@@ -85,6 +85,21 @@ h3 {
|
|
|
85
85
|
margin-bottom: 0.8rem;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
.filters {
|
|
89
|
+
display: grid;
|
|
90
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
91
|
+
gap: 0.6rem;
|
|
92
|
+
margin-bottom: 0.9rem;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.card {
|
|
96
|
+
border: 1px solid var(--line);
|
|
97
|
+
border-radius: 10px;
|
|
98
|
+
padding: 0.75rem 0.8rem;
|
|
99
|
+
background: #fbfdff;
|
|
100
|
+
margin-bottom: 0.9rem;
|
|
101
|
+
}
|
|
102
|
+
|
|
88
103
|
label {
|
|
89
104
|
display: block;
|
|
90
105
|
margin-bottom: 0.6rem;
|
|
@@ -149,6 +164,18 @@ th {
|
|
|
149
164
|
white-space: pre-wrap;
|
|
150
165
|
}
|
|
151
166
|
|
|
167
|
+
.mono {
|
|
168
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#logs-table td {
|
|
172
|
+
cursor: default;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#logs-table tr:hover td {
|
|
176
|
+
background: #f6fbff;
|
|
177
|
+
}
|
|
178
|
+
|
|
152
179
|
#notice {
|
|
153
180
|
margin-top: 1rem;
|
|
154
181
|
padding: 0.7rem 0.8rem;
|
package/src/index.js
CHANGED
|
@@ -19,6 +19,7 @@ const { A2AClient, A2AError } = require('./lib/client');
|
|
|
19
19
|
const { createRoutes } = require('./routes/a2a');
|
|
20
20
|
const { createDashboardApiRouter, createDashboardUiRouter } = require('./routes/dashboard');
|
|
21
21
|
const { createRuntimeAdapter, resolveRuntimeMode } = require('./lib/runtime-adapter');
|
|
22
|
+
const { createLogger, createTraceId } = require('./lib/logger');
|
|
22
23
|
|
|
23
24
|
// Lazy load optional dependencies
|
|
24
25
|
let ConversationStore = null;
|
|
@@ -53,6 +54,10 @@ module.exports = {
|
|
|
53
54
|
// Runtime adapter
|
|
54
55
|
createRuntimeAdapter,
|
|
55
56
|
resolveRuntimeMode,
|
|
57
|
+
|
|
58
|
+
// Structured logging
|
|
59
|
+
createLogger,
|
|
60
|
+
createTraceId,
|
|
56
61
|
|
|
57
62
|
// Conversation storage (requires better-sqlite3)
|
|
58
63
|
ConversationStore,
|