@auxiora/runtime 1.3.1 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/dist/enrichment/__tests__/architect-stage.test.d.ts +2 -0
  2. package/dist/enrichment/__tests__/architect-stage.test.d.ts.map +1 -0
  3. package/dist/enrichment/__tests__/architect-stage.test.js +189 -0
  4. package/dist/enrichment/__tests__/architect-stage.test.js.map +1 -0
  5. package/dist/enrichment/__tests__/integration.test.d.ts +2 -0
  6. package/dist/enrichment/__tests__/integration.test.d.ts.map +1 -0
  7. package/dist/enrichment/__tests__/integration.test.js +79 -0
  8. package/dist/enrichment/__tests__/integration.test.js.map +1 -0
  9. package/dist/enrichment/__tests__/memory-stage.test.d.ts +2 -0
  10. package/dist/enrichment/__tests__/memory-stage.test.d.ts.map +1 -0
  11. package/dist/enrichment/__tests__/memory-stage.test.js +43 -0
  12. package/dist/enrichment/__tests__/memory-stage.test.js.map +1 -0
  13. package/dist/enrichment/__tests__/mode-stage.test.d.ts +2 -0
  14. package/dist/enrichment/__tests__/mode-stage.test.d.ts.map +1 -0
  15. package/dist/enrichment/__tests__/mode-stage.test.js +139 -0
  16. package/dist/enrichment/__tests__/mode-stage.test.js.map +1 -0
  17. package/dist/enrichment/__tests__/model-identity-stage.test.d.ts +2 -0
  18. package/dist/enrichment/__tests__/model-identity-stage.test.d.ts.map +1 -0
  19. package/dist/enrichment/__tests__/model-identity-stage.test.js +74 -0
  20. package/dist/enrichment/__tests__/model-identity-stage.test.js.map +1 -0
  21. package/dist/enrichment/__tests__/pipeline.test.d.ts +2 -0
  22. package/dist/enrichment/__tests__/pipeline.test.d.ts.map +1 -0
  23. package/dist/enrichment/__tests__/pipeline.test.js +78 -0
  24. package/dist/enrichment/__tests__/pipeline.test.js.map +1 -0
  25. package/dist/enrichment/__tests__/self-awareness-stage.test.d.ts +2 -0
  26. package/dist/enrichment/__tests__/self-awareness-stage.test.d.ts.map +1 -0
  27. package/dist/enrichment/__tests__/self-awareness-stage.test.js +71 -0
  28. package/dist/enrichment/__tests__/self-awareness-stage.test.js.map +1 -0
  29. package/dist/enrichment/__tests__/types.test.d.ts +2 -0
  30. package/dist/enrichment/__tests__/types.test.d.ts.map +1 -0
  31. package/dist/enrichment/__tests__/types.test.js +31 -0
  32. package/dist/enrichment/__tests__/types.test.js.map +1 -0
  33. package/dist/enrichment/index.d.ts +8 -0
  34. package/dist/enrichment/index.d.ts.map +1 -0
  35. package/dist/enrichment/index.js +7 -0
  36. package/dist/enrichment/index.js.map +1 -0
  37. package/dist/enrichment/pipeline.d.ts +7 -0
  38. package/dist/enrichment/pipeline.d.ts.map +1 -0
  39. package/dist/enrichment/pipeline.js +29 -0
  40. package/dist/enrichment/pipeline.js.map +1 -0
  41. package/dist/enrichment/stages/architect-stage.d.ts +57 -0
  42. package/dist/enrichment/stages/architect-stage.d.ts.map +1 -0
  43. package/dist/enrichment/stages/architect-stage.js +112 -0
  44. package/dist/enrichment/stages/architect-stage.js.map +1 -0
  45. package/dist/enrichment/stages/memory-stage.d.ts +15 -0
  46. package/dist/enrichment/stages/memory-stage.d.ts.map +1 -0
  47. package/dist/enrichment/stages/memory-stage.js +19 -0
  48. package/dist/enrichment/stages/memory-stage.js.map +1 -0
  49. package/dist/enrichment/stages/mode-stage.d.ts +49 -0
  50. package/dist/enrichment/stages/mode-stage.d.ts.map +1 -0
  51. package/dist/enrichment/stages/mode-stage.js +60 -0
  52. package/dist/enrichment/stages/mode-stage.js.map +1 -0
  53. package/dist/enrichment/stages/model-identity-stage.d.ts +24 -0
  54. package/dist/enrichment/stages/model-identity-stage.d.ts.map +1 -0
  55. package/dist/enrichment/stages/model-identity-stage.js +23 -0
  56. package/dist/enrichment/stages/model-identity-stage.js.map +1 -0
  57. package/dist/enrichment/stages/self-awareness-stage.d.ts +23 -0
  58. package/dist/enrichment/stages/self-awareness-stage.d.ts.map +1 -0
  59. package/dist/enrichment/stages/self-awareness-stage.js +24 -0
  60. package/dist/enrichment/stages/self-awareness-stage.js.map +1 -0
  61. package/dist/enrichment/types.d.ts +45 -0
  62. package/dist/enrichment/types.d.ts.map +1 -0
  63. package/dist/enrichment/types.js +2 -0
  64. package/dist/enrichment/types.js.map +1 -0
  65. package/dist/index.d.ts +77 -4
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +3233 -230
  68. package/dist/index.js.map +1 -1
  69. package/dist/transparency/__tests__/collector.test.d.ts +2 -0
  70. package/dist/transparency/__tests__/collector.test.d.ts.map +1 -0
  71. package/dist/transparency/__tests__/collector.test.js +133 -0
  72. package/dist/transparency/__tests__/collector.test.js.map +1 -0
  73. package/dist/transparency/__tests__/confidence-scorer.test.d.ts +2 -0
  74. package/dist/transparency/__tests__/confidence-scorer.test.d.ts.map +1 -0
  75. package/dist/transparency/__tests__/confidence-scorer.test.js +118 -0
  76. package/dist/transparency/__tests__/confidence-scorer.test.js.map +1 -0
  77. package/dist/transparency/__tests__/source-attributor.test.d.ts +2 -0
  78. package/dist/transparency/__tests__/source-attributor.test.d.ts.map +1 -0
  79. package/dist/transparency/__tests__/source-attributor.test.js +47 -0
  80. package/dist/transparency/__tests__/source-attributor.test.js.map +1 -0
  81. package/dist/transparency/__tests__/types.test.d.ts +2 -0
  82. package/dist/transparency/__tests__/types.test.d.ts.map +1 -0
  83. package/dist/transparency/__tests__/types.test.js +62 -0
  84. package/dist/transparency/__tests__/types.test.js.map +1 -0
  85. package/dist/transparency/collector.d.ts +30 -0
  86. package/dist/transparency/collector.d.ts.map +1 -0
  87. package/dist/transparency/collector.js +79 -0
  88. package/dist/transparency/collector.js.map +1 -0
  89. package/dist/transparency/confidence-scorer.d.ts +17 -0
  90. package/dist/transparency/confidence-scorer.d.ts.map +1 -0
  91. package/dist/transparency/confidence-scorer.js +66 -0
  92. package/dist/transparency/confidence-scorer.js.map +1 -0
  93. package/dist/transparency/index.d.ts +8 -0
  94. package/dist/transparency/index.d.ts.map +1 -0
  95. package/dist/transparency/index.js +4 -0
  96. package/dist/transparency/index.js.map +1 -0
  97. package/dist/transparency/source-attributor.d.ts +10 -0
  98. package/dist/transparency/source-attributor.d.ts.map +1 -0
  99. package/dist/transparency/source-attributor.js +42 -0
  100. package/dist/transparency/source-attributor.js.map +1 -0
  101. package/dist/transparency/types.d.ts +53 -0
  102. package/dist/transparency/types.d.ts.map +1 -0
  103. package/dist/transparency/types.js +2 -0
  104. package/dist/transparency/types.js.map +1 -0
  105. package/package.json +74 -50
package/dist/index.js CHANGED
@@ -10,12 +10,12 @@ import { audit } from '@auxiora/audit';
10
10
  import { getWorkspacePath, getSoulPath, getAgentsPath, getIdentityPath, getUserPath, getBehaviorsPath, getWebhooksPath, getScreenshotsDir, } from '@auxiora/core';
11
11
  import { createArchitect, ARCHITECT_BASE_PROMPT, VaultStorageAdapter } from '@auxiora/personality/architect';
12
12
  import { toolRegistry, toolExecutor, initializeToolExecutor, ToolPermission, setBrowserManager, setWebhookManager, setBehaviorManager, setProviderFactory, setOrchestrationEngine, setResearchEngine, setClipboardMonitor, setAppController, setSystemStateMonitor, setEmailIntelligence, setCalendarIntelligence, setContactGraph, setContextRecall, setComposeEngine, setGrammarChecker, setLanguageDetector, } from '@auxiora/tools';
13
- import { ResearchEngine } from '@auxiora/research';
13
+ import { ResearchEngine, ResearchIntentDetector, DeepResearchOrchestrator, ReportGenerator } from '@auxiora/research';
14
14
  import { OrchestrationEngine } from '@auxiora/orchestrator';
15
15
  import * as crypto from 'node:crypto';
16
16
  import * as fs from 'node:fs/promises';
17
17
  import * as path from 'node:path';
18
- import { BehaviorManager } from '@auxiora/behaviors';
18
+ import { BehaviorManager, evaluateConditions } from '@auxiora/behaviors';
19
19
  import { BrowserManager } from '@auxiora/browser';
20
20
  import { ClipboardMonitor, AppController, SystemStateMonitor } from '@auxiora/os-bridge';
21
21
  import { VoiceManager } from '@auxiora/voice';
@@ -31,7 +31,8 @@ import { createLoopDetectionState, recordToolCall, recordToolOutcome, detectLoop
31
31
  import { UserManager } from '@auxiora/social';
32
32
  import { WorkflowEngine, ApprovalManager, AutonomousExecutor } from '@auxiora/workflows';
33
33
  import { AgentProtocol, MessageSigner, AgentDirectory } from '@auxiora/agent-protocol';
34
- import { AmbientPatternEngine, QuietNotificationManager, BriefingGenerator, AnticipationEngine, AmbientScheduler, DEFAULT_AMBIENT_SCHEDULER_CONFIG, NotificationOrchestrator } from '@auxiora/ambient';
34
+ import { Updater, InstallationDetector, VersionChecker, HealthChecker, createStrategyMap } from '@auxiora/updater';
35
+ import { AmbientPatternEngine, QuietNotificationManager, BriefingGenerator, AnticipationEngine, AmbientScheduler, DEFAULT_AMBIENT_SCHEDULER_CONFIG, NotificationOrchestrator, AmbientAwarenessCollector } from '@auxiora/ambient';
35
36
  import { NotificationHub, DoNotDisturbManager } from '@auxiora/notification-hub';
36
37
  import { ConnectorRegistry, AuthManager as ConnectorAuthManager, TriggerManager } from '@auxiora/connectors';
37
38
  import { googleWorkspaceConnector } from '@auxiora/connector-google-workspace';
@@ -48,6 +49,24 @@ import { ContactGraph, ContextRecall } from '@auxiora/contacts';
48
49
  import { ComposeEngine, GrammarChecker, LanguageDetector } from '@auxiora/compose';
49
50
  import { ScreenCapturer } from '@auxiora/screen';
50
51
  import { CapabilityCatalogImpl, HealthMonitorImpl, createIntrospectTool, generatePromptFragment } from '@auxiora/introspection';
52
+ import { JobQueue } from '@auxiora/job-queue';
53
+ import { Consciousness } from '@auxiora/consciousness';
54
+ import { McpClientManager } from '@auxiora/mcp';
55
+ import { GuardrailPipeline } from '@auxiora/guardrails';
56
+ import { collectTransparencyMeta } from './transparency/index.js';
57
+ import { IntentParser as NLIntentParser, AutomationBuilder } from '@auxiora/nl-automation';
58
+ import { BranchManager } from '@auxiora/conversation-branch';
59
+ import { ReActLoop } from '@auxiora/react-loop';
60
+ import { AgentCardBuilder, TaskManager as A2ATaskManager } from '@auxiora/a2a';
61
+ import { CanvasSession } from '@auxiora/canvas';
62
+ import { DocumentStore, ContextBuilder } from '@auxiora/rag';
63
+ import { GraphStore, EntityLinker } from '@auxiora/knowledge-graph';
64
+ import { EvalRunner, EvalStore, exactMatch, containsExpected, lengthRatio, keywordCoverage, sentenceCompleteness, responseRelevance, toxicityScore } from '@auxiora/evaluation';
65
+ import { CodeExecutor, SessionManager as CodeSessionManager } from '@auxiora/code-interpreter';
66
+ import { ImageGenManager, OpenAIImageProvider, ReplicateImageProvider } from '@auxiora/image-gen';
67
+ import { BackupManager } from '@auxiora/backup';
68
+ import { ApprovalQueue } from '@auxiora/approval-queue';
69
+ import { VectorStore } from '@auxiora/vector-store';
51
70
  import { setMemoryStore } from '@auxiora/tools';
52
71
  import { getAuditLogger } from '@auxiora/audit';
53
72
  import { Router } from 'express';
@@ -57,10 +76,20 @@ import { getModesDir } from '@auxiora/core';
57
76
  import { fileURLToPath } from 'node:url';
58
77
  import { getLogger, generateRequestId, runWithRequestId } from '@auxiora/logger';
59
78
  import { SelfAwarenessAssembler, InMemoryAwarenessStorage, ConversationReflector, CapacityMonitor, KnowledgeBoundary, RelationshipModel, TemporalTracker, EnvironmentSensor, MetaCognitor, } from '@auxiora/self-awareness';
79
+ import { EnrichmentPipeline, MemoryStage, ModeStage, ArchitectStage, SelfAwarenessStage } from './enrichment/index.js';
60
80
  /**
61
81
  * Map Claude Code emulation tool calls to our actual tool names + input format.
62
82
  * The model may call CC tools (WebSearch, Bash, etc.) since they're in the request for OAuth compat.
63
83
  */
84
+ /**
85
+ * Claude Code internal tools that have no Auxiora equivalent.
86
+ * When the model tries to call these, we return a helpful error message
87
+ * instead of letting them hit the tool executor and waste a round-trip.
88
+ */
89
+ const CC_ONLY_TOOLS = new Set([
90
+ 'EnterPlanMode', 'ExitPlanMode', 'AskUserQuestion',
91
+ 'NotebookEdit', 'Skill', 'Task', 'TaskOutput',
92
+ ]);
64
93
  function mapCCToolCall(name, input) {
65
94
  switch (name) {
66
95
  case 'WebSearch':
@@ -73,7 +102,16 @@ function mapCCToolCall(name, input) {
73
102
  return { name: 'file_read', input: { path: input.file_path } };
74
103
  case 'Write':
75
104
  return { name: 'file_write', input: { path: input.file_path, content: input.content } };
105
+ case 'Edit':
106
+ return { name: 'file_write', input: { path: input.file_path, content: input.new_string } };
107
+ case 'Glob':
108
+ return { name: 'file_list', input: { path: input.path || '.', pattern: input.pattern } };
109
+ case 'Grep':
110
+ return { name: 'bash', input: { command: `grep -r "${(input.pattern || '').replace(/"/g, '\\"')}" ${input.path || '.'} --include="${input.glob || '*'}" -l 2>/dev/null | head -20` } };
76
111
  default:
112
+ if (CC_ONLY_TOOLS.has(name)) {
113
+ return { name, input, skip: `Tool "${name}" is not available. Do not use Claude Code internal tools. Respond using text only or use the available tools: bash, web_browser, file_read, file_write, file_list.` };
114
+ }
77
115
  return { name, input };
78
116
  }
79
117
  }
@@ -113,6 +151,7 @@ export class Auxiora {
113
151
  intentParser;
114
152
  actionPlanner;
115
153
  orchestrationEngine;
154
+ jobQueue;
116
155
  // [P14] Team / Social
117
156
  userManager;
118
157
  workflowEngine;
@@ -139,18 +178,58 @@ export class Auxiora {
139
178
  connectorAuthManager;
140
179
  triggerManager;
141
180
  ambientScheduler;
181
+ ambientDetectTimer;
182
+ ambientAwarenessCollector;
142
183
  notificationHub;
143
184
  dndManager;
144
185
  notificationOrchestrator;
186
+ researchEngine;
187
+ intentDetector = new ResearchIntentDetector();
188
+ researchJobs = new Map();
189
+ researchJobExpiry;
145
190
  capabilityCatalog;
146
191
  healthMonitor;
147
192
  capabilityPromptFragment = '';
148
193
  selfAwarenessAssembler;
149
194
  architect;
150
195
  architectBridge = null;
196
+ architectResetChats = new Set();
151
197
  architectAwarenessCollector = null;
198
+ enrichmentPipeline;
199
+ lastToolsUsed = new Map();
200
+ consciousness;
201
+ mcpClientManager;
202
+ selfModelCache;
203
+ userModelCache;
204
+ static MODEL_CACHE_TTL = 60_000;
152
205
  // Security floor
153
206
  securityFloor;
207
+ guardrailPipeline;
208
+ evalRunner;
209
+ evalStore;
210
+ documentStore;
211
+ contextBuilder;
212
+ knowledgeGraph;
213
+ entityLinker;
214
+ imageGenManager;
215
+ updater;
216
+ installationDetector;
217
+ versionChecker;
218
+ codeExecutor;
219
+ codeSessionManager;
220
+ backupManager;
221
+ backupStore = new Map();
222
+ nlIntentParser = new NLIntentParser();
223
+ automationBuilder = new AutomationBuilder();
224
+ branchManagers = new Map();
225
+ approvalQueue;
226
+ vectorStore;
227
+ reactLoops = new Map();
228
+ reactResults = new Map();
229
+ a2aTaskManager = new A2ATaskManager();
230
+ a2aAgentCard;
231
+ canvasSessions = new Map();
232
+ sandboxManager;
154
233
  sessionEscalation = new Map();
155
234
  /** Tracks the most recent channel ID for each connected channel type (e.g. discord → snowflake).
156
235
  * Used for proactive delivery (behaviors, ambient briefings). Persisted to disk. */
@@ -178,6 +257,23 @@ export class Auxiora {
178
257
  this.logger.debug('Auto-approving tool', { toolName, params });
179
258
  return true;
180
259
  });
260
+ // Initialize MCP client connections
261
+ if (this.config.mcp && Object.keys(this.config.mcp.servers).length > 0) {
262
+ try {
263
+ this.mcpClientManager = new McpClientManager(toolRegistry, this.config.mcp);
264
+ await this.mcpClientManager.connectAll();
265
+ const status = this.mcpClientManager.getStatus();
266
+ this.logger.info('MCP client initialized', {
267
+ servers: status.size,
268
+ tools: [...status.values()].reduce((sum, s) => sum + s.toolCount, 0),
269
+ });
270
+ }
271
+ catch (err) {
272
+ this.logger.warn('Failed to initialize MCP client', {
273
+ error: err instanceof Error ? err : new Error(String(err)),
274
+ });
275
+ }
276
+ }
181
277
  // Initialize sessions
182
278
  this.sessions = new SessionManager({
183
279
  maxContextTokens: this.config.session.maxContextTokens,
@@ -226,6 +322,7 @@ export class Auxiora {
226
322
  searchTimeout: this.config.research?.searchTimeout ?? 10_000,
227
323
  fetchTimeout: this.config.research?.fetchTimeout ?? 15_000,
228
324
  });
325
+ this.researchEngine = researchEngine;
229
326
  setResearchEngine(researchEngine);
230
327
  this.logger.info(`Research engine initialized (Brave Search configured${provider ? ', AI extraction enabled' : ''})`);
231
328
  }
@@ -240,6 +337,104 @@ export class Auxiora {
240
337
  await this.loadPersonality();
241
338
  // Initialize modes system
242
339
  await this.initializeModes();
340
+ // Initialize guardrails pipeline (if enabled)
341
+ if (this.config.guardrails?.enabled !== false) {
342
+ this.guardrailPipeline = new GuardrailPipeline({
343
+ piiDetection: this.config.guardrails?.piiDetection,
344
+ promptInjection: this.config.guardrails?.promptInjection,
345
+ toxicityFilter: this.config.guardrails?.toxicityFilter,
346
+ blockThreshold: this.config.guardrails?.blockThreshold,
347
+ redactPii: this.config.guardrails?.redactPii,
348
+ });
349
+ this.logger.info('Guardrails pipeline initialized');
350
+ }
351
+ // Initialize evaluation system
352
+ this.evalStore = new EvalStore();
353
+ this.evalRunner = new EvalRunner({
354
+ exactMatch,
355
+ containsExpected,
356
+ lengthRatio,
357
+ keywordCoverage,
358
+ sentenceCompleteness,
359
+ responseRelevance,
360
+ toxicityScore,
361
+ });
362
+ this.logger.info('Evaluation system initialized');
363
+ // Initialize backup manager
364
+ this.backupManager = new BackupManager();
365
+ this.logger.info('Backup manager initialized');
366
+ // Initialize approval queue
367
+ this.approvalQueue = new ApprovalQueue();
368
+ this.logger.info('Approval queue initialized');
369
+ // Initialize vector store
370
+ this.vectorStore = new VectorStore({ dimensions: 1536, maxEntries: 100_000 });
371
+ this.logger.info('Vector store initialized');
372
+ // Initialize consciousness orchestrator (self-model, journal, monitor, repair)
373
+ if (this.architect && this.healthMonitor) {
374
+ try {
375
+ this.consciousness = new Consciousness({
376
+ vault: this.vault,
377
+ healthMonitor: this.healthMonitor,
378
+ feedbackStore: {
379
+ getInsights: () => {
380
+ const raw = this.architect.getFeedbackInsights();
381
+ return {
382
+ ...raw,
383
+ suggestedAdjustments: raw.suggestedAdjustments,
384
+ };
385
+ },
386
+ },
387
+ correctionStore: {
388
+ getStats: () => this.architect.getCorrectionStats(),
389
+ },
390
+ preferenceHistory: {
391
+ detectConflicts: () => this.architect.getPreferenceConflicts(),
392
+ },
393
+ getResourceMetrics: () => {
394
+ const mem = process.memoryUsage();
395
+ return {
396
+ memoryUsageMb: Math.round(mem.heapUsed / 1024 / 1024),
397
+ cpuPercent: 0, // CPU % requires sampling; omit for now
398
+ activeConnections: this.gateway?.getConnections().length ?? 0,
399
+ uptimeSeconds: Math.round(process.uptime()),
400
+ };
401
+ },
402
+ getCapabilityMetrics: () => {
403
+ const tools = toolRegistry.list();
404
+ return {
405
+ totalCapabilities: tools.length,
406
+ healthyCapabilities: tools.length,
407
+ degradedCapabilities: [],
408
+ };
409
+ },
410
+ actionExecutor: async (command) => {
411
+ this.logger.info('Consciousness repair action (log-only)', { command });
412
+ return `[log-only] ${command}`;
413
+ },
414
+ onNotify: (diagnosis, action) => {
415
+ this.logger.info('Consciousness repair notification', {
416
+ diagnosis: diagnosis?.description,
417
+ action: action.description,
418
+ });
419
+ },
420
+ onApprovalRequest: async () => false, // deny auto-repair initially
421
+ decisionLog: {
422
+ query: (q) => this.architect.queryDecisions(q),
423
+ getDueFollowUps: () => this.architect.getDueFollowUps(),
424
+ },
425
+ version: '1.4.0',
426
+ monitorIntervalMs: 60_000,
427
+ });
428
+ await this.consciousness.initialize();
429
+ this.logger.info('Consciousness orchestrator initialized');
430
+ }
431
+ catch (err) {
432
+ this.logger.warn('Failed to initialize consciousness', { error: err instanceof Error ? err : new Error(String(err)) });
433
+ }
434
+ }
435
+ // Build enrichment pipeline from available subsystems
436
+ this.buildEnrichmentPipeline();
437
+ this.logger.info('Enrichment pipeline initialized');
243
438
  // Initialize gateway
244
439
  this.gateway = new Gateway({
245
440
  config: this.config,
@@ -265,20 +460,48 @@ export class Auxiora {
265
460
  this.gateway.broadcast({ type: 'activity', payload: entry }, (client) => client.authenticated);
266
461
  }
267
462
  };
463
+ // Initialize durable job queue
464
+ const jobQueueDbPath = path.join(path.dirname(getBehaviorsPath()), 'jobs.db');
465
+ this.jobQueue = new JobQueue(jobQueueDbPath, {
466
+ pollIntervalMs: 2000,
467
+ concurrency: 5,
468
+ });
469
+ // Register behavior handler
470
+ this.jobQueue.register('behavior', async (payload) => {
471
+ if (this.behaviors) {
472
+ await this.behaviors.executeNow(payload.behaviorId);
473
+ }
474
+ });
475
+ // Register ambient pattern flush handler (re-enqueues itself)
476
+ this.jobQueue.register('ambient-flush', async (_payload, ctx) => {
477
+ if (this.ambientEngine) {
478
+ const serialized = this.ambientEngine.serialize();
479
+ ctx.checkpoint(serialized);
480
+ }
481
+ // Re-enqueue next flush in 5 minutes
482
+ if (this.jobQueue) {
483
+ this.jobQueue.enqueue('ambient-flush', {}, { scheduledAt: Date.now() + 5 * 60 * 1000 });
484
+ }
485
+ });
486
+ this.jobQueue.start();
487
+ this.logger.info('Durable job queue initialized');
268
488
  // Initialize behavior system
269
489
  if (this.providers) {
270
490
  this.behaviors = new BehaviorManager({
271
491
  storePath: getBehaviorsPath(),
492
+ jobQueue: this.jobQueue,
272
493
  executorDeps: {
273
494
  getProvider: () => this.providers.getPrimaryProvider(),
274
495
  sendToChannel: async (channelType, channelId, message) => {
275
496
  this.logger.info('sendToChannel called', { channelType, channelId, hasChannels: !!this.channels });
497
+ let delivered = false;
276
498
  // Always broadcast to webchat + persist
277
499
  this.gateway.broadcast({
278
500
  type: 'message',
279
501
  payload: { role: 'assistant', content: message.content },
280
502
  });
281
503
  this.persistToWebchat(message.content);
504
+ delivered = true; // webchat broadcast is best-effort but counts
282
505
  // Deliver to all connected external channels
283
506
  if (this.channels) {
284
507
  const connected = this.channels.getConnectedChannels();
@@ -290,12 +513,15 @@ export class Auxiora {
290
513
  if (!targetId)
291
514
  continue;
292
515
  const result = await this.channels.send(ct, targetId, { content: message.content });
293
- if (!result.success) {
516
+ if (result.success) {
517
+ delivered = true;
518
+ }
519
+ else {
294
520
  this.logger.warn('Channel delivery failed', { channel: ct, targetId, error: new Error(result.error ?? 'unknown') });
295
521
  }
296
522
  }
297
523
  }
298
- return { success: true };
524
+ return { success: delivered };
299
525
  },
300
526
  getSystemPrompt: () => this.systemPrompt,
301
527
  executeWithTools: async (messages, systemPrompt) => {
@@ -779,6 +1005,10 @@ export class Auxiora {
779
1005
  if (updates.agent) {
780
1006
  await this.loadPersonality();
781
1007
  }
1008
+ // Re-initialize channels when channel config changes
1009
+ if (updates.channels) {
1010
+ await this.reinitializeChannels();
1011
+ }
782
1012
  },
783
1013
  getAgentName: () => this.config.agent?.name ?? 'Auxiora',
784
1014
  getAgentPronouns: () => this.config.agent?.pronouns ?? 'they/them',
@@ -877,6 +1107,115 @@ export class Auxiora {
877
1107
  this.gateway.mountRouter('/dashboard', spaRouter);
878
1108
  this.logger.info('Dashboard enabled at /dashboard');
879
1109
  }
1110
+ // MCP management API routes
1111
+ this.gateway.mountRouter('/api/v1/mcp', this.createMcpRouter());
1112
+ // Personality management API routes
1113
+ if (this.architect) {
1114
+ const personalityRouter = this.createPersonalityRouter();
1115
+ this.gateway.mountRouter('/api/v1/personality', personalityRouter);
1116
+ }
1117
+ // Ambient agent API routes
1118
+ if (this.ambientEngine) {
1119
+ const ambientRouter = this.createAmbientRouter();
1120
+ this.gateway.mountRouter('/api/v1/ambient', ambientRouter);
1121
+ }
1122
+ // Deep research API routes
1123
+ const researchRouter = this.createResearchRouter();
1124
+ this.gateway.mountRouter('/api/v1/research', researchRouter);
1125
+ // Agent protocol API routes
1126
+ if (this.agentProtocol) {
1127
+ this.gateway.mountRouter('/api/v1/agent-protocol', this.createAgentProtocolRouter());
1128
+ }
1129
+ // Webhooks management API routes
1130
+ if (this.webhookManager) {
1131
+ this.gateway.mountRouter('/api/v1/webhooks', this.createWebhooksRouter());
1132
+ }
1133
+ // Consciousness API routes
1134
+ if (this.consciousness) {
1135
+ this.gateway.mountRouter('/api/v1/consciousness', this.createConsciousnessRouter());
1136
+ }
1137
+ // Voice API routes
1138
+ if (this.voiceManager) {
1139
+ this.gateway.mountRouter('/api/v1/voice', this.createVoiceRouter());
1140
+ }
1141
+ // Trust engine API routes
1142
+ if (this.trustEngine) {
1143
+ this.gateway.mountRouter('/api/v1/trust', this.createTrustRouter());
1144
+ }
1145
+ // Workflow API routes
1146
+ if (this.workflowEngine) {
1147
+ this.gateway.mountRouter('/api/v1/workflows', this.createWorkflowRouter());
1148
+ }
1149
+ // Connector API routes
1150
+ if (this.connectorRegistry) {
1151
+ this.gateway.mountRouter('/api/v1/connectors', this.createConnectorRouter());
1152
+ }
1153
+ // Self-update API routes
1154
+ if (this.updater) {
1155
+ this.gateway.mountRouter('/api/v1/update', this.createUpdateRouter());
1156
+ }
1157
+ // RAG API routes
1158
+ if (this.documentStore) {
1159
+ this.gateway.mountRouter('/api/v1/rag', this.createRagRouter());
1160
+ }
1161
+ // Evaluation API routes
1162
+ if (this.evalStore) {
1163
+ this.gateway.mountRouter('/api/v1/eval', this.createEvalRouter());
1164
+ }
1165
+ // Image generation API routes
1166
+ if (this.imageGenManager) {
1167
+ this.gateway.mountRouter('/api/v1/images', this.createImageRouter());
1168
+ }
1169
+ // Knowledge graph API routes
1170
+ if (this.knowledgeGraph) {
1171
+ this.gateway.mountRouter('/api/v1/knowledge', this.createKnowledgeRouter());
1172
+ }
1173
+ // Code interpreter API routes
1174
+ if (this.codeSessionManager) {
1175
+ this.gateway.mountRouter('/api/v1/code', this.createCodeRouter());
1176
+ }
1177
+ // Backup API routes
1178
+ if (this.backupManager) {
1179
+ this.gateway.mountRouter('/api/v1/backup', this.createBackupRouter());
1180
+ }
1181
+ // Approval queue API routes
1182
+ if (this.approvalQueue) {
1183
+ this.gateway.mountRouter('/api/v1/approvals', this.createApprovalQueueRouter());
1184
+ }
1185
+ // Vector store API routes
1186
+ if (this.vectorStore) {
1187
+ this.gateway.mountRouter('/api/v1/vectors', this.createVectorRouter());
1188
+ }
1189
+ // NL Automation API routes
1190
+ this.gateway.mountRouter('/api/v1/automation', this.createAutomationRouter());
1191
+ // Conversation branch API routes
1192
+ this.gateway.mountRouter('/api/v1/branches', this.createBranchRouter());
1193
+ // ReAct loop API routes
1194
+ this.gateway.mountRouter('/api/v1/react', this.createReactRouter());
1195
+ // A2A (Agent-to-Agent) API routes
1196
+ this.a2aAgentCard = new AgentCardBuilder()
1197
+ .setName('Auxiora')
1198
+ .setDescription('Auxiora AI assistant')
1199
+ .setUrl(`http://${this.config.gateway.host}:${this.config.gateway.port}`)
1200
+ .setVersion('1.0.0')
1201
+ .build();
1202
+ this.gateway.mountRouter('/api/v1/a2a', this.createA2ARouter());
1203
+ // Canvas API routes
1204
+ this.gateway.mountRouter('/api/v1/canvas', this.createCanvasRouter());
1205
+ // Sandbox API routes
1206
+ this.gateway.mountRouter('/api/v1/sandbox', this.createSandboxRouter());
1207
+ // Job queue status endpoint
1208
+ this.gateway.mountRouter('/api/v1/jobs', (() => {
1209
+ const jobsRouter = Router();
1210
+ jobsRouter.get('/status', (_req, res) => {
1211
+ if (!this.jobQueue) {
1212
+ res.status(503).json({ error: 'Job queue not initialized' });
1213
+ return;
1214
+ }
1215
+ res.json(this.jobQueue.getStats());
1216
+ });
1217
+ return jobsRouter;
1218
+ })());
880
1219
  // Initialize plugin system (if enabled)
881
1220
  if (this.config.plugins?.enabled !== false) {
882
1221
  const pluginsDir = this.config.plugins?.dir || undefined;
@@ -954,12 +1293,90 @@ export class Auxiora {
954
1293
  await this.agentDirectory.register(agentId, agentName, agentKeys.publicKey, `http://${agentHost}/api/v1/agent-protocol`);
955
1294
  this.agentProtocol = new AgentProtocol(agentId, agentSigner, this.agentDirectory);
956
1295
  this.logger.info('Agent protocol initialized');
1296
+ // Initialize self-update system
1297
+ try {
1298
+ this.installationDetector = new InstallationDetector();
1299
+ this.versionChecker = new VersionChecker('auxiora', 'auxiora');
1300
+ const healthChecker = new HealthChecker(`http://${agentHost}`);
1301
+ const strategies = createStrategyMap();
1302
+ this.updater = new Updater({
1303
+ detector: this.installationDetector,
1304
+ versionChecker: this.versionChecker,
1305
+ healthChecker,
1306
+ strategies,
1307
+ });
1308
+ // Recover from any incomplete previous update
1309
+ const recovery = await this.updater.recoverIfNeeded();
1310
+ if (recovery) {
1311
+ this.logger.warn('Recovered from incomplete update', {
1312
+ previousVersion: recovery.previousVersion,
1313
+ targetVersion: recovery.newVersion,
1314
+ });
1315
+ }
1316
+ this.logger.info('Self-update system initialized');
1317
+ }
1318
+ catch (err) {
1319
+ this.logger.warn('Failed to initialize self-update system', {
1320
+ error: err instanceof Error ? err : new Error(String(err)),
1321
+ });
1322
+ }
957
1323
  // [P15] Initialize ambient intelligence
958
- this.ambientEngine = new AmbientPatternEngine();
1324
+ // Restore persisted patterns from vault, or start fresh
1325
+ try {
1326
+ const storedPatterns = this.vault.get('ambient:patterns');
1327
+ this.ambientEngine = storedPatterns
1328
+ ? AmbientPatternEngine.deserialize(storedPatterns)
1329
+ : new AmbientPatternEngine();
1330
+ }
1331
+ catch {
1332
+ this.ambientEngine = new AmbientPatternEngine();
1333
+ }
959
1334
  this.ambientNotifications = new QuietNotificationManager();
960
1335
  this.briefingGenerator = new BriefingGenerator();
961
1336
  this.anticipationEngine = new AnticipationEngine();
1337
+ this.ambientAwarenessCollector = new AmbientAwarenessCollector();
1338
+ if (this.jobQueue) {
1339
+ this.jobQueue.enqueue('ambient-flush', {}, { scheduledAt: Date.now() + 5 * 60 * 1000 });
1340
+ }
962
1341
  this.logger.info('Ambient intelligence initialized');
1342
+ // Initialize RAG document store
1343
+ this.documentStore = new DocumentStore();
1344
+ this.contextBuilder = new ContextBuilder();
1345
+ this.logger.info('RAG document store initialized');
1346
+ // Initialize image generation (conditional on provider keys)
1347
+ {
1348
+ let openaiKey;
1349
+ let replicateToken;
1350
+ try {
1351
+ openaiKey = this.vault.get('OPENAI_API_KEY');
1352
+ }
1353
+ catch { /* vault locked */ }
1354
+ try {
1355
+ replicateToken = this.vault.get('REPLICATE_API_TOKEN');
1356
+ }
1357
+ catch { /* vault locked */ }
1358
+ if (openaiKey || replicateToken) {
1359
+ this.imageGenManager = new ImageGenManager();
1360
+ if (openaiKey) {
1361
+ this.imageGenManager.registerProvider(new OpenAIImageProvider(openaiKey));
1362
+ }
1363
+ if (replicateToken) {
1364
+ this.imageGenManager.registerProvider(new ReplicateImageProvider(replicateToken));
1365
+ }
1366
+ this.logger.info(`Image generation initialized with providers: ${this.imageGenManager.listProviders().join(', ')}`);
1367
+ }
1368
+ else {
1369
+ this.logger.info('Image generation skipped: no OPENAI_API_KEY or REPLICATE_API_TOKEN in vault');
1370
+ }
1371
+ }
1372
+ // Initialize knowledge graph
1373
+ this.knowledgeGraph = new GraphStore();
1374
+ this.entityLinker = new EntityLinker();
1375
+ this.logger.info('Knowledge graph initialized');
1376
+ // Initialize code interpreter
1377
+ this.codeExecutor = new CodeExecutor();
1378
+ this.codeSessionManager = new CodeSessionManager(this.codeExecutor);
1379
+ this.logger.info('Code interpreter initialized');
963
1380
  // Initialize notification orchestrator
964
1381
  this.notificationHub = new NotificationHub();
965
1382
  this.dndManager = new DoNotDisturbManager();
@@ -1060,6 +1477,38 @@ export class Auxiora {
1060
1477
  this.ambientScheduler.start();
1061
1478
  this.logger.info('Ambient scheduler started');
1062
1479
  }
1480
+ // Run pattern detection and persist to vault every 5 minutes
1481
+ const PATTERN_DETECT_INTERVAL = 5 * 60 * 1000;
1482
+ this.ambientDetectTimer = setInterval(async () => {
1483
+ // Poll triggers and route events
1484
+ if (this.triggerManager) {
1485
+ try {
1486
+ const events = await this.triggerManager.pollAll();
1487
+ await this.processEventTriggers(events);
1488
+ }
1489
+ catch { /* poll failure */ }
1490
+ }
1491
+ // Detect patterns and persist
1492
+ if (this.ambientEngine) {
1493
+ this.ambientEngine.detectPatterns();
1494
+ try {
1495
+ await this.vault.add('ambient:patterns', this.ambientEngine.serialize());
1496
+ }
1497
+ catch { /* vault locked */ }
1498
+ // Update awareness collector
1499
+ if (this.ambientAwarenessCollector) {
1500
+ this.ambientAwarenessCollector.updatePatterns(this.ambientEngine.getPatterns());
1501
+ if (this.anticipationEngine) {
1502
+ const anticipations = this.anticipationEngine.generateAnticipations(this.ambientEngine.getPatterns());
1503
+ this.ambientAwarenessCollector.updateAnticipations(anticipations);
1504
+ }
1505
+ this.ambientAwarenessCollector.updateActivity({
1506
+ eventRate: this.ambientEngine.getEventCount(),
1507
+ activeBehaviors: 0,
1508
+ });
1509
+ }
1510
+ }
1511
+ }, PATTERN_DETECT_INTERVAL);
1063
1512
  // [P15] Initialize conversation engine
1064
1513
  this.conversationEngine = new ConversationEngine();
1065
1514
  this.logger.info('Conversation engine initialized');
@@ -1103,12 +1552,13 @@ export class Auxiora {
1103
1552
  toolCount: p.toolCount, behaviorNames: p.behaviorNames,
1104
1553
  })),
1105
1554
  getFeatures: () => ({
1106
- behaviors: this.config.features?.behaviors !== false,
1107
- browser: this.config.features?.browser !== false,
1108
- voice: !!this.config.features?.voice,
1109
- webhooks: !!this.config.features?.webhooks,
1110
- plugins: !!this.config.features?.plugins,
1111
- memory: this.config.memory?.enabled !== false,
1555
+ behaviors: !!this.behaviors,
1556
+ browser: !!this.browserManager,
1557
+ voice: !!this.voiceManager,
1558
+ webhooks: !!this.webhookManager,
1559
+ plugins: !!(this.pluginLoader && this.pluginLoader.listPlugins().length > 0),
1560
+ memory: !!this.memoryStore,
1561
+ research: !!this.researchEngine,
1112
1562
  }),
1113
1563
  getAuditEntries: async (limit) => {
1114
1564
  const al = getAuditLogger();
@@ -1121,10 +1571,51 @@ export class Auxiora {
1121
1571
  const initialHealth = { overall: 'healthy', subsystems: [], issues: [], lastCheck: new Date().toISOString() };
1122
1572
  this.capabilityPromptFragment = generatePromptFragment(this.capabilityCatalog.getCatalog(), initialHealth, this.getSelfAwarenessContext());
1123
1573
  const autoFixActions = {
1124
- reconnectChannel: async () => false,
1125
- restartBehavior: async (_id) => {
1126
- // BehaviorManager does not yet expose a resume() method
1127
- return false;
1574
+ reconnectChannel: async (type) => {
1575
+ if (!this.channels)
1576
+ return false;
1577
+ try {
1578
+ await this.channels.disconnect(type);
1579
+ await this.channels.connect(type);
1580
+ this.logger.info('Auto-fix: reconnected channel', { type });
1581
+ return true;
1582
+ }
1583
+ catch (err) {
1584
+ this.logger.warn('Auto-fix: channel reconnect failed', { type, error: err instanceof Error ? err : new Error(String(err)) });
1585
+ return false;
1586
+ }
1587
+ },
1588
+ restartBehavior: async (id) => {
1589
+ if (!this.behaviors)
1590
+ return false;
1591
+ try {
1592
+ const result = await this.behaviors.update(id, { status: 'active' });
1593
+ if (!result)
1594
+ return false;
1595
+ this.logger.info('Auto-fix: restarted behavior', { id });
1596
+ return true;
1597
+ }
1598
+ catch (err) {
1599
+ this.logger.warn('Auto-fix: behavior restart failed', { id, error: err instanceof Error ? err : new Error(String(err)) });
1600
+ return false;
1601
+ }
1602
+ },
1603
+ switchToFallbackProvider: async () => {
1604
+ const fallbackName = this.config.provider.fallback;
1605
+ if (!fallbackName)
1606
+ return false;
1607
+ const fallback = this.providers.getFallbackProvider();
1608
+ if (!fallback)
1609
+ return false;
1610
+ try {
1611
+ this.providers.setPrimary(fallbackName);
1612
+ this.logger.info('Auto-fix: switched to fallback provider', { name: fallbackName });
1613
+ return true;
1614
+ }
1615
+ catch (err) {
1616
+ this.logger.warn('Auto-fix: provider switch failed', { error: err instanceof Error ? err : new Error(String(err)) });
1617
+ return false;
1618
+ }
1128
1619
  },
1129
1620
  };
1130
1621
  this.healthMonitor = new HealthMonitorImpl(introspectionSources, autoFixActions);
@@ -1503,6 +1994,38 @@ export class Auxiora {
1503
1994
  this.pluginLoader.setChannelManager(this.channels);
1504
1995
  }
1505
1996
  }
1997
+ async reinitializeChannels() {
1998
+ // Disconnect existing channels gracefully
1999
+ if (this.channels) {
2000
+ try {
2001
+ await this.channels.disconnectAll();
2002
+ }
2003
+ catch (error) {
2004
+ this.logger.warn('Error disconnecting channels during reinit', {
2005
+ error: error instanceof Error ? error : new Error(String(error)),
2006
+ });
2007
+ }
2008
+ this.channels = undefined;
2009
+ }
2010
+ // Re-initialize with updated config and vault
2011
+ await this.initializeChannels();
2012
+ // Connect the newly initialized channels (cast needed: initializeChannels may set this.channels)
2013
+ const channels = this.channels;
2014
+ if (channels) {
2015
+ try {
2016
+ await channels.connectAll();
2017
+ const connected = channels.getConnectedChannels();
2018
+ if (connected.length > 0) {
2019
+ this.logger.info(`Channels reconnected: ${connected.join(', ')}`);
2020
+ }
2021
+ }
2022
+ catch (error) {
2023
+ this.logger.warn('Some channels failed to connect during reinit', {
2024
+ error: error instanceof Error ? error : new Error(String(error)),
2025
+ });
2026
+ }
2027
+ }
2028
+ }
1506
2029
  buildIdentityPreamble(agent) {
1507
2030
  const lines = ['# Agent Identity'];
1508
2031
  lines.push(`You are ${agent.name} (${agent.pronouns}).`);
@@ -1676,6 +2199,9 @@ export class Auxiora {
1676
2199
  if (this.architectAwarenessCollector) {
1677
2200
  collectors.push(this.architectAwarenessCollector);
1678
2201
  }
2202
+ if (this.ambientAwarenessCollector) {
2203
+ collectors.push(this.ambientAwarenessCollector);
2204
+ }
1679
2205
  this.selfAwarenessAssembler = new SelfAwarenessAssembler(collectors, {
1680
2206
  tokenBudget: this.config.selfAwareness.tokenBudget ?? 500,
1681
2207
  });
@@ -1703,30 +2229,37 @@ export class Auxiora {
1703
2229
  personalityEngine: this.config.agent.personality ?? 'standard',
1704
2230
  };
1705
2231
  }
1706
- /** Append Architect context modifier when active, returning context metadata. */
1707
- applyArchitectEnrichment(prompt, userMessage, chatId) {
2232
+ async getCachedSelfModel() {
2233
+ if (!this.consciousness)
2234
+ return null;
2235
+ const now = Date.now();
2236
+ if (this.selfModelCache && (now - this.selfModelCache.cachedAt) < Auxiora.MODEL_CACHE_TTL) {
2237
+ return this.selfModelCache.snapshot;
2238
+ }
2239
+ try {
2240
+ const snapshot = await this.consciousness.model.synthesize();
2241
+ this.selfModelCache = { snapshot, cachedAt: now };
2242
+ return snapshot;
2243
+ }
2244
+ catch {
2245
+ return this.selfModelCache?.snapshot ?? null;
2246
+ }
2247
+ }
2248
+ getCachedUserModel() {
1708
2249
  if (!this.architect)
1709
- return { prompt };
1710
- const output = this.architect.generatePrompt(userMessage);
1711
- // Bridge handles side effects: persistence, awareness feeding, escalation logging
1712
- if (this.architectBridge && chatId) {
1713
- this.architectBridge.afterPrompt(output.detectedContext, output.emotionalTrajectory, output.escalationAlert, chatId);
2250
+ return null;
2251
+ const now = Date.now();
2252
+ if (this.userModelCache && (now - this.userModelCache.cachedAt) < Auxiora.MODEL_CACHE_TTL) {
2253
+ return this.userModelCache.model;
1714
2254
  }
1715
- const mix = this.architect.getTraitMix(output.detectedContext);
1716
- const traitWeights = {};
1717
- for (const [key, val] of Object.entries(mix)) {
1718
- traitWeights[key] = val;
2255
+ try {
2256
+ const model = this.architect.getUserModel();
2257
+ this.userModelCache = { model, cachedAt: now };
2258
+ return model;
2259
+ }
2260
+ catch {
2261
+ return this.userModelCache?.model ?? null;
1719
2262
  }
1720
- return {
1721
- prompt: prompt + '\n\n' + output.contextModifier,
1722
- architectMeta: {
1723
- detectedContext: output.detectedContext,
1724
- activeTraits: output.activeTraits,
1725
- traitWeights,
1726
- recommendation: output.recommendation,
1727
- escalationAlert: output.escalationAlert,
1728
- },
1729
- };
1730
2263
  }
1731
2264
  async initializeModes() {
1732
2265
  if (this.config.modes?.enabled === false)
@@ -1750,19 +2283,62 @@ export class Auxiora {
1750
2283
  }
1751
2284
  return state;
1752
2285
  }
1753
- /** Build enriched prompt with auto-detection (shared by handleMessage and handleChannelMessage). */
1754
- buildModeEnrichedPrompt(content, modeState, memorySection, channelType) {
1755
- if (modeState.activeMode === 'auto' && this.modeDetector && this.config.modes?.autoDetection !== false) {
1756
- const detection = this.modeDetector.detect(content, { currentState: modeState });
1757
- if (detection) {
1758
- modeState.lastAutoMode = detection.mode;
1759
- modeState.autoDetected = true;
1760
- modeState.lastSwitchAt = Date.now();
1761
- const tempState = { ...modeState, activeMode: detection.mode };
1762
- return this.promptAssembler.enrichForMessage(tempState, memorySection, this.userPreferences, undefined, channelType);
1763
- }
2286
+ buildEnrichmentPipeline() {
2287
+ this.enrichmentPipeline = new EnrichmentPipeline();
2288
+ // Stage 1: Memory (order 100)
2289
+ if (this.memoryStore && this.memoryRetriever) {
2290
+ this.enrichmentPipeline.addStage(new MemoryStage(this.memoryStore, this.memoryRetriever));
2291
+ }
2292
+ // Stage 2: Mode detection + security (order 200)
2293
+ if (this.modeDetector && this.promptAssembler) {
2294
+ this.enrichmentPipeline.addStage(new ModeStage({
2295
+ detector: this.modeDetector,
2296
+ assembler: this.promptAssembler,
2297
+ securityFloor: this.securityFloor,
2298
+ userPreferences: this.userPreferences,
2299
+ getModeState: (sessionId) => this.getSessionModeState(sessionId),
2300
+ }));
2301
+ }
2302
+ // Stage 3: Architect (order 300)
2303
+ if (this.architect) {
2304
+ this.enrichmentPipeline.addStage(new ArchitectStage(this.architect, this.architectBridge ?? undefined, this.architectAwarenessCollector ?? undefined, () => this.getCachedSelfModel(), () => this.getCachedUserModel()));
2305
+ }
2306
+ // Stage 4: Self-awareness (order 400)
2307
+ if (this.selfAwarenessAssembler) {
2308
+ this.enrichmentPipeline.addStage(new SelfAwarenessStage(this.selfAwarenessAssembler));
2309
+ }
2310
+ }
2311
+ GUARDRAIL_BLOCK_MESSAGE = 'I\'m not able to process that request. If you believe this is an error, please rephrase your message.';
2312
+ buildModelIdentityFragment(provider, model) {
2313
+ const activeModel = model ?? provider.defaultModel;
2314
+ const caps = provider.metadata.models[activeModel];
2315
+ return '\n\n[Model Identity]\n'
2316
+ + `You are running as ${activeModel} via ${provider.metadata.displayName}.`
2317
+ + (caps ? ` Context window: ${caps.maxContextTokens.toLocaleString()} tokens.` : '')
2318
+ + (caps?.supportsVision ? ' You have vision capabilities.' : '')
2319
+ + ` Today's date: ${new Date().toISOString().slice(0, 10)}.`;
2320
+ }
2321
+ checkInputGuardrails(content) {
2322
+ if (!this.guardrailPipeline)
2323
+ return null;
2324
+ const result = this.guardrailPipeline.scanInput(content);
2325
+ if (result.action !== 'allow') {
2326
+ this.logger.debug('Input guardrail triggered', { action: result.action, threatCount: result.threats.length });
2327
+ }
2328
+ return result;
2329
+ }
2330
+ checkOutputGuardrails(response) {
2331
+ if (!this.guardrailPipeline || this.config.guardrails?.scanOutput === false || !response) {
2332
+ return { response, wasModified: false, action: 'allow' };
2333
+ }
2334
+ const result = this.guardrailPipeline.scanOutput(response);
2335
+ if (result.action === 'block') {
2336
+ return { response: this.GUARDRAIL_BLOCK_MESSAGE, wasModified: true, action: 'block' };
1764
2337
  }
1765
- return this.promptAssembler.enrichForMessage(modeState, memorySection, this.userPreferences, undefined, channelType);
2338
+ if (result.action === 'redact' && result.redactedContent) {
2339
+ return { response: result.redactedContent, wasModified: true, action: 'redact' };
2340
+ }
2341
+ return { response, wasModified: false, action: result.action };
1766
2342
  }
1767
2343
  async handleMessage(client, message) {
1768
2344
  const { id: requestId, payload } = message;
@@ -1774,6 +2350,55 @@ export class Auxiora {
1774
2350
  }
1775
2351
  return;
1776
2352
  }
2353
+ // Handle message feedback (thumbs up/down for Architect learning)
2354
+ if (message.type === 'message_feedback') {
2355
+ const fbPayload = payload;
2356
+ if (this.architect && fbPayload?.messageId && fbPayload?.rating) {
2357
+ // Look up the message to get architectDomain from metadata
2358
+ let domain = 'general';
2359
+ if (fbPayload.sessionId) {
2360
+ const msgs = this.sessions.getMessages(fbPayload.sessionId);
2361
+ const msg = msgs.find((m) => m.id === fbPayload.messageId);
2362
+ if (msg?.metadata?.architectDomain) {
2363
+ domain = msg.metadata.architectDomain;
2364
+ }
2365
+ }
2366
+ const mapped = fbPayload.rating === 'up' ? 'helpful' : 'off_target';
2367
+ await this.architect.recordFeedback({
2368
+ domain: domain,
2369
+ rating: mapped,
2370
+ note: fbPayload.note,
2371
+ });
2372
+ audit('personality.feedback', {
2373
+ sessionId: fbPayload.sessionId,
2374
+ messageId: fbPayload.messageId,
2375
+ rating: fbPayload.rating,
2376
+ });
2377
+ }
2378
+ return;
2379
+ }
2380
+ // Handle deep research job requests
2381
+ if (message.type === 'start_research') {
2382
+ const researchPayload = payload;
2383
+ if (researchPayload?.question) {
2384
+ const job = {
2385
+ id: crypto.randomUUID(),
2386
+ question: researchPayload.question,
2387
+ depth: researchPayload.depth ?? 'deep',
2388
+ status: 'planning',
2389
+ createdAt: Date.now(),
2390
+ progress: [],
2391
+ };
2392
+ this.researchJobs.set(job.id, job);
2393
+ audit('research.started', { jobId: job.id, question: job.question, depth: job.depth });
2394
+ this.sendToClient(client, { type: 'research_started', id: requestId, payload: { jobId: job.id } });
2395
+ this.runResearchJob(job, client).catch((err) => {
2396
+ job.status = 'failed';
2397
+ this.logger.error('Research job failed', { error: err instanceof Error ? err : new Error(String(err)), jobId: job.id });
2398
+ });
2399
+ }
2400
+ return;
2401
+ }
1777
2402
  const msgPayload = payload;
1778
2403
  const content = msgPayload?.content;
1779
2404
  const modelOverride = msgPayload?.model;
@@ -1788,6 +2413,28 @@ export class Auxiora {
1788
2413
  });
1789
2414
  return;
1790
2415
  }
2416
+ // ── Research intent detection ──────────────────────────────────
2417
+ const researchIntent = this.intentDetector.detect(content);
2418
+ if (researchIntent.score >= 0.6) {
2419
+ this.sendToClient(client, { type: 'research_suggestion', id: requestId, payload: researchIntent });
2420
+ }
2421
+ // ── Guardrail input scan ──────────────────────────────────────
2422
+ const inputScan = this.checkInputGuardrails(content);
2423
+ if (inputScan && inputScan.action === 'block') {
2424
+ audit('guardrail.triggered', {
2425
+ action: 'block',
2426
+ direction: 'input',
2427
+ threatCount: inputScan.threats.length,
2428
+ channelType: 'webchat',
2429
+ });
2430
+ this.sendToClient(client, {
2431
+ type: 'message',
2432
+ id: requestId,
2433
+ payload: { role: 'assistant', content: this.GUARDRAIL_BLOCK_MESSAGE },
2434
+ });
2435
+ this.sendToClient(client, { type: 'done', id: requestId, payload: {} });
2436
+ return;
2437
+ }
1791
2438
  // Handle commands
1792
2439
  if (content.startsWith('/')) {
1793
2440
  await this.handleCommand(client, content, requestId);
@@ -1816,8 +2463,29 @@ export class Auxiora {
1816
2463
  senderId: client.senderId,
1817
2464
  });
1818
2465
  }
2466
+ // Apply redaction if guardrails flagged PII
2467
+ let processedContent = content;
2468
+ if (inputScan?.action === 'redact' && inputScan.redactedContent) {
2469
+ processedContent = inputScan.redactedContent;
2470
+ audit('guardrail.triggered', {
2471
+ action: 'redact',
2472
+ direction: 'input',
2473
+ threatCount: inputScan.threats.length,
2474
+ channelType: 'webchat',
2475
+ sessionId: session.id,
2476
+ });
2477
+ }
2478
+ else if (inputScan?.action === 'warn') {
2479
+ audit('guardrail.triggered', {
2480
+ action: 'warn',
2481
+ direction: 'input',
2482
+ threatCount: inputScan.threats.length,
2483
+ channelType: 'webchat',
2484
+ sessionId: session.id,
2485
+ });
2486
+ }
1819
2487
  // Add user message
1820
- await this.sessions.addMessage(session.id, 'user', content);
2488
+ await this.sessions.addMessage(session.id, 'user', processedContent);
1821
2489
  // Check if providers are available
1822
2490
  if (!this.providers) {
1823
2491
  this.sendToClient(client, {
@@ -1846,60 +2514,30 @@ export class Auxiora {
1846
2514
  ? chatPersonality === 'the-architect'
1847
2515
  : this.config.agent.personality === 'the-architect';
1848
2516
  const basePrompt = useArchitect ? this.architectPrompt : this.standardPrompt;
1849
- // Build enriched prompt with modes and memories
2517
+ // Build enriched prompt through pipeline
1850
2518
  let enrichedPrompt = basePrompt;
1851
- let memorySection = null;
1852
- if (this.memoryRetriever && this.memoryStore) {
1853
- const memories = await this.memoryStore.getAll();
1854
- memorySection = this.memoryRetriever.retrieve(memories, content);
1855
- }
1856
- if (this.promptAssembler && this.config.modes?.enabled !== false) {
1857
- const modeState = this.getSessionModeState(session.id);
1858
- // Security context check — BEFORE mode detection
1859
- if (this.securityFloor) {
1860
- const securityContext = this.securityFloor.detectSecurityContext({ userMessage: content });
1861
- if (securityContext.active) {
1862
- // Suspend current mode and use security floor prompt
1863
- modeState.suspendedMode = modeState.activeMode;
1864
- enrichedPrompt = this.promptAssembler.enrichForSecurityContext(securityContext, this.securityFloor, memorySection);
1865
- }
1866
- else if (modeState.suspendedMode) {
1867
- // Restore suspended mode
1868
- modeState.activeMode = modeState.suspendedMode;
1869
- delete modeState.suspendedMode;
1870
- enrichedPrompt = this.promptAssembler.enrichForMessage(modeState, memorySection, this.userPreferences, undefined, 'webchat');
1871
- }
1872
- else {
1873
- // Normal mode detection
1874
- enrichedPrompt = this.buildModeEnrichedPrompt(content, modeState, memorySection, 'webchat');
1875
- }
1876
- }
1877
- else {
1878
- // No security floor — normal mode detection
1879
- enrichedPrompt = this.buildModeEnrichedPrompt(content, modeState, memorySection, 'webchat');
1880
- }
1881
- }
1882
- else if (memorySection) {
1883
- enrichedPrompt = basePrompt + memorySection;
2519
+ let architectResult = { prompt: basePrompt };
2520
+ // Reset Architect conversation state for new chats
2521
+ if (useArchitect && this.architect && chatId && !this.architectResetChats.has(chatId)) {
2522
+ this.architectResetChats.add(chatId);
2523
+ this.architect.resetConversation();
2524
+ audit('personality.reset', { sessionId: session.id, chatId });
1884
2525
  }
1885
- // Only apply Architect enrichment if this chat uses the Architect
1886
- const architectResult = useArchitect
1887
- ? this.applyArchitectEnrichment(enrichedPrompt, content, chatId)
1888
- : { prompt: enrichedPrompt };
1889
- enrichedPrompt = architectResult.prompt;
1890
- // Inject dynamic self-awareness context
1891
- if (this.selfAwarenessAssembler) {
1892
- const awarenessContext = {
1893
- userId: client.senderId ?? 'anonymous',
1894
- sessionId: session.id,
2526
+ if (this.enrichmentPipeline) {
2527
+ const enrichCtx = {
2528
+ basePrompt,
2529
+ userMessage: processedContent,
2530
+ history: contextMessages,
2531
+ channelType: 'webchat',
1895
2532
  chatId: chatId ?? session.id,
1896
- currentMessage: content,
1897
- recentMessages: contextMessages,
2533
+ sessionId: session.id,
2534
+ userId: client.senderId ?? 'anonymous',
2535
+ toolsUsed: this.lastToolsUsed.get(session.id) ?? [],
2536
+ config: this.config,
1898
2537
  };
1899
- const awarenessFragment = await this.selfAwarenessAssembler.assemble(awarenessContext);
1900
- if (awarenessFragment) {
1901
- enrichedPrompt += '\n\n[Dynamic Self-Awareness]\n' + awarenessFragment;
1902
- }
2538
+ const result = await this.enrichmentPipeline.run(enrichCtx);
2539
+ enrichedPrompt = result.prompt;
2540
+ architectResult = { prompt: enrichedPrompt, architectMeta: result.metadata.architect };
1903
2541
  }
1904
2542
  // Route to best model for this message
1905
2543
  let provider;
@@ -1910,7 +2548,7 @@ export class Auxiora {
1910
2548
  }
1911
2549
  else if (this.modelRouter && this.config.routing?.enabled !== false) {
1912
2550
  try {
1913
- routingResult = this.modelRouter.route(content, { hasImages: false });
2551
+ routingResult = this.modelRouter.route(processedContent, { hasImages: false });
1914
2552
  provider = this.providers.getProvider(routingResult.selection.provider);
1915
2553
  }
1916
2554
  catch {
@@ -1920,30 +2558,89 @@ export class Auxiora {
1920
2558
  else {
1921
2559
  provider = this.providers.getPrimaryProvider();
1922
2560
  }
2561
+ // Inject model identity so the AI knows what it's running on
2562
+ enrichedPrompt += this.buildModelIdentityFragment(provider, routingResult?.selection.model ?? modelOverride);
1923
2563
  // Execute streaming AI call with tool follow-up loop
2564
+ const processingStartTime = Date.now();
1924
2565
  const fallbackCandidates = this.providers.resolveFallbackCandidates();
2566
+ const toolsUsed = [];
2567
+ let streamChunkCount = 0;
1925
2568
  const { response: fullResponse, usage } = await this.executeWithTools(session.id, chatMessages, enrichedPrompt, provider, (type, data) => {
1926
2569
  if (type === 'text') {
2570
+ streamChunkCount++;
1927
2571
  this.sendToClient(client, { type: 'chunk', id: requestId, payload: { content: data } });
1928
2572
  }
1929
2573
  else if (type === 'thinking') {
1930
2574
  this.sendToClient(client, { type: 'thinking', id: requestId, payload: { content: data } });
1931
2575
  }
1932
2576
  else if (type === 'tool_use') {
2577
+ toolsUsed.push({ name: data?.name ?? 'unknown', success: true });
1933
2578
  this.sendToClient(client, { type: 'tool_use', id: requestId, payload: data });
1934
2579
  }
1935
2580
  else if (type === 'tool_result') {
2581
+ // Update last tool's success based on result
2582
+ if (toolsUsed.length > 0 && data?.error) {
2583
+ toolsUsed[toolsUsed.length - 1].success = false;
2584
+ }
1936
2585
  this.sendToClient(client, { type: 'tool_result', id: requestId, payload: data });
1937
2586
  }
1938
2587
  else if (type === 'status') {
1939
2588
  this.sendToClient(client, { type: 'status', id: requestId, payload: data });
1940
2589
  }
1941
2590
  }, { tools, fallbackCandidates });
2591
+ // Feed tool usage to awareness collector
2592
+ if (this.architectAwarenessCollector && toolsUsed.length > 0) {
2593
+ this.architectAwarenessCollector.updateToolContext(toolsUsed);
2594
+ }
2595
+ // Store tools for next turn's enrichment context
2596
+ this.lastToolsUsed.set(session.id, toolsUsed);
2597
+ // ── Guardrail output scan ─────────────────────────────────────
2598
+ const outputScan = this.checkOutputGuardrails(fullResponse);
2599
+ const finalResponse = outputScan.response;
2600
+ if (outputScan.wasModified) {
2601
+ audit('guardrail.triggered', {
2602
+ action: outputScan.action,
2603
+ direction: 'output',
2604
+ channelType: 'webchat',
2605
+ sessionId: session.id,
2606
+ });
2607
+ // Send correction since chunks were already streamed
2608
+ this.sendToClient(client, {
2609
+ type: 'guardrail_correction',
2610
+ id: requestId,
2611
+ payload: { content: finalResponse },
2612
+ });
2613
+ }
2614
+ // Collect transparency metadata (best-effort)
2615
+ let transparencyMeta;
2616
+ try {
2617
+ const modelId = routingResult?.selection.model ?? modelOverride ?? provider.defaultModel;
2618
+ const caps = provider.metadata.models[modelId];
2619
+ if (caps) {
2620
+ transparencyMeta = collectTransparencyMeta({
2621
+ enrichment: this.enrichmentPipeline
2622
+ ? { prompt: enrichedPrompt, metadata: { architect: architectResult.architectMeta, stages: architectResult.stages ?? [] } }
2623
+ : { prompt: enrichedPrompt, metadata: { stages: [] } },
2624
+ completion: { content: finalResponse, usage, model: modelId, finishReason: 'stop', toolUse: toolsUsed.map(t => ({ name: t.name })) },
2625
+ capabilities: { costPer1kInput: caps.costPer1kInput, costPer1kOutput: caps.costPer1kOutput },
2626
+ providerName: provider.name,
2627
+ awarenessSignals: [],
2628
+ responseText: finalResponse,
2629
+ processingStartTime,
2630
+ });
2631
+ }
2632
+ }
2633
+ catch {
2634
+ // Transparency is best-effort — never block message delivery
2635
+ }
1942
2636
  // Save assistant message (skip if empty — happens when response is tool-only)
1943
- if (fullResponse) {
1944
- await this.sessions.addMessage(session.id, 'assistant', fullResponse, {
2637
+ if (finalResponse) {
2638
+ await this.sessions.addMessage(session.id, 'assistant', finalResponse, {
1945
2639
  input: usage.inputTokens,
1946
2640
  output: usage.outputTokens,
2641
+ }, {
2642
+ ...(architectResult.architectMeta ? { architectDomain: architectResult.architectMeta.detectedContext.domain } : {}),
2643
+ ...(transparencyMeta ? { transparency: transparencyMeta } : {}),
1947
2644
  });
1948
2645
  }
1949
2646
  // Record usage for cost tracking
@@ -1951,14 +2648,14 @@ export class Auxiora {
1951
2648
  this.modelRouter.recordUsage(routingResult.selection.provider, routingResult.selection.model, usage.inputTokens, usage.outputTokens);
1952
2649
  }
1953
2650
  // Extract memories and learn from conversation (if auto-extract enabled)
1954
- if (this.config.memory?.autoExtract !== false && this.memoryStore && fullResponse && content.length > 20) {
1955
- void this.extractAndLearn(content, fullResponse, session.id);
2651
+ if (this.config.memory?.autoExtract !== false && this.memoryStore && finalResponse && processedContent.length > 20) {
2652
+ void this.extractAndLearn(processedContent, finalResponse, session.id);
1956
2653
  }
1957
2654
  // Auto-title webchat chats after first exchange
1958
- if (fullResponse &&
2655
+ if (finalResponse &&
1959
2656
  session.metadata.channelType === 'webchat' &&
1960
2657
  session.messages.length <= 3) {
1961
- void this.generateChatTitle(session.id, content, fullResponse, client);
2658
+ void this.generateChatTitle(session.id, processedContent, finalResponse, client);
1962
2659
  }
1963
2660
  // Send done signal
1964
2661
  this.sendToClient(client, {
@@ -1977,6 +2674,7 @@ export class Auxiora {
1977
2674
  override: true,
1978
2675
  } : undefined,
1979
2676
  architect: architectResult.architectMeta,
2677
+ transparency: transparencyMeta,
1980
2678
  },
1981
2679
  });
1982
2680
  // Background self-awareness analysis
@@ -1985,13 +2683,33 @@ export class Auxiora {
1985
2683
  userId: client.senderId ?? 'anonymous',
1986
2684
  sessionId: session.id,
1987
2685
  chatId: chatId ?? session.id,
1988
- currentMessage: content,
2686
+ currentMessage: processedContent,
1989
2687
  recentMessages: contextMessages,
1990
- response: fullResponse,
2688
+ response: finalResponse,
1991
2689
  responseTime: Date.now() - (session.metadata.lastActiveAt ?? Date.now()),
1992
2690
  tokensUsed: { input: usage?.inputTokens ?? 0, output: usage?.outputTokens ?? 0 },
2691
+ streamChunks: streamChunkCount,
1993
2692
  }).catch(() => { });
1994
2693
  }
2694
+ // Record conversation in consciousness journal
2695
+ if (this.consciousness) {
2696
+ const journalBase = {
2697
+ sessionId: session.id,
2698
+ type: 'message',
2699
+ context: {
2700
+ domains: architectResult.architectMeta
2701
+ ? [architectResult.architectMeta.detectedContext.domain]
2702
+ : ['general'],
2703
+ },
2704
+ selfState: {
2705
+ health: (this.healthMonitor?.getHealthState().overall === 'unhealthy' ? 'degraded' : this.healthMonitor?.getHealthState().overall ?? 'healthy'),
2706
+ activeProviders: [this.config.provider.primary],
2707
+ uptime: Math.round(process.uptime()),
2708
+ },
2709
+ };
2710
+ this.consciousness.journal.record({ ...journalBase, message: { role: 'user', content: processedContent } }).catch(() => { });
2711
+ this.consciousness.journal.record({ ...journalBase, message: { role: 'assistant', content: finalResponse } }).catch(() => { });
2712
+ }
1995
2713
  audit('message.sent', {
1996
2714
  sessionId: session.id,
1997
2715
  inputTokens: usage.inputTokens,
@@ -2268,6 +2986,14 @@ export class Auxiora {
2268
2986
  for (const toolUse of toolUses) {
2269
2987
  // Map Claude Code emulation tool names to our actual tools
2270
2988
  const mapped = mapCCToolCall(toolUse.name, toolUse.input);
2989
+ // Skip CC-only tools that have no Auxiora equivalent
2990
+ if (mapped.skip) {
2991
+ onChunk('tool_result', { tool: toolUse.name, success: false, error: mapped.skip });
2992
+ toolResultParts.push(`[${toolUse.name}]: Error: ${mapped.skip}`);
2993
+ recordToolCall(loopState, toolUse.id, mapped.name, mapped.input);
2994
+ recordToolOutcome(loopState, toolUse.id, mapped.skip);
2995
+ continue;
2996
+ }
2271
2997
  recordToolCall(loopState, toolUse.id, mapped.name, mapped.input);
2272
2998
  try {
2273
2999
  const result = await toolExecutor.execute(mapped.name, mapped.input, context);
@@ -2511,7 +3237,11 @@ export class Auxiora {
2511
3237
  persistToWebchat(content) {
2512
3238
  this.sessions.getOrCreate('webchat', { channelType: 'webchat' })
2513
3239
  .then(session => this.sessions.addMessage(session.id, 'assistant', content))
2514
- .catch(() => { });
3240
+ .catch((err) => {
3241
+ this.logger.warn('Failed to persist webchat message', {
3242
+ error: err instanceof Error ? err : new Error(String(err)),
3243
+ });
3244
+ });
2515
3245
  }
2516
3246
  /** Deliver a proactive message to all connected channel adapters using tracked channel IDs.
2517
3247
  * Also persists to the webchat session so messages appear in chat history.
@@ -2563,149 +3293,253 @@ export class Auxiora {
2563
3293
  if (inbound.attachments && inbound.attachments.length > 0 && this.mediaProcessor) {
2564
3294
  messageContent = await this.mediaProcessor.process(inbound.attachments, inbound.content);
2565
3295
  }
2566
- await this.sessions.addMessage(session.id, 'user', messageContent);
2567
- // Check if providers are available
2568
- if (!this.providers) {
3296
+ // ── Guardrail input scan ──────────────────────────────────────
3297
+ const inputScan = this.checkInputGuardrails(messageContent);
3298
+ if (inputScan && inputScan.action === 'block') {
3299
+ audit('guardrail.triggered', {
3300
+ action: 'block',
3301
+ direction: 'input',
3302
+ threatCount: inputScan.threats.length,
3303
+ channelType: inbound.channelType,
3304
+ sessionId: session.id,
3305
+ });
2569
3306
  if (this.channels) {
2570
3307
  await this.channels.send(inbound.channelType, inbound.channelId, {
2571
- content: 'I need API keys to respond. Please configure them in the vault.',
3308
+ content: this.GUARDRAIL_BLOCK_MESSAGE,
2572
3309
  replyToId: inbound.id,
2573
3310
  });
2574
3311
  }
2575
3312
  return;
2576
3313
  }
2577
- // Get context messages
2578
- const contextMessages = this.sessions.getContextMessages(session.id, this.getProviderMaxTokens(this.providers.getPrimaryProvider()), 4096);
2579
- const chatMessages = sanitizeTranscript(contextMessages).map((m) => ({
2580
- role: m.role,
2581
- content: m.content,
2582
- }));
2583
- // Show typing indicator while generating response
2584
- const stopTyping = this.channels
2585
- ? await this.channels.startTyping(inbound.channelType, inbound.channelId)
2586
- : () => { };
3314
+ // Apply redaction if guardrails flagged PII
3315
+ if (inputScan?.action === 'redact' && inputScan.redactedContent) {
3316
+ messageContent = inputScan.redactedContent;
3317
+ audit('guardrail.triggered', {
3318
+ action: 'redact',
3319
+ direction: 'input',
3320
+ threatCount: inputScan.threats.length,
3321
+ channelType: inbound.channelType,
3322
+ });
3323
+ }
3324
+ else if (inputScan?.action === 'warn') {
3325
+ audit('guardrail.triggered', {
3326
+ action: 'warn',
3327
+ direction: 'input',
3328
+ threatCount: inputScan.threats.length,
3329
+ channelType: inbound.channelType,
3330
+ });
3331
+ }
3332
+ await this.sessions.addMessage(session.id, 'user', messageContent);
3333
+ // Check if providers are available
3334
+ if (!this.providers) {
3335
+ if (this.channels) {
3336
+ await this.channels.send(inbound.channelType, inbound.channelId, {
3337
+ content: 'I need API keys to respond. Please configure them in the vault.',
3338
+ replyToId: inbound.id,
3339
+ });
3340
+ }
3341
+ return;
3342
+ }
3343
+ // Get context messages — channel sessions use a capped token budget and turn limit
3344
+ // to prevent excessively long API calls from models with huge context windows.
3345
+ const contextMessages = this.sessions.getContextMessages(session.id, this.getProviderMaxTokens(this.providers.getPrimaryProvider()), 4096, { isChannel: true });
3346
+ const chatMessages = sanitizeTranscript(contextMessages).map((m) => ({
3347
+ role: m.role,
3348
+ content: m.content,
3349
+ }));
3350
+ // Show typing indicator while generating response
3351
+ const stopTyping = this.channels
3352
+ ? await this.channels.startTyping(inbound.channelType, inbound.channelId)
3353
+ : () => { };
2587
3354
  const channelAgentId = `channel:${inbound.channelType}:${inbound.channelId}:${Date.now()}`;
2588
- try {
2589
- // Get tool definitions from registry
2590
- const tools = toolRegistry.toProviderFormat();
2591
- // Build enriched prompt with modes and memories
2592
- let enrichedPrompt = this.systemPrompt;
2593
- let channelMemorySection = null;
2594
- if (this.memoryRetriever && this.memoryStore) {
2595
- const memories = await this.memoryStore.getAll();
2596
- channelMemorySection = this.memoryRetriever.retrieve(memories, inbound.content);
2597
- }
2598
- if (this.promptAssembler && this.config.modes?.enabled !== false) {
2599
- const modeState = this.getSessionModeState(session.id);
2600
- // Security context check — BEFORE mode detection
2601
- if (this.securityFloor) {
2602
- const securityContext = this.securityFloor.detectSecurityContext({ userMessage: inbound.content });
2603
- if (securityContext.active) {
2604
- modeState.suspendedMode = modeState.activeMode;
2605
- enrichedPrompt = this.promptAssembler.enrichForSecurityContext(securityContext, this.securityFloor, channelMemorySection);
3355
+ // 4-minute timeout for the entire LLM response cycle.
3356
+ // Increased from 2min to accommodate auto-continuations (max_tokens → "Continue")
3357
+ // and tool round-trips. If the provider stream hangs (network issue, overloaded API),
3358
+ // this ensures the user gets an error message instead of infinite "typing…".
3359
+ const CHANNEL_RESPONSE_TIMEOUT_MS = 240_000;
3360
+ let draftLoop = null;
3361
+ let draftMessageId = null;
3362
+ try { // outer try — finally block guarantees stopTyping() runs
3363
+ try {
3364
+ // Get tool definitions from registry
3365
+ const tools = toolRegistry.toProviderFormat();
3366
+ // Build enriched prompt through pipeline
3367
+ let enrichedPrompt = this.systemPrompt;
3368
+ const channelChatId = `${inbound.channelType}:${inbound.channelId}`;
3369
+ let channelArchitectResult = { prompt: this.systemPrompt };
3370
+ // Reset Architect conversation state for new channel chats
3371
+ const useChannelArchitect = this.config.agent.personality === 'the-architect';
3372
+ if (useChannelArchitect && this.architect && !this.architectResetChats.has(channelChatId)) {
3373
+ this.architectResetChats.add(channelChatId);
3374
+ this.architect.resetConversation();
3375
+ audit('personality.reset', { sessionId: session.id, chatId: channelChatId });
3376
+ }
3377
+ if (this.enrichmentPipeline) {
3378
+ const enrichCtx = {
3379
+ basePrompt: this.systemPrompt,
3380
+ userMessage: messageContent,
3381
+ history: contextMessages,
3382
+ channelType: inbound.channelType,
3383
+ chatId: channelChatId,
3384
+ sessionId: session.id,
3385
+ userId: inbound.senderId ?? 'anonymous',
3386
+ toolsUsed: this.lastToolsUsed.get(session.id) ?? [],
3387
+ config: this.config,
3388
+ };
3389
+ const result = await this.enrichmentPipeline.run(enrichCtx);
3390
+ enrichedPrompt = result.prompt;
3391
+ channelArchitectResult = { prompt: enrichedPrompt, architectMeta: result.metadata.architect };
3392
+ }
3393
+ // Use executeWithTools for channels — collect final text for channel reply
3394
+ const provider = this.providers.getPrimaryProvider();
3395
+ // Inject model identity so the AI knows what it's running on
3396
+ enrichedPrompt += this.buildModelIdentityFragment(provider);
3397
+ this.agentStart(channelAgentId, 'channel', `Processing message on ${inbound.channelType}`, inbound.channelType);
3398
+ // Draft streaming: edit message in place if adapter supports it
3399
+ const adapter = this.channels?.getAdapter(inbound.channelType);
3400
+ const supportsDraft = !!adapter?.editMessage;
3401
+ let accumulatedText = '';
3402
+ if (supportsDraft && this.channels) {
3403
+ const channels = this.channels;
3404
+ draftLoop = new DraftStreamLoop(async (text) => {
3405
+ try {
3406
+ if (!draftMessageId) {
3407
+ const result = await channels.send(inbound.channelType, inbound.channelId, {
3408
+ content: text,
3409
+ replyToId: inbound.id,
3410
+ });
3411
+ if (result.success && result.messageId) {
3412
+ draftMessageId = result.messageId;
3413
+ }
3414
+ return result.success;
3415
+ }
3416
+ else {
3417
+ const result = await channels.editMessage(inbound.channelType, inbound.channelId, draftMessageId, { content: text });
3418
+ return result.success;
3419
+ }
3420
+ }
3421
+ catch {
3422
+ return false;
3423
+ }
3424
+ }, 1000);
3425
+ }
3426
+ const fallbackCandidates = this.providers.resolveFallbackCandidates();
3427
+ const channelToolsUsed = [];
3428
+ const { response: channelResponse, usage: channelUsage } = await Promise.race([
3429
+ this.executeWithTools(session.id, chatMessages, enrichedPrompt, provider, (type, data) => {
3430
+ if (type === 'text' && data && draftLoop) {
3431
+ accumulatedText += data;
3432
+ draftLoop.update(accumulatedText);
3433
+ }
3434
+ else if (type === 'tool_use') {
3435
+ channelToolsUsed.push({ name: data?.name ?? 'unknown', success: true });
3436
+ }
3437
+ else if (type === 'tool_result') {
3438
+ if (channelToolsUsed.length > 0 && data?.error) {
3439
+ channelToolsUsed[channelToolsUsed.length - 1].success = false;
3440
+ }
3441
+ }
3442
+ }, { tools, fallbackCandidates }),
3443
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Response timed out — the AI provider did not respond within 4 minutes. Please try again.')), CHANNEL_RESPONSE_TIMEOUT_MS)),
3444
+ ]);
3445
+ // Feed tool usage to awareness collector
3446
+ if (this.architectAwarenessCollector && channelToolsUsed.length > 0) {
3447
+ this.architectAwarenessCollector.updateToolContext(channelToolsUsed);
3448
+ }
3449
+ this.lastToolsUsed.set(session.id, channelToolsUsed);
3450
+ // Flush final draft text
3451
+ if (draftLoop) {
3452
+ if (channelResponse && channelResponse !== accumulatedText) {
3453
+ draftLoop.update(channelResponse);
2606
3454
  }
2607
- else if (modeState.suspendedMode) {
2608
- modeState.activeMode = modeState.suspendedMode;
2609
- delete modeState.suspendedMode;
2610
- enrichedPrompt = this.buildModeEnrichedPrompt(inbound.content, modeState, channelMemorySection, inbound.channelType);
3455
+ await draftLoop.flush();
3456
+ draftLoop.stop();
3457
+ }
3458
+ // ── Guardrail output scan ─────────────────────────────────────
3459
+ const channelOutputScan = this.checkOutputGuardrails(channelResponse);
3460
+ const finalChannelResponse = channelOutputScan.response;
3461
+ if (channelOutputScan.wasModified) {
3462
+ audit('guardrail.triggered', {
3463
+ action: channelOutputScan.action,
3464
+ direction: 'output',
3465
+ channelType: inbound.channelType,
3466
+ sessionId: session.id,
3467
+ });
3468
+ // If draft streaming already sent partial text, do a final edit with clean version
3469
+ if (draftMessageId && adapter?.editMessage) {
3470
+ await adapter.editMessage(inbound.channelId, draftMessageId, { content: finalChannelResponse });
2611
3471
  }
2612
- else {
2613
- enrichedPrompt = this.buildModeEnrichedPrompt(inbound.content, modeState, channelMemorySection, inbound.channelType);
3472
+ }
3473
+ // Save assistant message
3474
+ await this.sessions.addMessage(session.id, 'assistant', finalChannelResponse, {
3475
+ input: channelUsage.inputTokens,
3476
+ output: channelUsage.outputTokens,
3477
+ }, channelArchitectResult.architectMeta ? { architectDomain: channelArchitectResult.architectMeta.detectedContext.domain } : undefined);
3478
+ // Extract memories and learn from conversation (if auto-extract enabled)
3479
+ if (this.config.memory?.autoExtract !== false && this.memoryStore && finalChannelResponse && messageContent.length > 20) {
3480
+ void this.extractAndLearn(messageContent, finalChannelResponse, session.id);
3481
+ }
3482
+ // Send final response. The draft stream loop edits a single message,
3483
+ // but Discord silently truncates edits at 2000 chars. For long responses,
3484
+ // replace the draft with a chunked send so nothing is lost.
3485
+ const DRAFT_SAFE_LENGTH = 1900; // leave margin below Discord's 2000 char limit
3486
+ if (draftMessageId && this.channels && finalChannelResponse.length > DRAFT_SAFE_LENGTH) {
3487
+ // Draft only showed partial content — replace it with a pointer and send full chunked response
3488
+ if (adapter?.editMessage) {
3489
+ await adapter.editMessage(inbound.channelId, draftMessageId, {
3490
+ content: '*\u2026 (full response below)*',
3491
+ });
2614
3492
  }
3493
+ await this.channels.send(inbound.channelType, inbound.channelId, {
3494
+ content: finalChannelResponse,
3495
+ });
2615
3496
  }
2616
- else {
2617
- enrichedPrompt = this.buildModeEnrichedPrompt(inbound.content, modeState, channelMemorySection, inbound.channelType);
3497
+ else if (!draftMessageId && this.channels) {
3498
+ await this.channels.send(inbound.channelType, inbound.channelId, {
3499
+ content: finalChannelResponse,
3500
+ replyToId: inbound.id,
3501
+ });
2618
3502
  }
3503
+ audit('message.sent', {
3504
+ channelType: inbound.channelType,
3505
+ sessionId: session.id,
3506
+ inputTokens: channelUsage.inputTokens,
3507
+ outputTokens: channelUsage.outputTokens,
3508
+ });
3509
+ this.agentEnd(channelAgentId, true);
2619
3510
  }
2620
- else if (channelMemorySection) {
2621
- enrichedPrompt = this.systemPrompt + channelMemorySection;
2622
- }
2623
- const channelArchitectResult = this.applyArchitectEnrichment(enrichedPrompt, inbound.content);
2624
- enrichedPrompt = channelArchitectResult.prompt;
2625
- // Use executeWithTools for channels collect final text for channel reply
2626
- const provider = this.providers.getPrimaryProvider();
2627
- this.agentStart(channelAgentId, 'channel', `Processing message on ${inbound.channelType}`, inbound.channelType);
2628
- // Draft streaming: edit message in place if adapter supports it
2629
- const adapter = this.channels?.getAdapter(inbound.channelType);
2630
- const supportsDraft = !!adapter?.editMessage;
2631
- let draftMessageId = null;
2632
- let accumulatedText = '';
2633
- let draftLoop = null;
2634
- if (supportsDraft && this.channels) {
2635
- const channels = this.channels;
2636
- draftLoop = new DraftStreamLoop(async (text) => {
2637
- try {
2638
- if (!draftMessageId) {
2639
- const result = await channels.send(inbound.channelType, inbound.channelId, {
2640
- content: text,
3511
+ catch (error) {
3512
+ if (draftLoop)
3513
+ draftLoop.stop();
3514
+ this.agentEnd(channelAgentId, false);
3515
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
3516
+ audit('channel.error', { sessionId: session.id, error: errorMessage });
3517
+ if (this.channels) {
3518
+ const errorContent = `Error: ${errorMessage}`;
3519
+ // If a draft message exists, edit it with the error instead of sending a new one
3520
+ if (draftMessageId) {
3521
+ try {
3522
+ await this.channels.editMessage(inbound.channelType, inbound.channelId, draftMessageId, { content: errorContent });
3523
+ }
3524
+ catch {
3525
+ // Edit failed fall back to new message
3526
+ await this.channels.send(inbound.channelType, inbound.channelId, {
3527
+ content: errorContent,
2641
3528
  replyToId: inbound.id,
2642
3529
  });
2643
- if (result.success && result.messageId) {
2644
- draftMessageId = result.messageId;
2645
- }
2646
- return result.success;
2647
- }
2648
- else {
2649
- const result = await channels.editMessage(inbound.channelType, inbound.channelId, draftMessageId, { content: text });
2650
- return result.success;
2651
3530
  }
2652
3531
  }
2653
- catch {
2654
- return false;
3532
+ else {
3533
+ await this.channels.send(inbound.channelType, inbound.channelId, {
3534
+ content: errorContent,
3535
+ replyToId: inbound.id,
3536
+ });
2655
3537
  }
2656
- }, 1000);
2657
- }
2658
- const fallbackCandidates = this.providers.resolveFallbackCandidates();
2659
- const { response: channelResponse, usage: channelUsage } = await this.executeWithTools(session.id, chatMessages, enrichedPrompt, provider, (type, data) => {
2660
- if (type === 'text' && data && draftLoop) {
2661
- accumulatedText += data;
2662
- draftLoop.update(accumulatedText);
2663
- }
2664
- }, { tools, fallbackCandidates });
2665
- // Flush final draft text
2666
- if (draftLoop) {
2667
- if (channelResponse && channelResponse !== accumulatedText) {
2668
- draftLoop.update(channelResponse);
2669
3538
  }
2670
- await draftLoop.flush();
2671
- draftLoop.stop();
2672
3539
  }
2673
- stopTyping();
2674
- // Save assistant message
2675
- await this.sessions.addMessage(session.id, 'assistant', channelResponse, {
2676
- input: channelUsage.inputTokens,
2677
- output: channelUsage.outputTokens,
2678
- });
2679
- // Extract memories and learn from conversation (if auto-extract enabled)
2680
- if (this.config.memory?.autoExtract !== false && this.memoryStore && channelResponse && inbound.content.length > 20) {
2681
- void this.extractAndLearn(inbound.content, channelResponse, session.id);
2682
- }
2683
- // Send response (skip if draft streaming already delivered it)
2684
- if (!draftMessageId && this.channels) {
2685
- await this.channels.send(inbound.channelType, inbound.channelId, {
2686
- content: channelResponse,
2687
- replyToId: inbound.id,
2688
- });
2689
- }
2690
- audit('message.sent', {
2691
- channelType: inbound.channelType,
2692
- sessionId: session.id,
2693
- inputTokens: channelUsage.inputTokens,
2694
- outputTokens: channelUsage.outputTokens,
2695
- });
2696
- this.agentEnd(channelAgentId, true);
2697
3540
  }
2698
- catch (error) {
3541
+ finally {
2699
3542
  stopTyping();
2700
- this.agentEnd(channelAgentId, false);
2701
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
2702
- audit('channel.error', { sessionId: session.id, error: errorMessage });
2703
- if (this.channels) {
2704
- await this.channels.send(inbound.channelType, inbound.channelId, {
2705
- content: `Error: ${errorMessage}`,
2706
- replyToId: inbound.id,
2707
- });
2708
- }
2709
3543
  }
2710
3544
  }); // end runWithRequestId
2711
3545
  }
@@ -2912,10 +3746,52 @@ export class Auxiora {
2912
3746
  }
2913
3747
  // Load persisted channel targets for proactive delivery (behaviors, ambient)
2914
3748
  await this.loadChannelTargets();
3749
+ // Research job expiry (every 60s, prune jobs older than 1 hour)
3750
+ this.researchJobExpiry = setInterval(() => {
3751
+ const ONE_HOUR = 3_600_000;
3752
+ const now = Date.now();
3753
+ for (const [id, job] of this.researchJobs) {
3754
+ if (now - job.createdAt > ONE_HOUR)
3755
+ this.researchJobs.delete(id);
3756
+ }
3757
+ }, 60_000);
2915
3758
  this.running = true;
2916
3759
  console.log(`\n${this.getAgentName()} is ready!`);
2917
3760
  console.log(`Open http://${this.config.gateway.host}:${this.config.gateway.port} in your browser\n`);
2918
3761
  }
3762
+ async processEventTriggers(events) {
3763
+ if (!this.behaviors || events.length === 0)
3764
+ return;
3765
+ const allBehaviors = await this.behaviors.list();
3766
+ const eventBehaviors = allBehaviors.filter((b) => b.type === 'event' && b.status === 'active' && b.eventTrigger);
3767
+ for (const event of events) {
3768
+ // Feed to ambient pattern engine
3769
+ this.ambientEngine?.observe({
3770
+ type: `${event.connectorId}:${event.triggerId}`,
3771
+ timestamp: event.timestamp,
3772
+ data: event.data,
3773
+ });
3774
+ // Match against event behaviors
3775
+ for (const behavior of eventBehaviors) {
3776
+ const trigger = behavior.eventTrigger;
3777
+ if (trigger.source !== event.connectorId || trigger.event !== event.triggerId)
3778
+ continue;
3779
+ if (evaluateConditions(event.data, trigger.conditions, trigger.combinator)) {
3780
+ try {
3781
+ await this.behaviors.executeNow(behavior.id);
3782
+ await audit('behavior.event_triggered', {
3783
+ behaviorId: behavior.id,
3784
+ source: event.connectorId,
3785
+ event: event.triggerId,
3786
+ });
3787
+ }
3788
+ catch {
3789
+ // Execution failures tracked by BehaviorManager
3790
+ }
3791
+ }
3792
+ }
3793
+ }
3794
+ }
2919
3795
  async stop() {
2920
3796
  if (!this.running)
2921
3797
  return;
@@ -2938,6 +3814,9 @@ export class Auxiora {
2938
3814
  if (this.ambientScheduler) {
2939
3815
  this.ambientScheduler.stop();
2940
3816
  }
3817
+ if (this.ambientDetectTimer) {
3818
+ clearInterval(this.ambientDetectTimer);
3819
+ }
2941
3820
  if (this.autonomousExecutor) {
2942
3821
  this.autonomousExecutor.stop();
2943
3822
  }
@@ -2945,6 +3824,17 @@ export class Auxiora {
2945
3824
  clearInterval(this.memoryCleanupInterval);
2946
3825
  this.memoryCleanupInterval = undefined;
2947
3826
  }
3827
+ if (this.researchJobExpiry) {
3828
+ clearInterval(this.researchJobExpiry);
3829
+ this.researchJobExpiry = undefined;
3830
+ }
3831
+ if (this.mcpClientManager) {
3832
+ await this.mcpClientManager.disconnectAll();
3833
+ }
3834
+ if (this.jobQueue) {
3835
+ await this.jobQueue.stop(30000);
3836
+ }
3837
+ this.consciousness?.shutdown();
2948
3838
  this.sessions.destroy();
2949
3839
  this.vault.lock();
2950
3840
  this.running = false;
@@ -2955,6 +3845,2119 @@ export class Auxiora {
2955
3845
  getAgentName() {
2956
3846
  return this.config.agent?.name ?? 'Auxiora';
2957
3847
  }
3848
+ createPersonalityRouter() {
3849
+ const router = Router();
3850
+ const guard = (_req, res) => {
3851
+ if (!this.architect) {
3852
+ res.status(503).json({ error: 'Architect not available' });
3853
+ return false;
3854
+ }
3855
+ return true;
3856
+ };
3857
+ // --- Decisions (Gap 4) ---
3858
+ router.post('/decisions', async (req, res) => {
3859
+ if (!guard(req, res))
3860
+ return;
3861
+ const { domain, summary, context, followUpDate } = req.body ?? {};
3862
+ if (!domain || !summary || !context) {
3863
+ res.status(400).json({ error: 'Missing required fields: domain, summary, context' });
3864
+ return;
3865
+ }
3866
+ try {
3867
+ const decision = await this.architect.recordDecision({ domain, summary, context, followUpDate, status: 'active' });
3868
+ audit('personality.decision.created', { decisionId: decision.id, domain });
3869
+ res.status(201).json(decision);
3870
+ }
3871
+ catch (err) {
3872
+ res.status(500).json({ error: err.message ?? 'Failed to record decision' });
3873
+ }
3874
+ });
3875
+ router.patch('/decisions/:id', async (req, res) => {
3876
+ if (!guard(req, res))
3877
+ return;
3878
+ const { status, outcome, followUpDate } = req.body ?? {};
3879
+ try {
3880
+ await this.architect.updateDecision(req.params.id, { status, outcome, followUpDate });
3881
+ audit('personality.decision.updated', { decisionId: req.params.id });
3882
+ res.json({ ok: true });
3883
+ }
3884
+ catch (err) {
3885
+ res.status(500).json({ error: err.message ?? 'Failed to update decision' });
3886
+ }
3887
+ });
3888
+ router.get('/decisions', async (req, res) => {
3889
+ if (!guard(req, res))
3890
+ return;
3891
+ try {
3892
+ const { domain, status, since, search, limit } = req.query;
3893
+ const query = {};
3894
+ if (domain)
3895
+ query.domain = domain;
3896
+ if (status)
3897
+ query.status = status;
3898
+ if (since)
3899
+ query.since = since;
3900
+ if (search)
3901
+ query.search = search;
3902
+ if (limit)
3903
+ query.limit = Number(limit);
3904
+ const decisions = await this.architect.queryDecisions(query);
3905
+ res.json({ decisions });
3906
+ }
3907
+ catch (err) {
3908
+ res.status(500).json({ error: err.message ?? 'Failed to query decisions' });
3909
+ }
3910
+ });
3911
+ router.get('/decisions/due', async (req, res) => {
3912
+ if (!guard(req, res))
3913
+ return;
3914
+ try {
3915
+ const due = await this.architect.getDueFollowUps();
3916
+ res.json({ decisions: due });
3917
+ }
3918
+ catch (err) {
3919
+ res.status(500).json({ error: err.message ?? 'Failed to get due follow-ups' });
3920
+ }
3921
+ });
3922
+ // --- Traits (Gap 5) ---
3923
+ router.get('/traits', (req, res) => {
3924
+ if (!guard(req, res))
3925
+ return;
3926
+ try {
3927
+ const mix = this.architect.getTraitMix({
3928
+ domain: 'general',
3929
+ emotionalRegister: 'neutral',
3930
+ stakes: 'moderate',
3931
+ complexity: 'moderate',
3932
+ mode: 'solo_work',
3933
+ });
3934
+ const overrides = this.architect.getActiveOverrides();
3935
+ res.json({ mix, overrides });
3936
+ }
3937
+ catch (err) {
3938
+ res.status(500).json({ error: err.message ?? 'Failed to get traits' });
3939
+ }
3940
+ });
3941
+ router.put('/traits/:trait', async (req, res) => {
3942
+ if (!guard(req, res))
3943
+ return;
3944
+ const { offset, source, reason } = req.body ?? {};
3945
+ if (typeof offset !== 'number') {
3946
+ res.status(400).json({ error: 'Missing or invalid field: offset must be a number' });
3947
+ return;
3948
+ }
3949
+ try {
3950
+ await this.architect.setTraitOverride(req.params.trait, offset);
3951
+ audit('personality.trait.override', { trait: req.params.trait, offset, source, reason });
3952
+ res.json({ ok: true });
3953
+ }
3954
+ catch (err) {
3955
+ res.status(500).json({ error: err.message ?? 'Failed to set trait override' });
3956
+ }
3957
+ });
3958
+ router.delete('/traits/:trait', async (req, res) => {
3959
+ if (!guard(req, res))
3960
+ return;
3961
+ try {
3962
+ await this.architect.removeTraitOverride(req.params.trait);
3963
+ audit('personality.trait.override', { trait: req.params.trait, action: 'removed' });
3964
+ res.json({ ok: true });
3965
+ }
3966
+ catch (err) {
3967
+ res.status(500).json({ error: err.message ?? 'Failed to remove trait override' });
3968
+ }
3969
+ });
3970
+ // --- Presets (Gap 5) ---
3971
+ router.get('/presets', (req, res) => {
3972
+ if (!guard(req, res))
3973
+ return;
3974
+ try {
3975
+ const presets = this.architect.listPresets();
3976
+ res.json({ presets });
3977
+ }
3978
+ catch (err) {
3979
+ res.status(500).json({ error: err.message ?? 'Failed to list presets' });
3980
+ }
3981
+ });
3982
+ router.post('/presets/:name/apply', async (req, res) => {
3983
+ if (!guard(req, res))
3984
+ return;
3985
+ try {
3986
+ await this.architect.loadPreset(req.params.name);
3987
+ audit('personality.preset.applied', { preset: req.params.name });
3988
+ res.json({ ok: true });
3989
+ }
3990
+ catch (err) {
3991
+ res.status(500).json({ error: err.message ?? 'Failed to apply preset' });
3992
+ }
3993
+ });
3994
+ // --- Preferences (Gap 12) ---
3995
+ router.get('/preferences', async (req, res) => {
3996
+ if (!guard(req, res))
3997
+ return;
3998
+ try {
3999
+ const prefs = await this.architect.getPreferences();
4000
+ res.json(prefs);
4001
+ }
4002
+ catch (err) {
4003
+ res.status(500).json({ error: err.message ?? 'Failed to get preferences' });
4004
+ }
4005
+ });
4006
+ router.put('/preferences', async (req, res) => {
4007
+ if (!guard(req, res))
4008
+ return;
4009
+ const body = req.body ?? {};
4010
+ try {
4011
+ for (const [key, value] of Object.entries(body)) {
4012
+ await this.architect.updatePreference(key, value);
4013
+ }
4014
+ audit('personality.preferences.updated', { keys: Object.keys(body) });
4015
+ res.json({ ok: true });
4016
+ }
4017
+ catch (err) {
4018
+ res.status(500).json({ error: err.message ?? 'Failed to update preferences' });
4019
+ }
4020
+ });
4021
+ // --- Feedback insights ---
4022
+ router.get('/feedback/insights', (req, res) => {
4023
+ if (!guard(req, res))
4024
+ return;
4025
+ try {
4026
+ const insights = this.architect.getFeedbackInsights();
4027
+ res.json(insights);
4028
+ }
4029
+ catch (err) {
4030
+ res.status(500).json({ error: err.message ?? 'Failed to get feedback insights' });
4031
+ }
4032
+ });
4033
+ // --- User model ---
4034
+ router.get('/user-model', (_req, res) => {
4035
+ if (!guard(_req, res))
4036
+ return;
4037
+ const model = this.getCachedUserModel();
4038
+ if (!model) {
4039
+ res.status(404).json({ error: 'User model not available' });
4040
+ return;
4041
+ }
4042
+ res.json(model);
4043
+ });
4044
+ // --- Corrections (Gap 8) ---
4045
+ router.post('/corrections', async (req, res) => {
4046
+ if (!guard(req, res))
4047
+ return;
4048
+ const { userMessage, detectedDomain, correctedDomain } = req.body ?? {};
4049
+ if (!userMessage || !detectedDomain || !correctedDomain) {
4050
+ res.status(400).json({ error: 'Missing required fields: userMessage, detectedDomain, correctedDomain' });
4051
+ return;
4052
+ }
4053
+ try {
4054
+ await this.architect.recordCorrection(userMessage, detectedDomain, correctedDomain);
4055
+ audit('personality.correction', { detectedDomain, correctedDomain });
4056
+ res.status(201).json({ ok: true });
4057
+ }
4058
+ catch (err) {
4059
+ res.status(500).json({ error: err.message ?? 'Failed to record correction' });
4060
+ }
4061
+ });
4062
+ router.get('/corrections/stats', (req, res) => {
4063
+ if (!guard(req, res))
4064
+ return;
4065
+ try {
4066
+ const stats = this.architect.getCorrectionStats();
4067
+ res.json(stats);
4068
+ }
4069
+ catch (err) {
4070
+ res.status(500).json({ error: err.message ?? 'Failed to get correction stats' });
4071
+ }
4072
+ });
4073
+ // --- Data portability (Gap 10) ---
4074
+ router.get('/export', async (req, res) => {
4075
+ if (!guard(req, res))
4076
+ return;
4077
+ try {
4078
+ const data = await this.architect.exportData();
4079
+ audit('personality.data.exported', {});
4080
+ res.set('Content-Type', 'application/json');
4081
+ res.send(data);
4082
+ }
4083
+ catch (err) {
4084
+ res.status(500).json({ error: err.message ?? 'Failed to export data' });
4085
+ }
4086
+ });
4087
+ router.delete('/data', async (req, res) => {
4088
+ if (!guard(req, res))
4089
+ return;
4090
+ try {
4091
+ await this.architect.clearAllData();
4092
+ audit('personality.data.cleared', {});
4093
+ res.json({ ok: true });
4094
+ }
4095
+ catch (err) {
4096
+ res.status(500).json({ error: err.message ?? 'Failed to clear data' });
4097
+ }
4098
+ });
4099
+ // --- Conversation export (Gap 9 / Task 7) ---
4100
+ router.get('/sessions/:sessionId/export', (req, res) => {
4101
+ if (!guard(req, res))
4102
+ return;
4103
+ const format = req.query.format || 'json';
4104
+ if (!['json', 'markdown', 'csv'].includes(format)) {
4105
+ res.status(400).json({ error: 'Invalid format. Must be json, markdown, or csv' });
4106
+ return;
4107
+ }
4108
+ try {
4109
+ const msgs = this.sessions.getMessages(req.params.sessionId);
4110
+ const chatMessages = msgs
4111
+ .filter((m) => m.role === 'user' || m.role === 'assistant')
4112
+ .map((m) => ({
4113
+ role: m.role,
4114
+ content: m.content,
4115
+ timestamp: m.timestamp,
4116
+ metadata: m.metadata,
4117
+ }));
4118
+ const exported = this.architect.exportConversationAs(chatMessages, req.params.sessionId, format);
4119
+ if (format === 'json') {
4120
+ res.set('Content-Type', 'application/json');
4121
+ }
4122
+ else if (format === 'markdown') {
4123
+ res.set('Content-Type', 'text/markdown');
4124
+ }
4125
+ else {
4126
+ res.set('Content-Type', 'text/csv');
4127
+ }
4128
+ res.send(exported);
4129
+ }
4130
+ catch (err) {
4131
+ res.status(500).json({ error: err.message ?? 'Failed to export conversation' });
4132
+ }
4133
+ });
4134
+ // --- Feedback REST (Task 9) ---
4135
+ router.post('/sessions/:sessionId/messages/:messageId/feedback', async (req, res) => {
4136
+ if (!guard(req, res))
4137
+ return;
4138
+ const { rating, note } = req.body ?? {};
4139
+ if (!rating || !['up', 'down'].includes(rating)) {
4140
+ res.status(400).json({ error: 'Missing or invalid rating. Must be "up" or "down"' });
4141
+ return;
4142
+ }
4143
+ try {
4144
+ let domain = 'general';
4145
+ const msgs = this.sessions.getMessages(req.params.sessionId);
4146
+ const msg = msgs.find((m) => m.id === req.params.messageId);
4147
+ if (msg?.metadata?.architectDomain) {
4148
+ domain = msg.metadata.architectDomain;
4149
+ }
4150
+ const mapped = rating === 'up' ? 'helpful' : 'off_target';
4151
+ await this.architect.recordFeedback({
4152
+ domain: domain,
4153
+ rating: mapped,
4154
+ note,
4155
+ });
4156
+ audit('personality.feedback', {
4157
+ sessionId: req.params.sessionId,
4158
+ messageId: req.params.messageId,
4159
+ rating,
4160
+ });
4161
+ res.status(201).json({ ok: true });
4162
+ }
4163
+ catch (err) {
4164
+ res.status(500).json({ error: err.message ?? 'Failed to record feedback' });
4165
+ }
4166
+ });
4167
+ return router;
4168
+ }
4169
+ async runResearchJob(job, client) {
4170
+ const onProgress = (event) => {
4171
+ job.progress.push(event);
4172
+ this.sendToClient(client, { type: 'research_progress', payload: { jobId: job.id, ...event } });
4173
+ };
4174
+ const provider = this.providers.getPrimaryProvider();
4175
+ // NOTE: DeepResearchOrchestrator does not currently accept a DocumentStore parameter.
4176
+ // When it gains that support, pass this.documentStore here.
4177
+ const orchestrator = new DeepResearchOrchestrator(provider, undefined, this.researchEngine);
4178
+ const result = await orchestrator.research(job.question, job.depth, onProgress);
4179
+ if (job.depth === 'deep') {
4180
+ const reportGen = new ReportGenerator(provider);
4181
+ job.report = await reportGen.generateReport({
4182
+ ...result,
4183
+ question: job.question,
4184
+ depth: job.depth,
4185
+ });
4186
+ }
4187
+ job.status = 'completed';
4188
+ job.completedAt = Date.now();
4189
+ await audit('research.completed', {
4190
+ jobId: job.id,
4191
+ sourceCount: result.sources.length,
4192
+ duration: job.completedAt - job.createdAt,
4193
+ });
4194
+ this.sendToClient(client, { type: 'research_completed', payload: { jobId: job.id } });
4195
+ }
4196
+ createResearchRouter() {
4197
+ const router = Router();
4198
+ const self = this;
4199
+ router.post('/', (req, res) => {
4200
+ const { question, depth = 'deep' } = req.body;
4201
+ if (!question || typeof question !== 'string') {
4202
+ return res.status(400).json({ error: 'question required' });
4203
+ }
4204
+ const job = {
4205
+ id: crypto.randomUUID(),
4206
+ question,
4207
+ depth,
4208
+ status: 'planning',
4209
+ createdAt: Date.now(),
4210
+ progress: [],
4211
+ };
4212
+ self.researchJobs.set(job.id, job);
4213
+ audit('research.started', { jobId: job.id, question: job.question, depth: job.depth });
4214
+ res.status(202).json({ jobId: job.id, status: job.status });
4215
+ });
4216
+ router.get('/', (_req, res) => {
4217
+ const limit = Number(_req.query.limit) || 20;
4218
+ const offset = Number(_req.query.offset) || 0;
4219
+ const all = [...self.researchJobs.values()].sort((a, b) => b.createdAt - a.createdAt);
4220
+ res.json({ jobs: all.slice(offset, offset + limit), total: all.length });
4221
+ });
4222
+ router.get('/:jobId', (req, res) => {
4223
+ const job = self.researchJobs.get(req.params.jobId);
4224
+ if (!job)
4225
+ return res.status(404).json({ error: 'not found' });
4226
+ res.json(job);
4227
+ });
4228
+ router.delete('/:jobId', (req, res) => {
4229
+ const job = self.researchJobs.get(req.params.jobId);
4230
+ if (!job)
4231
+ return res.status(404).json({ error: 'not found' });
4232
+ if (job.status === 'completed' || job.status === 'failed') {
4233
+ return res.status(409).json({ error: 'job already finished' });
4234
+ }
4235
+ job.status = 'cancelled';
4236
+ audit('research.cancelled', { jobId: job.id });
4237
+ res.json({ jobId: job.id, status: 'cancelled' });
4238
+ });
4239
+ router.get('/:jobId/sources', (req, res) => {
4240
+ const job = self.researchJobs.get(req.params.jobId);
4241
+ if (!job)
4242
+ return res.status(404).json({ error: 'not found' });
4243
+ res.json({ sources: job.report?.sources ?? [] });
4244
+ });
4245
+ return router;
4246
+ }
4247
+ createAmbientRouter() {
4248
+ const router = Router();
4249
+ const self = this;
4250
+ function guard(res) {
4251
+ if (!self.ambientEngine || !self.ambientNotifications) {
4252
+ res.status(503).json({ error: 'Ambient system not available' });
4253
+ return false;
4254
+ }
4255
+ return true;
4256
+ }
4257
+ // Pattern management
4258
+ router.get('/patterns', (_req, res) => {
4259
+ if (!guard(res))
4260
+ return;
4261
+ res.json({ patterns: self.ambientEngine.getPatterns() });
4262
+ });
4263
+ router.get('/patterns/:id', (req, res) => {
4264
+ if (!guard(res))
4265
+ return;
4266
+ const pattern = self.ambientEngine.getPattern(req.params.id);
4267
+ if (!pattern)
4268
+ return res.status(404).json({ error: 'Pattern not found' });
4269
+ res.json(pattern);
4270
+ });
4271
+ router.post('/patterns/detect', async (_req, res) => {
4272
+ if (!guard(res))
4273
+ return;
4274
+ const detected = self.ambientEngine.detectPatterns();
4275
+ await audit('ambient.patterns.detected', { count: detected.length });
4276
+ res.json({ detected: detected.length });
4277
+ });
4278
+ router.delete('/patterns', async (_req, res) => {
4279
+ if (!guard(res))
4280
+ return;
4281
+ self.ambientEngine.reset();
4282
+ await audit('ambient.patterns.reset', {});
4283
+ res.json({ ok: true });
4284
+ });
4285
+ // Anticipations
4286
+ router.get('/anticipations', (_req, res) => {
4287
+ if (!self.anticipationEngine)
4288
+ return res.status(503).json({ error: 'Anticipation engine not available' });
4289
+ res.json({ anticipations: self.anticipationEngine.getAnticipations() });
4290
+ });
4291
+ // Notifications
4292
+ router.get('/notifications', (req, res) => {
4293
+ if (!guard(res))
4294
+ return;
4295
+ const priority = req.query.priority;
4296
+ const items = priority
4297
+ ? self.ambientNotifications.getByPriority(priority)
4298
+ : self.ambientNotifications.getQueue();
4299
+ res.json({ notifications: items });
4300
+ });
4301
+ router.post('/notifications/:id/dismiss', (req, res) => {
4302
+ if (!guard(res))
4303
+ return;
4304
+ const ok = self.ambientNotifications.dismiss(req.params.id);
4305
+ if (!ok)
4306
+ return res.status(404).json({ error: 'Notification not found' });
4307
+ res.json({ ok: true });
4308
+ });
4309
+ router.get('/notifications/stats', (_req, res) => {
4310
+ if (!guard(res))
4311
+ return;
4312
+ res.json({ pending: self.ambientNotifications.getPendingCount() });
4313
+ });
4314
+ // Scheduler control
4315
+ router.get('/scheduler/status', (_req, res) => {
4316
+ if (!self.ambientScheduler)
4317
+ return res.status(503).json({ error: 'Scheduler not available' });
4318
+ res.json({ running: self.ambientScheduler.isRunning(), config: self.ambientScheduler.getConfig() });
4319
+ });
4320
+ router.post('/scheduler/start', async (_req, res) => {
4321
+ if (!self.ambientScheduler)
4322
+ return res.status(503).json({ error: 'Scheduler not available' });
4323
+ self.ambientScheduler.start();
4324
+ await audit('ambient.scheduler.started', {});
4325
+ res.json({ ok: true });
4326
+ });
4327
+ router.post('/scheduler/stop', async (_req, res) => {
4328
+ if (!self.ambientScheduler)
4329
+ return res.status(503).json({ error: 'Scheduler not available' });
4330
+ self.ambientScheduler.stop();
4331
+ await audit('ambient.scheduler.stopped', {});
4332
+ res.json({ ok: true });
4333
+ });
4334
+ router.put('/scheduler/config', (_req, res) => {
4335
+ if (!self.ambientScheduler)
4336
+ return res.status(503).json({ error: 'Scheduler not available' });
4337
+ res.json({ config: self.ambientScheduler.getConfig() });
4338
+ });
4339
+ return router;
4340
+ }
4341
+ createVoiceRouter() {
4342
+ const router = Router();
4343
+ router.get('/status', (_req, res) => {
4344
+ try {
4345
+ if (!this.voiceManager) {
4346
+ return res.json({ enabled: false });
4347
+ }
4348
+ res.json({ enabled: true });
4349
+ }
4350
+ catch (err) {
4351
+ res.status(500).json({ error: err.message });
4352
+ }
4353
+ });
4354
+ router.get('/sessions/:clientId', (req, res) => {
4355
+ try {
4356
+ if (!this.voiceManager) {
4357
+ return res.status(503).json({ error: 'Voice not initialized' });
4358
+ }
4359
+ const active = this.voiceManager.hasActiveSession(req.params.clientId);
4360
+ res.json({ active });
4361
+ }
4362
+ catch (err) {
4363
+ res.status(500).json({ error: err.message });
4364
+ }
4365
+ });
4366
+ return router;
4367
+ }
4368
+ createWebhooksRouter() {
4369
+ const router = Router();
4370
+ router.get('/', async (_req, res) => {
4371
+ if (!this.webhookManager)
4372
+ return res.status(503).json({ error: 'Webhooks not configured' });
4373
+ try {
4374
+ const webhooks = await this.webhookManager.list();
4375
+ res.json({ webhooks });
4376
+ }
4377
+ catch (err) {
4378
+ res.status(500).json({ error: err.message });
4379
+ }
4380
+ });
4381
+ router.post('/', async (req, res) => {
4382
+ if (!this.webhookManager)
4383
+ return res.status(503).json({ error: 'Webhooks not configured' });
4384
+ try {
4385
+ const webhook = await this.webhookManager.create(req.body);
4386
+ res.json(webhook);
4387
+ }
4388
+ catch (err) {
4389
+ res.status(500).json({ error: err.message });
4390
+ }
4391
+ });
4392
+ router.put('/:id', async (req, res) => {
4393
+ if (!this.webhookManager)
4394
+ return res.status(503).json({ error: 'Webhooks not configured' });
4395
+ try {
4396
+ const updated = await this.webhookManager.update(req.params.id, req.body);
4397
+ if (!updated)
4398
+ return res.status(404).json({ error: 'Webhook not found' });
4399
+ res.json(updated);
4400
+ }
4401
+ catch (err) {
4402
+ res.status(500).json({ error: err.message });
4403
+ }
4404
+ });
4405
+ router.delete('/:id', async (req, res) => {
4406
+ if (!this.webhookManager)
4407
+ return res.status(503).json({ error: 'Webhooks not configured' });
4408
+ try {
4409
+ const deleted = await this.webhookManager.delete(req.params.id);
4410
+ if (!deleted)
4411
+ return res.status(404).json({ error: 'Webhook not found' });
4412
+ res.json({ deleted: true });
4413
+ }
4414
+ catch (err) {
4415
+ res.status(500).json({ error: err.message });
4416
+ }
4417
+ });
4418
+ return router;
4419
+ }
4420
+ createConsciousnessRouter() {
4421
+ const router = Router();
4422
+ const self = this;
4423
+ router.get('/pulse', (_req, res) => {
4424
+ if (!self.consciousness)
4425
+ return res.status(503).json({ error: 'Consciousness not initialized' });
4426
+ try {
4427
+ const pulse = self.consciousness.monitor.getPulse();
4428
+ res.json(pulse);
4429
+ }
4430
+ catch (err) {
4431
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
4432
+ }
4433
+ });
4434
+ router.get('/self-model', async (_req, res) => {
4435
+ if (!self.consciousness)
4436
+ return res.status(503).json({ error: 'Consciousness not initialized' });
4437
+ try {
4438
+ const snapshot = await self.consciousness.model.synthesize();
4439
+ res.json(snapshot);
4440
+ }
4441
+ catch (err) {
4442
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
4443
+ }
4444
+ });
4445
+ router.get('/journal/sessions', async (req, res) => {
4446
+ if (!self.consciousness)
4447
+ return res.status(503).json({ error: 'Consciousness not initialized' });
4448
+ try {
4449
+ const limit = Number(req.query.limit) || 10;
4450
+ const sessions = await self.consciousness.journal.getRecentSessions(limit);
4451
+ res.json(sessions);
4452
+ }
4453
+ catch (err) {
4454
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
4455
+ }
4456
+ });
4457
+ router.get('/journal/sessions/:sessionId', async (req, res) => {
4458
+ if (!self.consciousness)
4459
+ return res.status(503).json({ error: 'Consciousness not initialized' });
4460
+ try {
4461
+ const session = await self.consciousness.journal.getSession(req.params.sessionId);
4462
+ res.json(session);
4463
+ }
4464
+ catch (err) {
4465
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
4466
+ }
4467
+ });
4468
+ router.get('/repairs', async (req, res) => {
4469
+ if (!self.consciousness)
4470
+ return res.status(503).json({ error: 'Consciousness not initialized' });
4471
+ try {
4472
+ const limit = Number(req.query.limit) || 20;
4473
+ const history = await self.consciousness.repair.getRepairHistory(limit);
4474
+ res.json(history);
4475
+ }
4476
+ catch (err) {
4477
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
4478
+ }
4479
+ });
4480
+ router.get('/repairs/pending', async (_req, res) => {
4481
+ if (!self.consciousness)
4482
+ return res.status(503).json({ error: 'Consciousness not initialized' });
4483
+ try {
4484
+ const pending = await self.consciousness.repair.getPendingApprovals();
4485
+ res.json(pending);
4486
+ }
4487
+ catch (err) {
4488
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
4489
+ }
4490
+ });
4491
+ return router;
4492
+ }
4493
+ createAgentProtocolRouter() {
4494
+ const router = Router();
4495
+ router.get('/identity', (_req, res) => {
4496
+ if (!this.agentProtocol || !this.agentDirectory) {
4497
+ return res.status(503).json({ error: 'Agent protocol not initialized' });
4498
+ }
4499
+ try {
4500
+ const identity = this.agentProtocol.getIdentity();
4501
+ res.json(identity);
4502
+ }
4503
+ catch (err) {
4504
+ res.status(500).json({ error: err.message });
4505
+ }
4506
+ });
4507
+ router.get('/inbox', (_req, res) => {
4508
+ if (!this.agentProtocol || !this.agentDirectory) {
4509
+ return res.status(503).json({ error: 'Agent protocol not initialized' });
4510
+ }
4511
+ try {
4512
+ const limit = _req.query.limit ? parseInt(_req.query.limit, 10) : 50;
4513
+ const messages = this.agentProtocol.getInbox(limit);
4514
+ res.json({ messages });
4515
+ }
4516
+ catch (err) {
4517
+ res.status(500).json({ error: err.message });
4518
+ }
4519
+ });
4520
+ router.post('/messages', async (req, res) => {
4521
+ if (!this.agentProtocol || !this.agentDirectory) {
4522
+ return res.status(503).json({ error: 'Agent protocol not initialized' });
4523
+ }
4524
+ try {
4525
+ const { to, type, payload, replyTo } = req.body;
4526
+ const message = await this.agentProtocol.send(to, type, payload, replyTo);
4527
+ res.json(message);
4528
+ }
4529
+ catch (err) {
4530
+ res.status(500).json({ error: err.message });
4531
+ }
4532
+ });
4533
+ router.post('/receive', async (req, res) => {
4534
+ if (!this.agentProtocol || !this.agentDirectory) {
4535
+ return res.status(503).json({ error: 'Agent protocol not initialized' });
4536
+ }
4537
+ try {
4538
+ const response = await this.agentProtocol.receive(req.body);
4539
+ res.json(response ?? { accepted: true });
4540
+ }
4541
+ catch (err) {
4542
+ res.status(500).json({ error: err.message });
4543
+ }
4544
+ });
4545
+ router.get('/directory', async (_req, res) => {
4546
+ if (!this.agentProtocol || !this.agentDirectory) {
4547
+ return res.status(503).json({ error: 'Agent protocol not initialized' });
4548
+ }
4549
+ try {
4550
+ const agents = await this.agentDirectory.listAll();
4551
+ res.json({ agents });
4552
+ }
4553
+ catch (err) {
4554
+ res.status(500).json({ error: err.message });
4555
+ }
4556
+ });
4557
+ router.get('/directory/search', async (req, res) => {
4558
+ if (!this.agentProtocol || !this.agentDirectory) {
4559
+ return res.status(503).json({ error: 'Agent protocol not initialized' });
4560
+ }
4561
+ try {
4562
+ const results = await this.agentDirectory.search(req.query.q);
4563
+ res.json({ results });
4564
+ }
4565
+ catch (err) {
4566
+ res.status(500).json({ error: err.message });
4567
+ }
4568
+ });
4569
+ return router;
4570
+ }
4571
+ createTrustRouter() {
4572
+ const router = Router();
4573
+ const self = this;
4574
+ router.get('/levels', (_req, res) => {
4575
+ if (!self.trustEngine)
4576
+ return res.status(503).json({ error: 'Trust engine not initialized' });
4577
+ try {
4578
+ const levels = self.trustEngine.getAllLevels();
4579
+ res.json({ levels });
4580
+ }
4581
+ catch (err) {
4582
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
4583
+ }
4584
+ });
4585
+ router.get('/levels/:domain', (_req, res) => {
4586
+ if (!self.trustEngine)
4587
+ return res.status(503).json({ error: 'Trust engine not initialized' });
4588
+ try {
4589
+ const domain = _req.params.domain;
4590
+ const level = self.trustEngine.getTrustLevel(domain);
4591
+ const evidence = self.trustEngine.getEvidence(domain);
4592
+ res.json({ domain, level, evidence });
4593
+ }
4594
+ catch (err) {
4595
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
4596
+ }
4597
+ });
4598
+ router.put('/levels/:domain', async (req, res) => {
4599
+ if (!self.trustEngine)
4600
+ return res.status(503).json({ error: 'Trust engine not initialized' });
4601
+ try {
4602
+ const domain = req.params.domain;
4603
+ const { level, reason } = req.body;
4604
+ await self.trustEngine.setTrustLevel(domain, level, reason);
4605
+ res.json({ success: true });
4606
+ }
4607
+ catch (err) {
4608
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
4609
+ }
4610
+ });
4611
+ router.get('/audit', (_req, res) => {
4612
+ if (!self.trustEngine)
4613
+ return res.status(503).json({ error: 'Trust engine not initialized' });
4614
+ try {
4615
+ const history = self.trustAuditTrail ? self.trustAuditTrail.getAll() : [];
4616
+ res.json({ history });
4617
+ }
4618
+ catch (err) {
4619
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
4620
+ }
4621
+ });
4622
+ return router;
4623
+ }
4624
+ createWorkflowRouter() {
4625
+ const router = Router();
4626
+ const self = this;
4627
+ // Static routes MUST come before parameterized /:id routes
4628
+ // GET / — list all workflows
4629
+ router.get('/', async (_req, res) => {
4630
+ if (!self.workflowEngine)
4631
+ return res.status(503).json({ error: 'Workflow engine not initialized' });
4632
+ try {
4633
+ const workflows = await self.workflowEngine.listAll();
4634
+ res.json({ workflows });
4635
+ }
4636
+ catch (err) {
4637
+ res.status(500).json({ error: err.message });
4638
+ }
4639
+ });
4640
+ // POST / — create workflow
4641
+ router.post('/', async (req, res) => {
4642
+ if (!self.workflowEngine)
4643
+ return res.status(503).json({ error: 'Workflow engine not initialized' });
4644
+ try {
4645
+ const workflow = await self.workflowEngine.createWorkflow(req.body);
4646
+ res.json(workflow);
4647
+ }
4648
+ catch (err) {
4649
+ res.status(500).json({ error: err.message });
4650
+ }
4651
+ });
4652
+ // GET /active — list active workflows
4653
+ router.get('/active', async (_req, res) => {
4654
+ if (!self.workflowEngine)
4655
+ return res.status(503).json({ error: 'Workflow engine not initialized' });
4656
+ try {
4657
+ const workflows = await self.workflowEngine.listActive();
4658
+ res.json({ workflows });
4659
+ }
4660
+ catch (err) {
4661
+ res.status(500).json({ error: err.message });
4662
+ }
4663
+ });
4664
+ // GET /approvals/pending — list pending approvals
4665
+ router.get('/approvals/pending', async (req, res) => {
4666
+ if (!self.approvalManager)
4667
+ return res.status(503).json({ error: 'Workflow engine not initialized' });
4668
+ try {
4669
+ const approvals = await self.approvalManager.getPending(req.query.userId);
4670
+ res.json({ approvals });
4671
+ }
4672
+ catch (err) {
4673
+ res.status(500).json({ error: err.message });
4674
+ }
4675
+ });
4676
+ // POST /approvals/:id/approve
4677
+ router.post('/approvals/:id/approve', async (req, res) => {
4678
+ if (!self.approvalManager)
4679
+ return res.status(503).json({ error: 'Workflow engine not initialized' });
4680
+ try {
4681
+ const approval = await self.approvalManager.approve(req.params.id, req.body.decidedBy, req.body.reason);
4682
+ if (!approval)
4683
+ return res.status(404).json({ error: 'Approval not found' });
4684
+ res.json(approval);
4685
+ }
4686
+ catch (err) {
4687
+ res.status(500).json({ error: err.message });
4688
+ }
4689
+ });
4690
+ // POST /approvals/:id/reject
4691
+ router.post('/approvals/:id/reject', async (req, res) => {
4692
+ if (!self.approvalManager)
4693
+ return res.status(503).json({ error: 'Workflow engine not initialized' });
4694
+ try {
4695
+ const rejection = await self.approvalManager.reject(req.params.id, req.body.decidedBy, req.body.reason);
4696
+ if (!rejection)
4697
+ return res.status(404).json({ error: 'Approval not found' });
4698
+ res.json(rejection);
4699
+ }
4700
+ catch (err) {
4701
+ res.status(500).json({ error: err.message });
4702
+ }
4703
+ });
4704
+ // GET /:id — get workflow by ID
4705
+ router.get('/:id', async (req, res) => {
4706
+ if (!self.workflowEngine)
4707
+ return res.status(503).json({ error: 'Workflow engine not initialized' });
4708
+ try {
4709
+ const workflow = await self.workflowEngine.getWorkflow(req.params.id);
4710
+ if (!workflow)
4711
+ return res.status(404).json({ error: 'Workflow not found' });
4712
+ res.json(workflow);
4713
+ }
4714
+ catch (err) {
4715
+ res.status(500).json({ error: err.message });
4716
+ }
4717
+ });
4718
+ // GET /:id/status — get workflow status
4719
+ router.get('/:id/status', async (req, res) => {
4720
+ if (!self.workflowEngine)
4721
+ return res.status(503).json({ error: 'Workflow engine not initialized' });
4722
+ try {
4723
+ const status = await self.workflowEngine.getStatus(req.params.id);
4724
+ if (!status)
4725
+ return res.status(404).json({ error: 'Workflow not found' });
4726
+ res.json(status);
4727
+ }
4728
+ catch (err) {
4729
+ res.status(500).json({ error: err.message });
4730
+ }
4731
+ });
4732
+ // POST /:id/start — start workflow
4733
+ router.post('/:id/start', async (req, res) => {
4734
+ if (!self.workflowEngine)
4735
+ return res.status(503).json({ error: 'Workflow engine not initialized' });
4736
+ try {
4737
+ const workflow = await self.workflowEngine.startWorkflow(req.params.id);
4738
+ if (!workflow)
4739
+ return res.status(404).json({ error: 'Workflow not found' });
4740
+ res.json(workflow);
4741
+ }
4742
+ catch (err) {
4743
+ res.status(500).json({ error: err.message });
4744
+ }
4745
+ });
4746
+ // POST /:id/cancel — cancel workflow
4747
+ router.post('/:id/cancel', async (req, res) => {
4748
+ if (!self.workflowEngine)
4749
+ return res.status(503).json({ error: 'Workflow engine not initialized' });
4750
+ try {
4751
+ const result = await self.workflowEngine.cancelWorkflow(req.params.id);
4752
+ if (!result)
4753
+ return res.status(404).json({ error: 'Workflow not found' });
4754
+ res.json({ cancelled: true });
4755
+ }
4756
+ catch (err) {
4757
+ res.status(500).json({ error: err.message });
4758
+ }
4759
+ });
4760
+ return router;
4761
+ }
4762
+ createUpdateRouter() {
4763
+ const router = Router();
4764
+ const self = this;
4765
+ // GET /status — installation info + current version
4766
+ router.get('/status', async (_req, res) => {
4767
+ if (!self.installationDetector || !self.versionChecker) {
4768
+ return res.status(503).json({ error: 'Update system not initialized' });
4769
+ }
4770
+ try {
4771
+ const info = self.installationDetector.detect();
4772
+ res.json({
4773
+ method: info.method,
4774
+ currentVersion: info.currentVersion,
4775
+ installPath: info.installPath,
4776
+ canSelfUpdate: info.canSelfUpdate,
4777
+ requiresSudo: info.requiresSudo,
4778
+ });
4779
+ }
4780
+ catch (err) {
4781
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
4782
+ }
4783
+ });
4784
+ // POST /check — check for available updates
4785
+ router.post('/check', async (req, res) => {
4786
+ if (!self.installationDetector || !self.versionChecker) {
4787
+ return res.status(503).json({ error: 'Update system not initialized' });
4788
+ }
4789
+ try {
4790
+ const channel = (req.body?.channel ?? 'stable');
4791
+ const info = self.installationDetector.detect();
4792
+ const result = await self.versionChecker.check(info.currentVersion, channel);
4793
+ res.json(result);
4794
+ }
4795
+ catch (err) {
4796
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
4797
+ }
4798
+ });
4799
+ // POST /apply — trigger an update
4800
+ router.post('/apply', async (req, res) => {
4801
+ if (!self.updater) {
4802
+ return res.status(503).json({ error: 'Update system not initialized' });
4803
+ }
4804
+ try {
4805
+ const channel = (req.body?.channel ?? 'stable');
4806
+ const result = await self.updater.update(channel);
4807
+ res.json(result);
4808
+ }
4809
+ catch (err) {
4810
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
4811
+ }
4812
+ });
4813
+ // POST /rollback — rollback a staged update
4814
+ router.post('/rollback', async (_req, res) => {
4815
+ if (!self.updater) {
4816
+ return res.status(503).json({ error: 'Update system not initialized' });
4817
+ }
4818
+ try {
4819
+ await self.updater.rollback();
4820
+ res.json({ success: true });
4821
+ }
4822
+ catch (err) {
4823
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
4824
+ }
4825
+ });
4826
+ return router;
4827
+ }
4828
+ createConnectorRouter() {
4829
+ const router = Router();
4830
+ // GET / — list all connectors
4831
+ router.get('/', (_req, res) => {
4832
+ if (!this.connectorRegistry)
4833
+ return res.status(503).json({ error: 'Connectors not configured' });
4834
+ try {
4835
+ res.json({ connectors: this.connectorRegistry.list() });
4836
+ }
4837
+ catch (err) {
4838
+ res.status(500).json({ error: err.message });
4839
+ }
4840
+ });
4841
+ // GET /:id — get connector by id
4842
+ router.get('/:id', (req, res) => {
4843
+ if (!this.connectorRegistry)
4844
+ return res.status(503).json({ error: 'Connectors not configured' });
4845
+ try {
4846
+ const connector = this.connectorRegistry.get(req.params.id);
4847
+ if (!connector)
4848
+ return res.status(404).json({ error: 'Connector not found' });
4849
+ res.json(connector);
4850
+ }
4851
+ catch (err) {
4852
+ res.status(500).json({ error: err.message });
4853
+ }
4854
+ });
4855
+ // GET /:id/actions — get actions for connector
4856
+ router.get('/:id/actions', (req, res) => {
4857
+ if (!this.connectorRegistry)
4858
+ return res.status(503).json({ error: 'Connectors not configured' });
4859
+ try {
4860
+ if (!this.connectorRegistry.has(req.params.id))
4861
+ return res.status(404).json({ error: 'Connector not found' });
4862
+ const actions = this.connectorRegistry.getActions(req.params.id);
4863
+ res.json({ actions });
4864
+ }
4865
+ catch (err) {
4866
+ res.status(500).json({ error: err.message });
4867
+ }
4868
+ });
4869
+ // GET /:id/triggers — get triggers for connector
4870
+ router.get('/:id/triggers', (req, res) => {
4871
+ if (!this.connectorRegistry)
4872
+ return res.status(503).json({ error: 'Connectors not configured' });
4873
+ try {
4874
+ if (!this.connectorRegistry.has(req.params.id))
4875
+ return res.status(404).json({ error: 'Connector not found' });
4876
+ const triggers = this.connectorRegistry.getTriggers(req.params.id);
4877
+ res.json({ triggers });
4878
+ }
4879
+ catch (err) {
4880
+ res.status(500).json({ error: err.message });
4881
+ }
4882
+ });
4883
+ // POST /:id/authenticate — authenticate connector
4884
+ router.post('/:id/authenticate', async (req, res) => {
4885
+ if (!this.connectorRegistry)
4886
+ return res.status(503).json({ error: 'Connectors not configured' });
4887
+ if (!this.connectorAuthManager)
4888
+ return res.status(503).json({ error: 'Auth manager not configured' });
4889
+ try {
4890
+ if (!this.connectorRegistry.has(req.params.id))
4891
+ return res.status(404).json({ error: 'Connector not found' });
4892
+ await this.connectorAuthManager.authenticate(req.params.id, req.params.id, req.body.credentials);
4893
+ res.json({ authenticated: true });
4894
+ }
4895
+ catch (err) {
4896
+ res.status(500).json({ error: err.message });
4897
+ }
4898
+ });
4899
+ // POST /:id/disconnect — revoke connector token
4900
+ router.post('/:id/disconnect', async (req, res) => {
4901
+ if (!this.connectorRegistry)
4902
+ return res.status(503).json({ error: 'Connectors not configured' });
4903
+ if (!this.connectorAuthManager)
4904
+ return res.status(503).json({ error: 'Auth manager not configured' });
4905
+ try {
4906
+ if (!this.connectorRegistry.has(req.params.id))
4907
+ return res.status(404).json({ error: 'Connector not found' });
4908
+ await this.connectorAuthManager.revokeToken(req.params.id);
4909
+ res.json({ disconnected: true });
4910
+ }
4911
+ catch (err) {
4912
+ res.status(500).json({ error: err.message });
4913
+ }
4914
+ });
4915
+ // GET /:id/status — get connector auth status
4916
+ router.get('/:id/status', (req, res) => {
4917
+ if (!this.connectorRegistry)
4918
+ return res.status(503).json({ error: 'Connectors not configured' });
4919
+ if (!this.connectorAuthManager)
4920
+ return res.status(503).json({ error: 'Auth manager not configured' });
4921
+ try {
4922
+ if (!this.connectorRegistry.has(req.params.id))
4923
+ return res.status(404).json({ error: 'Connector not found' });
4924
+ res.json({
4925
+ connected: this.connectorAuthManager.hasToken(req.params.id),
4926
+ expired: this.connectorAuthManager.isTokenExpired(req.params.id),
4927
+ });
4928
+ }
4929
+ catch (err) {
4930
+ res.status(500).json({ error: err.message });
4931
+ }
4932
+ });
4933
+ return router;
4934
+ }
4935
+ createImageRouter() {
4936
+ const router = Router();
4937
+ router.get('/providers', (_req, res) => {
4938
+ const providers = this.imageGenManager.listProviders();
4939
+ const details = providers.map((name) => {
4940
+ const p = this.imageGenManager.getProvider(name);
4941
+ return {
4942
+ name,
4943
+ supportedSizes: p?.supportedSizes ?? [],
4944
+ supportedFormats: p?.supportedFormats ?? [],
4945
+ defaultModel: p?.defaultModel ?? '',
4946
+ };
4947
+ });
4948
+ res.json({ providers: details });
4949
+ });
4950
+ router.post('/generate', async (req, res) => {
4951
+ const { prompt, size, format, provider, negativePrompt, count, model, seed, style } = req.body;
4952
+ if (!prompt || typeof prompt !== 'string') {
4953
+ return res.status(400).json({ error: 'prompt required' });
4954
+ }
4955
+ const request = {
4956
+ prompt,
4957
+ ...(size && { size }),
4958
+ ...(format && { format }),
4959
+ ...(provider && { provider }),
4960
+ ...(negativePrompt && { negativePrompt }),
4961
+ ...(count && { count }),
4962
+ ...(model && { model }),
4963
+ ...(seed !== undefined && { seed }),
4964
+ ...(style && { style }),
4965
+ };
4966
+ const result = await this.imageGenManager.generate(request);
4967
+ if (!result.success) {
4968
+ return res.status(502).json({ error: result.error, durationMs: result.durationMs });
4969
+ }
4970
+ res.json(result);
4971
+ });
4972
+ return router;
4973
+ }
4974
+ createRagRouter() {
4975
+ const router = Router();
4976
+ router.get('/documents', (_req, res) => {
4977
+ res.json({ documents: this.documentStore.listDocuments() });
4978
+ });
4979
+ router.post('/documents', (req, res) => {
4980
+ const { title, content, type, metadata } = req.body;
4981
+ if (!title || typeof title !== 'string') {
4982
+ return res.status(400).json({ error: 'title required' });
4983
+ }
4984
+ if (!content || typeof content !== 'string') {
4985
+ return res.status(400).json({ error: 'content required' });
4986
+ }
4987
+ if (!type || typeof type !== 'string') {
4988
+ return res.status(400).json({ error: 'type required' });
4989
+ }
4990
+ const doc = this.documentStore.ingest(title, content, type, metadata);
4991
+ res.status(201).json(doc);
4992
+ });
4993
+ router.get('/documents/:id', (req, res) => {
4994
+ const doc = this.documentStore.getDocument(req.params.id);
4995
+ if (!doc)
4996
+ return res.status(404).json({ error: 'document not found' });
4997
+ res.json(doc);
4998
+ });
4999
+ router.delete('/documents/:id', (req, res) => {
5000
+ const doc = this.documentStore.getDocument(req.params.id);
5001
+ if (!doc)
5002
+ return res.status(404).json({ error: 'document not found' });
5003
+ this.documentStore.removeDocument(req.params.id);
5004
+ res.json({ deleted: true });
5005
+ });
5006
+ router.post('/search', (req, res) => {
5007
+ const { query, limit, minScore, type } = req.body;
5008
+ if (!query || typeof query !== 'string') {
5009
+ return res.status(400).json({ error: 'query required' });
5010
+ }
5011
+ const results = this.documentStore.search(query, { limit, minScore, type });
5012
+ res.json({ results });
5013
+ });
5014
+ router.post('/context', (req, res) => {
5015
+ const { query, maxTokens, maxChunks } = req.body;
5016
+ if (!query || typeof query !== 'string') {
5017
+ return res.status(400).json({ error: 'query required' });
5018
+ }
5019
+ const context = this.contextBuilder.buildContext(query, this.documentStore, { maxTokens, maxChunks });
5020
+ res.json({ context });
5021
+ });
5022
+ router.get('/stats', (_req, res) => {
5023
+ res.json(this.documentStore.stats());
5024
+ });
5025
+ return router;
5026
+ }
5027
+ createMcpRouter() {
5028
+ const router = Router();
5029
+ router.get('/servers', (_req, res) => {
5030
+ if (!this.mcpClientManager) {
5031
+ return res.json({ servers: {} });
5032
+ }
5033
+ const status = this.mcpClientManager.getStatus();
5034
+ const servers = {};
5035
+ for (const [name, s] of status) {
5036
+ servers[name] = s;
5037
+ }
5038
+ res.json({ servers });
5039
+ });
5040
+ router.post('/servers/:name/connect', async (req, res) => {
5041
+ if (!this.mcpClientManager) {
5042
+ return res.status(503).json({ error: 'MCP not configured' });
5043
+ }
5044
+ try {
5045
+ await this.mcpClientManager.connect(req.params.name);
5046
+ await audit('connector.connected', { connector: req.params.name, type: 'mcp' });
5047
+ res.json({ status: 'connected' });
5048
+ }
5049
+ catch (err) {
5050
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
5051
+ }
5052
+ });
5053
+ router.post('/servers/:name/disconnect', async (req, res) => {
5054
+ if (!this.mcpClientManager) {
5055
+ return res.status(503).json({ error: 'MCP not configured' });
5056
+ }
5057
+ try {
5058
+ await this.mcpClientManager.disconnect(req.params.name);
5059
+ await audit('connector.disconnected', { connector: req.params.name, type: 'mcp' });
5060
+ res.json({ status: 'disconnected' });
5061
+ }
5062
+ catch (err) {
5063
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
5064
+ }
5065
+ });
5066
+ router.get('/servers/:name/tools', (req, res) => {
5067
+ if (!this.mcpClientManager) {
5068
+ return res.status(503).json({ error: 'MCP not configured' });
5069
+ }
5070
+ const tools = this.mcpClientManager.getToolsForServer(req.params.name);
5071
+ res.json({ tools });
5072
+ });
5073
+ return router;
5074
+ }
5075
+ createEvalRouter() {
5076
+ const router = Router();
5077
+ // GET /history/:suiteName — returns suite run history
5078
+ router.get('/history/:suiteName', (_req, res) => {
5079
+ if (!this.evalStore) {
5080
+ return res.status(503).json({ error: 'Evaluation system not initialized' });
5081
+ }
5082
+ try {
5083
+ const history = this.evalStore.getHistory(_req.params.suiteName);
5084
+ res.json({ history });
5085
+ }
5086
+ catch (err) {
5087
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
5088
+ }
5089
+ });
5090
+ // GET /latest/:suiteName — returns latest suite result or 404
5091
+ router.get('/latest/:suiteName', (_req, res) => {
5092
+ if (!this.evalStore) {
5093
+ return res.status(503).json({ error: 'Evaluation system not initialized' });
5094
+ }
5095
+ try {
5096
+ const latest = this.evalStore.getLatest(_req.params.suiteName);
5097
+ if (!latest) {
5098
+ return res.status(404).json({ error: 'No results found for suite' });
5099
+ }
5100
+ res.json(latest);
5101
+ }
5102
+ catch (err) {
5103
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
5104
+ }
5105
+ });
5106
+ // GET /trend/:suiteName/:metricName — returns score trend for a metric
5107
+ router.get('/trend/:suiteName/:metricName', (_req, res) => {
5108
+ if (!this.evalStore) {
5109
+ return res.status(503).json({ error: 'Evaluation system not initialized' });
5110
+ }
5111
+ try {
5112
+ const trend = this.evalStore.getTrend(_req.params.suiteName, _req.params.metricName);
5113
+ res.json({ trend });
5114
+ }
5115
+ catch (err) {
5116
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
5117
+ }
5118
+ });
5119
+ // POST /run — run evaluation suite
5120
+ router.post('/run', async (req, res) => {
5121
+ if (!this.evalRunner || !this.evalStore) {
5122
+ return res.status(503).json({ error: 'Evaluation system not initialized' });
5123
+ }
5124
+ try {
5125
+ const { suiteName, cases, mode } = req.body;
5126
+ if (!suiteName || !cases || !Array.isArray(cases) || cases.length === 0) {
5127
+ return res.status(400).json({ error: 'suiteName and non-empty cases array required' });
5128
+ }
5129
+ const handler = mode === 'echo' || !mode
5130
+ ? async (input) => input
5131
+ : async (input) => input;
5132
+ const result = await this.evalRunner.runSuite(suiteName, cases, handler);
5133
+ this.evalStore.record(result);
5134
+ res.json(result);
5135
+ }
5136
+ catch (err) {
5137
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
5138
+ }
5139
+ });
5140
+ // POST /compare — compare latest results of two suites
5141
+ router.post('/compare', (_req, res) => {
5142
+ if (!this.evalRunner || !this.evalStore) {
5143
+ return res.status(503).json({ error: 'Evaluation system not initialized' });
5144
+ }
5145
+ try {
5146
+ const { suiteNameA, suiteNameB } = _req.body;
5147
+ if (!suiteNameA || !suiteNameB) {
5148
+ return res.status(400).json({ error: 'suiteNameA and suiteNameB required' });
5149
+ }
5150
+ const latestA = this.evalStore.getLatest(suiteNameA);
5151
+ const latestB = this.evalStore.getLatest(suiteNameB);
5152
+ if (!latestA) {
5153
+ return res.status(404).json({ error: `No results found for suite: ${suiteNameA}` });
5154
+ }
5155
+ if (!latestB) {
5156
+ return res.status(404).json({ error: `No results found for suite: ${suiteNameB}` });
5157
+ }
5158
+ const comparison = this.evalRunner.compareSuites(latestA, latestB);
5159
+ res.json(comparison);
5160
+ }
5161
+ catch (err) {
5162
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
5163
+ }
5164
+ });
5165
+ return router;
5166
+ }
5167
+ createCodeRouter() {
5168
+ const router = Router();
5169
+ // POST /execute — one-shot code execution (no session required)
5170
+ router.post('/execute', async (req, res) => {
5171
+ try {
5172
+ const { language, code, timeout } = req.body;
5173
+ if (!language || typeof language !== 'string') {
5174
+ return res.status(400).json({ error: 'language required' });
5175
+ }
5176
+ if (!code || typeof code !== 'string') {
5177
+ return res.status(400).json({ error: 'code required' });
5178
+ }
5179
+ const validLanguages = ['javascript', 'typescript', 'python', 'shell'];
5180
+ if (!validLanguages.includes(language)) {
5181
+ return res.status(400).json({ error: `Invalid language. Allowed: ${validLanguages.join(', ')}` });
5182
+ }
5183
+ const result = await this.codeExecutor.execute({
5184
+ language: language,
5185
+ code,
5186
+ timeoutMs: timeout,
5187
+ });
5188
+ res.json(result);
5189
+ }
5190
+ catch (err) {
5191
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
5192
+ }
5193
+ });
5194
+ // GET /sessions — list active sessions
5195
+ router.get('/sessions', (_req, res) => {
5196
+ const sessions = this.codeSessionManager.listSessions().map((s) => ({
5197
+ id: s.id,
5198
+ language: s.language,
5199
+ createdAt: s.createdAt,
5200
+ lastActivity: s.lastActivity,
5201
+ historyLength: s.history.length,
5202
+ }));
5203
+ res.json({ sessions });
5204
+ });
5205
+ // POST /sessions — create a new REPL session
5206
+ router.post('/sessions', (req, res) => {
5207
+ try {
5208
+ const { language } = req.body;
5209
+ if (!language || typeof language !== 'string') {
5210
+ return res.status(400).json({ error: 'language required' });
5211
+ }
5212
+ const validLanguages = ['javascript', 'typescript', 'python', 'shell'];
5213
+ if (!validLanguages.includes(language)) {
5214
+ return res.status(400).json({ error: `Invalid language. Allowed: ${validLanguages.join(', ')}` });
5215
+ }
5216
+ const session = this.codeSessionManager.createSession(language);
5217
+ res.status(201).json({
5218
+ id: session.id,
5219
+ language: session.language,
5220
+ createdAt: session.createdAt,
5221
+ });
5222
+ }
5223
+ catch (err) {
5224
+ const message = err instanceof Error ? err.message : String(err);
5225
+ const status = message.includes('Maximum number of sessions') ? 409 : 500;
5226
+ res.status(status).json({ error: message });
5227
+ }
5228
+ });
5229
+ // GET /sessions/:id — get session details
5230
+ router.get('/sessions/:id', (req, res) => {
5231
+ const session = this.codeSessionManager.getSession(req.params.id);
5232
+ if (!session) {
5233
+ return res.status(404).json({ error: 'session not found' });
5234
+ }
5235
+ res.json({
5236
+ id: session.id,
5237
+ language: session.language,
5238
+ createdAt: session.createdAt,
5239
+ lastActivity: session.lastActivity,
5240
+ historyLength: session.history.length,
5241
+ history: session.history,
5242
+ });
5243
+ });
5244
+ // POST /sessions/:id/execute — execute code in a session
5245
+ router.post('/sessions/:id/execute', async (req, res) => {
5246
+ try {
5247
+ const { code } = req.body;
5248
+ if (!code || typeof code !== 'string') {
5249
+ return res.status(400).json({ error: 'code required' });
5250
+ }
5251
+ const result = await this.codeSessionManager.execute(req.params.id, code);
5252
+ res.json(result);
5253
+ }
5254
+ catch (err) {
5255
+ const message = err instanceof Error ? err.message : String(err);
5256
+ if (message.includes('not found')) {
5257
+ return res.status(404).json({ error: message });
5258
+ }
5259
+ res.status(500).json({ error: message });
5260
+ }
5261
+ });
5262
+ // DELETE /sessions/:id — destroy a session
5263
+ router.delete('/sessions/:id', (req, res) => {
5264
+ const session = this.codeSessionManager.getSession(req.params.id);
5265
+ if (!session) {
5266
+ return res.status(404).json({ error: 'session not found' });
5267
+ }
5268
+ this.codeSessionManager.destroySession(req.params.id);
5269
+ res.json({ deleted: true });
5270
+ });
5271
+ return router;
5272
+ }
5273
+ createBackupRouter() {
5274
+ const router = Router();
5275
+ let nextId = 1;
5276
+ // POST /create — create a new backup
5277
+ router.post('/create', async (req, res) => {
5278
+ if (!this.backupManager) {
5279
+ return res.status(503).json({ error: 'Backup system not initialized' });
5280
+ }
5281
+ try {
5282
+ const { categories } = req.body;
5283
+ const result = await this.backupManager.createBackup(categories);
5284
+ if (result.status === 'failed') {
5285
+ return res.status(500).json({ error: result.error ?? 'Backup failed' });
5286
+ }
5287
+ const id = `backup-${nextId++}`;
5288
+ this.backupStore.set(id, result);
5289
+ res.status(201).json({ id, ...result });
5290
+ }
5291
+ catch (err) {
5292
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
5293
+ }
5294
+ });
5295
+ // GET /list — list all stored backups
5296
+ router.get('/list', (_req, res) => {
5297
+ const backups = [...this.backupStore.entries()].map(([id, b]) => ({
5298
+ id,
5299
+ status: b.status,
5300
+ manifest: b.manifest,
5301
+ }));
5302
+ res.json({ backups });
5303
+ });
5304
+ // POST /restore — restore from a backup
5305
+ router.post('/restore', async (req, res) => {
5306
+ if (!this.backupManager) {
5307
+ return res.status(503).json({ error: 'Backup system not initialized' });
5308
+ }
5309
+ try {
5310
+ const { backupId, categories } = req.body;
5311
+ if (!backupId || typeof backupId !== 'string') {
5312
+ return res.status(400).json({ error: 'backupId required' });
5313
+ }
5314
+ const backup = this.backupStore.get(backupId);
5315
+ if (!backup) {
5316
+ return res.status(404).json({ error: 'Backup not found' });
5317
+ }
5318
+ const result = await this.backupManager.restore(backup, categories);
5319
+ if (result.status === 'failed') {
5320
+ return res.status(500).json({ error: result.error ?? 'Restore failed' });
5321
+ }
5322
+ res.json(result);
5323
+ }
5324
+ catch (err) {
5325
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
5326
+ }
5327
+ });
5328
+ // DELETE /:id — delete a stored backup
5329
+ router.delete('/:id', (req, res) => {
5330
+ const { id } = req.params;
5331
+ if (!this.backupStore.has(id)) {
5332
+ return res.status(404).json({ error: 'Backup not found' });
5333
+ }
5334
+ this.backupStore.delete(id);
5335
+ res.json({ deleted: true });
5336
+ });
5337
+ return router;
5338
+ }
5339
+ createKnowledgeRouter() {
5340
+ const router = Router();
5341
+ router.post('/entities', (req, res) => {
5342
+ const { name, type, properties, aliases } = req.body;
5343
+ if (!name || typeof name !== 'string')
5344
+ return res.status(400).json({ error: 'name required' });
5345
+ if (!type || typeof type !== 'string')
5346
+ return res.status(400).json({ error: 'type required' });
5347
+ try {
5348
+ const node = this.knowledgeGraph.addNode({ name, type: type, aliases: aliases ?? [], properties: properties ?? {}, confidence: 1.0 });
5349
+ res.status(201).json(node);
5350
+ }
5351
+ catch (err) {
5352
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
5353
+ }
5354
+ });
5355
+ router.get('/entities', (req, res) => {
5356
+ const query = req.query.query;
5357
+ if (query) {
5358
+ const extracted = this.entityLinker.extractEntities(query);
5359
+ const nodes = extracted.map((e) => this.knowledgeGraph.getNode(e.name)).filter(Boolean);
5360
+ return res.json({ entities: nodes });
5361
+ }
5362
+ return res.json({ entities: [], stats: this.knowledgeGraph.stats() });
5363
+ });
5364
+ router.get('/entities/:id', (req, res) => {
5365
+ const node = this.knowledgeGraph.getNode(req.params.id);
5366
+ if (!node)
5367
+ return res.status(404).json({ error: 'entity not found' });
5368
+ res.json(node);
5369
+ });
5370
+ router.delete('/entities/:id', (req, res) => {
5371
+ const node = this.knowledgeGraph.getNode(req.params.id);
5372
+ if (!node)
5373
+ return res.status(404).json({ error: 'entity not found' });
5374
+ this.knowledgeGraph.removeNode(node.id);
5375
+ res.json({ deleted: true });
5376
+ });
5377
+ router.post('/relations', (req, res) => {
5378
+ const { from, to, type, properties, weight, label, evidence } = req.body;
5379
+ if (!from || typeof from !== 'string')
5380
+ return res.status(400).json({ error: 'from required' });
5381
+ if (!to || typeof to !== 'string')
5382
+ return res.status(400).json({ error: 'to required' });
5383
+ if (!type || typeof type !== 'string')
5384
+ return res.status(400).json({ error: 'type required' });
5385
+ try {
5386
+ const edge = this.knowledgeGraph.addEdge(from, to, type, { weight, label, evidence, properties });
5387
+ res.status(201).json(edge);
5388
+ }
5389
+ catch (err) {
5390
+ const message = err instanceof Error ? err.message : String(err);
5391
+ if (message.includes('not found'))
5392
+ return res.status(404).json({ error: message });
5393
+ res.status(500).json({ error: message });
5394
+ }
5395
+ });
5396
+ router.get('/relations', (req, res) => {
5397
+ const nodeId = req.query.nodeId;
5398
+ const direction = req.query.direction ?? 'both';
5399
+ if (!nodeId)
5400
+ return res.status(400).json({ error: 'nodeId query parameter required' });
5401
+ const node = this.knowledgeGraph.getNode(nodeId);
5402
+ if (!node)
5403
+ return res.status(404).json({ error: 'node not found' });
5404
+ res.json({ relations: this.knowledgeGraph.getEdges(node.id, direction) });
5405
+ });
5406
+ router.post('/query', (req, res) => {
5407
+ const { startNode, relation, targetType, maxDepth, minConfidence } = req.body;
5408
+ try {
5409
+ res.json(this.knowledgeGraph.query({ startNode, relation, targetType, maxDepth, minConfidence }));
5410
+ }
5411
+ catch (err) {
5412
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
5413
+ }
5414
+ });
5415
+ router.post('/extract', (req, res) => {
5416
+ const { text } = req.body;
5417
+ if (!text || typeof text !== 'string')
5418
+ return res.status(400).json({ error: 'text required' });
5419
+ try {
5420
+ const result = this.entityLinker.linkToGraph(text, this.knowledgeGraph);
5421
+ res.json({ newNodes: result.newNodes, newEdges: result.newEdges });
5422
+ }
5423
+ catch (err) {
5424
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
5425
+ }
5426
+ });
5427
+ router.get('/stats', (_req, res) => {
5428
+ res.json(this.knowledgeGraph.stats());
5429
+ });
5430
+ return router;
5431
+ }
5432
+ createAutomationRouter() {
5433
+ const router = Router();
5434
+ router.post('/parse', (req, res) => {
5435
+ const { input } = req.body;
5436
+ if (!input || typeof input !== 'string') {
5437
+ return res.status(400).json({ error: 'input required' });
5438
+ }
5439
+ const spec = this.nlIntentParser.parse(input);
5440
+ res.json({ spec });
5441
+ });
5442
+ router.post('/build', (req, res) => {
5443
+ const { spec } = req.body;
5444
+ if (!spec || typeof spec !== 'object') {
5445
+ return res.status(400).json({ error: 'spec required' });
5446
+ }
5447
+ try {
5448
+ const behavior = this.automationBuilder.build(spec);
5449
+ res.json({ behavior });
5450
+ }
5451
+ catch (err) {
5452
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
5453
+ }
5454
+ });
5455
+ router.post('/validate', (req, res) => {
5456
+ const { spec } = req.body;
5457
+ if (!spec || typeof spec !== 'object') {
5458
+ return res.status(400).json({ error: 'spec required' });
5459
+ }
5460
+ const result = this.automationBuilder.validate(spec, [], []);
5461
+ res.json({ result });
5462
+ });
5463
+ return router;
5464
+ }
5465
+ createBranchRouter() {
5466
+ const router = Router();
5467
+ const getOrCreateManager = (conversationId) => {
5468
+ let mgr = this.branchManagers.get(conversationId);
5469
+ if (!mgr) {
5470
+ mgr = new BranchManager(conversationId);
5471
+ this.branchManagers.set(conversationId, mgr);
5472
+ }
5473
+ return mgr;
5474
+ };
5475
+ router.get('/:conversationId', (req, res) => {
5476
+ const mgr = getOrCreateManager(req.params.conversationId);
5477
+ res.json({ branches: mgr.listBranches(), active: mgr.getActiveBranch() });
5478
+ });
5479
+ router.post('/:conversationId/fork', (req, res) => {
5480
+ const mgr = getOrCreateManager(req.params.conversationId);
5481
+ const { messageId, label } = req.body;
5482
+ try {
5483
+ const branch = mgr.fork(messageId, label);
5484
+ res.status(201).json(branch);
5485
+ }
5486
+ catch (err) {
5487
+ res.status(400).json({ error: err instanceof Error ? err.message : String(err) });
5488
+ }
5489
+ });
5490
+ router.post('/:conversationId/switch', (req, res) => {
5491
+ const mgr = getOrCreateManager(req.params.conversationId);
5492
+ const { branchId } = req.body;
5493
+ if (!branchId || typeof branchId !== 'string') {
5494
+ return res.status(400).json({ error: 'branchId required' });
5495
+ }
5496
+ try {
5497
+ const branch = mgr.switchBranch(branchId);
5498
+ res.json(branch);
5499
+ }
5500
+ catch (err) {
5501
+ res.status(400).json({ error: err instanceof Error ? err.message : String(err) });
5502
+ }
5503
+ });
5504
+ router.get('/:conversationId/tree', (req, res) => {
5505
+ const mgr = getOrCreateManager(req.params.conversationId);
5506
+ res.json({ tree: mgr.getTree() });
5507
+ });
5508
+ router.post('/:conversationId/merge', (req, res) => {
5509
+ const mgr = getOrCreateManager(req.params.conversationId);
5510
+ const { sourceBranchId, targetBranchId } = req.body;
5511
+ if (!sourceBranchId || typeof sourceBranchId !== 'string') {
5512
+ return res.status(400).json({ error: 'sourceBranchId required' });
5513
+ }
5514
+ if (!targetBranchId || typeof targetBranchId !== 'string') {
5515
+ return res.status(400).json({ error: 'targetBranchId required' });
5516
+ }
5517
+ try {
5518
+ mgr.mergeBranch(sourceBranchId, targetBranchId);
5519
+ res.json({ merged: true });
5520
+ }
5521
+ catch (err) {
5522
+ res.status(400).json({ error: err instanceof Error ? err.message : String(err) });
5523
+ }
5524
+ });
5525
+ router.delete('/:conversationId/:branchId', (req, res) => {
5526
+ const mgr = getOrCreateManager(req.params.conversationId);
5527
+ try {
5528
+ mgr.deleteBranch(req.params.branchId);
5529
+ res.json({ deleted: true });
5530
+ }
5531
+ catch (err) {
5532
+ res.status(400).json({ error: err instanceof Error ? err.message : String(err) });
5533
+ }
5534
+ });
5535
+ return router;
5536
+ }
5537
+ createApprovalQueueRouter() {
5538
+ const router = Router();
5539
+ // GET / — list all approval requests
5540
+ router.get('/', (_req, res) => {
5541
+ if (!this.approvalQueue) {
5542
+ return res.status(503).json({ error: 'Approval queue not initialized' });
5543
+ }
5544
+ res.json(this.approvalQueue.listAll());
5545
+ });
5546
+ // GET /pending — list pending approval requests
5547
+ router.get('/pending', (_req, res) => {
5548
+ if (!this.approvalQueue) {
5549
+ return res.status(503).json({ error: 'Approval queue not initialized' });
5550
+ }
5551
+ res.json(this.approvalQueue.listPending());
5552
+ });
5553
+ // POST /expire — expire stale requests
5554
+ router.post('/expire', (_req, res) => {
5555
+ if (!this.approvalQueue) {
5556
+ return res.status(503).json({ error: 'Approval queue not initialized' });
5557
+ }
5558
+ const expired = this.approvalQueue.expireStale();
5559
+ res.json({ expired });
5560
+ });
5561
+ // GET /:id — get a specific approval request
5562
+ router.get('/:id', (req, res) => {
5563
+ if (!this.approvalQueue) {
5564
+ return res.status(503).json({ error: 'Approval queue not initialized' });
5565
+ }
5566
+ const request = this.approvalQueue.get(req.params.id);
5567
+ if (!request) {
5568
+ return res.status(404).json({ error: 'Approval request not found' });
5569
+ }
5570
+ res.json(request);
5571
+ });
5572
+ // POST /:id/approve — approve a request
5573
+ router.post('/:id/approve', (req, res) => {
5574
+ if (!this.approvalQueue) {
5575
+ return res.status(503).json({ error: 'Approval queue not initialized' });
5576
+ }
5577
+ try {
5578
+ const { decidedBy } = req.body;
5579
+ const result = this.approvalQueue.approve(req.params.id, decidedBy);
5580
+ res.json(result);
5581
+ }
5582
+ catch (err) {
5583
+ const message = err instanceof Error ? err.message : String(err);
5584
+ if (message.includes('not found')) {
5585
+ return res.status(404).json({ error: message });
5586
+ }
5587
+ if (message.includes('already')) {
5588
+ return res.status(409).json({ error: message });
5589
+ }
5590
+ res.status(500).json({ error: message });
5591
+ }
5592
+ });
5593
+ // POST /:id/deny — deny a request
5594
+ router.post('/:id/deny', (req, res) => {
5595
+ if (!this.approvalQueue) {
5596
+ return res.status(503).json({ error: 'Approval queue not initialized' });
5597
+ }
5598
+ try {
5599
+ const { decidedBy, reason } = req.body;
5600
+ const result = this.approvalQueue.deny(req.params.id, reason, decidedBy);
5601
+ res.json(result);
5602
+ }
5603
+ catch (err) {
5604
+ const message = err instanceof Error ? err.message : String(err);
5605
+ if (message.includes('not found')) {
5606
+ return res.status(404).json({ error: message });
5607
+ }
5608
+ if (message.includes('already')) {
5609
+ return res.status(409).json({ error: message });
5610
+ }
5611
+ res.status(500).json({ error: message });
5612
+ }
5613
+ });
5614
+ return router;
5615
+ }
5616
+ createVectorRouter() {
5617
+ const router = Router();
5618
+ // GET /stats — get store statistics
5619
+ router.get('/stats', (_req, res) => {
5620
+ if (!this.vectorStore) {
5621
+ return res.status(503).json({ error: 'Vector store not initialized' });
5622
+ }
5623
+ res.json({ size: this.vectorStore.size() });
5624
+ });
5625
+ // POST / — add a vector entry
5626
+ router.post('/', (req, res) => {
5627
+ if (!this.vectorStore) {
5628
+ return res.status(503).json({ error: 'Vector store not initialized' });
5629
+ }
5630
+ try {
5631
+ const { id, vector, content, metadata } = req.body;
5632
+ if (!id || typeof id !== 'string') {
5633
+ return res.status(400).json({ error: 'id required' });
5634
+ }
5635
+ if (!vector || !Array.isArray(vector)) {
5636
+ return res.status(400).json({ error: 'vector required' });
5637
+ }
5638
+ const entry = this.vectorStore.add(id, vector, content ?? '', metadata);
5639
+ res.status(201).json(entry);
5640
+ }
5641
+ catch (err) {
5642
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
5643
+ }
5644
+ });
5645
+ // POST /search — search vectors
5646
+ router.post('/search', (req, res) => {
5647
+ if (!this.vectorStore) {
5648
+ return res.status(503).json({ error: 'Vector store not initialized' });
5649
+ }
5650
+ try {
5651
+ const { vector, limit, minScore } = req.body;
5652
+ if (!vector || !Array.isArray(vector)) {
5653
+ return res.status(400).json({ error: 'vector required' });
5654
+ }
5655
+ const results = this.vectorStore.search(vector, limit, minScore);
5656
+ res.json(results);
5657
+ }
5658
+ catch (err) {
5659
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
5660
+ }
5661
+ });
5662
+ // GET /:id — get a vector entry by ID
5663
+ router.get('/:id', (req, res) => {
5664
+ if (!this.vectorStore) {
5665
+ return res.status(503).json({ error: 'Vector store not initialized' });
5666
+ }
5667
+ const entry = this.vectorStore.get(req.params.id);
5668
+ if (!entry) {
5669
+ return res.status(404).json({ error: 'Vector entry not found' });
5670
+ }
5671
+ res.json(entry);
5672
+ });
5673
+ // DELETE /:id — remove a vector entry
5674
+ router.delete('/:id', (req, res) => {
5675
+ if (!this.vectorStore) {
5676
+ return res.status(503).json({ error: 'Vector store not initialized' });
5677
+ }
5678
+ const removed = this.vectorStore.remove(req.params.id);
5679
+ if (!removed) {
5680
+ return res.status(404).json({ error: 'Vector entry not found' });
5681
+ }
5682
+ res.json({ deleted: true });
5683
+ });
5684
+ return router;
5685
+ }
5686
+ createReactRouter() {
5687
+ const router = Router();
5688
+ // POST /run — start a new ReAct loop
5689
+ router.post('/run', (req, res) => {
5690
+ const { goal, maxSteps } = req.body;
5691
+ if (!goal || typeof goal !== 'string') {
5692
+ return res.status(400).json({ error: 'goal is required' });
5693
+ }
5694
+ const id = crypto.randomUUID();
5695
+ const config = {};
5696
+ if (maxSteps) {
5697
+ config.maxSteps = maxSteps;
5698
+ }
5699
+ const callbacks = {
5700
+ think: async (g, history) => {
5701
+ // Simple stub: produce an answer after gathering context
5702
+ if (history.length > 0) {
5703
+ return { thought: `Analyzed goal: ${g}`, answer: `Completed goal: ${g}` };
5704
+ }
5705
+ return { thought: `Planning approach for: ${g}` };
5706
+ },
5707
+ executeTool: async (toolName, params) => {
5708
+ try {
5709
+ const result = await toolExecutor.execute(toolName, params, {
5710
+ sessionId: id,
5711
+ userId: 'react-loop',
5712
+ });
5713
+ return typeof result === 'string' ? result : JSON.stringify(result);
5714
+ }
5715
+ catch (err) {
5716
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
5717
+ }
5718
+ },
5719
+ };
5720
+ const loop = new ReActLoop(callbacks, config);
5721
+ this.reactLoops.set(id, loop);
5722
+ // Run async — do not await
5723
+ loop.run(goal).then((result) => {
5724
+ this.reactResults.set(id, result);
5725
+ }).catch(() => {
5726
+ // Errors captured in result
5727
+ });
5728
+ res.status(201).json({ id, status: loop.getStatus() });
5729
+ });
5730
+ // GET /:id/status — get loop status
5731
+ router.get('/:id/status', (req, res) => {
5732
+ const loop = this.reactLoops.get(req.params.id);
5733
+ if (!loop) {
5734
+ return res.status(404).json({ error: 'Loop not found' });
5735
+ }
5736
+ const result = this.reactResults.get(req.params.id);
5737
+ res.json({ status: loop.getStatus(), result: result ?? null });
5738
+ });
5739
+ // GET /:id/steps — get loop steps
5740
+ router.get('/:id/steps', (req, res) => {
5741
+ const loop = this.reactLoops.get(req.params.id);
5742
+ if (!loop) {
5743
+ return res.status(404).json({ error: 'Loop not found' });
5744
+ }
5745
+ res.json(loop.getSteps());
5746
+ });
5747
+ // POST /:id/pause — pause loop
5748
+ router.post('/:id/pause', (req, res) => {
5749
+ const loop = this.reactLoops.get(req.params.id);
5750
+ if (!loop) {
5751
+ return res.status(404).json({ error: 'Loop not found' });
5752
+ }
5753
+ loop.pause();
5754
+ res.json({ status: loop.getStatus() });
5755
+ });
5756
+ // POST /:id/resume — resume loop
5757
+ router.post('/:id/resume', (req, res) => {
5758
+ const loop = this.reactLoops.get(req.params.id);
5759
+ if (!loop) {
5760
+ return res.status(404).json({ error: 'Loop not found' });
5761
+ }
5762
+ loop.resume();
5763
+ res.json({ status: loop.getStatus() });
5764
+ });
5765
+ // POST /:id/abort — abort loop
5766
+ router.post('/:id/abort', (req, res) => {
5767
+ const loop = this.reactLoops.get(req.params.id);
5768
+ if (!loop) {
5769
+ return res.status(404).json({ error: 'Loop not found' });
5770
+ }
5771
+ loop.abort(req.body?.reason);
5772
+ res.json({ status: loop.getStatus() });
5773
+ });
5774
+ return router;
5775
+ }
5776
+ createA2ARouter() {
5777
+ const router = Router();
5778
+ // GET /card — return this agent's capability card
5779
+ router.get('/card', (_req, res) => {
5780
+ if (!this.a2aAgentCard) {
5781
+ return res.status(503).json({ error: 'A2A not initialized' });
5782
+ }
5783
+ res.json(this.a2aAgentCard);
5784
+ });
5785
+ // POST /tasks — send a task (create via task manager)
5786
+ router.post('/tasks', (req, res) => {
5787
+ const { targetUrl, task } = req.body;
5788
+ if (!task?.message) {
5789
+ return res.status(400).json({ error: 'task.message is required' });
5790
+ }
5791
+ const a2aTask = this.a2aTaskManager.createTask({
5792
+ role: 'user',
5793
+ parts: [{ type: 'text', text: task.message }],
5794
+ timestamp: Date.now(),
5795
+ });
5796
+ if (targetUrl) {
5797
+ // Store target URL as metadata for potential forwarding
5798
+ a2aTask.metadata = { targetUrl };
5799
+ }
5800
+ res.status(201).json(a2aTask);
5801
+ });
5802
+ // GET /tasks/:id — get task status
5803
+ router.get('/tasks/:id', (req, res) => {
5804
+ const task = this.a2aTaskManager.getTask(req.params.id);
5805
+ if (!task) {
5806
+ return res.status(404).json({ error: 'Task not found' });
5807
+ }
5808
+ res.json(task);
5809
+ });
5810
+ // POST /tasks/:id/cancel — cancel task
5811
+ router.post('/tasks/:id/cancel', (req, res) => {
5812
+ const task = this.a2aTaskManager.getTask(req.params.id);
5813
+ if (!task) {
5814
+ return res.status(404).json({ error: 'Task not found' });
5815
+ }
5816
+ try {
5817
+ this.a2aTaskManager.cancelTask(req.params.id);
5818
+ res.json(this.a2aTaskManager.getTask(req.params.id));
5819
+ }
5820
+ catch (err) {
5821
+ res.status(400).json({ error: err instanceof Error ? err.message : String(err) });
5822
+ }
5823
+ });
5824
+ return router;
5825
+ }
5826
+ createCanvasRouter() {
5827
+ const router = Router();
5828
+ // POST /sessions — create a new canvas session
5829
+ router.post('/sessions', (req, res) => {
5830
+ const { width, height } = req.body;
5831
+ const session = new CanvasSession({ width, height });
5832
+ this.canvasSessions.set(session.id, session);
5833
+ res.status(201).json({ id: session.id, ...session.getSize() });
5834
+ });
5835
+ // GET /sessions/:id — get session state
5836
+ router.get('/sessions/:id', (req, res) => {
5837
+ const session = this.canvasSessions.get(req.params.id);
5838
+ if (!session) {
5839
+ return res.status(404).json({ error: 'Session not found' });
5840
+ }
5841
+ res.json({ id: session.id, objects: session.getObjects(), size: session.getSize() });
5842
+ });
5843
+ // POST /sessions/:id/objects — add object
5844
+ router.post('/sessions/:id/objects', (req, res) => {
5845
+ const session = this.canvasSessions.get(req.params.id);
5846
+ if (!session) {
5847
+ return res.status(404).json({ error: 'Session not found' });
5848
+ }
5849
+ try {
5850
+ const obj = session.addObject(req.body);
5851
+ res.status(201).json(obj);
5852
+ }
5853
+ catch (err) {
5854
+ res.status(400).json({ error: err instanceof Error ? err.message : String(err) });
5855
+ }
5856
+ });
5857
+ // PUT /sessions/:id/objects/:objectId — update object
5858
+ router.put('/sessions/:id/objects/:objectId', (req, res) => {
5859
+ const session = this.canvasSessions.get(req.params.id);
5860
+ if (!session) {
5861
+ return res.status(404).json({ error: 'Session not found' });
5862
+ }
5863
+ const updated = session.updateObject(req.params.objectId, req.body);
5864
+ if (!updated) {
5865
+ return res.status(404).json({ error: 'Object not found' });
5866
+ }
5867
+ res.json(updated);
5868
+ });
5869
+ // DELETE /sessions/:id/objects/:objectId — remove object
5870
+ router.delete('/sessions/:id/objects/:objectId', (req, res) => {
5871
+ const session = this.canvasSessions.get(req.params.id);
5872
+ if (!session) {
5873
+ return res.status(404).json({ error: 'Session not found' });
5874
+ }
5875
+ const removed = session.removeObject(req.params.objectId);
5876
+ if (!removed) {
5877
+ return res.status(404).json({ error: 'Object not found' });
5878
+ }
5879
+ res.json({ deleted: true });
5880
+ });
5881
+ // DELETE /sessions/:id — delete session
5882
+ router.delete('/sessions/:id', (req, res) => {
5883
+ const existed = this.canvasSessions.delete(req.params.id);
5884
+ if (!existed) {
5885
+ return res.status(404).json({ error: 'Session not found' });
5886
+ }
5887
+ res.json({ deleted: true });
5888
+ });
5889
+ return router;
5890
+ }
5891
+ createSandboxRouter() {
5892
+ const router = Router();
5893
+ // POST /sessions — create a sandbox session
5894
+ router.post('/sessions', async (req, res) => {
5895
+ if (!this.sandboxManager) {
5896
+ return res.status(503).json({ error: 'Sandbox not initialized' });
5897
+ }
5898
+ const { workspaceDir } = req.body;
5899
+ if (!workspaceDir || typeof workspaceDir !== 'string') {
5900
+ return res.status(400).json({ error: 'workspaceDir is required' });
5901
+ }
5902
+ try {
5903
+ const sessionId = crypto.randomUUID();
5904
+ const session = await this.sandboxManager.createSession(sessionId, workspaceDir);
5905
+ res.status(201).json(session.getInfo());
5906
+ }
5907
+ catch (err) {
5908
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
5909
+ }
5910
+ });
5911
+ // POST /sessions/:id/run — run command in sandbox
5912
+ router.post('/sessions/:id/run', async (req, res) => {
5913
+ if (!this.sandboxManager) {
5914
+ return res.status(503).json({ error: 'Sandbox not initialized' });
5915
+ }
5916
+ const { command } = req.body;
5917
+ if (!command || !Array.isArray(command)) {
5918
+ return res.status(400).json({ error: 'command array is required' });
5919
+ }
5920
+ try {
5921
+ const result = await this.sandboxManager.runInSandbox(req.params.id, command);
5922
+ res.json(result);
5923
+ }
5924
+ catch (err) {
5925
+ const message = err instanceof Error ? err.message : String(err);
5926
+ if (message.includes('No sandbox session found')) {
5927
+ return res.status(404).json({ error: message });
5928
+ }
5929
+ res.status(500).json({ error: message });
5930
+ }
5931
+ });
5932
+ // GET /sessions/:id — get session info
5933
+ router.get('/sessions/:id', (req, res) => {
5934
+ if (!this.sandboxManager) {
5935
+ return res.status(503).json({ error: 'Sandbox not initialized' });
5936
+ }
5937
+ const session = this.sandboxManager.getSession(req.params.id);
5938
+ if (!session) {
5939
+ return res.status(404).json({ error: 'Session not found' });
5940
+ }
5941
+ res.json(session.getInfo());
5942
+ });
5943
+ // DELETE /sessions/:id — stop and remove
5944
+ router.delete('/sessions/:id', async (req, res) => {
5945
+ if (!this.sandboxManager) {
5946
+ return res.status(503).json({ error: 'Sandbox not initialized' });
5947
+ }
5948
+ try {
5949
+ const removed = await this.sandboxManager.destroySession(req.params.id);
5950
+ if (!removed) {
5951
+ return res.status(404).json({ error: 'Session not found' });
5952
+ }
5953
+ res.json({ deleted: true });
5954
+ }
5955
+ catch (err) {
5956
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
5957
+ }
5958
+ });
5959
+ return router;
5960
+ }
2958
5961
  }
2959
5962
  function deepMerge(target, source) {
2960
5963
  const result = { ...target };