dexto 1.1.7 → 1.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/agents/agent-registry.json +8 -1
  2. package/dist/agents/github-agent/README.md +58 -0
  3. package/dist/agents/github-agent/github-agent.yml +57 -0
  4. package/dist/api/server.d.ts +2 -2
  5. package/dist/api/server.d.ts.map +1 -1
  6. package/dist/api/server.js +239 -57
  7. package/dist/api/webhook-subscriber.d.ts +4 -0
  8. package/dist/api/webhook-subscriber.d.ts.map +1 -1
  9. package/dist/api/webhook-subscriber.js +15 -0
  10. package/dist/api/websocket-subscriber.d.ts +5 -0
  11. package/dist/api/websocket-subscriber.d.ts.map +1 -1
  12. package/dist/api/websocket-subscriber.js +16 -0
  13. package/dist/cli/cli.js +1 -1
  14. package/dist/index.js +2 -2
  15. package/dist/utils/graceful-shutdown.d.ts +1 -1
  16. package/dist/utils/graceful-shutdown.d.ts.map +1 -1
  17. package/dist/utils/graceful-shutdown.js +9 -6
  18. package/dist/webui/.next/standalone/.next/static/chunks/179-78abc2eacbc41da9.js +1 -0
  19. package/dist/webui/.next/standalone/{packages/webui/.next/static/chunks/app/page-cf95b233c1df6dcd.js → .next/static/chunks/app/page-1a1b5591a5f62ca5.js} +1 -1
  20. package/dist/webui/.next/standalone/.next/static/css/045cc65741e38fbd.css +3 -0
  21. package/dist/webui/.next/standalone/packages/webui/.next/BUILD_ID +1 -1
  22. package/dist/webui/.next/standalone/packages/webui/.next/app-build-manifest.json +3 -3
  23. package/dist/webui/.next/standalone/packages/webui/.next/build-manifest.json +2 -2
  24. package/dist/webui/.next/standalone/packages/webui/.next/prerender-manifest.json +3 -3
  25. package/dist/webui/.next/standalone/packages/webui/.next/required-server-files.json +1 -1
  26. package/dist/webui/.next/standalone/packages/webui/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  27. package/dist/webui/.next/standalone/packages/webui/.next/server/app/page.js +2 -2
  28. package/dist/webui/.next/standalone/packages/webui/.next/server/app/page_client-reference-manifest.js +1 -1
  29. package/dist/webui/.next/standalone/packages/webui/.next/server/app/playground/page_client-reference-manifest.js +1 -1
  30. package/dist/webui/.next/standalone/packages/webui/.next/server/pages/500.html +1 -1
  31. package/dist/webui/.next/standalone/packages/webui/.next/server/server-reference-manifest.json +1 -1
  32. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/179-78abc2eacbc41da9.js +1 -0
  33. package/dist/webui/.next/standalone/{.next/static/chunks/app/page-cf95b233c1df6dcd.js → packages/webui/.next/static/chunks/app/page-1a1b5591a5f62ca5.js} +1 -1
  34. package/dist/webui/.next/standalone/packages/webui/.next/static/css/045cc65741e38fbd.css +3 -0
  35. package/dist/webui/.next/standalone/packages/webui/package.json +1 -1
  36. package/dist/webui/.next/standalone/packages/webui/server.js +1 -1
  37. package/dist/webui/.next/static/chunks/179-78abc2eacbc41da9.js +1 -0
  38. package/dist/webui/.next/static/chunks/app/{page-cf95b233c1df6dcd.js → page-1a1b5591a5f62ca5.js} +1 -1
  39. package/dist/webui/.next/static/css/045cc65741e38fbd.css +3 -0
  40. package/dist/webui/package.json +1 -1
  41. package/package.json +2 -2
  42. package/dist/webui/.next/standalone/.next/static/chunks/762-8cfe2287ad3f2d16.js +0 -1
  43. package/dist/webui/.next/standalone/.next/static/css/9cdfb06589a2f6ce.css +0 -3
  44. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/762-8cfe2287ad3f2d16.js +0 -1
  45. package/dist/webui/.next/standalone/packages/webui/.next/static/css/9cdfb06589a2f6ce.css +0 -3
  46. package/dist/webui/.next/static/chunks/762-8cfe2287ad3f2d16.js +0 -1
  47. package/dist/webui/.next/static/css/9cdfb06589a2f6ce.css +0 -3
  48. /package/dist/webui/.next/standalone/.next/static/{s008aheUrlJbknTlTTTmY → _5RTE-15cb-kjZPtF45Oe}/_buildManifest.js +0 -0
  49. /package/dist/webui/.next/standalone/.next/static/{s008aheUrlJbknTlTTTmY → _5RTE-15cb-kjZPtF45Oe}/_ssgManifest.js +0 -0
  50. /package/dist/webui/.next/standalone/packages/webui/.next/static/{s008aheUrlJbknTlTTTmY → _5RTE-15cb-kjZPtF45Oe}/_buildManifest.js +0 -0
  51. /package/dist/webui/.next/standalone/packages/webui/.next/static/{s008aheUrlJbknTlTTTmY → _5RTE-15cb-kjZPtF45Oe}/_ssgManifest.js +0 -0
  52. /package/dist/webui/.next/static/{s008aheUrlJbknTlTTTmY → _5RTE-15cb-kjZPtF45Oe}/_buildManifest.js +0 -0
  53. /package/dist/webui/.next/static/{s008aheUrlJbknTlTTTmY → _5RTE-15cb-kjZPtF45Oe}/_ssgManifest.js +0 -0
@@ -6,7 +6,7 @@ import { WebhookEventSubscriber } from './webhook-subscriber.js';
6
6
  import { logger, redactSensitiveData } from '@dexto/core';
7
7
  import { setupA2ARoutes } from './a2a.js';
8
8
  import { createMcpTransport, initializeMcpServer, initializeMcpServerApiEndpoints, } from './mcp/mcp_handler.js';
9
- import { createAgentCard } from '@dexto/core';
9
+ import { createAgentCard, DextoAgent } from '@dexto/core';
10
10
  import { stringify as yamlStringify } from 'yaml';
11
11
  import os from 'os';
12
12
  import { expressRedactionMiddleware } from './middleware/expressRedactionMiddleware.js';
@@ -19,7 +19,7 @@ import { getProviderKeyStatus, saveProviderApiKey } from '@dexto/core';
19
19
  import { errorHandler } from './middleware/errorHandler.js';
20
20
  import { McpServerConfigSchema } from '@dexto/core';
21
21
  import { sendWebSocketError, sendWebSocketValidationError } from './websocket-error-handler.js';
22
- import { DextoValidationError, ErrorScope, ErrorType, AgentErrorCode } from '@dexto/core';
22
+ import { DextoValidationError, ErrorScope, ErrorType, AgentErrorCode, AgentError, } from '@dexto/core';
23
23
  /**
24
24
  * Helper function to send JSON response with optional pretty printing
25
25
  */
@@ -95,27 +95,125 @@ function parseQuery(schema, query) {
95
95
  return schema.parse(query); // ZodError handled by error middleware
96
96
  }
97
97
  // TODO: API endpoint names are work in progress and might be refactored/renamed in future versions
98
- export async function initializeApi(agent, agentCardOverride, listenPort) {
98
+ export async function initializeApi(agent, agentCardOverride, listenPort, agentName) {
99
99
  const app = express();
100
- registerGracefulShutdown(agent);
100
+ // Declare before registering shutdown hook to avoid TDZ on signals
101
+ let activeAgent = agent;
102
+ let activeAgentName = agentName || 'default';
103
+ let isSwitchingAgent = false;
104
+ registerGracefulShutdown(() => activeAgent);
101
105
  // this will apply middleware to all /api/llm/* routes
102
106
  app.use('/api/llm', expressRedactionMiddleware);
103
107
  app.use('/api/config.yaml', expressRedactionMiddleware);
104
108
  const server = http.createServer(app);
105
109
  const wss = new WebSocketServer({ server });
106
- // set up event broadcasting over WebSocket
110
+ logger.info(`Initializing API server with agent: ${activeAgentName}`);
111
+ // Ensure the initial agent is started
112
+ if (!activeAgent.isStarted() && !activeAgent.isStopped()) {
113
+ logger.info('Starting initial agent...');
114
+ await activeAgent.start();
115
+ }
116
+ else if (activeAgent.isStopped()) {
117
+ logger.warn('Initial agent is stopped, this may cause issues');
118
+ }
107
119
  const webSubscriber = new WebSocketEventSubscriber(wss);
108
120
  logger.info('Setting up API event subscriptions...');
109
- webSubscriber.subscribe(agent.agentEventBus);
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);
110
126
  // Tool confirmation responses are handled by the main WebSocket handler below
127
+ function ensureAgentAvailable() {
128
+ // Gate requests during agent switching
129
+ if (isSwitchingAgent) {
130
+ throw AgentError.apiValidationError('Agent switch already in progress');
131
+ }
132
+ // Fast path: most common case is agent is started and running
133
+ if (activeAgent.isStarted() && !activeAgent.isStopped()) {
134
+ return;
135
+ }
136
+ // Provide specific error messages for better debugging
137
+ if (activeAgent.isStopped()) {
138
+ throw AgentError.stopped();
139
+ }
140
+ if (!activeAgent.isStarted()) {
141
+ throw AgentError.notStarted();
142
+ }
143
+ }
144
+ async function switchAgentByName(name) {
145
+ if (isSwitchingAgent) {
146
+ throw AgentError.apiValidationError('Agent switch already in progress');
147
+ }
148
+ isSwitchingAgent = true;
149
+ let newAgent;
150
+ try {
151
+ // Use domain layer method to create new agent
152
+ newAgent = await DextoAgent.createAgent(name);
153
+ logger.info(`Starting new agent: ${name}`);
154
+ 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
+ // Stop previous agent last (only after new one is fully operational)
172
+ const previousAgent = activeAgent;
173
+ activeAgent = newAgent;
174
+ activeAgentName = name;
175
+ logger.info(`Successfully switched to agent: ${name}`);
176
+ // Now safely stop the previous agent
177
+ try {
178
+ if (previousAgent && previousAgent !== newAgent) {
179
+ logger.info('Stopping previous agent...');
180
+ await previousAgent.stop();
181
+ }
182
+ }
183
+ catch (err) {
184
+ logger.warn(`Stopping previous agent failed: ${err}`);
185
+ // Don't throw here as the switch was successful
186
+ }
187
+ return { name };
188
+ }
189
+ catch (error) {
190
+ logger.error(`Failed to switch to agent '${name}': ${error instanceof Error ? error.message : String(error)}`, { error });
191
+ // Clean up the failed new agent if it was created
192
+ if (newAgent) {
193
+ try {
194
+ await newAgent.stop();
195
+ }
196
+ catch (cleanupErr) {
197
+ logger.warn(`Failed to cleanup new agent: ${cleanupErr}`);
198
+ }
199
+ }
200
+ throw error;
201
+ }
202
+ finally {
203
+ isSwitchingAgent = false;
204
+ }
205
+ }
111
206
  // HTTP endpoints
112
207
  // Health check endpoint
113
208
  app.get('/health', (req, res) => {
114
209
  res.status(200).send('OK');
115
210
  });
116
- app.post('/api/message', express.json(), async (req, res, next) => {
211
+ // JSON body size limit for message endpoints supporting base64 image/file payloads
212
+ // Both /api/message and /api/message-sync accept base64 attachments; increased limit to avoid 413s.
213
+ app.post('/api/message', express.json({ limit: process.env.MESSAGE_JSON_LIMIT || '10mb' }), async (req, res, next) => {
117
214
  logger.info('Received message via POST /api/message');
118
215
  try {
216
+ ensureAgentAvailable();
119
217
  const { message, sessionId, stream, imageData, fileData } = parseBody(MessageRequestSchema, req.body);
120
218
  const imageDataInput = imageData
121
219
  ? { image: imageData.base64, mimeType: imageData.mimeType }
@@ -134,7 +232,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
134
232
  logger.info('File data included in message.');
135
233
  if (sessionId)
136
234
  logger.info(`Message for session: ${sessionId}`);
137
- const response = await agent.run(message || '', imageDataInput, fileDataInput, sessionId, stream || false);
235
+ const response = await activeAgent.run(message || '', imageDataInput, fileDataInput, sessionId, stream || false);
138
236
  return res.status(202).send({ response, sessionId });
139
237
  }
140
238
  catch (error) {
@@ -145,7 +243,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
145
243
  app.post('/api/sessions/:sessionId/cancel', async (req, res, next) => {
146
244
  try {
147
245
  const { sessionId } = parseQuery(CancelRequestSchema, req.params);
148
- const cancelled = await agent.cancel(sessionId);
246
+ const cancelled = await activeAgent.cancel(sessionId);
149
247
  if (!cancelled) {
150
248
  logger.debug(`No in-flight run to cancel for session: ${sessionId}`);
151
249
  }
@@ -156,9 +254,11 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
156
254
  }
157
255
  });
158
256
  // Synchronous endpoint: await the full AI response and return it in one go
159
- app.post('/api/message-sync', express.json(), async (req, res, next) => {
257
+ // JSON body size limit increased for image/file uploads
258
+ app.post('/api/message-sync', express.json({ limit: process.env.MESSAGE_JSON_LIMIT || '10mb' }), async (req, res, next) => {
160
259
  logger.info('Received message via POST /api/message-sync');
161
260
  try {
261
+ ensureAgentAvailable();
162
262
  const { message, sessionId, imageData, fileData } = parseBody(MessageRequestSchema, req.body);
163
263
  // Extract optional image and file data
164
264
  const imageDataInput = imageData
@@ -178,7 +278,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
178
278
  logger.info('File data included in message.');
179
279
  if (sessionId)
180
280
  logger.info(`Message for session: ${sessionId}`);
181
- const response = await agent.run(message || '', imageDataInput, fileDataInput, sessionId, false // Force non-streaming for sync endpoint
281
+ const response = await activeAgent.run(message || '', imageDataInput, fileDataInput, sessionId, false // Force non-streaming for sync endpoint
182
282
  );
183
283
  return res.status(200).json({ response, sessionId });
184
284
  }
@@ -189,8 +289,9 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
189
289
  app.post('/api/reset', express.json(), async (req, res, next) => {
190
290
  logger.info('Received request via POST /api/reset');
191
291
  try {
292
+ ensureAgentAvailable();
192
293
  const { sessionId } = parseBody(z.object({ sessionId: z.string().optional() }), req.body);
193
- await agent.resetConversation(sessionId);
294
+ await activeAgent.resetConversation(sessionId);
194
295
  return res.status(200).send({ status: 'reset initiated', sessionId });
195
296
  }
196
297
  catch (error) {
@@ -200,8 +301,9 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
200
301
  // Dynamic MCP server connection endpoint (legacy)
201
302
  app.post('/api/connect-server', express.json(), async (req, res, next) => {
202
303
  try {
304
+ ensureAgentAvailable();
203
305
  const { name, config } = parseBody(McpServerRequestSchema, req.body);
204
- await agent.connectMcpServer(name, config);
306
+ await activeAgent.connectMcpServer(name, config);
205
307
  logger.info(`Successfully connected to new server '${name}' via API request.`);
206
308
  return res.status(200).send({ status: 'connected', name });
207
309
  }
@@ -212,8 +314,9 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
212
314
  // Add a new MCP server
213
315
  app.post('/api/mcp/servers', express.json(), async (req, res, next) => {
214
316
  try {
317
+ ensureAgentAvailable();
215
318
  const { name, config } = parseBody(McpServerRequestSchema, req.body);
216
- await agent.connectMcpServer(name, config);
319
+ await activeAgent.connectMcpServer(name, config);
217
320
  return res.status(201).json({ status: 'connected', name });
218
321
  }
219
322
  catch (error) {
@@ -223,8 +326,9 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
223
326
  // Add MCP servers listing endpoint
224
327
  app.get('/api/mcp/servers', async (req, res, next) => {
225
328
  try {
226
- const clientsMap = agent.getMcpClients();
227
- const failedConnections = agent.getMcpFailedConnections();
329
+ ensureAgentAvailable();
330
+ const clientsMap = activeAgent.getMcpClients();
331
+ const failedConnections = activeAgent.getMcpFailedConnections();
228
332
  const servers = [];
229
333
  for (const name of clientsMap.keys()) {
230
334
  servers.push({ id: name, name, status: 'connected' });
@@ -240,12 +344,13 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
240
344
  });
241
345
  // Add MCP server tools listing endpoint
242
346
  app.get('/api/mcp/servers/:serverId/tools', async (req, res, next) => {
243
- const serverId = req.params.serverId;
244
- const client = agent.getMcpClients().get(serverId);
245
- if (!client) {
246
- return res.status(404).json({ error: `Server '${serverId}' not found` });
247
- }
248
347
  try {
348
+ ensureAgentAvailable();
349
+ const serverId = req.params.serverId;
350
+ const client = activeAgent.getMcpClients().get(serverId);
351
+ if (!client) {
352
+ return res.status(404).json({ error: `Server '${serverId}' not found` });
353
+ }
249
354
  const toolsMap = await client.getTools();
250
355
  const tools = Object.entries(toolsMap).map(([toolName, toolDef]) => ({
251
356
  id: toolName,
@@ -265,12 +370,13 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
265
370
  logger.info(`Received request to DELETE /api/mcp/servers/${serverId}`);
266
371
  try {
267
372
  // Check if server exists before attempting to disconnect
268
- const clientExists = agent.getMcpClients().has(serverId) || agent.getMcpFailedConnections()[serverId];
373
+ const clientExists = activeAgent.getMcpClients().has(serverId) ||
374
+ activeAgent.getMcpFailedConnections()[serverId];
269
375
  if (!clientExists) {
270
376
  logger.warn(`Attempted to delete non-existent server: ${serverId}`);
271
377
  return res.status(404).json({ error: `Server '${serverId}' not found.` });
272
378
  }
273
- await agent.removeMcpServer(serverId);
379
+ await activeAgent.removeMcpServer(serverId);
274
380
  return res.status(200).json({ status: 'disconnected', id: serverId });
275
381
  }
276
382
  catch (error) {
@@ -281,7 +387,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
281
387
  app.post('/api/mcp/servers/:serverId/tools/:toolName/execute', express.json(), async (req, res, next) => {
282
388
  const { serverId, toolName } = req.params;
283
389
  // Verify server exists
284
- const client = agent.getMcpClients().get(serverId);
390
+ const client = activeAgent.getMcpClients().get(serverId);
285
391
  if (!client) {
286
392
  return res
287
393
  .status(404)
@@ -289,7 +395,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
289
395
  }
290
396
  try {
291
397
  // Execute tool through the agent's unified wrapper method
292
- const rawResult = await agent.executeTool(toolName, req.body);
398
+ const rawResult = await activeAgent.executeTool(toolName, req.body);
293
399
  // Return standardized result shape
294
400
  return res.json({ success: true, data: rawResult });
295
401
  }
@@ -320,7 +426,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
320
426
  const data = JSON.parse(messageString);
321
427
  if (data.type === 'toolConfirmationResponse' && data.data) {
322
428
  // Route confirmation back via AgentEventBus and do not broadcast an error
323
- agent.agentEventBus.emit('dexto:toolConfirmationResponse', data.data);
429
+ activeAgent.agentEventBus.emit('dexto:toolConfirmationResponse', data.data);
324
430
  return;
325
431
  }
326
432
  else if (data.type === 'message' &&
@@ -349,8 +455,17 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
349
455
  logger.info('File data included in message.');
350
456
  if (sessionId)
351
457
  logger.info(`Message for session: ${sessionId}`);
458
+ // Check if agent is available before processing
459
+ try {
460
+ ensureAgentAvailable();
461
+ }
462
+ catch (error) {
463
+ logger.error(`Agent not available for WebSocket message: ${error}`);
464
+ sendWebSocketError(ws, error instanceof Error ? error.message : 'Agent not available', sessionId);
465
+ return;
466
+ }
352
467
  // Comprehensive input validation
353
- const currentConfig = agent.getEffectiveConfig(sessionId);
468
+ const currentConfig = activeAgent.getEffectiveConfig(sessionId);
354
469
  const validation = validateInputForLLM({
355
470
  text: data.content,
356
471
  ...(imageDataInput && { imageData: imageDataInput }),
@@ -386,17 +501,35 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
386
501
  sendWebSocketError(ws, hierarchicalError, sessionId);
387
502
  return;
388
503
  }
389
- await agent.run(data.content, imageDataInput, fileDataInput, sessionId, stream);
504
+ await activeAgent.run(data.content, imageDataInput, fileDataInput, sessionId, stream);
390
505
  }
391
506
  else if (data.type === 'reset') {
392
507
  const sessionId = data.sessionId;
393
508
  logger.info(`Processing reset command from WebSocket${sessionId ? ` for session: ${sessionId}` : ''}.`);
394
- await agent.resetConversation(sessionId);
509
+ // Check if agent is available before processing
510
+ try {
511
+ ensureAgentAvailable();
512
+ }
513
+ catch (error) {
514
+ logger.error(`Agent not available for WebSocket reset: ${error}`);
515
+ sendWebSocketError(ws, error instanceof Error ? error.message : 'Agent not available', sessionId || 'unknown');
516
+ return;
517
+ }
518
+ await activeAgent.resetConversation(sessionId);
395
519
  }
396
520
  else if (data.type === 'cancel') {
397
521
  const sessionId = data.sessionId;
398
522
  logger.info(`Processing cancel command from WebSocket${sessionId ? ` for session: ${sessionId}` : ''}.`);
399
- const cancelled = await agent.cancel(sessionId);
523
+ // Check if agent is available before processing
524
+ try {
525
+ ensureAgentAvailable();
526
+ }
527
+ catch (error) {
528
+ logger.error(`Agent not available for WebSocket cancel: ${error}`);
529
+ sendWebSocketError(ws, error instanceof Error ? error.message : 'Agent not available', sessionId || 'unknown');
530
+ return;
531
+ }
532
+ const cancelled = await activeAgent.cancel(sessionId);
400
533
  if (!cancelled) {
401
534
  logger.debug('No in-flight run to cancel');
402
535
  }
@@ -459,7 +592,9 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
459
592
  try {
460
593
  const transportType = process.env.DEXTO_MCP_TRANSPORT_TYPE || 'http';
461
594
  const mcpTransport = await createMcpTransport(transportType);
462
- // TODO: Think of a better way to handle the MCP implementation
595
+ // TODO: MCP server is bound to the initial agent; breaks after agent switch
596
+ // initializeMcpServer receives the original agent, so MCP endpoints keep talking to the stale instance post-switch.
597
+ // Make MCP consume the current agent via a getter to stay in sync.
463
598
  await initializeMcpServer(agent, agentCardData, // Pass the agent card data for the MCP resource
464
599
  mcpTransport);
465
600
  await initializeMcpServerApiEndpoints(app, mcpTransport);
@@ -471,6 +606,57 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
471
606
  res.status(500).json({ error: 'MCP server initialization failed' });
472
607
  });
473
608
  }
609
+ // ===== Agents API =====
610
+ app.get('/api/agents', async (_req, res, next) => {
611
+ try {
612
+ ensureAgentAvailable();
613
+ const agents = await activeAgent.listAgents();
614
+ return sendJsonResponse(res, {
615
+ installed: agents.installed,
616
+ available: agents.available,
617
+ current: { name: activeAgentName ?? 'default' },
618
+ });
619
+ }
620
+ catch (error) {
621
+ return next(error);
622
+ }
623
+ });
624
+ app.get('/api/agents/current', async (_req, res, next) => {
625
+ try {
626
+ // TODO: Consider exposing agent.getName() method or config.name for more accurate tracking
627
+ return sendJsonResponse(res, { name: activeAgentName ?? 'default' });
628
+ }
629
+ catch (error) {
630
+ return next(error);
631
+ }
632
+ });
633
+ const AgentNameSchema = z.object({ name: z.string().min(1) }).strict();
634
+ app.post('/api/agents/install', express.json(), async (req, res, next) => {
635
+ try {
636
+ ensureAgentAvailable();
637
+ const { name } = AgentNameSchema.parse(req.body);
638
+ await activeAgent.installAgent(name);
639
+ return sendJsonResponse(res, { installed: true, name }, 201);
640
+ }
641
+ catch (error) {
642
+ return next(error);
643
+ }
644
+ });
645
+ app.post('/api/agents/switch', express.json(), async (req, res, next) => {
646
+ try {
647
+ const { name } = AgentNameSchema.parse(req.body);
648
+ const result = await switchAgentByName(name);
649
+ return sendJsonResponse(res, { switched: true, ...result });
650
+ }
651
+ catch (error) {
652
+ if (error instanceof Error &&
653
+ error.message &&
654
+ error.message.includes('already in progress')) {
655
+ return res.status(409).json({ error: error.message });
656
+ }
657
+ return next(error);
658
+ }
659
+ });
474
660
  // Configuration export endpoint
475
661
  /**
476
662
  * Helper function to redact sensitive environment variables
@@ -513,7 +699,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
513
699
  app.get('/api/config.yaml', async (req, res, next) => {
514
700
  try {
515
701
  const sessionId = req.query.sessionId;
516
- const config = agent.getEffectiveConfig(sessionId);
702
+ const config = activeAgent.getEffectiveConfig(sessionId);
517
703
  // Export config as YAML, masking sensitive data
518
704
  const maskedConfig = {
519
705
  ...config,
@@ -535,7 +721,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
535
721
  app.get('/api/greeting', async (req, res, next) => {
536
722
  try {
537
723
  const sessionId = req.query.sessionId;
538
- const config = agent.getEffectiveConfig(sessionId);
724
+ const config = activeAgent.getEffectiveConfig(sessionId);
539
725
  res.json({ greeting: config.greeting });
540
726
  }
541
727
  catch (error) {
@@ -548,8 +734,8 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
548
734
  const { sessionId } = req.query;
549
735
  // Use session-specific config if sessionId is provided, otherwise use default
550
736
  const currentConfig = sessionId
551
- ? agent.getEffectiveConfig(sessionId).llm
552
- : agent.getCurrentLLMConfig();
737
+ ? activeAgent.getEffectiveConfig(sessionId).llm
738
+ : activeAgent.getCurrentLLMConfig();
553
739
  // Attach displayName for the current model if available in registry
554
740
  let displayName;
555
741
  try {
@@ -711,7 +897,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
711
897
  const sessionId = typeof body.sessionId === 'string' ? body.sessionId : undefined;
712
898
  const { sessionId: _omit, ...llmCandidate } = body;
713
899
  const llmConfig = LLMUpdatesSchema.parse(llmCandidate);
714
- const config = await agent.switchLLM(llmConfig, sessionId);
900
+ const config = await activeAgent.switchLLM(llmConfig, sessionId);
715
901
  return res.status(200).json({ config, sessionId });
716
902
  }
717
903
  catch (error) {
@@ -722,10 +908,10 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
722
908
  // List all active sessions
723
909
  app.get('/api/sessions', async (req, res, next) => {
724
910
  try {
725
- const sessionIds = await agent.listSessions();
911
+ const sessionIds = await activeAgent.listSessions();
726
912
  const sessions = await Promise.all(sessionIds.map(async (id) => {
727
913
  try {
728
- const metadata = await agent.getSessionMetadata(id);
914
+ const metadata = await activeAgent.getSessionMetadata(id);
729
915
  return {
730
916
  id,
731
917
  createdAt: metadata?.createdAt || null,
@@ -753,8 +939,8 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
753
939
  app.post('/api/sessions', express.json(), async (req, res, next) => {
754
940
  try {
755
941
  const { sessionId } = req.body;
756
- const session = await agent.createSession(sessionId);
757
- const metadata = await agent.getSessionMetadata(session.id);
942
+ const session = await activeAgent.createSession(sessionId);
943
+ const metadata = await activeAgent.getSessionMetadata(session.id);
758
944
  return res.status(201).json({
759
945
  session: {
760
946
  id: session.id,
@@ -771,7 +957,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
771
957
  // Get current working session (must come before parameterized route)
772
958
  app.get('/api/sessions/current', async (req, res, next) => {
773
959
  try {
774
- const currentSessionId = agent.getCurrentSessionId();
960
+ const currentSessionId = activeAgent.getCurrentSessionId();
775
961
  return res.json({ currentSessionId });
776
962
  }
777
963
  catch (error) {
@@ -782,8 +968,8 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
782
968
  app.get('/api/sessions/:sessionId', async (req, res, next) => {
783
969
  try {
784
970
  const { sessionId } = req.params;
785
- const metadata = await agent.getSessionMetadata(sessionId);
786
- const history = await agent.getSessionHistory(sessionId);
971
+ const metadata = await activeAgent.getSessionMetadata(sessionId);
972
+ const history = await activeAgent.getSessionHistory(sessionId);
787
973
  return res.json({
788
974
  session: {
789
975
  id: sessionId,
@@ -803,7 +989,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
803
989
  try {
804
990
  const { sessionId } = req.params;
805
991
  // getSessionHistory already checks existence via getSession
806
- const history = await agent.getSessionHistory(sessionId);
992
+ const history = await activeAgent.getSessionHistory(sessionId);
807
993
  return res.json({ history });
808
994
  }
809
995
  catch (error) {
@@ -820,7 +1006,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
820
1006
  ...(sessionId && { sessionId }),
821
1007
  ...(role && { role }),
822
1008
  };
823
- const searchResults = await agent.searchMessages(query, options);
1009
+ const searchResults = await activeAgent.searchMessages(query, options);
824
1010
  return sendJsonResponse(res, searchResults);
825
1011
  }
826
1012
  catch (error) {
@@ -831,7 +1017,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
831
1017
  app.get('/api/search/sessions', async (req, res, next) => {
832
1018
  try {
833
1019
  const { q: query } = parseQuery(z.object({ q: z.string().min(1, 'Search query is required') }), req.query);
834
- const searchResults = await agent.searchSessions(query);
1020
+ const searchResults = await activeAgent.searchSessions(query);
835
1021
  return sendJsonResponse(res, searchResults);
836
1022
  }
837
1023
  catch (error) {
@@ -843,7 +1029,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
843
1029
  try {
844
1030
  const { sessionId } = req.params;
845
1031
  // deleteSession already checks existence internally
846
- await agent.deleteSession(sessionId);
1032
+ await activeAgent.deleteSession(sessionId);
847
1033
  return res.json({ status: 'deleted', sessionId });
848
1034
  }
849
1035
  catch (error) {
@@ -856,20 +1042,20 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
856
1042
  const { sessionId } = req.params;
857
1043
  // Handle null/reset case
858
1044
  if (sessionId === 'null' || sessionId === 'undefined') {
859
- await agent.loadSessionAsDefault(null);
1045
+ await activeAgent.loadSessionAsDefault(null);
860
1046
  res.json({
861
1047
  status: 'reset',
862
1048
  sessionId: null,
863
- currentSession: agent.getCurrentSessionId(),
1049
+ currentSession: activeAgent.getCurrentSessionId(),
864
1050
  });
865
1051
  return;
866
1052
  }
867
1053
  // loadSession already checks session existence
868
- await agent.loadSessionAsDefault(sessionId);
1054
+ await activeAgent.loadSessionAsDefault(sessionId);
869
1055
  return res.json({
870
1056
  status: 'loaded',
871
1057
  sessionId,
872
- currentSession: agent.getCurrentSessionId(),
1058
+ currentSession: activeAgent.getCurrentSessionId(),
873
1059
  });
874
1060
  }
875
1061
  catch (error) {
@@ -877,10 +1063,6 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
877
1063
  }
878
1064
  });
879
1065
  // Webhook Management APIs
880
- // Initialize webhook subscriber
881
- const webhookSubscriber = new WebhookEventSubscriber();
882
- logger.info('Setting up webhook event subscriptions...');
883
- webhookSubscriber.subscribe(agent.agentEventBus);
884
1066
  // Register a new webhook endpoint
885
1067
  app.post('/api/webhooks', express.json(), async (req, res, next) => {
886
1068
  try {
@@ -988,8 +1170,8 @@ export async function initializeApi(agent, agentCardOverride, listenPort) {
988
1170
  app.use(errorHandler);
989
1171
  return { app, server, wss, webSubscriber, webhookSubscriber };
990
1172
  }
991
- export async function startApiServer(agent, port = 3000, agentCardOverride) {
992
- const { server, wss, webSubscriber, webhookSubscriber } = await initializeApi(agent, agentCardOverride, port);
1173
+ export async function startApiServer(agent, port = 3000, agentCardOverride, agentName) {
1174
+ const { server, wss, webSubscriber, webhookSubscriber } = await initializeApi(agent, agentCardOverride, port, agentName);
993
1175
  // API server for REST endpoints and WebSocket connections
994
1176
  server.listen(port, '0.0.0.0', () => {
995
1177
  const networkInterfaces = os.networkInterfaces();
@@ -41,6 +41,10 @@ export declare class WebhookEventSubscriber implements EventSubscriber {
41
41
  * Clean up event listeners and resources
42
42
  */
43
43
  cleanup(): void;
44
+ /**
45
+ * Unsubscribe from current event bus without clearing registered webhooks
46
+ */
47
+ unsubscribe(): void;
44
48
  /**
45
49
  * Deliver an event to all registered webhooks
46
50
  */
@@ -1 +1 @@
1
- {"version":3,"file":"webhook-subscriber.d.ts","sourceRoot":"","sources":["../../src/api/webhook-subscriber.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAmD,MAAM,aAAa,CAAC;AAC7F,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EACH,KAAK,aAAa,EAElB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC9B,MAAM,oBAAoB,CAAC;AAW5B;;;GAGG;AACH,qBAAa,sBAAuB,YAAW,eAAe;IAC1D,OAAO,CAAC,QAAQ,CAAyC;IACzD,OAAO,CAAC,eAAe,CAAC,CAAkB;IAC1C,OAAO,CAAC,eAAe,CAAmC;IAC1D,OAAO,CAAC,OAAO,CAA0B;gBAE7B,EACR,OAAO,EACP,GAAG,eAAe,EACrB,GAAE,sBAAsB,GAAG;QAAE,OAAO,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;KAAO;IAOtE;;OAEG;IACH,SAAS,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IA0CxC;;OAEG;IACH,UAAU,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI;IAKxC;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAUzC;;OAEG;IACH,WAAW,IAAI,aAAa,EAAE;IAI9B;;OAEG;IACH,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAIxD;;OAEG;IACG,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAoBpE;;OAEG;IACH,OAAO,IAAI,IAAI;IAUf;;OAEG;YACW,YAAY;IA+C1B;;OAEG;YACW,gBAAgB;IAoD9B;;OAEG;YACW,kBAAkB;IA+DhC;;OAEG;IACH,OAAO,CAAC,iBAAiB;CAK5B"}
1
+ {"version":3,"file":"webhook-subscriber.d.ts","sourceRoot":"","sources":["../../src/api/webhook-subscriber.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAmD,MAAM,aAAa,CAAC;AAC7F,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EACH,KAAK,aAAa,EAElB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC9B,MAAM,oBAAoB,CAAC;AAW5B;;;GAGG;AACH,qBAAa,sBAAuB,YAAW,eAAe;IAC1D,OAAO,CAAC,QAAQ,CAAyC;IACzD,OAAO,CAAC,eAAe,CAAC,CAAkB;IAC1C,OAAO,CAAC,eAAe,CAAmC;IAC1D,OAAO,CAAC,OAAO,CAA0B;gBAE7B,EACR,OAAO,EACP,GAAG,eAAe,EACrB,GAAE,sBAAsB,GAAG;QAAE,OAAO,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;KAAO;IAOtE;;OAEG;IACH,SAAS,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IA0CxC;;OAEG;IACH,UAAU,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI;IAKxC;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAUzC;;OAEG;IACH,WAAW,IAAI,aAAa,EAAE;IAI9B;;OAEG;IACH,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAIxD;;OAEG;IACG,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAoBpE;;OAEG;IACH,OAAO,IAAI,IAAI;IAUf;;OAEG;IACH,WAAW,IAAI,IAAI;IAYnB;;OAEG;YACW,YAAY;IA+C1B;;OAEG;YACW,gBAAgB;IAoD9B;;OAEG;YACW,kBAAkB;IA+DhC;;OAEG;IACH,OAAO,CAAC,iBAAiB;CAK5B"}
@@ -122,6 +122,21 @@ export class WebhookEventSubscriber {
122
122
  this.webhooks.clear();
123
123
  logger.debug('Webhook event subscriber cleaned up');
124
124
  }
125
+ /**
126
+ * Unsubscribe from current event bus without clearing registered webhooks
127
+ */
128
+ unsubscribe() {
129
+ if (this.abortController) {
130
+ const controller = this.abortController;
131
+ delete this.abortController;
132
+ try {
133
+ controller.abort();
134
+ }
135
+ catch (error) {
136
+ logger.debug('Error aborting controller during unsubscribe:', error);
137
+ }
138
+ }
139
+ }
125
140
  /**
126
141
  * Deliver an event to all registered webhooks
127
142
  */
@@ -17,6 +17,11 @@ export declare class WebSocketEventSubscriber implements EventSubscriber {
17
17
  * Clean up event listeners and resources
18
18
  */
19
19
  cleanup(): void;
20
+ /**
21
+ * Unsubscribe from current event bus without closing WebSocket clients.
22
+ * Useful when switching the active agent and re-subscribing to a new bus.
23
+ */
24
+ unsubscribe(): void;
20
25
  private broadcast;
21
26
  }
22
27
  //# sourceMappingURL=websocket-subscriber.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"websocket-subscriber.d.ts","sourceRoot":"","sources":["../../src/api/websocket-subscriber.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAa,MAAM,IAAI,CAAC;AAEhD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C;;GAEG;AACH,qBAAa,wBAAyB,YAAW,eAAe;IAIhD,OAAO,CAAC,GAAG;IAHvB,OAAO,CAAC,WAAW,CAA6B;IAChD,OAAO,CAAC,eAAe,CAAC,CAAkB;gBAEtB,GAAG,EAAE,eAAe;IAmBxC;;OAEG;IACH,SAAS,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IAyKxC;;OAEG;IACH,OAAO,IAAI,IAAI;IAiBf,OAAO,CAAC,SAAS;CAUpB"}
1
+ {"version":3,"file":"websocket-subscriber.d.ts","sourceRoot":"","sources":["../../src/api/websocket-subscriber.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAa,MAAM,IAAI,CAAC;AAEhD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C;;GAEG;AACH,qBAAa,wBAAyB,YAAW,eAAe;IAIhD,OAAO,CAAC,GAAG;IAHvB,OAAO,CAAC,WAAW,CAA6B;IAChD,OAAO,CAAC,eAAe,CAAC,CAAkB;gBAEtB,GAAG,EAAE,eAAe;IAmBxC;;OAEG;IACH,SAAS,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IAyKxC;;OAEG;IACH,OAAO,IAAI,IAAI;IAiBf;;;OAGG;IACH,WAAW,IAAI,IAAI;IAYnB,OAAO,CAAC,SAAS;CAUpB"}
@@ -159,6 +159,22 @@ export class WebSocketEventSubscriber {
159
159
  this.connections.clear();
160
160
  logger.debug('WebSocket event subscriber cleaned up');
161
161
  }
162
+ /**
163
+ * Unsubscribe from current event bus without closing WebSocket clients.
164
+ * Useful when switching the active agent and re-subscribing to a new bus.
165
+ */
166
+ unsubscribe() {
167
+ if (this.abortController) {
168
+ const controller = this.abortController;
169
+ delete this.abortController;
170
+ try {
171
+ controller.abort();
172
+ }
173
+ catch (error) {
174
+ logger.debug('Error aborting controller during unsubscribe:', error);
175
+ }
176
+ }
177
+ }
162
178
  broadcast(message) {
163
179
  const messageString = JSON.stringify(message);
164
180
  for (const client of this.connections) {