@defai.digital/cli 13.4.5 → 13.4.8

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.
Files changed (43) hide show
  1. package/bundled/agents/architect.json +117 -0
  2. package/bundled/agents/auditor.json +114 -0
  3. package/bundled/agents/bug-hunter.json +128 -0
  4. package/bundled/agents/builder.json +128 -0
  5. package/bundled/agents/ceo.json +6 -1
  6. package/bundled/agents/executor.json +150 -0
  7. package/bundled/agents/fullstack.json +10 -2
  8. package/bundled/agents/operator.json +119 -0
  9. package/bundled/agents/researcher.json +42 -13
  10. package/bundled/agents/reviewer.json +90 -42
  11. package/dist/bootstrap.d.ts.map +1 -1
  12. package/dist/bootstrap.js +10 -6
  13. package/dist/bootstrap.js.map +1 -1
  14. package/dist/commands/discuss.d.ts.map +1 -1
  15. package/dist/commands/discuss.js +4 -1
  16. package/dist/commands/discuss.js.map +1 -1
  17. package/dist/commands/doctor.d.ts +1 -1
  18. package/dist/commands/doctor.js +3 -3
  19. package/dist/commands/doctor.js.map +1 -1
  20. package/dist/commands/init.d.ts.map +1 -1
  21. package/dist/commands/init.js +65 -5
  22. package/dist/commands/init.js.map +1 -1
  23. package/dist/commands/monitor.d.ts.map +1 -1
  24. package/dist/commands/monitor.js +29 -1
  25. package/dist/commands/monitor.js.map +1 -1
  26. package/dist/commands/setup.d.ts.map +1 -1
  27. package/dist/commands/setup.js +119 -3
  28. package/dist/commands/setup.js.map +1 -1
  29. package/dist/commands/status.d.ts +10 -0
  30. package/dist/commands/status.d.ts.map +1 -1
  31. package/dist/commands/status.js +151 -49
  32. package/dist/commands/status.js.map +1 -1
  33. package/dist/commands/update.d.ts.map +1 -1
  34. package/dist/commands/update.js +1 -43
  35. package/dist/commands/update.js.map +1 -1
  36. package/dist/web/api.d.ts +28 -0
  37. package/dist/web/api.d.ts.map +1 -1
  38. package/dist/web/api.js +508 -40
  39. package/dist/web/api.js.map +1 -1
  40. package/dist/web/dashboard.d.ts.map +1 -1
  41. package/dist/web/dashboard.js +1460 -134
  42. package/dist/web/dashboard.js.map +1 -1
  43. package/package.json +21 -21
package/dist/web/api.js CHANGED
@@ -3,12 +3,45 @@
3
3
  *
4
4
  * Provides REST API endpoints for the web dashboard.
5
5
  * Wraps existing domain services to expose via HTTP.
6
+ *
7
+ * KNOWN PERFORMANCE ISSUE (INV-API-PERF-001):
8
+ * Several handlers use an N+1 query pattern where traces are fetched first,
9
+ * then events are fetched for each trace individually. This is acceptable for
10
+ * the local dashboard use case (typically < 200 traces), but would need
11
+ * refactoring for production scale. Consider batch fetching or pagination
12
+ * if performance becomes an issue.
13
+ *
14
+ * Affected handlers: handleStatus, handleProviderHistory, handleAgentHistory,
15
+ * handleTraces, handleWorkflowEvents, handleClassificationStats
6
16
  */
7
17
  import * as os from 'node:os';
8
18
  import * as fs from 'node:fs';
9
19
  import * as path from 'node:path';
10
20
  import { fileURLToPath } from 'node:url';
11
21
  import { DATA_DIR_NAME, AGENTS_FILENAME, getErrorMessage } from '@defai.digital/contracts';
22
+ // ============================================================================
23
+ // INV-API-VAL-001: Input Validation Constants
24
+ // ============================================================================
25
+ /** Maximum length for any ID parameter to prevent DoS via long strings */
26
+ const MAX_ID_LENGTH = 128;
27
+ /** UUID v4 format regex for trace IDs */
28
+ const UUID_REGEX = /^[a-f0-9]{8}-[a-f0-9]{4}-[4][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
29
+ /**
30
+ * Validates a trace ID is a valid UUID and within length limits
31
+ * INV-API-VAL-001: Strict UUID validation for trace IDs
32
+ */
33
+ function isValidTraceId(id) {
34
+ if (!id || id.length > MAX_ID_LENGTH)
35
+ return false;
36
+ return UUID_REGEX.test(id);
37
+ }
38
+ /**
39
+ * Validates a generic ID (workflow, agent, provider) is within length limits
40
+ * INV-API-VAL-001: Length validation for all IDs
41
+ */
42
+ function isValidId(id) {
43
+ return typeof id === 'string' && id.length > 0 && id.length <= MAX_ID_LENGTH;
44
+ }
12
45
  // Get the directory of this module (for finding bundled examples)
13
46
  const __filename = fileURLToPath(import.meta.url);
14
47
  const __dirname = path.dirname(__filename);
@@ -16,6 +49,7 @@ import { getProviderRegistry, getTraceStore, getSessionManager as getSharedSessi
16
49
  import { createPersistentAgentRegistry, } from '@defai.digital/agent-domain';
17
50
  import { createWorkflowLoader, findWorkflowDir, } from '@defai.digital/workflow-engine';
18
51
  import { CLI_VERSION } from '../commands/help.js';
52
+ import { getProviderStatus as checkProviderHealth } from '../commands/status.js';
19
53
  // Workflow loader singleton
20
54
  let workflowLoader;
21
55
  let workflowLoaderPromise;
@@ -109,6 +143,16 @@ async function getAgentRegistry() {
109
143
  }
110
144
  return agentRegistryPromise;
111
145
  }
146
+ /**
147
+ * Cached provider status (set at monitor startup)
148
+ */
149
+ let cachedProviderStatus = null;
150
+ /**
151
+ * Set the cached provider status (called from monitor.ts at startup)
152
+ */
153
+ export function setCachedProviderStatus(providers) {
154
+ cachedProviderStatus = providers;
155
+ }
112
156
  /**
113
157
  * Create the API handler function
114
158
  */
@@ -120,6 +164,7 @@ export function createAPIHandler() {
120
164
  let response;
121
165
  // Check for parameterized routes first
122
166
  const traceTreeMatch = apiPath.match(/^\/traces\/([a-f0-9-]+)\/tree$/i);
167
+ const traceClassificationMatch = apiPath.match(/^\/traces\/([a-f0-9-]+)\/classification$/i);
123
168
  const traceDetailMatch = apiPath.match(/^\/traces\/([a-f0-9-]+)$/i);
124
169
  const workflowDetailMatch = apiPath.match(/^\/workflows\/([a-z0-9-]+)$/i);
125
170
  const providerHistoryMatch = apiPath.match(/^\/providers\/([a-z0-9-]+)\/history$/i);
@@ -128,31 +173,37 @@ export function createAPIHandler() {
128
173
  const workflowEventsMatch = apiPath.match(/^\/workflows\/([a-z0-9-]+)\/events$/i);
129
174
  const traceSearchMatch = apiPath.match(/^\/traces\/search\?(.*)$/i) || apiPath.match(/^\/traces\/search$/i);
130
175
  // INV-TR-020 through INV-TR-024: Hierarchical trace tree endpoint
131
- if (traceTreeMatch && traceTreeMatch[1]) {
176
+ // INV-API-VAL-001: Validate all IDs before processing
177
+ if (traceTreeMatch && isValidTraceId(traceTreeMatch[1])) {
132
178
  const traceId = traceTreeMatch[1];
133
179
  response = await handleTraceTree(traceId);
134
180
  }
135
- else if (traceDetailMatch && traceDetailMatch[1]) {
181
+ else if (traceClassificationMatch && isValidTraceId(traceClassificationMatch[1])) {
182
+ // PRD-2026-003: Classification observability endpoint
183
+ const traceId = traceClassificationMatch[1];
184
+ response = await handleTraceClassification(traceId);
185
+ }
186
+ else if (traceDetailMatch && isValidTraceId(traceDetailMatch[1])) {
136
187
  const traceId = traceDetailMatch[1];
137
188
  response = await handleTraceDetail(traceId);
138
189
  }
139
- else if (workflowDetailMatch && workflowDetailMatch[1]) {
190
+ else if (workflowDetailMatch && isValidId(workflowDetailMatch[1])) {
140
191
  const workflowId = workflowDetailMatch[1];
141
192
  response = await handleWorkflowDetail(workflowId);
142
193
  }
143
- else if (providerHistoryMatch && providerHistoryMatch[1]) {
194
+ else if (providerHistoryMatch && isValidId(providerHistoryMatch[1])) {
144
195
  const providerId = providerHistoryMatch[1];
145
196
  response = await handleProviderHistory(providerId);
146
197
  }
147
- else if (agentHistoryMatch && agentHistoryMatch[1]) {
198
+ else if (agentHistoryMatch && isValidId(agentHistoryMatch[1])) {
148
199
  const agentId = agentHistoryMatch[1];
149
200
  response = await handleAgentHistory(agentId);
150
201
  }
151
- else if (agentDetailMatch && agentDetailMatch[1]) {
202
+ else if (agentDetailMatch && isValidId(agentDetailMatch[1])) {
152
203
  const agentId = agentDetailMatch[1];
153
204
  response = await handleAgentDetail(agentId);
154
205
  }
155
- else if (workflowEventsMatch && workflowEventsMatch[1]) {
206
+ else if (workflowEventsMatch && isValidId(workflowEventsMatch[1])) {
156
207
  const workflowId = workflowEventsMatch[1];
157
208
  response = await handleWorkflowEvents(workflowId);
158
209
  }
@@ -187,6 +238,17 @@ export function createAPIHandler() {
187
238
  case '/providers':
188
239
  response = await handleProviders();
189
240
  break;
241
+ case '/providers/refresh':
242
+ // POST endpoint to refresh provider health status
243
+ if (req.method === 'POST') {
244
+ response = await handleProviderRefresh();
245
+ }
246
+ else {
247
+ res.writeHead(405, { 'Content-Type': 'application/json' });
248
+ res.end(JSON.stringify({ success: false, error: 'Method not allowed' }));
249
+ return;
250
+ }
251
+ break;
190
252
  case '/sessions':
191
253
  response = await handleSessions();
192
254
  break;
@@ -232,12 +294,13 @@ export function createAPIHandler() {
232
294
  * Get full system status
233
295
  */
234
296
  async function handleStatus() {
235
- const [providers, sessions, agents, traces, metrics] = await Promise.all([
297
+ const [providers, sessions, agents, traces, metrics, classificationMetrics] = await Promise.all([
236
298
  getProviderData(),
237
299
  getSessionData(),
238
300
  getAgentData(),
239
301
  getTraceData(),
240
302
  getMetricsData(),
303
+ getClassificationMetrics(),
241
304
  ]);
242
305
  // Determine health status
243
306
  const healthyProviders = providers.filter(p => p.available).length;
@@ -256,6 +319,8 @@ async function handleStatus() {
256
319
  agents,
257
320
  traces,
258
321
  metrics,
322
+ // PRD-2026-003: Classification observability
323
+ classification: classificationMetrics,
259
324
  },
260
325
  };
261
326
  }
@@ -266,6 +331,50 @@ async function handleProviders() {
266
331
  const providers = await getProviderData();
267
332
  return { success: true, data: providers };
268
333
  }
334
+ /**
335
+ * Helper to normalize circuit state for dashboard
336
+ */
337
+ function normalizeCircuitState(state) {
338
+ if (state === 'halfOpen')
339
+ return 'half-open';
340
+ if (state === 'open')
341
+ return 'open';
342
+ return 'closed';
343
+ }
344
+ /**
345
+ * Refresh provider status by running actual health checks
346
+ * This triggers real LLM calls to test each provider
347
+ */
348
+ async function handleProviderRefresh() {
349
+ try {
350
+ // Run real health checks (sends "hello" to each provider)
351
+ const providerStatuses = await checkProviderHealth(false);
352
+ // Convert to dashboard format and update cache
353
+ const dashboardProviders = providerStatuses.map(p => ({
354
+ providerId: p.providerId,
355
+ name: p.providerId,
356
+ available: p.available,
357
+ latencyMs: p.latencyMs,
358
+ circuitState: normalizeCircuitState(p.circuitState),
359
+ lastUsed: undefined,
360
+ }));
361
+ // Update the cached status
362
+ setCachedProviderStatus(dashboardProviders);
363
+ return {
364
+ success: true,
365
+ data: {
366
+ providers: dashboardProviders,
367
+ refreshedAt: new Date().toISOString(),
368
+ },
369
+ };
370
+ }
371
+ catch (error) {
372
+ return {
373
+ success: false,
374
+ error: getErrorMessage(error, 'Failed to refresh provider status'),
375
+ };
376
+ }
377
+ }
269
378
  /**
270
379
  * Get sessions
271
380
  */
@@ -310,37 +419,85 @@ async function handleAgentDetail(agentId) {
310
419
  totalDurationMs += trace.durationMs;
311
420
  }
312
421
  }
422
+ // PRD-2026-004: Build response with all meta-agent fields
423
+ const responseData = {
424
+ agentId: agent.agentId,
425
+ displayName: agent.displayName ?? agent.agentId,
426
+ description: agent.description,
427
+ version: agent.version,
428
+ role: agent.role,
429
+ team: agent.team,
430
+ enabled: agent.enabled,
431
+ capabilities: agent.capabilities ?? [],
432
+ tags: agent.tags ?? [],
433
+ systemPrompt: agent.systemPrompt,
434
+ workflow: agent.workflow?.map(step => ({
435
+ stepId: step.stepId,
436
+ name: step.name,
437
+ type: step.type,
438
+ config: step.config,
439
+ dependencies: step.dependencies,
440
+ condition: step.condition,
441
+ })) ?? [],
442
+ orchestration: agent.orchestration,
443
+ personality: agent.personality,
444
+ expertise: agent.expertise,
445
+ // Stats
446
+ stats: {
447
+ executionCount,
448
+ successRate: executionCount > 0 ? successCount / executionCount : 0,
449
+ avgDurationMs: executionCount > 0 ? Math.round(totalDurationMs / executionCount) : 0,
450
+ },
451
+ };
452
+ // PRD-2026-004: Add meta-agent architecture fields
453
+ if (agent.metaAgent !== undefined)
454
+ responseData.metaAgent = agent.metaAgent;
455
+ if (agent.archetype !== undefined)
456
+ responseData.archetype = agent.archetype;
457
+ if (agent.orchestrates?.length)
458
+ responseData.orchestrates = agent.orchestrates;
459
+ if (agent.replaces?.length)
460
+ responseData.replaces = agent.replaces;
461
+ if (agent.canDelegateToArchetypes?.length)
462
+ responseData.canDelegateToArchetypes = agent.canDelegateToArchetypes;
463
+ if (agent.canDelegateToMetaAgents?.length)
464
+ responseData.canDelegateToMetaAgents = agent.canDelegateToMetaAgents;
465
+ // PRD-2026-004: Add task classifier config
466
+ if (agent.taskClassifier) {
467
+ responseData.taskClassifier = {
468
+ enabled: agent.taskClassifier.enabled ?? true,
469
+ rules: agent.taskClassifier.rules?.map(rule => ({
470
+ pattern: rule.pattern,
471
+ taskType: rule.taskType,
472
+ workflow: rule.workflow,
473
+ priority: rule.priority ?? 50,
474
+ })) ?? [],
475
+ defaultWorkflow: agent.taskClassifier.defaultWorkflow,
476
+ fuzzyMatching: agent.taskClassifier.fuzzyMatching,
477
+ minConfidence: agent.taskClassifier.minConfidence,
478
+ };
479
+ }
480
+ // PRD-2026-004: Add dynamic capabilities config
481
+ if (agent.dynamicCapabilities) {
482
+ responseData.dynamicCapabilities = agent.dynamicCapabilities;
483
+ }
484
+ // PRD-2026-004: Add review modes config
485
+ if (agent.reviewModes) {
486
+ responseData.reviewModes = agent.reviewModes;
487
+ }
488
+ // PRD-2026-004: Add capability mappings
489
+ if (agent.capabilityMappings?.length) {
490
+ responseData.capabilityMappings = agent.capabilityMappings.map(mapping => ({
491
+ taskType: mapping.taskType,
492
+ workflowRef: mapping.workflowRef,
493
+ abilities: mapping.abilities,
494
+ priority: mapping.priority,
495
+ description: mapping.description,
496
+ }));
497
+ }
313
498
  return {
314
499
  success: true,
315
- data: {
316
- agentId: agent.agentId,
317
- displayName: agent.displayName ?? agent.agentId,
318
- description: agent.description,
319
- version: agent.version,
320
- role: agent.role,
321
- team: agent.team,
322
- enabled: agent.enabled,
323
- capabilities: agent.capabilities ?? [],
324
- tags: agent.tags ?? [],
325
- systemPrompt: agent.systemPrompt,
326
- workflow: agent.workflow?.map(step => ({
327
- stepId: step.stepId,
328
- name: step.name,
329
- type: step.type,
330
- config: step.config,
331
- dependencies: step.dependencies,
332
- condition: step.condition,
333
- })) ?? [],
334
- orchestration: agent.orchestration,
335
- personality: agent.personality,
336
- expertise: agent.expertise,
337
- // Stats
338
- stats: {
339
- executionCount,
340
- successRate: executionCount > 0 ? successCount / executionCount : 0,
341
- avgDurationMs: executionCount > 0 ? Math.round(totalDurationMs / executionCount) : 0,
342
- },
343
- },
500
+ data: responseData,
344
501
  };
345
502
  }
346
503
  catch (error) {
@@ -402,7 +559,9 @@ async function handleTraceDetail(traceId) {
402
559
  const context = startEvent?.context;
403
560
  // Determine command type and extract relevant info
404
561
  const command = startPayload?.command;
405
- // First try to detect from explicit command field
562
+ const tool = startPayload?.tool;
563
+ const workflowId = context?.workflowId;
564
+ // First try to detect from explicit command/tool field
406
565
  let commandType;
407
566
  if (command?.includes('discuss')) {
408
567
  commandType = 'discuss';
@@ -413,14 +572,22 @@ async function handleTraceDetail(traceId) {
413
572
  else if (command?.includes('call')) {
414
573
  commandType = 'call';
415
574
  }
575
+ else if (tool?.startsWith('research') || workflowId?.startsWith('research')) {
576
+ commandType = 'research';
577
+ }
578
+ else if (tool?.startsWith('review') || workflowId?.startsWith('review')) {
579
+ commandType = 'review';
580
+ }
416
581
  else {
417
582
  // Fallback: detect from context/payload fields when command is not set
418
583
  const hasAgentId = context?.agentId || startPayload?.agentId;
419
584
  const hasTopic = startPayload?.topic;
420
585
  const hasPrompt = startPayload?.prompt;
586
+ const hasQuery = startPayload?.query;
421
587
  commandType = hasAgentId ? 'agent' :
422
588
  hasTopic ? 'discuss' :
423
- hasPrompt ? 'call' : 'unknown';
589
+ hasQuery ? 'research' :
590
+ hasPrompt ? 'call' : 'unknown';
424
591
  }
425
592
  // Extract input/output based on command type
426
593
  let input;
@@ -465,6 +632,8 @@ async function handleTraceDetail(traceId) {
465
632
  const agentId = startPayload?.agentId;
466
633
  // Support both 'task' (CLI) and 'input' (MCP) field names
467
634
  const taskOrInput = startPayload?.task ?? startPayload?.input;
635
+ // Extract provider from run.end payload (finalProvider)
636
+ provider = endPayload?.finalProvider;
468
637
  input = {
469
638
  agentId,
470
639
  task: taskOrInput,
@@ -473,6 +642,62 @@ async function handleTraceDetail(traceId) {
473
642
  output = {
474
643
  stepCount: endPayload?.stepCount,
475
644
  result: endPayload?.result ?? endPayload?.output,
645
+ // Include rich payload data for dashboard visibility
646
+ finalContent: endPayload?.finalContent,
647
+ agentDisplayName: endPayload?.agentDisplayName,
648
+ agentDescription: endPayload?.agentDescription,
649
+ inputTask: endPayload?.inputTask,
650
+ tokenUsage: endPayload?.tokenUsage,
651
+ stepResults: endPayload?.stepResults,
652
+ success: endPayload?.success,
653
+ error: endPayload?.error,
654
+ };
655
+ }
656
+ else if (commandType === 'research') {
657
+ input = {
658
+ query: startPayload?.query,
659
+ tool: startPayload?.tool,
660
+ sources: startPayload?.sources,
661
+ maxSources: startPayload?.maxSources,
662
+ synthesize: startPayload?.synthesize,
663
+ };
664
+ output = {
665
+ // Full artifacts for dashboard visibility
666
+ query: endPayload?.query,
667
+ sources: endPayload?.sources,
668
+ synthesis: endPayload?.synthesis,
669
+ codeExamples: endPayload?.codeExamples,
670
+ confidence: endPayload?.confidence,
671
+ warnings: endPayload?.warnings,
672
+ // For fetch operations
673
+ url: endPayload?.url,
674
+ title: endPayload?.title,
675
+ contentPreview: endPayload?.contentPreview,
676
+ codeBlocks: endPayload?.codeBlocks,
677
+ reliability: endPayload?.reliability,
678
+ success: endPayload?.success,
679
+ error: endPayload?.error,
680
+ };
681
+ }
682
+ else if (commandType === 'review') {
683
+ // Extract provider from run.end payload (providerId)
684
+ provider = endPayload?.providerId;
685
+ input = {
686
+ paths: startPayload?.paths,
687
+ focus: startPayload?.focus,
688
+ context: startPayload?.context,
689
+ tool: startPayload?.tool,
690
+ dryRun: startPayload?.dryRun,
691
+ };
692
+ output = {
693
+ // Full review results for dashboard visibility
694
+ summary: endPayload?.summary,
695
+ comments: endPayload?.comments,
696
+ filesReviewed: endPayload?.filesReviewed,
697
+ filesReviewedCount: endPayload?.filesReviewedCount,
698
+ linesAnalyzed: endPayload?.linesAnalyzed,
699
+ commentCount: endPayload?.commentCount,
700
+ providerId: endPayload?.providerId,
476
701
  success: endPayload?.success,
477
702
  error: endPayload?.error,
478
703
  };
@@ -499,6 +724,66 @@ async function handleTraceDetail(traceId) {
499
724
  timestamp: step.timestamp,
500
725
  };
501
726
  });
727
+ // Extract provider conversations from discussion.provider events
728
+ // This provides real-time visibility into running discussions
729
+ const discussionProviderEvents = events.filter(e => e.type === 'discussion.provider');
730
+ const providerConversations = discussionProviderEvents.map(event => {
731
+ const payload = event.payload;
732
+ const context = event.context;
733
+ const promptValue = payload?.prompt;
734
+ const contentValue = payload?.content;
735
+ const durationMsValue = event.durationMs ?? payload?.durationMs;
736
+ const tokenCountValue = payload?.tokenCount;
737
+ const result = {
738
+ provider: (context?.providerId ?? payload?.providerId ?? 'unknown'),
739
+ round: (payload?.roundNumber ?? 1),
740
+ success: (payload?.success ?? event.status === 'success'),
741
+ timestamp: event.timestamp,
742
+ };
743
+ // Only add optional fields if they have values
744
+ if (promptValue !== undefined)
745
+ result.prompt = promptValue;
746
+ if (contentValue !== undefined)
747
+ result.content = contentValue;
748
+ if (durationMsValue !== undefined)
749
+ result.durationMs = durationMsValue;
750
+ if (tokenCountValue !== undefined)
751
+ result.tokenCount = tokenCountValue;
752
+ return result;
753
+ });
754
+ // Extract agent step conversations from workflow.step events
755
+ // This provides visibility into agent execution steps with LLM responses
756
+ const agentStepEvents = events.filter(e => e.type === 'workflow.step');
757
+ const agentStepConversations = agentStepEvents.map(event => {
758
+ const payload = event.payload;
759
+ const context = event.context;
760
+ const contentValue = payload?.content;
761
+ const durationMsValue = event.durationMs ?? payload?.durationMs;
762
+ const tokenUsage = context?.tokenUsage;
763
+ const result = {
764
+ stepId: (payload?.stepId ?? 'unknown'),
765
+ stepIndex: (payload?.stepIndex ?? 0),
766
+ success: (payload?.success ?? event.status === 'success'),
767
+ timestamp: event.timestamp,
768
+ };
769
+ // Only add optional fields if they have values
770
+ const providerValue = (context?.providerId ?? payload?.provider);
771
+ if (providerValue !== undefined)
772
+ result.provider = providerValue;
773
+ if (contentValue !== undefined)
774
+ result.content = contentValue;
775
+ if (durationMsValue !== undefined)
776
+ result.durationMs = durationMsValue;
777
+ if (tokenUsage?.total !== undefined)
778
+ result.tokenCount = tokenUsage.total;
779
+ else if (tokenUsage?.input !== undefined && tokenUsage?.output !== undefined) {
780
+ result.tokenCount = tokenUsage.input + tokenUsage.output;
781
+ }
782
+ const errorValue = payload?.error;
783
+ if (errorValue !== undefined)
784
+ result.error = errorValue;
785
+ return result;
786
+ });
502
787
  // Determine status from events
503
788
  let status = 'pending';
504
789
  if (endEvent) {
@@ -515,6 +800,8 @@ async function handleTraceDetail(traceId) {
515
800
  (endEvent && startEvent ?
516
801
  new Date(endEvent.timestamp).getTime() - new Date(startEvent.timestamp).getTime() :
517
802
  undefined);
803
+ // PRD-2026-003: Extract classification data from start event
804
+ const classificationData = startPayload?.classification;
518
805
  return {
519
806
  success: true,
520
807
  data: {
@@ -533,12 +820,18 @@ async function handleTraceDetail(traceId) {
533
820
  output,
534
821
  // Workflow steps (for agent runs)
535
822
  workflowSteps: workflowSteps.length > 0 ? workflowSteps : undefined,
823
+ // Provider conversations from discussion.provider events (for discussions)
824
+ providerConversations: providerConversations.length > 0 ? providerConversations : undefined,
825
+ // Agent step conversations from workflow.step events (for agents)
826
+ agentStepConversations: agentStepConversations.length > 0 ? agentStepConversations : undefined,
536
827
  // Summary
537
828
  summary: {
538
829
  eventCount: events.length,
539
830
  stepCount: stepEvents.length,
540
831
  errorCount: errorEvents.length,
541
832
  },
833
+ // PRD-2026-003: Classification data
834
+ classification: classificationData,
542
835
  // Full timeline for advanced view
543
836
  timeline,
544
837
  },
@@ -581,6 +874,25 @@ async function handleWorkflowDetail(workflowId) {
581
874
  });
582
875
  }
583
876
  }
877
+ // Get recent executions of this workflow from trace store
878
+ const traceStore = getTraceStore();
879
+ const allTraces = await traceStore.listTraces(100);
880
+ const workflowExecutions = [];
881
+ for (const trace of allTraces) {
882
+ const events = await traceStore.getTrace(trace.traceId);
883
+ const startEvent = events.find(e => e.type === 'run.start');
884
+ const context = startEvent?.context;
885
+ // Check if this trace is for this workflow
886
+ if (context?.workflowId === workflowId ||
887
+ startEvent?.payload?.workflowId === workflowId) {
888
+ workflowExecutions.push({
889
+ traceId: trace.traceId,
890
+ status: trace.status,
891
+ startTime: trace.startTime,
892
+ ...(trace.durationMs !== undefined && { durationMs: trace.durationMs }),
893
+ });
894
+ }
895
+ }
584
896
  return {
585
897
  success: true,
586
898
  data: {
@@ -588,8 +900,22 @@ async function handleWorkflowDetail(workflowId) {
588
900
  version: workflow.version,
589
901
  name: workflow.name,
590
902
  description: workflow.description,
591
- steps: workflow.steps,
903
+ // Full step details with configs
904
+ steps: workflow.steps.map(step => ({
905
+ stepId: step.stepId,
906
+ name: step.name ?? step.stepId,
907
+ type: step.type,
908
+ timeout: step.timeout,
909
+ dependencies: step.dependencies,
910
+ config: step.config,
911
+ })),
912
+ // Metadata if available
913
+ metadata: workflow.metadata,
914
+ // DAG for visualization
592
915
  dag: { nodes, edges },
916
+ // Recent executions
917
+ recentExecutions: workflowExecutions.slice(0, 10),
918
+ executionCount: workflowExecutions.length,
593
919
  },
594
920
  };
595
921
  }
@@ -874,6 +1200,74 @@ async function handleTraceTree(traceId) {
874
1200
  };
875
1201
  }
876
1202
  }
1203
+ /**
1204
+ * Get classification data for a trace
1205
+ * PRD-2026-003: Classification observability endpoint
1206
+ * INV-TR-030: Classification snapshot is immutable once recorded
1207
+ */
1208
+ async function handleTraceClassification(traceId) {
1209
+ try {
1210
+ const traceStore = getTraceStore();
1211
+ const events = await traceStore.getTrace(traceId);
1212
+ if (!events || events.length === 0) {
1213
+ return { success: false, error: `Trace not found: ${traceId}` };
1214
+ }
1215
+ // Find run.start event which contains classification data
1216
+ const startEvent = events.find(e => e.type === 'run.start');
1217
+ if (!startEvent) {
1218
+ return {
1219
+ success: true,
1220
+ data: {
1221
+ traceId,
1222
+ hasClassification: false,
1223
+ message: 'No classification data available for this trace',
1224
+ },
1225
+ };
1226
+ }
1227
+ const payload = startEvent.payload;
1228
+ const classification = payload?.classification;
1229
+ if (!classification) {
1230
+ return {
1231
+ success: true,
1232
+ data: {
1233
+ traceId,
1234
+ hasClassification: false,
1235
+ message: 'No classification data available for this trace',
1236
+ },
1237
+ };
1238
+ }
1239
+ // Calculate guard pass rate
1240
+ const guardResults = classification.guardResults ?? [];
1241
+ const guardPassRate = guardResults.length > 0
1242
+ ? guardResults.filter(g => g.passed).length / guardResults.length
1243
+ : 1.0;
1244
+ return {
1245
+ success: true,
1246
+ data: {
1247
+ traceId,
1248
+ hasClassification: true,
1249
+ classification: {
1250
+ taskType: classification.taskType,
1251
+ confidence: classification.confidence,
1252
+ matchedPatterns: classification.matchedPatterns ?? [],
1253
+ selectedMapping: classification.selectedMapping,
1254
+ alternatives: classification.alternatives ?? [],
1255
+ classificationTimeMs: classification.classificationTimeMs,
1256
+ guardResults,
1257
+ guardPassRate,
1258
+ taskDescription: classification.taskDescription,
1259
+ },
1260
+ timestamp: startEvent.timestamp,
1261
+ },
1262
+ };
1263
+ }
1264
+ catch (error) {
1265
+ return {
1266
+ success: false,
1267
+ error: getErrorMessage(error, 'Failed to fetch classification data'),
1268
+ };
1269
+ }
1270
+ }
877
1271
  /**
878
1272
  * Get workflow execution events - timeline of a workflow execution
879
1273
  * Uses workflow.start, workflow.step, workflow.end events (INV-TR-013)
@@ -985,8 +1379,14 @@ async function handleWorkflowEvents(workflowId) {
985
1379
  }
986
1380
  /**
987
1381
  * Fetch provider data
1382
+ * Uses cached status if available (set at monitor startup), otherwise checks CLI availability only
988
1383
  */
989
1384
  async function getProviderData() {
1385
+ // Use cached status if available (set at monitor startup with real health check)
1386
+ if (cachedProviderStatus !== null) {
1387
+ return cachedProviderStatus;
1388
+ }
1389
+ // Fallback: quick CLI availability check (no actual LLM call)
990
1390
  try {
991
1391
  const registry = getProviderRegistry();
992
1392
  const providerIds = registry.getProviderIds();
@@ -1060,6 +1460,9 @@ async function getAgentData() {
1060
1460
  capabilities: agent.capabilities ?? [],
1061
1461
  executionCount: 0,
1062
1462
  lastExecuted: undefined,
1463
+ // PRD-2026-004: Meta-agent fields for list display
1464
+ metaAgent: agent.metaAgent,
1465
+ archetype: agent.archetype,
1063
1466
  }));
1064
1467
  }
1065
1468
  catch {
@@ -1172,6 +1575,71 @@ async function getWorkflowData() {
1172
1575
  return [];
1173
1576
  }
1174
1577
  }
1578
+ /**
1579
+ * Calculate classification metrics from recent traces
1580
+ * PRD-2026-003: Classification observability
1581
+ */
1582
+ async function getClassificationMetrics() {
1583
+ try {
1584
+ const traceStore = getTraceStore();
1585
+ const traces = await traceStore.listTraces(200);
1586
+ let totalClassifications = 0;
1587
+ const byTaskType = {};
1588
+ let totalConfidence = 0;
1589
+ let totalGuardChecks = 0;
1590
+ let passedGuardChecks = 0;
1591
+ let fallbackCount = 0;
1592
+ for (const trace of traces) {
1593
+ try {
1594
+ const events = await traceStore.getTrace(trace.traceId);
1595
+ const startEvent = events.find(e => e.type === 'run.start');
1596
+ if (!startEvent)
1597
+ continue;
1598
+ const payload = startEvent.payload;
1599
+ const classification = payload?.classification;
1600
+ if (!classification || !classification.taskType)
1601
+ continue;
1602
+ totalClassifications++;
1603
+ const taskType = classification.taskType;
1604
+ byTaskType[taskType] = (byTaskType[taskType] ?? 0) + 1;
1605
+ if (classification.confidence !== undefined) {
1606
+ totalConfidence += classification.confidence;
1607
+ }
1608
+ if (classification.selectedMapping === null) {
1609
+ fallbackCount++;
1610
+ }
1611
+ if (classification.guardResults) {
1612
+ for (const gate of classification.guardResults) {
1613
+ totalGuardChecks++;
1614
+ if (gate.passed)
1615
+ passedGuardChecks++;
1616
+ }
1617
+ }
1618
+ }
1619
+ catch {
1620
+ // Ignore individual trace errors
1621
+ }
1622
+ }
1623
+ return {
1624
+ totalClassifications,
1625
+ byTaskType,
1626
+ guardPassRate: totalGuardChecks > 0 ? passedGuardChecks / totalGuardChecks : 1.0,
1627
+ averageConfidence: totalClassifications > 0 ? totalConfidence / totalClassifications : 0,
1628
+ fallbackRate: totalClassifications > 0 ? fallbackCount / totalClassifications : 0,
1629
+ sampleSize: traces.length,
1630
+ };
1631
+ }
1632
+ catch {
1633
+ return {
1634
+ totalClassifications: 0,
1635
+ byTaskType: {},
1636
+ guardPassRate: 1.0,
1637
+ averageConfidence: 0,
1638
+ fallbackRate: 0,
1639
+ sampleSize: 0,
1640
+ };
1641
+ }
1642
+ }
1175
1643
  /**
1176
1644
  * Fetch metrics data
1177
1645
  */