dexto 1.1.10 → 1.1.11

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 (152) hide show
  1. package/README.md +4 -2
  2. package/dist/agents/agent-registry.json +30 -1
  3. package/dist/agents/database-agent/database-agent.yml +3 -0
  4. package/dist/agents/default-agent.yml +102 -12
  5. package/dist/agents/github-agent/github-agent.yml +3 -0
  6. package/dist/agents/image-editor-agent/image-editor-agent.yml +3 -0
  7. package/dist/agents/music-agent/music-agent.yml +3 -0
  8. package/dist/agents/nano-banana-agent/nano-banana-agent.yml +3 -0
  9. package/dist/agents/podcast-agent/podcast-agent.yml +3 -0
  10. package/dist/agents/product-name-researcher/product-name-researcher.yml +3 -0
  11. package/dist/agents/talk2pdf-agent/talk2pdf-agent.yml +3 -0
  12. package/dist/agents/triage-demo/triage-agent.yml +3 -0
  13. package/dist/api/mcp/tool-aggregation-handler.d.ts.map +1 -1
  14. package/dist/api/mcp/tool-aggregation-handler.js +34 -42
  15. package/dist/api/memory/memory-handler.d.ts +15 -0
  16. package/dist/api/memory/memory-handler.d.ts.map +1 -0
  17. package/dist/api/memory/memory-handler.js +129 -0
  18. package/dist/api/server.d.ts +2 -2
  19. package/dist/api/server.d.ts.map +1 -1
  20. package/dist/api/server.js +987 -231
  21. package/dist/api/webhook-subscriber.d.ts.map +1 -1
  22. package/dist/api/webhook-subscriber.js +2 -1
  23. package/dist/api/websocket-subscriber.d.ts.map +1 -1
  24. package/dist/api/websocket-subscriber.js +61 -10
  25. package/dist/cli/cli-subscriber.d.ts +2 -1
  26. package/dist/cli/cli-subscriber.d.ts.map +1 -1
  27. package/dist/cli/cli-subscriber.js +11 -3
  28. package/dist/cli/cli.d.ts.map +1 -1
  29. package/dist/cli/cli.js +1 -0
  30. package/dist/cli/commands/install.d.ts +3 -3
  31. package/dist/cli/commands/install.d.ts.map +1 -1
  32. package/dist/cli/commands/install.js +223 -41
  33. package/dist/cli/commands/interactive-commands/prompt-commands.d.ts +8 -1
  34. package/dist/cli/commands/interactive-commands/prompt-commands.d.ts.map +1 -1
  35. package/dist/cli/commands/interactive-commands/prompt-commands.js +252 -4
  36. package/dist/cli/commands/list-agents.d.ts.map +1 -1
  37. package/dist/cli/commands/list-agents.js +22 -3
  38. package/dist/cli/commands/setup.d.ts +4 -4
  39. package/dist/cli/commands/uninstall.d.ts +1 -1
  40. package/dist/cli/tool-confirmation/cli-confirmation-handler.d.ts +36 -7
  41. package/dist/cli/tool-confirmation/cli-confirmation-handler.d.ts.map +1 -1
  42. package/dist/cli/tool-confirmation/cli-confirmation-handler.js +314 -34
  43. package/dist/index.js +53 -46
  44. package/dist/webui/.next/standalone/.next/static/VoeDi3iuGmMdZu_kx-R2X/_buildManifest.js +1 -0
  45. package/dist/webui/.next/standalone/.next/static/chunks/419-5526a47c95a2fa60.js +1 -0
  46. package/dist/webui/.next/standalone/.next/static/chunks/429-838829c1391e496d.js +25 -0
  47. package/dist/webui/.next/standalone/.next/static/chunks/459-62011998b002cbf6.js +1 -0
  48. package/dist/webui/.next/standalone/.next/static/chunks/711-76a7d2bf4d6f69e5.js +1 -0
  49. package/dist/webui/.next/standalone/.next/static/chunks/854-8cad9404fc78e0cc.js +1 -0
  50. package/dist/webui/.next/standalone/.next/static/chunks/935-07f9df196b13275e.js +1 -0
  51. package/dist/webui/.next/standalone/.next/static/chunks/app/chat/[sessionId]/page-b8acc47b0d8c5c0a.js +1 -0
  52. package/dist/webui/.next/standalone/.next/static/chunks/app/layout-f4a6ee5a028899d1.js +1 -0
  53. package/dist/webui/.next/standalone/.next/static/chunks/app/page-e117ae372850d25f.js +1 -0
  54. package/dist/webui/.next/standalone/.next/static/chunks/app/playground/page-09340fb6b3f4caa2.js +1 -0
  55. package/dist/webui/.next/standalone/.next/static/chunks/{webpack-7c234e7e7e272295.js → webpack-7229fd0786f0483c.js} +1 -1
  56. package/dist/webui/.next/standalone/.next/static/css/21e6c142ca3cdc42.css +1 -0
  57. package/dist/webui/.next/standalone/.next/static/css/de70bee13400563f.css +1 -0
  58. package/dist/webui/.next/standalone/package.json +6 -2
  59. package/dist/webui/.next/standalone/packages/webui/.next/BUILD_ID +1 -1
  60. package/dist/webui/.next/standalone/packages/webui/.next/app-build-manifest.json +30 -15
  61. package/dist/webui/.next/standalone/packages/webui/.next/app-path-routes-manifest.json +1 -0
  62. package/dist/webui/.next/standalone/packages/webui/.next/build-manifest.json +5 -5
  63. package/dist/webui/.next/standalone/packages/webui/.next/prerender-manifest.json +3 -3
  64. package/dist/webui/.next/standalone/packages/webui/.next/required-server-files.json +1 -1
  65. package/dist/webui/.next/standalone/packages/webui/.next/routes-manifest.json +10 -1
  66. package/dist/webui/.next/standalone/packages/webui/.next/server/app/_not-found/page.js +1 -1
  67. package/dist/webui/.next/standalone/packages/webui/.next/server/app/_not-found/page.js.nft.json +1 -1
  68. package/dist/webui/.next/standalone/packages/webui/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  69. package/dist/webui/.next/standalone/packages/webui/.next/server/app/chat/[sessionId]/page.js +2 -0
  70. package/dist/webui/.next/standalone/packages/webui/.next/server/app/chat/[sessionId]/page.js.nft.json +1 -0
  71. package/dist/webui/.next/standalone/packages/webui/.next/server/app/chat/[sessionId]/page_client-reference-manifest.js +1 -0
  72. package/dist/webui/.next/standalone/packages/webui/.next/server/app/page.js +2 -11
  73. package/dist/webui/.next/standalone/packages/webui/.next/server/app/page.js.nft.json +1 -1
  74. package/dist/webui/.next/standalone/packages/webui/.next/server/app/page_client-reference-manifest.js +1 -1
  75. package/dist/webui/.next/standalone/packages/webui/.next/server/app/playground/page.js +4 -4
  76. package/dist/webui/.next/standalone/packages/webui/.next/server/app/playground/page.js.nft.json +1 -1
  77. package/dist/webui/.next/standalone/packages/webui/.next/server/app/playground/page_client-reference-manifest.js +1 -1
  78. package/dist/webui/.next/standalone/packages/webui/.next/server/app-paths-manifest.json +1 -0
  79. package/dist/webui/.next/standalone/packages/webui/.next/server/chunks/1.js +12 -0
  80. package/dist/webui/.next/standalone/packages/webui/.next/server/chunks/419.js +25 -0
  81. package/dist/webui/.next/standalone/packages/webui/.next/server/chunks/{619.js → 426.js} +2 -2
  82. package/dist/webui/.next/standalone/packages/webui/.next/server/chunks/43.js +1 -1
  83. package/dist/webui/.next/standalone/packages/webui/.next/server/chunks/654.js +1 -0
  84. package/dist/webui/.next/standalone/packages/webui/.next/server/chunks/71.js +5 -0
  85. package/dist/webui/.next/standalone/packages/webui/.next/server/middleware-build-manifest.js +1 -1
  86. package/dist/webui/.next/standalone/packages/webui/.next/server/pages/500.html +1 -1
  87. package/dist/webui/.next/standalone/packages/webui/.next/server/pages-manifest.json +1 -1
  88. package/dist/webui/.next/standalone/packages/webui/.next/server/server-reference-manifest.json +1 -1
  89. package/dist/webui/.next/standalone/packages/webui/.next/static/VoeDi3iuGmMdZu_kx-R2X/_buildManifest.js +1 -0
  90. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/419-5526a47c95a2fa60.js +1 -0
  91. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/429-838829c1391e496d.js +25 -0
  92. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/459-62011998b002cbf6.js +1 -0
  93. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/711-76a7d2bf4d6f69e5.js +1 -0
  94. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/854-8cad9404fc78e0cc.js +1 -0
  95. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/935-07f9df196b13275e.js +1 -0
  96. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/app/chat/[sessionId]/page-b8acc47b0d8c5c0a.js +1 -0
  97. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/app/layout-f4a6ee5a028899d1.js +1 -0
  98. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/app/page-e117ae372850d25f.js +1 -0
  99. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/app/playground/page-09340fb6b3f4caa2.js +1 -0
  100. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/{webpack-7c234e7e7e272295.js → webpack-7229fd0786f0483c.js} +1 -1
  101. package/dist/webui/.next/standalone/packages/webui/.next/static/css/21e6c142ca3cdc42.css +1 -0
  102. package/dist/webui/.next/standalone/packages/webui/.next/static/css/de70bee13400563f.css +1 -0
  103. package/dist/webui/.next/standalone/packages/webui/package.json +7 -4
  104. package/dist/webui/.next/standalone/packages/webui/server.js +1 -1
  105. package/dist/webui/.next/static/VoeDi3iuGmMdZu_kx-R2X/_buildManifest.js +1 -0
  106. package/dist/webui/.next/static/chunks/419-5526a47c95a2fa60.js +1 -0
  107. package/dist/webui/.next/static/chunks/429-838829c1391e496d.js +25 -0
  108. package/dist/webui/.next/static/chunks/459-62011998b002cbf6.js +1 -0
  109. package/dist/webui/.next/static/chunks/711-76a7d2bf4d6f69e5.js +1 -0
  110. package/dist/webui/.next/static/chunks/854-8cad9404fc78e0cc.js +1 -0
  111. package/dist/webui/.next/static/chunks/935-07f9df196b13275e.js +1 -0
  112. package/dist/webui/.next/static/chunks/app/chat/[sessionId]/page-b8acc47b0d8c5c0a.js +1 -0
  113. package/dist/webui/.next/static/chunks/app/layout-f4a6ee5a028899d1.js +1 -0
  114. package/dist/webui/.next/static/chunks/app/page-e117ae372850d25f.js +1 -0
  115. package/dist/webui/.next/static/chunks/app/playground/page-09340fb6b3f4caa2.js +1 -0
  116. package/dist/webui/.next/static/chunks/{webpack-7c234e7e7e272295.js → webpack-7229fd0786f0483c.js} +1 -1
  117. package/dist/webui/.next/static/css/21e6c142ca3cdc42.css +1 -0
  118. package/dist/webui/.next/static/css/de70bee13400563f.css +1 -0
  119. package/dist/webui/package.json +7 -4
  120. package/package.json +5 -4
  121. package/dist/webui/.next/standalone/.next/static/PvkEd_BO6ZXxX99T_gUvs/_buildManifest.js +0 -1
  122. package/dist/webui/.next/standalone/.next/static/chunks/179-78abc2eacbc41da9.js +0 -1
  123. package/dist/webui/.next/standalone/.next/static/chunks/442-b1916bec348454b3.js +0 -1
  124. package/dist/webui/.next/standalone/.next/static/chunks/544-c4a8f278ed1a25d7.js +0 -1
  125. package/dist/webui/.next/standalone/.next/static/chunks/854-2a6d5a5297a15d52.js +0 -1
  126. package/dist/webui/.next/standalone/.next/static/chunks/app/layout-dde711766eda096b.js +0 -1
  127. package/dist/webui/.next/standalone/.next/static/chunks/app/page-5e94d5a49dc718d0.js +0 -1
  128. package/dist/webui/.next/standalone/.next/static/chunks/app/playground/page-9ae40e0b219583e3.js +0 -1
  129. package/dist/webui/.next/standalone/.next/static/css/045cc65741e38fbd.css +0 -3
  130. package/dist/webui/.next/standalone/packages/webui/.next/server/chunks/549.js +0 -1
  131. package/dist/webui/.next/standalone/packages/webui/.next/server/chunks/950.js +0 -5
  132. package/dist/webui/.next/standalone/packages/webui/.next/static/PvkEd_BO6ZXxX99T_gUvs/_buildManifest.js +0 -1
  133. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/179-78abc2eacbc41da9.js +0 -1
  134. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/442-b1916bec348454b3.js +0 -1
  135. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/544-c4a8f278ed1a25d7.js +0 -1
  136. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/854-2a6d5a5297a15d52.js +0 -1
  137. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/app/layout-dde711766eda096b.js +0 -1
  138. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/app/page-5e94d5a49dc718d0.js +0 -1
  139. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/app/playground/page-9ae40e0b219583e3.js +0 -1
  140. package/dist/webui/.next/standalone/packages/webui/.next/static/css/045cc65741e38fbd.css +0 -3
  141. package/dist/webui/.next/static/PvkEd_BO6ZXxX99T_gUvs/_buildManifest.js +0 -1
  142. package/dist/webui/.next/static/chunks/179-78abc2eacbc41da9.js +0 -1
  143. package/dist/webui/.next/static/chunks/442-b1916bec348454b3.js +0 -1
  144. package/dist/webui/.next/static/chunks/544-c4a8f278ed1a25d7.js +0 -1
  145. package/dist/webui/.next/static/chunks/854-2a6d5a5297a15d52.js +0 -1
  146. package/dist/webui/.next/static/chunks/app/layout-dde711766eda096b.js +0 -1
  147. package/dist/webui/.next/static/chunks/app/page-5e94d5a49dc718d0.js +0 -1
  148. package/dist/webui/.next/static/chunks/app/playground/page-9ae40e0b219583e3.js +0 -1
  149. package/dist/webui/.next/static/css/045cc65741e38fbd.css +0 -3
  150. /package/dist/webui/.next/standalone/.next/static/{PvkEd_BO6ZXxX99T_gUvs → VoeDi3iuGmMdZu_kx-R2X}/_ssgManifest.js +0 -0
  151. /package/dist/webui/.next/standalone/packages/webui/.next/static/{PvkEd_BO6ZXxX99T_gUvs → VoeDi3iuGmMdZu_kx-R2X}/_ssgManifest.js +0 -0
  152. /package/dist/webui/.next/static/{PvkEd_BO6ZXxX99T_gUvs → VoeDi3iuGmMdZu_kx-R2X}/_ssgManifest.js +0 -0
@@ -3,23 +3,28 @@ import http from 'http';
3
3
  import { WebSocketServer } from 'ws';
4
4
  import { WebSocketEventSubscriber } from './websocket-subscriber.js';
5
5
  import { WebhookEventSubscriber } from './webhook-subscriber.js';
6
- import { logger, redactSensitiveData } from '@dexto/core';
6
+ import { logger, redactSensitiveData, deriveDisplayName } from '@dexto/core';
7
7
  import { setupA2ARoutes } from './a2a.js';
8
+ import { setupMemoryRoutes } from './memory/memory-handler.js';
8
9
  import { createMcpTransport, initializeMcpServer, initializeMcpServerApiEndpoints, } from './mcp/mcp_handler.js';
9
- import { createAgentCard, DextoAgent } from '@dexto/core';
10
- import { stringify as yamlStringify } from 'yaml';
10
+ import { createAgentCard, Dexto, getDexto } from '@dexto/core';
11
+ import { stringify as yamlStringify, parse as yamlParse } from 'yaml';
11
12
  import os from 'os';
13
+ import { promises as fs } from 'fs';
14
+ import path from 'path';
12
15
  import { expressRedactionMiddleware } from './middleware/expressRedactionMiddleware.js';
13
16
  import { z } from 'zod';
14
17
  import { LLMUpdatesSchema } from '@dexto/core';
15
18
  import { registerGracefulShutdown } from '../utils/graceful-shutdown.js';
16
19
  import { validateInputForLLM } from '@dexto/core';
17
20
  import { LLM_REGISTRY, LLM_PROVIDERS, LLM_ROUTERS, SUPPORTED_FILE_TYPES, getSupportedRoutersForProvider, supportsBaseURL, isRouterSupportedForModel, } from '@dexto/core';
18
- import { getProviderKeyStatus, saveProviderApiKey } from '@dexto/core';
21
+ import { getProviderKeyStatus, saveProviderApiKey, getPrimaryApiKeyEnvVar } from '@dexto/core';
19
22
  import { errorHandler } from './middleware/errorHandler.js';
20
23
  import { McpServerConfigSchema } from '@dexto/core';
21
24
  import { sendWebSocketError, sendWebSocketValidationError } from './websocket-error-handler.js';
22
- import { DextoValidationError, ErrorScope, ErrorType, AgentErrorCode, AgentError, } from '@dexto/core';
25
+ import { DextoValidationError, ErrorScope, ErrorType, AgentErrorCode, AgentError, AgentConfigSchema, ApprovalResponseSchema, } from '@dexto/core';
26
+ import { ResourceError } from '@dexto/core';
27
+ import { PromptError } from '@dexto/core';
23
28
  /**
24
29
  * Helper function to send JSON response with optional pretty printing
25
30
  */
@@ -34,58 +39,6 @@ function sendJsonResponse(res, data, statusCode = 200) {
34
39
  res.json(data);
35
40
  }
36
41
  }
37
- // Note: Request body may include a sessionId alongside LLM updates.
38
- // We parse sessionId separately and validate the rest against LLMUpdatesSchema
39
- /**
40
- * API request validation schemas based on actual usage
41
- */
42
- const MessageRequestSchema = z
43
- .object({
44
- message: z.string().optional(),
45
- sessionId: z.string().optional(),
46
- stream: z.boolean().optional(),
47
- imageData: z
48
- .object({
49
- base64: z.string(),
50
- mimeType: z.string(),
51
- })
52
- .optional(),
53
- fileData: z
54
- .object({
55
- base64: z.string(),
56
- mimeType: z.string(),
57
- filename: z.string().optional(),
58
- })
59
- .optional(),
60
- })
61
- .refine((data) => {
62
- const msg = (data.message ?? '').trim();
63
- // Must have either message text, image data, or file data
64
- return msg.length > 0 || !!data.imageData || !!data.fileData;
65
- }, { message: 'Must provide either message text, image data, or file data' });
66
- // Reuse existing MCP server config schema
67
- const McpServerRequestSchema = z.object({
68
- name: z.string().min(1, 'Server name is required'),
69
- config: McpServerConfigSchema,
70
- });
71
- // Based on existing WebhookRegistrationRequest interface
72
- const WebhookRequestSchema = z.object({
73
- url: z.string().url('Invalid URL format'),
74
- secret: z.string().optional(),
75
- description: z.string().optional(),
76
- });
77
- // Schema for search query parameters
78
- const SearchQuerySchema = z.object({
79
- q: z.string().min(1, 'Search query is required'),
80
- limit: z.coerce.number().min(1).max(100).optional(),
81
- offset: z.coerce.number().min(0).optional(),
82
- sessionId: z.string().optional(),
83
- role: z.enum(['user', 'assistant', 'system', 'tool']).optional(),
84
- });
85
- // Schema for cancel request parameters
86
- const CancelRequestSchema = z.object({
87
- sessionId: z.string().min(1, 'Session ID is required'),
88
- });
89
42
  // Helper to parse and validate request body
90
43
  function parseBody(schema, body) {
91
44
  return schema.parse(body); // ZodError handled by error middleware
@@ -95,11 +48,11 @@ function parseQuery(schema, query) {
95
48
  return schema.parse(query); // ZodError handled by error middleware
96
49
  }
97
50
  // TODO: API endpoint names are work in progress and might be refactored/renamed in future versions
98
- export async function initializeApi(agent, agentCardOverride, listenPort, agentName) {
51
+ export async function initializeApi(agent, agentCardOverride, listenPort, agentId) {
99
52
  const app = express();
100
53
  // Declare before registering shutdown hook to avoid TDZ on signals
101
54
  let activeAgent = agent;
102
- let activeAgentName = agentName || 'default';
55
+ let activeAgentId = agentId || 'default-agent';
103
56
  let isSwitchingAgent = false;
104
57
  registerGracefulShutdown(() => activeAgent);
105
58
  // this will apply middleware to all /api/llm/* routes
@@ -107,7 +60,14 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
107
60
  app.use('/api/config.yaml', expressRedactionMiddleware);
108
61
  const server = http.createServer(app);
109
62
  const wss = new WebSocketServer({ server });
110
- logger.info(`Initializing API server with agent: ${activeAgentName}`);
63
+ logger.info(`Initializing API server with agent: ${activeAgentId}`);
64
+ // Initialize event subscribers
65
+ const webSubscriber = new WebSocketEventSubscriber(wss);
66
+ const webhookSubscriber = new WebhookEventSubscriber();
67
+ // Register subscribers before starting agent
68
+ logger.info('Registering event subscribers with agent...');
69
+ activeAgent.registerSubscriber(webSubscriber);
70
+ activeAgent.registerSubscriber(webhookSubscriber);
111
71
  // Ensure the initial agent is started
112
72
  if (!activeAgent.isStarted() && !activeAgent.isStopped()) {
113
73
  logger.info('Starting initial agent...');
@@ -116,13 +76,6 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
116
76
  else if (activeAgent.isStopped()) {
117
77
  logger.warn('Initial agent is stopped, this may cause issues');
118
78
  }
119
- const webSubscriber = new WebSocketEventSubscriber(wss);
120
- logger.info('Setting up API event subscriptions...');
121
- webSubscriber.subscribe(activeAgent.agentEventBus);
122
- // Initialize webhook subscriber
123
- const webhookSubscriber = new WebhookEventSubscriber();
124
- logger.info('Setting up webhook event subscriptions...');
125
- webhookSubscriber.subscribe(activeAgent.agentEventBus);
126
79
  // Tool confirmation responses are handled by the main WebSocket handler below
127
80
  function ensureAgentAvailable() {
128
81
  // Gate requests during agent switching
@@ -141,38 +94,26 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
141
94
  throw AgentError.notStarted();
142
95
  }
143
96
  }
144
- async function switchAgentByName(name) {
97
+ async function switchAgentById(agentId) {
145
98
  if (isSwitchingAgent) {
146
99
  throw AgentError.apiValidationError('Agent switch already in progress');
147
100
  }
148
101
  isSwitchingAgent = true;
149
102
  let newAgent;
150
103
  try {
151
- // Use domain layer method to create new agent
152
- newAgent = await DextoAgent.createAgent(name);
153
- logger.info(`Starting new agent: ${name}`);
104
+ // Use orchestrator to create new agent
105
+ newAgent = await getDexto().createAgent(agentId);
106
+ // Register event subscribers with new agent before starting
107
+ logger.info('Registering event subscribers with new agent...');
108
+ newAgent.registerSubscriber(webSubscriber);
109
+ newAgent.registerSubscriber(webhookSubscriber);
110
+ logger.info(`Starting new agent: ${agentId}`);
154
111
  await newAgent.start();
155
- // Rewire event/webhook subscribers to new agent bus
156
- logger.info('Rewiring event subscribers...');
157
- try {
158
- webSubscriber.unsubscribe();
159
- }
160
- catch (_err) {
161
- logger.debug(`Failed to unsubscribe webSubscriber: ${_err instanceof Error ? _err.message : String(_err)}`);
162
- }
163
- webSubscriber.subscribe(newAgent.agentEventBus);
164
- try {
165
- webhookSubscriber.unsubscribe();
166
- }
167
- catch (_err) {
168
- logger.debug(`Failed to unsubscribe webhookSubscriber: ${_err instanceof Error ? _err.message : String(_err)}`);
169
- }
170
- webhookSubscriber.subscribe(newAgent.agentEventBus);
171
112
  // Stop previous agent last (only after new one is fully operational)
172
113
  const previousAgent = activeAgent;
173
114
  activeAgent = newAgent;
174
- activeAgentName = name;
175
- logger.info(`Successfully switched to agent: ${name}`);
115
+ activeAgentId = agentId;
116
+ logger.info(`Successfully switched to agent: ${agentId}`);
176
117
  // Now safely stop the previous agent
177
118
  try {
178
119
  if (previousAgent && previousAgent !== newAgent) {
@@ -184,10 +125,10 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
184
125
  logger.warn(`Stopping previous agent failed: ${err}`);
185
126
  // Don't throw here as the switch was successful
186
127
  }
187
- return { name };
128
+ return await resolveAgentInfo(agentId);
188
129
  }
189
130
  catch (error) {
190
- logger.error(`Failed to switch to agent '${name}': ${error instanceof Error ? error.message : String(error)}`, { error });
131
+ logger.error(`Failed to switch to agent '${agentId}': ${error instanceof Error ? error.message : String(error)}`, { error });
191
132
  // Clean up the failed new agent if it was created
192
133
  if (newAgent) {
193
134
  try {
@@ -204,10 +145,244 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
204
145
  }
205
146
  }
206
147
  // HTTP endpoints
148
+ // ---- Helpers (local) ----
149
+ /**
150
+ * Helper to decode URI components with consistent error handling.
151
+ *
152
+ * Wraps native decodeURIComponent() to provide domain-specific error handling.
153
+ * While normally 1-line wrappers are discouraged, this is justified because:
154
+ * 1. Native TS function with no control over error type
155
+ * 2. Ensures consistent ResourceError across all URI decoding
156
+ * 3. Reused in 5+ Zod transform schemas
157
+ */
158
+ function decodeUriComponent(encoded) {
159
+ try {
160
+ return decodeURIComponent(encoded);
161
+ }
162
+ catch (_error) {
163
+ throw ResourceError.invalidUriFormat(encoded, 'valid URI-encoded resource identifier');
164
+ }
165
+ }
166
+ /**
167
+ * Helper function to redact sensitive environment variables
168
+ */
169
+ function redactEnvValue(value) {
170
+ if (value && typeof value === 'string' && value.length > 0) {
171
+ return '[REDACTED]';
172
+ }
173
+ return String(value ?? '');
174
+ }
175
+ /**
176
+ * Helper function to redact environment variables in a server config
177
+ */
178
+ function redactServerEnvVars(serverConfig) {
179
+ if (serverConfig.type !== 'stdio' || !serverConfig.env) {
180
+ return serverConfig;
181
+ }
182
+ const redactedEnv = {};
183
+ for (const [key, value] of Object.entries(serverConfig.env)) {
184
+ redactedEnv[key] = redactEnvValue(value);
185
+ }
186
+ return {
187
+ ...serverConfig,
188
+ env: redactedEnv,
189
+ };
190
+ }
191
+ /**
192
+ * Helper function to redact all MCP servers configuration
193
+ */
194
+ function redactMcpServersConfig(mcpServers) {
195
+ if (!mcpServers) {
196
+ return {};
197
+ }
198
+ const redactedServers = {};
199
+ for (const [name, serverConfig] of Object.entries(mcpServers)) {
200
+ redactedServers[name] = redactServerEnvVars(serverConfig);
201
+ }
202
+ return redactedServers;
203
+ }
207
204
  // Health check endpoint
208
- app.get('/health', (req, res) => {
205
+ app.get('/health', (_req, res) => {
209
206
  res.status(200).send('OK');
210
207
  });
208
+ // Prompts listing endpoint (for WebUI slash command autocomplete)
209
+ app.get('/api/prompts', async (_req, res, next) => {
210
+ try {
211
+ ensureAgentAvailable();
212
+ const prompts = await activeAgent.listPrompts();
213
+ const list = Object.values(prompts);
214
+ return res.status(200).json({ prompts: list });
215
+ }
216
+ catch (error) {
217
+ return next(error);
218
+ }
219
+ });
220
+ const CustomPromptRequestSchema = z
221
+ .object({
222
+ name: z.string().min(1, 'Prompt name is required'),
223
+ title: z.string().optional(),
224
+ description: z.string().optional(),
225
+ content: z.string().min(1, 'Prompt content is required'),
226
+ arguments: z
227
+ .array(z
228
+ .object({
229
+ name: z.string().min(1, 'Argument name is required'),
230
+ description: z.string().optional(),
231
+ required: z.boolean().optional(),
232
+ })
233
+ .strict())
234
+ .optional(),
235
+ resource: z
236
+ .object({
237
+ base64: z.string().min(1, 'Resource data is required'),
238
+ mimeType: z.string().min(1, 'Resource MIME type is required'),
239
+ filename: z.string().optional(),
240
+ })
241
+ .strict()
242
+ .optional(),
243
+ })
244
+ .strict();
245
+ app.post('/api/prompts/custom', express.json({ limit: '10mb' }), async (req, res, next) => {
246
+ try {
247
+ ensureAgentAvailable();
248
+ const payload = parseBody(CustomPromptRequestSchema, req.body);
249
+ const promptArguments = payload.arguments
250
+ ?.map((arg) => ({
251
+ name: arg.name,
252
+ ...(arg.description ? { description: arg.description } : {}),
253
+ ...(typeof arg.required === 'boolean' ? { required: arg.required } : {}),
254
+ }))
255
+ .filter(Boolean);
256
+ const createPayload = {
257
+ name: payload.name,
258
+ content: payload.content,
259
+ ...(payload.title ? { title: payload.title } : {}),
260
+ ...(payload.description ? { description: payload.description } : {}),
261
+ ...(promptArguments && promptArguments.length > 0
262
+ ? { arguments: promptArguments }
263
+ : {}),
264
+ ...(payload.resource
265
+ ? {
266
+ resource: {
267
+ base64: payload.resource.base64,
268
+ mimeType: payload.resource.mimeType,
269
+ ...(payload.resource.filename
270
+ ? { filename: payload.resource.filename }
271
+ : {}),
272
+ },
273
+ }
274
+ : {}),
275
+ };
276
+ const prompt = await activeAgent.createCustomPrompt(createPayload);
277
+ return res.status(201).json({ prompt });
278
+ }
279
+ catch (error) {
280
+ return next(error);
281
+ }
282
+ });
283
+ const DeleteCustomPromptParamsSchema = z.object({
284
+ name: z
285
+ .string()
286
+ .min(1, 'Prompt name is required')
287
+ .transform((encoded) => decodeUriComponent(encoded)),
288
+ });
289
+ app.delete('/api/prompts/custom/:name', async (req, res, next) => {
290
+ try {
291
+ ensureAgentAvailable();
292
+ const { name } = parseQuery(DeleteCustomPromptParamsSchema, req.params);
293
+ await activeAgent.deleteCustomPrompt(name);
294
+ return res.status(204).send();
295
+ }
296
+ catch (error) {
297
+ return next(error);
298
+ }
299
+ });
300
+ // Get a specific prompt definition
301
+ const GetPromptDefinitionParamsSchema = z.object({
302
+ name: z.string().min(1, 'Prompt name is required'),
303
+ });
304
+ app.get('/api/prompts/:name', async (req, res, next) => {
305
+ try {
306
+ ensureAgentAvailable();
307
+ const { name } = parseQuery(GetPromptDefinitionParamsSchema, req.params);
308
+ const definition = await activeAgent.getPromptDefinition(name);
309
+ if (!definition)
310
+ throw PromptError.notFound(name);
311
+ return sendJsonResponse(res, { definition }, 200);
312
+ }
313
+ catch (error) {
314
+ return next(error);
315
+ }
316
+ });
317
+ // Resolve a prompt to text content (without sending to the agent)
318
+ // Supports optional args via query string. For natural language after the
319
+ // slash command, pass as `context`.
320
+ const ResolvePromptParamsSchema = z.object({
321
+ name: z.string().min(1, 'Prompt name is required'),
322
+ });
323
+ const ResolvePromptQuerySchema = z.object({
324
+ context: z.string().optional(),
325
+ args: z.string().optional(),
326
+ });
327
+ app.get('/api/prompts/:name/resolve', async (req, res, next) => {
328
+ try {
329
+ ensureAgentAvailable();
330
+ const { name: inputName } = parseQuery(ResolvePromptParamsSchema, req.params);
331
+ const { context, args: argsString } = parseQuery(ResolvePromptQuerySchema, req.query);
332
+ // Optional structured args in `args` query param as JSON
333
+ let parsedArgs;
334
+ if (argsString) {
335
+ try {
336
+ const parsed = JSON.parse(argsString);
337
+ if (parsed && typeof parsed === 'object') {
338
+ parsedArgs = parsed;
339
+ }
340
+ }
341
+ catch {
342
+ // Ignore malformed args JSON; continue with whatever we have
343
+ }
344
+ }
345
+ // Build options object with only defined values (exactOptionalPropertyTypes compatibility)
346
+ const options = {};
347
+ if (context !== undefined)
348
+ options.context = context;
349
+ if (parsedArgs !== undefined)
350
+ options.args = parsedArgs;
351
+ // Use DextoAgent's resolvePrompt method
352
+ const result = await activeAgent.resolvePrompt(inputName, options);
353
+ return sendJsonResponse(res, { text: result.text, resources: result.resources }, 200);
354
+ }
355
+ catch (error) {
356
+ return next(error);
357
+ }
358
+ });
359
+ // Note: We intentionally omit an "execute" endpoint; clients resolve prompts
360
+ // and then call the regular message endpoint, keeping server surface minimal.
361
+ // Message request schema (shared by /api/message and /api/message-sync)
362
+ const MessageRequestSchema = z
363
+ .object({
364
+ message: z.string().optional(),
365
+ sessionId: z.string().optional(),
366
+ stream: z.boolean().optional(),
367
+ imageData: z
368
+ .object({
369
+ base64: z.string(),
370
+ mimeType: z.string(),
371
+ })
372
+ .optional(),
373
+ fileData: z
374
+ .object({
375
+ base64: z.string(),
376
+ mimeType: z.string(),
377
+ filename: z.string().optional(),
378
+ })
379
+ .optional(),
380
+ })
381
+ .refine((data) => {
382
+ const msg = (data.message ?? '').trim();
383
+ // Must have either message text, image data, or file data
384
+ return msg.length > 0 || !!data.imageData || !!data.fileData;
385
+ }, { message: 'Must provide either message text, image data, or file data' });
211
386
  // JSON body size limit for message endpoints supporting base64 image/file payloads
212
387
  // Both /api/message and /api/message-sync accept base64 attachments; increased limit to avoid 413s.
213
388
  app.post('/api/message', express.json({ limit: process.env.MESSAGE_JSON_LIMIT || '10mb' }), async (req, res, next) => {
@@ -240,8 +415,12 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
240
415
  }
241
416
  });
242
417
  // Cancel an in-flight run for a session
418
+ const CancelRequestSchema = z.object({
419
+ sessionId: z.string().min(1, 'Session ID is required'),
420
+ });
243
421
  app.post('/api/sessions/:sessionId/cancel', async (req, res, next) => {
244
422
  try {
423
+ ensureAgentAvailable();
245
424
  const { sessionId } = parseQuery(CancelRequestSchema, req.params);
246
425
  const cancelled = await activeAgent.cancel(sessionId);
247
426
  if (!cancelled) {
@@ -286,11 +465,14 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
286
465
  return next(error);
287
466
  }
288
467
  });
468
+ const ResetRequestSchema = z.object({
469
+ sessionId: z.string().optional(),
470
+ });
289
471
  app.post('/api/reset', express.json(), async (req, res, next) => {
290
472
  logger.info('Received request via POST /api/reset');
291
473
  try {
292
474
  ensureAgentAvailable();
293
- const { sessionId } = parseBody(z.object({ sessionId: z.string().optional() }), req.body);
475
+ const { sessionId } = parseBody(ResetRequestSchema, req.body);
294
476
  await activeAgent.resetConversation(sessionId);
295
477
  return res.status(200).send({ status: 'reset initiated', sessionId });
296
478
  }
@@ -299,25 +481,40 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
299
481
  }
300
482
  });
301
483
  // Dynamic MCP server connection endpoint (legacy)
302
- app.post('/api/connect-server', express.json(), async (req, res, next) => {
303
- try {
304
- ensureAgentAvailable();
305
- const { name, config } = parseBody(McpServerRequestSchema, req.body);
306
- await activeAgent.connectMcpServer(name, config);
307
- logger.info(`Successfully connected to new server '${name}' via API request.`);
308
- return res.status(200).send({ status: 'connected', name });
309
- }
310
- catch (error) {
311
- return next(error);
312
- }
484
+ const McpServerRequestSchema = z.object({
485
+ name: z.string().min(1, 'Server name is required'),
486
+ config: McpServerConfigSchema,
487
+ persistToAgent: z.boolean().optional(),
313
488
  });
314
489
  // Add a new MCP server
315
490
  app.post('/api/mcp/servers', express.json(), async (req, res, next) => {
316
491
  try {
317
492
  ensureAgentAvailable();
318
- const { name, config } = parseBody(McpServerRequestSchema, req.body);
493
+ const { name, config, persistToAgent } = parseBody(McpServerRequestSchema, req.body);
494
+ // Connect the server
319
495
  await activeAgent.connectMcpServer(name, config);
320
- return res.status(201).json({ status: 'connected', name });
496
+ logger.info(`Successfully connected to new server '${name}' via API request.`);
497
+ // If persistToAgent is true, save to agent config file
498
+ if (persistToAgent === true) {
499
+ try {
500
+ // Get the current effective config to read existing mcpServers
501
+ const currentConfig = activeAgent.getEffectiveConfig();
502
+ // Create update with new server added to mcpServers
503
+ const updates = {
504
+ mcpServers: {
505
+ ...(currentConfig.mcpServers || {}),
506
+ [name]: config,
507
+ },
508
+ };
509
+ await activeAgent.updateAndSaveConfig(updates);
510
+ logger.info(`Saved server '${name}' to agent configuration file`);
511
+ }
512
+ catch (saveError) {
513
+ logger.warn(`Failed to save server '${name}' to agent config:`, saveError);
514
+ // Don't fail the request if saving fails - server is still connected
515
+ }
516
+ }
517
+ return res.status(200).send({ status: 'connected', name });
321
518
  }
322
519
  catch (error) {
323
520
  return next(error);
@@ -343,10 +540,13 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
343
540
  }
344
541
  });
345
542
  // Add MCP server tools listing endpoint
543
+ const ListServerToolsParamsSchema = z.object({
544
+ serverId: z.string().min(1, 'Server ID is required'),
545
+ });
346
546
  app.get('/api/mcp/servers/:serverId/tools', async (req, res, next) => {
347
547
  try {
348
548
  ensureAgentAvailable();
349
- const serverId = req.params.serverId;
549
+ const { serverId } = parseQuery(ListServerToolsParamsSchema, req.params);
350
550
  const client = activeAgent.getMcpClients().get(serverId);
351
551
  if (!client) {
352
552
  return res.status(404).json({ error: `Server '${serverId}' not found` });
@@ -365,10 +565,14 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
365
565
  }
366
566
  });
367
567
  // Endpoint to remove/disconnect an MCP server
568
+ const DeleteMcpServerParamsSchema = z.object({
569
+ serverId: z.string().min(1, 'Server ID is required'),
570
+ });
368
571
  app.delete('/api/mcp/servers/:serverId', async (req, res, next) => {
369
- const { serverId } = req.params;
370
- logger.info(`Received request to DELETE /api/mcp/servers/${serverId}`);
371
572
  try {
573
+ ensureAgentAvailable();
574
+ const { serverId } = parseQuery(DeleteMcpServerParamsSchema, req.params);
575
+ logger.info(`Received request to DELETE /api/mcp/servers/${serverId}`);
372
576
  // Check if server exists before attempting to disconnect
373
577
  const clientExists = activeAgent.getMcpClients().has(serverId) ||
374
578
  activeAgent.getMcpFailedConnections()[serverId];
@@ -384,18 +588,22 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
384
588
  }
385
589
  });
386
590
  // Execute an MCP tool via REST wrapper
591
+ const ExecuteMcpToolParamsSchema = z.object({
592
+ serverId: z.string().min(1, 'Server ID is required'),
593
+ toolName: z.string().min(1, 'Tool name is required'),
594
+ });
387
595
  app.post('/api/mcp/servers/:serverId/tools/:toolName/execute', express.json(), async (req, res, next) => {
388
- const { serverId, toolName } = req.params;
389
- // Verify server exists
390
- const client = activeAgent.getMcpClients().get(serverId);
391
- if (!client) {
392
- return res
393
- .status(404)
394
- .json({ success: false, error: `Server '${serverId}' not found` });
395
- }
396
596
  try {
397
- // Execute tool through the agent's unified wrapper method
398
- const rawResult = await activeAgent.executeTool(toolName, req.body);
597
+ const { serverId, toolName } = parseQuery(ExecuteMcpToolParamsSchema, req.params);
598
+ // Verify server exists
599
+ const client = activeAgent.getMcpClients().get(serverId);
600
+ if (!client) {
601
+ return res
602
+ .status(404)
603
+ .json({ success: false, error: `Server '${serverId}' not found` });
604
+ }
605
+ // Execute tool directly on the specified server
606
+ const rawResult = await client.callTool(toolName, req.body);
399
607
  // Return standardized result shape
400
608
  return res.json({ success: true, data: rawResult });
401
609
  }
@@ -403,6 +611,87 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
403
611
  return next(error);
404
612
  }
405
613
  });
614
+ // ============= RESOURCE MANAGEMENT ENDPOINTS =============
615
+ // Get all available resources
616
+ app.get('/api/resources', async (_req, res, next) => {
617
+ try {
618
+ ensureAgentAvailable();
619
+ const resources = await activeAgent.listResources();
620
+ return res.status(200).json({ ok: true, resources: Object.values(resources) });
621
+ }
622
+ catch (error) {
623
+ return next(error);
624
+ }
625
+ });
626
+ // Read resource content
627
+ const ReadResourceContentParamsSchema = z.object({
628
+ resourceId: z
629
+ .string()
630
+ .min(1, 'Resource ID is required')
631
+ .transform((encoded) => decodeUriComponent(encoded)),
632
+ });
633
+ app.get('/api/resources/:resourceId/content', async (req, res, next) => {
634
+ try {
635
+ ensureAgentAvailable();
636
+ const { resourceId } = parseQuery(ReadResourceContentParamsSchema, req.params);
637
+ const content = await activeAgent.readResource(resourceId);
638
+ return res.status(200).json({ ok: true, content });
639
+ }
640
+ catch (error) {
641
+ return next(error);
642
+ }
643
+ });
644
+ // Check if resource exists
645
+ const CheckResourceExistsParamsSchema = z.object({
646
+ resourceId: z
647
+ .string()
648
+ .min(1, 'Resource ID is required')
649
+ .transform((encoded) => decodeUriComponent(encoded)),
650
+ });
651
+ app.head('/api/resources/:resourceId', async (req, res, next) => {
652
+ try {
653
+ const { resourceId } = parseQuery(CheckResourceExistsParamsSchema, req.params);
654
+ const exists = await activeAgent.hasResource(resourceId);
655
+ return res.status(exists ? 200 : 404).end();
656
+ }
657
+ catch (error) {
658
+ return next(error);
659
+ }
660
+ });
661
+ // List resources for a specific MCP server
662
+ const ListServerResourcesParamsSchema = z.object({
663
+ serverId: z.string().min(1, 'Server ID is required'),
664
+ });
665
+ app.get('/api/mcp/servers/:serverId/resources', async (req, res, next) => {
666
+ try {
667
+ ensureAgentAvailable();
668
+ const { serverId } = parseQuery(ListServerResourcesParamsSchema, req.params);
669
+ const resources = await activeAgent.listResourcesForServer(serverId);
670
+ return sendJsonResponse(res, { success: true, resources }, 200);
671
+ }
672
+ catch (error) {
673
+ return next(error);
674
+ }
675
+ });
676
+ // Read resource content from specific MCP server
677
+ const ReadServerResourceContentParamsSchema = z.object({
678
+ serverId: z.string().min(1, 'Server ID is required'),
679
+ resourceId: z
680
+ .string()
681
+ .min(1, 'Resource ID is required')
682
+ .transform((encoded) => decodeUriComponent(encoded)),
683
+ });
684
+ app.get('/api/mcp/servers/:serverId/resources/:resourceId/content', async (req, res, next) => {
685
+ try {
686
+ const { serverId, resourceId } = parseQuery(ReadServerResourceContentParamsSchema, req.params);
687
+ const qualifiedUri = `mcp:${serverId}:${resourceId}`;
688
+ const content = await activeAgent.readResource(qualifiedUri);
689
+ return sendJsonResponse(res, { success: true, data: { content } }, 200);
690
+ }
691
+ catch (error) {
692
+ return next(error);
693
+ }
694
+ });
406
695
  // WebSocket handling
407
696
  // handle inbound client messages over WebSocket
408
697
  wss.on('connection', (ws) => {
@@ -424,9 +713,16 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
424
713
  }
425
714
  try {
426
715
  const data = JSON.parse(messageString);
427
- if (data.type === 'toolConfirmationResponse' && data.data) {
428
- // Route confirmation back via AgentEventBus and do not broadcast an error
429
- activeAgent.agentEventBus.emit('dexto:toolConfirmationResponse', data.data);
716
+ if (data.type === 'approvalResponse' && data.data) {
717
+ // Validate the approval response payload with Zod schema
718
+ const validationResult = ApprovalResponseSchema.safeParse(data.data);
719
+ if (!validationResult.success) {
720
+ logger.warn(`Received invalid approval response payload: ${validationResult.error.message}`);
721
+ // Do not emit invalid payloads
722
+ return;
723
+ }
724
+ // Route validated approval response back via AgentEventBus
725
+ activeAgent.agentEventBus.emit('dexto:approvalResponse', validationResult.data);
430
726
  return;
431
727
  }
432
728
  else if (data.type === 'message' &&
@@ -458,6 +754,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
458
754
  // Check if agent is available before processing
459
755
  try {
460
756
  ensureAgentAvailable();
757
+ logger.debug('Agent availability check passed');
461
758
  }
462
759
  catch (error) {
463
760
  logger.error(`Agent not available for WebSocket message: ${error}`);
@@ -465,7 +762,9 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
465
762
  return;
466
763
  }
467
764
  // Comprehensive input validation
765
+ logger.debug('Getting effective config for validation');
468
766
  const currentConfig = activeAgent.getEffectiveConfig(sessionId);
767
+ logger.debug('Validating input for LLM');
469
768
  const validation = validateInputForLLM({
470
769
  text: data.content,
471
770
  ...(imageDataInput && { imageData: imageDataInput }),
@@ -501,7 +800,9 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
501
800
  sendWebSocketError(ws, hierarchicalError, sessionId);
502
801
  return;
503
802
  }
803
+ logger.debug('Validation passed, calling activeAgent.run()');
504
804
  await activeAgent.run(data.content, imageDataInput, fileDataInput, sessionId, stream);
805
+ logger.debug('activeAgent.run() completed');
505
806
  }
506
807
  else if (data.type === 'reset') {
507
808
  const sessionId = data.sessionId;
@@ -587,6 +888,8 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
587
888
  const _agentVersion = agentCardData.version;
588
889
  // Setup A2A routes
589
890
  setupA2ARoutes(app, agentCardData);
891
+ // Setup Memory routes
892
+ app.use('/api/memory', setupMemoryRoutes(activeAgent));
590
893
  // --- Initialize and Setup MCP Server and Endpoints ---
591
894
  // Get transport type from environment variable or default to http
592
895
  try {
@@ -607,14 +910,30 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
607
910
  });
608
911
  }
609
912
  // ===== Agents API =====
913
+ // TODO: Consider moving to AgentRegistry.getAgentInfo() if this pattern is needed
914
+ // outside of API response formatting (e.g., in CLI commands, WebUI hooks, client SDK)
915
+ /**
916
+ * Helper to resolve agent ID to { id, name } by looking up in registry
917
+ * @param agentId - The agent ID to resolve
918
+ * @returns Object with id and name (uses deriveDisplayName as fallback)
919
+ */
920
+ async function resolveAgentInfo(agentId) {
921
+ const agents = await Dexto.listAgents();
922
+ const agent = agents.installed.find((a) => a.id === agentId) ??
923
+ agents.available.find((a) => a.id === agentId);
924
+ return {
925
+ id: agentId,
926
+ name: agent?.name ?? deriveDisplayName(agentId),
927
+ };
928
+ }
610
929
  app.get('/api/agents', async (_req, res, next) => {
611
930
  try {
612
- ensureAgentAvailable();
613
- const agents = await activeAgent.listAgents();
931
+ const agents = await Dexto.listAgents();
932
+ const currentId = activeAgentId ?? null;
614
933
  return sendJsonResponse(res, {
615
934
  installed: agents.installed,
616
935
  available: agents.available,
617
- current: { name: activeAgentName ?? 'default' },
936
+ current: currentId ? await resolveAgentInfo(currentId) : { id: null, name: null },
618
937
  });
619
938
  }
620
939
  catch (error) {
@@ -623,20 +942,98 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
623
942
  });
624
943
  app.get('/api/agents/current', async (_req, res, next) => {
625
944
  try {
626
- // TODO: Consider exposing agent.getName() method or config.name for more accurate tracking
627
- return sendJsonResponse(res, { name: activeAgentName ?? 'default' });
945
+ const currentId = activeAgentId ?? null;
946
+ if (!currentId) {
947
+ return sendJsonResponse(res, { id: null, name: null });
948
+ }
949
+ return sendJsonResponse(res, await resolveAgentInfo(currentId));
628
950
  }
629
951
  catch (error) {
630
952
  return next(error);
631
953
  }
632
954
  });
633
- const AgentNameSchema = z.object({ name: z.string().min(1) }).strict();
955
+ const AgentIdentifierSchema = z
956
+ .object({
957
+ id: z
958
+ .string()
959
+ .min(1, 'Agent id is required')
960
+ .describe('Unique agent identifier (e.g., "database-agent")'),
961
+ })
962
+ .strict();
963
+ const UninstallAgentSchema = z
964
+ .object({
965
+ id: z
966
+ .string()
967
+ .min(1, 'Agent id is required')
968
+ .describe('Unique agent identifier to uninstall'),
969
+ force: z
970
+ .boolean()
971
+ .default(false)
972
+ .describe('Force uninstall even if agent is currently active'),
973
+ })
974
+ .strict();
975
+ // Schema for custom agent installation (CLI/automation entrypoint)
976
+ const CustomAgentInstallSchema = z
977
+ .object({
978
+ id: z.string().min(1, 'Agent id is required').describe('Unique agent identifier'),
979
+ name: z.string().optional().describe('Display name (defaults to derived from id)'),
980
+ sourcePath: z.string().min(1).describe('Path to agent configuration file or directory'),
981
+ metadata: z
982
+ .object({
983
+ description: z
984
+ .string()
985
+ .min(1)
986
+ .describe('Human-readable description of the agent'),
987
+ author: z.string().min(1).describe('Agent author or organization name'),
988
+ tags: z.array(z.string()).describe('Tags for categorizing the agent'),
989
+ main: z
990
+ .string()
991
+ .optional()
992
+ .describe('Main configuration file name within source directory'),
993
+ })
994
+ .strict(),
995
+ injectPreferences: z
996
+ .boolean()
997
+ .default(true)
998
+ .describe('Whether to inject user preferences into agent config'),
999
+ })
1000
+ .strict()
1001
+ .transform((value) => {
1002
+ const displayName = value.name?.trim() || deriveDisplayName(value.id);
1003
+ return {
1004
+ id: value.id,
1005
+ displayName,
1006
+ sourcePath: value.sourcePath,
1007
+ metadata: value.metadata,
1008
+ injectPreferences: value.injectPreferences,
1009
+ };
1010
+ });
634
1011
  app.post('/api/agents/install', express.json(), async (req, res, next) => {
635
1012
  try {
636
- ensureAgentAvailable();
637
- const { name } = AgentNameSchema.parse(req.body);
638
- await activeAgent.installAgent(name);
639
- return sendJsonResponse(res, { installed: true, name }, 201);
1013
+ // Check if this is a custom agent installation (has sourcePath and metadata)
1014
+ if (req.body.sourcePath && req.body.metadata) {
1015
+ const { id, displayName, sourcePath, metadata, injectPreferences } = CustomAgentInstallSchema.parse(req.body);
1016
+ // Clean metadata to match exact optional property types
1017
+ await Dexto.installCustomAgent(id, sourcePath, {
1018
+ name: displayName,
1019
+ description: metadata.description,
1020
+ author: metadata.author,
1021
+ tags: metadata.tags,
1022
+ ...(metadata.main ? { main: metadata.main } : {}),
1023
+ }, injectPreferences);
1024
+ return sendJsonResponse(res, { installed: true, id, name: displayName, type: 'custom' }, 201);
1025
+ }
1026
+ else {
1027
+ // Registry agent installation
1028
+ const { id } = parseBody(AgentIdentifierSchema, req.body);
1029
+ await Dexto.installAgent(id);
1030
+ const agentInfo = await resolveAgentInfo(id);
1031
+ return sendJsonResponse(res, {
1032
+ installed: true,
1033
+ ...agentInfo,
1034
+ type: 'builtin',
1035
+ }, 201);
1036
+ }
640
1037
  }
641
1038
  catch (error) {
642
1039
  return next(error);
@@ -644,8 +1041,8 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
644
1041
  });
645
1042
  app.post('/api/agents/switch', express.json(), async (req, res, next) => {
646
1043
  try {
647
- const { name } = AgentNameSchema.parse(req.body);
648
- const result = await switchAgentByName(name);
1044
+ const { id } = parseBody(AgentIdentifierSchema, req.body);
1045
+ const result = await switchAgentById(id);
649
1046
  return sendJsonResponse(res, { switched: true, ...result });
650
1047
  }
651
1048
  catch (error) {
@@ -657,48 +1054,342 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
657
1054
  return next(error);
658
1055
  }
659
1056
  });
1057
+ app.post('/api/agents/validate-name', express.json(), async (req, res, next) => {
1058
+ try {
1059
+ const { id } = parseBody(AgentIdentifierSchema, req.body);
1060
+ const agents = await Dexto.listAgents();
1061
+ // Check if name exists in installed agents
1062
+ const installedAgent = agents.installed.find((a) => a.id === id);
1063
+ if (installedAgent) {
1064
+ return sendJsonResponse(res, {
1065
+ valid: false,
1066
+ conflict: installedAgent.type,
1067
+ message: `Agent id '${id}' already exists (${installedAgent.type})`,
1068
+ });
1069
+ }
1070
+ // Check if name exists in available agents (registry)
1071
+ const availableAgent = agents.available.find((a) => a.id === id);
1072
+ if (availableAgent) {
1073
+ return sendJsonResponse(res, {
1074
+ valid: false,
1075
+ conflict: availableAgent.type,
1076
+ message: `Agent id '${id}' conflicts with ${availableAgent.type} agent`,
1077
+ });
1078
+ }
1079
+ return sendJsonResponse(res, { valid: true });
1080
+ }
1081
+ catch (error) {
1082
+ return next(error);
1083
+ }
1084
+ });
1085
+ app.post('/api/agents/uninstall', express.json(), async (req, res, next) => {
1086
+ try {
1087
+ const { id, force } = parseBody(UninstallAgentSchema, req.body);
1088
+ await Dexto.uninstallAgent(id, force);
1089
+ return sendJsonResponse(res, { uninstalled: true, id });
1090
+ }
1091
+ catch (error) {
1092
+ return next(error);
1093
+ }
1094
+ });
1095
+ // Schema for creating custom agents via UI
1096
+ const CustomAgentCreateSchema = z
1097
+ .object({
1098
+ // Registry metadata
1099
+ id: z
1100
+ .string()
1101
+ .min(1, 'Agent ID is required')
1102
+ .regex(/^[a-z0-9-]+$/, 'Agent ID must contain only lowercase letters, numbers, and hyphens')
1103
+ .describe('Unique agent identifier'),
1104
+ name: z
1105
+ .string()
1106
+ .min(1, 'Agent name is required')
1107
+ .describe('Display name for the agent'),
1108
+ description: z
1109
+ .string()
1110
+ .min(1, 'Description is required')
1111
+ .describe('One-line description of the agent'),
1112
+ author: z.string().optional().describe('Author or organization'),
1113
+ tags: z.array(z.string()).default([]).describe('Tags for discovery'),
1114
+ // Agent configuration
1115
+ llm: z
1116
+ .object({
1117
+ provider: z.enum(LLM_PROVIDERS).describe('LLM provider id'),
1118
+ model: z.string().min(1, 'Model is required').describe('Model name'),
1119
+ apiKey: z
1120
+ .string()
1121
+ .optional()
1122
+ .describe('API key or environment variable reference (e.g., $OPENAI_API_KEY)'),
1123
+ })
1124
+ .strict()
1125
+ .describe('LLM configuration'),
1126
+ systemPrompt: z
1127
+ .string()
1128
+ .min(1, 'System prompt is required')
1129
+ .describe('System prompt for the agent'),
1130
+ })
1131
+ .strict();
1132
+ // Create a new custom agent from UI
1133
+ app.post('/api/agents/custom/create', express.json(), async (req, res, next) => {
1134
+ try {
1135
+ const { id, name, description, author, tags, llm, systemPrompt } = parseBody(CustomAgentCreateSchema, req.body);
1136
+ const provider = llm.provider;
1137
+ // Handle API key: if it's a raw key, store securely and use env var reference
1138
+ let apiKeyRef;
1139
+ if (llm.apiKey && !llm.apiKey.startsWith('$')) {
1140
+ // Raw API key provided - store securely and get env var reference
1141
+ const meta = await saveProviderApiKey(provider, llm.apiKey, process.cwd());
1142
+ apiKeyRef = `$${meta.envVar}`;
1143
+ logger.info(`Stored API key securely for ${provider}, using env var: ${meta.envVar}`);
1144
+ }
1145
+ else if (llm.apiKey) {
1146
+ // Already an env var reference
1147
+ apiKeyRef = llm.apiKey;
1148
+ }
1149
+ // Create agent YAML content (with env var reference instead of raw key)
1150
+ const agentConfig = {
1151
+ llm: {
1152
+ provider,
1153
+ model: llm.model,
1154
+ apiKey: apiKeyRef || `$${getPrimaryApiKeyEnvVar(provider)}`,
1155
+ },
1156
+ systemPrompt,
1157
+ };
1158
+ const yamlContent = yamlStringify(agentConfig);
1159
+ logger.info(`Creating agent config for ${id}:`, { agentConfig, yamlContent });
1160
+ // Create temporary file
1161
+ const tmpDir = os.tmpdir();
1162
+ const tmpFile = path.join(tmpDir, `${id}-${Date.now()}.yml`);
1163
+ await fs.writeFile(tmpFile, yamlContent, 'utf-8');
1164
+ try {
1165
+ // Install the custom agent
1166
+ await Dexto.installCustomAgent(id, tmpFile, {
1167
+ name,
1168
+ description,
1169
+ author: author || 'Custom',
1170
+ tags: tags || [],
1171
+ }, false // Don't inject preferences
1172
+ );
1173
+ // Clean up temp file
1174
+ await fs.unlink(tmpFile).catch(() => { });
1175
+ return sendJsonResponse(res, { created: true, id, name }, 201);
1176
+ }
1177
+ catch (installError) {
1178
+ // Clean up temp file on error
1179
+ await fs.unlink(tmpFile).catch(() => { });
1180
+ throw installError;
1181
+ }
1182
+ }
1183
+ catch (error) {
1184
+ return next(error);
1185
+ }
1186
+ });
660
1187
  // Configuration export endpoint
661
- /**
662
- * Helper function to redact sensitive environment variables
663
- */
664
- function redactEnvValue(value) {
665
- if (value && typeof value === 'string' && value.length > 0) {
666
- return '[REDACTED]';
1188
+ // Get default greeting (for UI consumption)
1189
+ const GetGreetingQuerySchema = z.object({
1190
+ sessionId: z.string().optional(),
1191
+ });
1192
+ app.get('/api/greeting', async (req, res, next) => {
1193
+ try {
1194
+ ensureAgentAvailable();
1195
+ const { sessionId } = parseQuery(GetGreetingQuerySchema, req.query);
1196
+ const config = activeAgent.getEffectiveConfig(sessionId);
1197
+ res.json({ greeting: config.greeting });
667
1198
  }
668
- return value;
669
- }
670
- /**
671
- * Helper function to redact environment variables in a server config
672
- */
673
- function redactServerEnvVars(serverConfig) {
674
- if (!serverConfig.env) {
675
- return serverConfig;
1199
+ catch (error) {
1200
+ return next(error);
676
1201
  }
677
- const redactedEnv = {};
678
- for (const [key, value] of Object.entries(serverConfig.env)) {
679
- redactedEnv[key] = redactEnvValue(value);
1202
+ });
1203
+ // ============= AGENT CONFIGURATION MANAGEMENT =============
1204
+ // Get agent file path
1205
+ app.get('/api/agent/path', async (req, res, next) => {
1206
+ try {
1207
+ ensureAgentAvailable();
1208
+ const agentPath = activeAgent.getAgentFilePath();
1209
+ const relativePath = path.basename(agentPath);
1210
+ const ext = path.extname(agentPath);
1211
+ const name = path.basename(agentPath, ext);
1212
+ res.json({
1213
+ path: agentPath,
1214
+ relativePath,
1215
+ name,
1216
+ isDefault: name === 'default-agent',
1217
+ });
680
1218
  }
681
- return {
682
- ...serverConfig,
683
- env: redactedEnv,
684
- };
685
- }
686
- /**
687
- * Helper function to redact all MCP servers configuration
688
- */
689
- function redactMcpServersConfig(mcpServers) {
690
- if (!mcpServers) {
691
- return {};
1219
+ catch (error) {
1220
+ return next(error);
692
1221
  }
693
- const redactedServers = {};
694
- for (const [name, serverConfig] of Object.entries(mcpServers)) {
695
- redactedServers[name] = redactServerEnvVars(serverConfig);
1222
+ });
1223
+ // Get editable agent configuration (non-redacted YAML)
1224
+ app.get('/api/agent/config', async (req, res, next) => {
1225
+ try {
1226
+ ensureAgentAvailable();
1227
+ // Get the agent file path being used
1228
+ const agentPath = activeAgent.getAgentFilePath();
1229
+ // Read raw YAML from file (not expanded env vars)
1230
+ const yamlContent = await fs.readFile(agentPath, 'utf-8');
1231
+ // Get metadata
1232
+ const stats = await fs.stat(agentPath);
1233
+ res.json({
1234
+ yaml: yamlContent,
1235
+ path: agentPath,
1236
+ relativePath: path.basename(agentPath),
1237
+ lastModified: stats.mtime,
1238
+ warnings: [
1239
+ 'Environment variables ($VAR) will be resolved at runtime',
1240
+ 'API keys should use environment variables',
1241
+ ],
1242
+ });
696
1243
  }
697
- return redactedServers;
698
- }
699
- app.get('/api/config.yaml', async (req, res, next) => {
1244
+ catch (error) {
1245
+ return next(error);
1246
+ }
1247
+ });
1248
+ // Validate agent configuration without saving
1249
+ const AgentConfigValidateSchema = z.object({
1250
+ yaml: z.string().min(1, 'YAML content is required'),
1251
+ });
1252
+ app.post('/api/agent/validate', express.json(), async (req, res, next) => {
1253
+ try {
1254
+ ensureAgentAvailable();
1255
+ const { yaml } = parseBody(AgentConfigValidateSchema, req.body);
1256
+ // Parse YAML
1257
+ let parsed;
1258
+ try {
1259
+ parsed = yamlParse(yaml);
1260
+ }
1261
+ catch (parseError) {
1262
+ return res.json({
1263
+ valid: false,
1264
+ errors: [
1265
+ {
1266
+ line: parseError.linePos?.[0]?.line || 1,
1267
+ column: parseError.linePos?.[0]?.col || 1,
1268
+ message: parseError.message,
1269
+ code: 'YAML_PARSE_ERROR',
1270
+ },
1271
+ ],
1272
+ warnings: [],
1273
+ });
1274
+ }
1275
+ // Validate against schema
1276
+ const result = AgentConfigSchema.safeParse(parsed);
1277
+ if (!result.success) {
1278
+ const errors = result.error.errors.map((err) => ({
1279
+ path: err.path.join('.'),
1280
+ message: err.message,
1281
+ code: 'SCHEMA_VALIDATION_ERROR',
1282
+ }));
1283
+ return res.json({
1284
+ valid: false,
1285
+ errors,
1286
+ warnings: [],
1287
+ });
1288
+ }
1289
+ // Check for warnings (e.g., plain text API keys)
1290
+ const warnings = [];
1291
+ if (parsed.llm?.apiKey && !parsed.llm.apiKey.startsWith('$')) {
1292
+ warnings.push({
1293
+ path: 'llm.apiKey',
1294
+ message: 'Consider using environment variable instead of plain text',
1295
+ code: 'SECURITY_WARNING',
1296
+ });
1297
+ }
1298
+ res.json({
1299
+ valid: true,
1300
+ errors: [],
1301
+ warnings,
1302
+ });
1303
+ }
1304
+ catch (error) {
1305
+ return next(error);
1306
+ }
1307
+ });
1308
+ // Save agent configuration
1309
+ const AgentConfigSaveSchema = z.object({
1310
+ yaml: z.string().min(1, 'YAML content is required'),
1311
+ });
1312
+ app.post('/api/agent/config', express.json(), async (req, res, next) => {
700
1313
  try {
701
- const sessionId = req.query.sessionId;
1314
+ ensureAgentAvailable();
1315
+ const { yaml } = parseBody(AgentConfigSaveSchema, req.body);
1316
+ // Validate YAML syntax first
1317
+ let parsed;
1318
+ try {
1319
+ parsed = yamlParse(yaml);
1320
+ }
1321
+ catch (parseError) {
1322
+ throw new DextoValidationError([
1323
+ {
1324
+ code: AgentErrorCode.INVALID_CONFIG,
1325
+ message: `Invalid YAML syntax: ${parseError.message}`,
1326
+ scope: ErrorScope.AGENT,
1327
+ type: ErrorType.USER,
1328
+ severity: 'error',
1329
+ },
1330
+ ]);
1331
+ }
1332
+ // Validate schema
1333
+ const validationResult = AgentConfigSchema.safeParse(parsed);
1334
+ if (!validationResult.success) {
1335
+ throw new DextoValidationError(validationResult.error.errors.map((err) => ({
1336
+ code: AgentErrorCode.INVALID_CONFIG,
1337
+ message: `${err.path.join('.')}: ${err.message}`,
1338
+ scope: ErrorScope.AGENT,
1339
+ type: ErrorType.USER,
1340
+ severity: 'error',
1341
+ })));
1342
+ }
1343
+ // Get target file path
1344
+ const agentPath = activeAgent.getAgentFilePath();
1345
+ // Create backup
1346
+ const backupPath = `${agentPath}.backup`;
1347
+ await fs.copyFile(agentPath, backupPath);
1348
+ try {
1349
+ // Write new config
1350
+ await fs.writeFile(agentPath, yaml, 'utf-8');
1351
+ // Reload configuration to detect what changed
1352
+ const reloadResult = await activeAgent.reloadConfig();
1353
+ // If any changes require restart, automatically restart the agent
1354
+ if (reloadResult.restartRequired.length > 0) {
1355
+ logger.info(`Auto-restarting agent to apply changes: ${reloadResult.restartRequired.join(', ')}`);
1356
+ await activeAgent.restart();
1357
+ logger.info('Agent restarted successfully with all event subscribers reconnected');
1358
+ }
1359
+ // Clean up backup file after successful save
1360
+ await fs.unlink(backupPath).catch(() => {
1361
+ // Ignore errors if backup file doesn't exist
1362
+ });
1363
+ logger.info(`Agent configuration saved and applied: ${agentPath}`);
1364
+ res.json({
1365
+ ok: true,
1366
+ path: agentPath,
1367
+ reloaded: true,
1368
+ restarted: reloadResult.restartRequired.length > 0,
1369
+ changesApplied: reloadResult.restartRequired,
1370
+ message: reloadResult.restartRequired.length > 0
1371
+ ? 'Configuration saved and applied successfully (agent restarted)'
1372
+ : 'Configuration saved successfully (no changes detected)',
1373
+ });
1374
+ }
1375
+ catch (writeError) {
1376
+ // Restore backup on error
1377
+ await fs.copyFile(backupPath, agentPath);
1378
+ throw writeError;
1379
+ }
1380
+ }
1381
+ catch (error) {
1382
+ return next(error);
1383
+ }
1384
+ });
1385
+ // Export effective agent configuration (with masked secrets)
1386
+ const ExportConfigQuerySchema = z.object({
1387
+ sessionId: z.string().optional(),
1388
+ });
1389
+ app.get('/api/agent/config/export', async (req, res, next) => {
1390
+ try {
1391
+ ensureAgentAvailable();
1392
+ const { sessionId } = parseQuery(ExportConfigQuerySchema, req.query);
702
1393
  const config = activeAgent.getEffectiveConfig(sessionId);
703
1394
  // Export config as YAML, masking sensitive data
704
1395
  const maskedConfig = {
@@ -717,21 +1408,14 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
717
1408
  return next(error);
718
1409
  }
719
1410
  });
720
- // Get default greeting (for UI consumption)
721
- app.get('/api/greeting', async (req, res, next) => {
722
- try {
723
- const sessionId = req.query.sessionId;
724
- const config = activeAgent.getEffectiveConfig(sessionId);
725
- res.json({ greeting: config.greeting });
726
- }
727
- catch (error) {
728
- return next(error);
729
- }
730
- });
1411
+ // ============= LLM MANAGEMENT =============
731
1412
  // Get current LLM configuration
1413
+ const GetCurrentLLMQuerySchema = z.object({
1414
+ sessionId: z.string().optional(),
1415
+ });
732
1416
  app.get('/api/llm/current', async (req, res, next) => {
733
1417
  try {
734
- const { sessionId } = req.query;
1418
+ const { sessionId } = parseQuery(GetCurrentLLMQuerySchema, req.query);
735
1419
  // Use session-specific config if sessionId is provided, otherwise use default
736
1420
  const currentConfig = sessionId
737
1421
  ? activeAgent.getEffectiveConfig(sessionId).llm
@@ -751,37 +1435,35 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
751
1435
  return next(error);
752
1436
  }
753
1437
  });
754
- // (Deprecated) /api/llm/providers has been replaced by /api/llm/catalog
755
1438
  // LLM Catalog: providers, models, and API key presence (with filters)
1439
+ const LLMCatalogQuerySchema = z.object({
1440
+ provider: z
1441
+ .union([z.string(), z.array(z.string())])
1442
+ .optional()
1443
+ .transform((value) => Array.isArray(value) ? value : value ? value.split(',') : undefined),
1444
+ hasKey: z
1445
+ .union([z.literal('true'), z.literal('false'), z.literal('1'), z.literal('0')])
1446
+ .optional()
1447
+ .transform((raw) => raw === 'true' || raw === '1'
1448
+ ? true
1449
+ : raw === 'false' || raw === '0'
1450
+ ? false
1451
+ : undefined),
1452
+ router: z.enum(LLM_ROUTERS).optional(),
1453
+ fileType: z.enum(SUPPORTED_FILE_TYPES).optional(),
1454
+ defaultOnly: z
1455
+ .union([z.literal('true'), z.literal('false'), z.literal('1'), z.literal('0')])
1456
+ .optional()
1457
+ .transform((raw) => raw === 'true' || raw === '1'
1458
+ ? true
1459
+ : raw === 'false' || raw === '0'
1460
+ ? false
1461
+ : undefined),
1462
+ mode: z.enum(['grouped', 'flat']).default('grouped'),
1463
+ });
756
1464
  app.get('/api/llm/catalog', async (req, res, next) => {
757
1465
  try {
758
- // Parse query parameters with Zod
759
- const QuerySchema = z.object({
760
- provider: z
761
- .union([z.string(), z.array(z.string())])
762
- .optional()
763
- .transform((value) => Array.isArray(value) ? value : value ? value.split(',') : undefined),
764
- hasKey: z
765
- .union([z.literal('true'), z.literal('false'), z.literal('1'), z.literal('0')])
766
- .optional()
767
- .transform((raw) => raw === 'true' || raw === '1'
768
- ? true
769
- : raw === 'false' || raw === '0'
770
- ? false
771
- : undefined),
772
- router: z.enum(LLM_ROUTERS).optional(),
773
- fileType: z.enum(SUPPORTED_FILE_TYPES).optional(),
774
- defaultOnly: z
775
- .union([z.literal('true'), z.literal('false'), z.literal('1'), z.literal('0')])
776
- .optional()
777
- .transform((raw) => raw === 'true' || raw === '1'
778
- ? true
779
- : raw === 'false' || raw === '0'
780
- ? false
781
- : undefined),
782
- mode: z.enum(['grouped', 'flat']).optional().default('grouped'),
783
- });
784
- const queryParams = QuerySchema.parse(req.query);
1466
+ const queryParams = LLMCatalogQuerySchema.parse(req.query);
785
1467
  const providers = {};
786
1468
  for (const provider of LLM_PROVIDERS) {
787
1469
  const info = LLM_REGISTRY[provider];
@@ -876,13 +1558,13 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
876
1558
  }
877
1559
  });
878
1560
  // Save provider API key (never echoes the key back)
1561
+ const SaveProviderApiKeyBodySchema = z.object({
1562
+ provider: z.enum(LLM_PROVIDERS),
1563
+ apiKey: z.string().min(1, 'API key is required'),
1564
+ });
879
1565
  app.post('/api/llm/key', express.json({ limit: '4kb' }), async (req, res, next) => {
880
1566
  try {
881
- const schema = z.object({
882
- provider: z.enum(LLM_PROVIDERS),
883
- apiKey: z.string().min(1, 'API key is required'),
884
- });
885
- const body = schema.parse(req.body);
1567
+ const body = parseBody(SaveProviderApiKeyBodySchema, req.body);
886
1568
  const meta = await saveProviderApiKey(body.provider, body.apiKey, process.cwd());
887
1569
  return sendJsonResponse(res, { ok: true, provider: body.provider, envVar: meta.envVar }, 200);
888
1570
  }
@@ -891,11 +1573,15 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
891
1573
  }
892
1574
  });
893
1575
  // Switch LLM configuration
1576
+ const SwitchLLMBodySchema = z
1577
+ .object({
1578
+ sessionId: z.string().optional(),
1579
+ })
1580
+ .passthrough(); // Allow additional LLM config fields
894
1581
  app.post('/api/llm/switch', express.json(), async (req, res, next) => {
895
1582
  try {
896
- const body = (req.body ?? {});
897
- const sessionId = typeof body.sessionId === 'string' ? body.sessionId : undefined;
898
- const { sessionId: _omit, ...llmCandidate } = body;
1583
+ const parsed = parseBody(SwitchLLMBodySchema, req.body);
1584
+ const { sessionId, ...llmCandidate } = parsed;
899
1585
  const llmConfig = LLMUpdatesSchema.parse(llmCandidate);
900
1586
  const config = await activeAgent.switchLLM(llmConfig, sessionId);
901
1587
  return res.status(200).json({ config, sessionId });
@@ -906,7 +1592,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
906
1592
  });
907
1593
  // Session Management APIs
908
1594
  // List all active sessions
909
- app.get('/api/sessions', async (req, res, next) => {
1595
+ app.get('/api/sessions', async (_req, res, next) => {
910
1596
  try {
911
1597
  const sessionIds = await activeAgent.listSessions();
912
1598
  const sessions = await Promise.all(sessionIds.map(async (id) => {
@@ -917,6 +1603,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
917
1603
  createdAt: metadata?.createdAt || null,
918
1604
  lastActivity: metadata?.lastActivity || null,
919
1605
  messageCount: metadata?.messageCount || 0,
1606
+ title: metadata?.title || null,
920
1607
  };
921
1608
  }
922
1609
  catch (_error) {
@@ -926,6 +1613,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
926
1613
  createdAt: null,
927
1614
  lastActivity: null,
928
1615
  messageCount: 0,
1616
+ title: null,
929
1617
  };
930
1618
  }
931
1619
  }));
@@ -936,9 +1624,12 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
936
1624
  }
937
1625
  });
938
1626
  // Create a new session
1627
+ const CreateSessionBodySchema = z.object({
1628
+ sessionId: z.string().optional(),
1629
+ });
939
1630
  app.post('/api/sessions', express.json(), async (req, res, next) => {
940
1631
  try {
941
- const { sessionId } = req.body;
1632
+ const { sessionId } = parseBody(CreateSessionBodySchema, req.body);
942
1633
  const session = await activeAgent.createSession(sessionId);
943
1634
  const metadata = await activeAgent.getSessionMetadata(session.id);
944
1635
  return res.status(201).json({
@@ -947,6 +1638,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
947
1638
  createdAt: metadata?.createdAt || Date.now(),
948
1639
  lastActivity: metadata?.lastActivity || Date.now(),
949
1640
  messageCount: metadata?.messageCount || 0,
1641
+ title: metadata?.title || null,
950
1642
  },
951
1643
  });
952
1644
  }
@@ -955,7 +1647,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
955
1647
  }
956
1648
  });
957
1649
  // Get current working session (must come before parameterized route)
958
- app.get('/api/sessions/current', async (req, res, next) => {
1650
+ app.get('/api/sessions/current', async (_req, res, next) => {
959
1651
  try {
960
1652
  const currentSessionId = activeAgent.getCurrentSessionId();
961
1653
  return res.json({ currentSessionId });
@@ -965,9 +1657,12 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
965
1657
  }
966
1658
  });
967
1659
  // Get session details
1660
+ const GetSessionDetailsParamsSchema = z.object({
1661
+ sessionId: z.string().min(1, 'Session ID is required'),
1662
+ });
968
1663
  app.get('/api/sessions/:sessionId', async (req, res, next) => {
969
1664
  try {
970
- const { sessionId } = req.params;
1665
+ const { sessionId } = parseQuery(GetSessionDetailsParamsSchema, req.params);
971
1666
  const metadata = await activeAgent.getSessionMetadata(sessionId);
972
1667
  const history = await activeAgent.getSessionHistory(sessionId);
973
1668
  return res.json({
@@ -976,6 +1671,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
976
1671
  createdAt: metadata?.createdAt || null,
977
1672
  lastActivity: metadata?.lastActivity || null,
978
1673
  messageCount: metadata?.messageCount || 0,
1674
+ title: metadata?.title || null,
979
1675
  history: history.length,
980
1676
  },
981
1677
  });
@@ -985,9 +1681,12 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
985
1681
  }
986
1682
  });
987
1683
  // Get session conversation history
1684
+ const GetSessionHistoryParamsSchema = z.object({
1685
+ sessionId: z.string().min(1, 'Session ID is required'),
1686
+ });
988
1687
  app.get('/api/sessions/:sessionId/history', async (req, res, next) => {
989
1688
  try {
990
- const { sessionId } = req.params;
1689
+ const { sessionId } = parseQuery(GetSessionHistoryParamsSchema, req.params);
991
1690
  // getSessionHistory already checks existence via getSession
992
1691
  const history = await activeAgent.getSessionHistory(sessionId);
993
1692
  return res.json({ history });
@@ -997,6 +1696,13 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
997
1696
  }
998
1697
  });
999
1698
  // Search messages across all sessions or within a specific session
1699
+ const SearchQuerySchema = z.object({
1700
+ q: z.string().min(1, 'Search query is required'),
1701
+ limit: z.coerce.number().min(1).max(100).optional(),
1702
+ offset: z.coerce.number().min(0).optional(),
1703
+ sessionId: z.string().optional(),
1704
+ role: z.enum(['user', 'assistant', 'system', 'tool']).optional(),
1705
+ });
1000
1706
  app.get('/api/search/messages', async (req, res, next) => {
1001
1707
  try {
1002
1708
  const { q: query, limit, offset, sessionId, role, } = parseQuery(SearchQuerySchema, req.query);
@@ -1014,9 +1720,12 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
1014
1720
  }
1015
1721
  });
1016
1722
  // Search sessions that contain the query
1723
+ const SearchSessionsQuerySchema = z.object({
1724
+ q: z.string().min(1, 'Search query is required'),
1725
+ });
1017
1726
  app.get('/api/search/sessions', async (req, res, next) => {
1018
1727
  try {
1019
- const { q: query } = parseQuery(z.object({ q: z.string().min(1, 'Search query is required') }), req.query);
1728
+ const { q: query } = parseQuery(SearchSessionsQuerySchema, req.query);
1020
1729
  const searchResults = await activeAgent.searchSessions(query);
1021
1730
  return sendJsonResponse(res, searchResults);
1022
1731
  }
@@ -1025,9 +1734,12 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
1025
1734
  }
1026
1735
  });
1027
1736
  // Delete a session
1737
+ const DeleteSessionParamsSchema = z.object({
1738
+ sessionId: z.string().min(1, 'Session ID is required'),
1739
+ });
1028
1740
  app.delete('/api/sessions/:sessionId', async (req, res, next) => {
1029
1741
  try {
1030
- const { sessionId } = req.params;
1742
+ const { sessionId } = parseQuery(DeleteSessionParamsSchema, req.params);
1031
1743
  // deleteSession already checks existence internally
1032
1744
  await activeAgent.deleteSession(sessionId);
1033
1745
  return res.json({ status: 'deleted', sessionId });
@@ -1036,10 +1748,40 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
1036
1748
  return next(error);
1037
1749
  }
1038
1750
  });
1751
+ // Rename session title
1752
+ const PatchSessionBodySchema = z.object({
1753
+ title: z.string().min(1, 'Title is required').max(120, 'Title too long'),
1754
+ });
1755
+ const PatchSessionParamsSchema = z.object({
1756
+ sessionId: z.string().min(1, 'Session ID is required'),
1757
+ });
1758
+ app.patch('/api/sessions/:sessionId', express.json(), async (req, res, next) => {
1759
+ try {
1760
+ const { sessionId } = parseQuery(PatchSessionParamsSchema, req.params);
1761
+ const { title } = parseBody(PatchSessionBodySchema, req.body);
1762
+ await activeAgent.setSessionTitle(sessionId, title);
1763
+ const metadata = await activeAgent.getSessionMetadata(sessionId);
1764
+ return res.json({
1765
+ session: {
1766
+ id: sessionId,
1767
+ createdAt: metadata?.createdAt || null,
1768
+ lastActivity: metadata?.lastActivity || null,
1769
+ messageCount: metadata?.messageCount || 0,
1770
+ title: metadata?.title || title,
1771
+ },
1772
+ });
1773
+ }
1774
+ catch (error) {
1775
+ return next(error);
1776
+ }
1777
+ });
1039
1778
  // Load session as current working session and set as default
1779
+ const LoadSessionParamsSchema = z.object({
1780
+ sessionId: z.string().min(1, 'Session ID is required'),
1781
+ });
1040
1782
  app.post('/api/sessions/:sessionId/load', async (req, res, next) => {
1041
1783
  try {
1042
- const { sessionId } = req.params;
1784
+ const { sessionId } = parseQuery(LoadSessionParamsSchema, req.params);
1043
1785
  // Handle null/reset case
1044
1786
  if (sessionId === 'null' || sessionId === 'undefined') {
1045
1787
  await activeAgent.loadSessionAsDefault(null);
@@ -1064,6 +1806,11 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
1064
1806
  });
1065
1807
  // Webhook Management APIs
1066
1808
  // Register a new webhook endpoint
1809
+ const WebhookRequestSchema = z.object({
1810
+ url: z.string().url('Invalid URL format'),
1811
+ secret: z.string().optional(),
1812
+ description: z.string().optional(),
1813
+ });
1067
1814
  app.post('/api/webhooks', express.json(), async (req, res, next) => {
1068
1815
  try {
1069
1816
  const { url, secret, description } = parseBody(WebhookRequestSchema, req.body);
@@ -1092,7 +1839,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
1092
1839
  }
1093
1840
  });
1094
1841
  // List all registered webhooks
1095
- app.get('/api/webhooks', async (req, res, next) => {
1842
+ app.get('/api/webhooks', async (_req, res, next) => {
1096
1843
  try {
1097
1844
  const webhooks = webhookSubscriber.getWebhooks().map((webhook) => ({
1098
1845
  id: webhook.id,
@@ -1107,9 +1854,12 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
1107
1854
  }
1108
1855
  });
1109
1856
  // Get a specific webhook
1857
+ const GetWebhookParamsSchema = z.object({
1858
+ webhookId: z.string().min(1, 'Webhook ID is required'),
1859
+ });
1110
1860
  app.get('/api/webhooks/:webhookId', async (req, res, next) => {
1111
1861
  try {
1112
- const { webhookId } = req.params;
1862
+ const { webhookId } = parseQuery(GetWebhookParamsSchema, req.params);
1113
1863
  const webhook = webhookSubscriber.getWebhook(webhookId);
1114
1864
  if (!webhook) {
1115
1865
  return res.status(404).json({ error: 'Webhook not found' });
@@ -1128,9 +1878,12 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
1128
1878
  }
1129
1879
  });
1130
1880
  // Remove a webhook endpoint
1881
+ const DeleteWebhookParamsSchema = z.object({
1882
+ webhookId: z.string().min(1, 'Webhook ID is required'),
1883
+ });
1131
1884
  app.delete('/api/webhooks/:webhookId', async (req, res, next) => {
1132
1885
  try {
1133
- const { webhookId } = req.params;
1886
+ const { webhookId } = parseQuery(DeleteWebhookParamsSchema, req.params);
1134
1887
  const removed = webhookSubscriber.removeWebhook(webhookId);
1135
1888
  if (!removed) {
1136
1889
  return res.status(404).json({ error: 'Webhook not found' });
@@ -1143,9 +1896,12 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
1143
1896
  }
1144
1897
  });
1145
1898
  // Test a webhook endpoint
1899
+ const TestWebhookParamsSchema = z.object({
1900
+ webhookId: z.string().min(1, 'Webhook ID is required'),
1901
+ });
1146
1902
  app.post('/api/webhooks/:webhookId/test', async (req, res, next) => {
1147
1903
  try {
1148
- const { webhookId } = req.params;
1904
+ const { webhookId } = parseQuery(TestWebhookParamsSchema, req.params);
1149
1905
  const webhook = webhookSubscriber.getWebhook(webhookId);
1150
1906
  if (!webhook) {
1151
1907
  return res.status(404).json({ error: 'Webhook not found' });
@@ -1170,8 +1926,8 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
1170
1926
  app.use(errorHandler);
1171
1927
  return { app, server, wss, webSubscriber, webhookSubscriber };
1172
1928
  }
1173
- export async function startApiServer(agent, port = 3000, agentCardOverride, agentName) {
1174
- const { server, wss, webSubscriber, webhookSubscriber } = await initializeApi(agent, agentCardOverride, port, agentName);
1929
+ export async function startApiServer(agent, port = 3000, agentCardOverride, agentId) {
1930
+ const { server, wss, webSubscriber, webhookSubscriber } = await initializeApi(agent, agentCardOverride, port, agentId);
1175
1931
  // API server for REST endpoints and WebSocket connections
1176
1932
  server.listen(port, '0.0.0.0', () => {
1177
1933
  const networkInterfaces = os.networkInterfaces();