a2acalling 0.5.1 → 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 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 phone system for agents.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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 candidate = typeof pluginEntry.backendUrl === "string" && pluginEntry.backendUrl
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 existingEntry = config.plugins.entries[DASHBOARD_PLUGIN_ID] || {};
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
- backendUrl
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('&', '&amp;')
55
+ .replaceAll('<', '&lt;')
56
+ .replaceAll('>', '&gt;')
57
+ .replaceAll('"', '&quot;')
58
+ .replaceAll("'", '&#039;');
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
- const tab = btn.dataset.tab;
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,