dexto 1.13.5 → 1.14.1

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 (62) hide show
  1. package/dist/api/mcp/http-gateway.d.ts +21 -0
  2. package/dist/api/mcp/http-gateway.d.ts.map +1 -0
  3. package/dist/api/mcp/http-gateway.js +110 -0
  4. package/dist/api/mcp/tool-aggregation-handler.d.ts +9 -6
  5. package/dist/api/mcp/tool-aggregation-handler.d.ts.map +1 -1
  6. package/dist/api/mcp/tool-aggregation-handler.js +203 -70
  7. package/dist/api/server-hono.d.ts +2 -0
  8. package/dist/api/server-hono.d.ts.map +1 -1
  9. package/dist/api/server-hono.js +377 -276
  10. package/dist/cli/approval/headless-approval-handler.d.ts +4 -0
  11. package/dist/cli/approval/headless-approval-handler.d.ts.map +1 -0
  12. package/dist/cli/approval/headless-approval-handler.js +19 -0
  13. package/dist/cli/auth/api-client.js +1 -1
  14. package/dist/cli/auth/constants.d.ts +1 -0
  15. package/dist/cli/auth/constants.d.ts.map +1 -1
  16. package/dist/cli/auth/constants.js +2 -1
  17. package/dist/cli/auth/dexto-api-key.d.ts.map +1 -1
  18. package/dist/cli/auth/dexto-api-key.js +3 -0
  19. package/dist/cli/auth/login-persistence.d.ts +1 -1
  20. package/dist/cli/auth/login-persistence.d.ts.map +1 -1
  21. package/dist/cli/auth/login-persistence.js +9 -1
  22. package/dist/cli/auth/service.d.ts +2 -0
  23. package/dist/cli/auth/service.d.ts.map +1 -1
  24. package/dist/cli/auth/service.js +1 -0
  25. package/dist/cli/commands/agents/register.d.ts.map +1 -1
  26. package/dist/cli/commands/agents/register.js +0 -1
  27. package/dist/cli/commands/auth/login.d.ts.map +1 -1
  28. package/dist/cli/commands/auth/login.js +9 -4
  29. package/dist/cli/commands/cloud/discovery.d.ts +17 -0
  30. package/dist/cli/commands/cloud/discovery.d.ts.map +1 -0
  31. package/dist/cli/commands/cloud/discovery.js +88 -0
  32. package/dist/cli/commands/cloud/register.d.ts +3 -0
  33. package/dist/cli/commands/cloud/register.d.ts.map +1 -0
  34. package/dist/cli/commands/cloud/register.js +27 -0
  35. package/dist/cli/commands/mcp/register.d.ts +1 -1
  36. package/dist/cli/commands/mcp/register.d.ts.map +1 -1
  37. package/dist/cli/commands/mcp/register.js +259 -26
  38. package/dist/cli/commands/mcp/standalone.d.ts +98 -0
  39. package/dist/cli/commands/mcp/standalone.d.ts.map +1 -0
  40. package/dist/cli/commands/mcp/standalone.js +489 -0
  41. package/dist/cli/commands/run/headless.d.ts +3 -1
  42. package/dist/cli/commands/run/headless.d.ts.map +1 -1
  43. package/dist/cli/commands/run/headless.js +13 -1
  44. package/dist/cli/commands/run/output.d.ts +21 -0
  45. package/dist/cli/commands/run/output.d.ts.map +1 -0
  46. package/dist/cli/commands/run/output.js +95 -0
  47. package/dist/cli/commands/run/register.d.ts.map +1 -1
  48. package/dist/cli/commands/run/register.js +27 -3
  49. package/dist/cli/commands/setup.d.ts +2 -2
  50. package/dist/cli/commands/trace/client.d.ts +2 -2
  51. package/dist/cli/utils/options.js +3 -1
  52. package/dist/config/cli-overrides.d.ts +3 -1
  53. package/dist/config/cli-overrides.d.ts.map +1 -1
  54. package/dist/config/cli-overrides.js +9 -3
  55. package/dist/index-main.js +14 -11
  56. package/dist/utils/env.d.ts +2 -0
  57. package/dist/utils/env.d.ts.map +1 -1
  58. package/dist/utils/env.js +6 -0
  59. package/dist/utils/graceful-shutdown.d.ts +1 -1
  60. package/dist/utils/graceful-shutdown.d.ts.map +1 -1
  61. package/dist/utils/graceful-shutdown.js +28 -9
  62. package/package.json +14 -14
@@ -77,12 +77,10 @@ function resolveBaseUrl(port) {
77
77
  }
78
78
  //TODO (migration): consider moving this to the server package
79
79
  export async function initializeHonoApi(agent, agentCardOverride, listenPort, agentId, configFilePath, workspaceRoot, webRoot, webUIConfig) {
80
- // Declare before registering shutdown hook to avoid TDZ on signals
81
80
  let activeAgent = agent;
81
+ const ownedAgents = new Set([agent]);
82
82
  let activeAgentId = agentId || 'coding-agent';
83
83
  let activeAgentConfigPath = configFilePath;
84
- let isSwitchingAgent = false;
85
- registerGracefulShutdown(() => activeAgent);
86
84
  const resolvedPort = resolvePort(listenPort);
87
85
  const baseApiUrl = resolveBaseUrl(resolvedPort);
88
86
  // Apply agentCard overrides (if any)
@@ -98,317 +96,420 @@ export async function initializeHonoApi(agent, agentCardOverride, listenPort, ag
98
96
  const sessionSseSubscriber = new SessionSseEventSubscriber();
99
97
  const approvalCoordinator = new ApprovalCoordinator();
100
98
  let approvalEventBridge = null;
101
- /**
102
- * Wire services (SSE subscribers) to an agent.
103
- * Called for agent switching to re-subscribe to the new agent's event bus.
104
- * Note: Approval handler and coordinator are set before agent.start() for each agent.
105
- */
106
- async function wireServicesToAgent(agent) {
107
- logger.debug('Wiring services to agent...');
108
- approvalEventBridge?.abort();
109
- approvalEventBridge = wireApprovalCoordinatorToAgent(agent, approvalCoordinator);
110
- // Register subscribers (DextoAgent handles (re-)subscription on start/restart)
111
- agent.registerSubscriber(webhookSubscriber);
112
- agent.registerSubscriber(sseSubscriber);
113
- agent.registerSubscriber(sessionSseSubscriber);
114
- }
115
- /**
116
- * Helper to resolve agent ID to { id, name } by looking up in registry
117
- */
118
- async function resolveAgentInfo(agentId) {
119
- const agents = await listAgents();
120
- const agent = agents.installed.find((a) => a.id === agentId) ??
121
- agents.available.find((a) => a.id === agentId);
122
- return {
123
- id: agentId,
124
- name: agent?.name ?? deriveDisplayName(agentId),
125
- };
99
+ let bridgeRef = null;
100
+ let mcpTransport;
101
+ let disposeShutdown;
102
+ let stopPromise;
103
+ let pendingAgentSwitch;
104
+ function runAgentSwitch(operation) {
105
+ if (stopPromise)
106
+ return Promise.reject(AgentError.stopped());
107
+ if (pendingAgentSwitch)
108
+ return Promise.reject(AgentError.switchInProgress());
109
+ pendingAgentSwitch = operation();
110
+ return pendingAgentSwitch.finally(() => {
111
+ pendingAgentSwitch = undefined;
112
+ });
126
113
  }
127
- function ensureAgentAvailable() {
128
- // Gate requests during agent switching
129
- if (isSwitchingAgent) {
130
- throw AgentError.switchInProgress();
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();
114
+ function stop() {
115
+ if (!stopPromise) {
116
+ stopPromise = (async () => {
117
+ await pendingAgentSwitch?.catch(() => undefined);
118
+ const results = await Promise.allSettled([
119
+ Promise.resolve().then(() => disposeShutdown?.()),
120
+ Promise.resolve().then(() => approvalEventBridge?.abort()),
121
+ Promise.resolve().then(() => sseSubscriber.cleanup()),
122
+ Promise.resolve().then(() => sessionSseSubscriber.cleanup()),
123
+ Promise.resolve().then(() => {
124
+ if (!bridgeRef) {
125
+ webhookSubscriber.cleanup();
126
+ return;
127
+ }
128
+ const server = bridgeRef.server;
129
+ return new Promise((resolve, reject) => {
130
+ server.close((error) => {
131
+ if (error &&
132
+ (!('code' in error) || error.code !== 'ERR_SERVER_NOT_RUNNING'))
133
+ reject(error);
134
+ else
135
+ resolve();
136
+ });
137
+ server.closeAllConnections();
138
+ });
139
+ }),
140
+ Promise.resolve().then(() => mcpTransport?.close()),
141
+ ...Array.from(ownedAgents, (ownedAgent) => Promise.resolve().then(() => ownedAgent.stop())),
142
+ ]);
143
+ const failures = results
144
+ .filter((result) => result.status === 'rejected')
145
+ .map((result) => result.reason);
146
+ if (failures.length)
147
+ throw new AggregateError(failures, 'Failed to stop CLI HTTP host');
148
+ })();
142
149
  }
150
+ return stopPromise;
143
151
  }
144
- /**
145
- * Common agent switching logic shared by switchAgentById and switchAgentByPath.
146
- */
147
- async function performAgentSwitch(newAgent, agentId, agentConfigPath, bridge) {
148
- logger.info('Preparing new agent for switch...');
149
- // Register webhook subscriber for LLM streaming events
150
- if (bridge.webhookSubscriber) {
151
- newAgent.registerSubscriber(bridge.webhookSubscriber);
152
- }
153
- // Switch activeAgent reference first
154
- const previousAgent = activeAgent;
155
- activeAgent = newAgent;
156
- activeAgentId = agentId;
157
- activeAgentConfigPath = agentConfigPath;
158
- // Set approval handler if manual mode OR elicitation enabled (before start() for validation)
159
- const needsHandler = newAgent.config.permissions.mode === 'manual' || newAgent.config.elicitation.enabled;
160
- if (needsHandler) {
161
- logger.debug('Setting up manual approval handler for new agent...');
162
- const handler = createManualApprovalHandler(approvalCoordinator);
163
- newAgent.setApprovalHandler(handler);
152
+ try {
153
+ /**
154
+ * Wire services (SSE subscribers) to an agent.
155
+ * Called for agent switching to re-subscribe to the new agent's event bus.
156
+ * Note: Approval handler and coordinator are set before agent.start() for each agent.
157
+ */
158
+ async function wireServicesToAgent(agent) {
159
+ logger.debug('Wiring services to agent...');
160
+ approvalEventBridge?.abort();
161
+ approvalEventBridge = wireApprovalCoordinatorToAgent(agent, approvalCoordinator);
162
+ // Register subscribers (DextoAgent handles (re-)subscription on start/restart)
163
+ agent.registerSubscriber(webhookSubscriber);
164
+ agent.registerSubscriber(sseSubscriber);
165
+ agent.registerSubscriber(sessionSseSubscriber);
164
166
  }
165
- // Wire SSE subscribers BEFORE starting
166
- logger.info('Wiring services to new agent...');
167
- await wireServicesToAgent(newAgent);
168
- logger.info(`Starting new agent: ${agentId}`);
169
- await newAgent.start();
170
- if (workspaceRoot) {
171
- await applyWorkspaceToAgent(newAgent, workspaceRoot);
167
+ /**
168
+ * Helper to resolve agent ID to { id, name } by looking up in registry
169
+ */
170
+ async function resolveAgentInfo(agentId) {
171
+ const agents = await listAgents();
172
+ const agent = agents.installed.find((a) => a.id === agentId) ??
173
+ agents.available.find((a) => a.id === agentId);
174
+ return {
175
+ id: agentId,
176
+ name: agent?.name ?? deriveDisplayName(agentId),
177
+ };
172
178
  }
173
- // Update agent card for A2A and MCP routes
174
- agentCardData = createAgentCard({
175
- defaultName: agentId,
176
- defaultVersion: overrides.version ?? DEFAULT_AGENT_VERSION,
177
- defaultBaseUrl: baseApiUrl,
178
- }, overrides);
179
- logger.info(`Successfully switched to agent: ${agentId}`);
180
- // Now safely stop the previous agent
181
- try {
182
- if (previousAgent && previousAgent !== newAgent) {
183
- logger.info('Stopping previous agent...');
184
- await previousAgent.stop();
179
+ function ensureAgentAvailable() {
180
+ if (stopPromise)
181
+ throw AgentError.stopped();
182
+ // Gate requests during agent switching
183
+ if (pendingAgentSwitch) {
184
+ throw AgentError.switchInProgress();
185
+ }
186
+ // Fast path: most common case is agent is started and running
187
+ if (activeAgent.isStarted() && !activeAgent.isStopped()) {
188
+ return;
189
+ }
190
+ // Provide specific error messages for better debugging
191
+ if (activeAgent.isStopped()) {
192
+ throw AgentError.stopped();
193
+ }
194
+ if (!activeAgent.isStarted()) {
195
+ throw AgentError.notStarted();
185
196
  }
186
197
  }
187
- catch (err) {
188
- logger.warn(`Stopping previous agent failed: ${err}`);
189
- // Don't throw here as the switch was successful
190
- }
191
- return await resolveAgentInfo(agentId);
192
- }
193
- async function switchAgentById(agentId, bridge) {
194
- if (isSwitchingAgent) {
195
- throw AgentError.switchInProgress();
196
- }
197
- isSwitchingAgent = true;
198
- let newAgent;
199
- let newAgentConfigPath;
200
- try {
201
- // 1. SHUTDOWN OLD TELEMETRY FIRST (before creating new agent)
202
- logger.info('Shutting down telemetry for agent switch...');
203
- const { Telemetry } = await import('@dexto/core');
204
- await Telemetry.shutdownGlobal();
205
- // 2. Create new agent from registry (will initialize fresh telemetry in createAgentServices)
206
- const registry = getAgentRegistry();
207
- newAgentConfigPath = await registry.resolveAgent(agentId, true);
208
- newAgent = await createAgentFromId(agentId, workspaceRoot);
209
- // 3. Use common switch logic (register subscribers, start agent, stop previous)
210
- return await performAgentSwitch(newAgent, agentId, newAgentConfigPath, bridge);
211
- }
212
- catch (error) {
213
- logger.error(`Failed to switch to agent '${agentId}': ${error instanceof Error ? error.message : String(error)}`, { error });
214
- // Clean up the failed new agent if it was created
215
- if (newAgent) {
198
+ /**
199
+ * Common agent switching logic shared by switchAgentById and switchAgentByPath.
200
+ */
201
+ async function performAgentSwitch(newAgent, agentId, agentConfigPath, bridge) {
202
+ ownedAgents.add(newAgent);
203
+ logger.info('Preparing new agent for switch...');
204
+ const previousAgent = activeAgent;
205
+ try {
206
+ // Register webhook subscriber for LLM streaming events
207
+ if (bridge.webhookSubscriber) {
208
+ newAgent.registerSubscriber(bridge.webhookSubscriber);
209
+ }
210
+ // Set approval handler if manual mode OR elicitation enabled (before start() for validation)
211
+ const needsHandler = newAgent.config.permissions.mode === 'manual' ||
212
+ newAgent.config.elicitation.enabled;
213
+ if (needsHandler) {
214
+ logger.debug('Setting up manual approval handler for new agent...');
215
+ const handler = createManualApprovalHandler(approvalCoordinator);
216
+ newAgent.setApprovalHandler(handler);
217
+ }
218
+ // Wire SSE subscribers BEFORE starting
219
+ logger.info('Wiring services to new agent...');
220
+ await wireServicesToAgent(newAgent);
221
+ logger.info(`Starting new agent: ${agentId}`);
222
+ await newAgent.start();
223
+ if (workspaceRoot) {
224
+ await applyWorkspaceToAgent(newAgent, workspaceRoot);
225
+ }
226
+ const agentInfo = await resolveAgentInfo(agentId);
227
+ const nextAgentCard = createAgentCard({
228
+ defaultName: agentId,
229
+ defaultVersion: overrides.version ?? DEFAULT_AGENT_VERSION,
230
+ defaultBaseUrl: baseApiUrl,
231
+ }, overrides);
232
+ // Publish only after the replacement is ready; failed preparation leaves routes intact.
233
+ activeAgent = newAgent;
234
+ activeAgentId = agentId;
235
+ activeAgentConfigPath = agentConfigPath;
236
+ agentCardData = nextAgentCard;
237
+ logger.info(`Successfully switched to agent: ${agentId}`);
238
+ // Now safely stop the previous agent
216
239
  try {
217
- await newAgent.stop();
240
+ if (previousAgent && previousAgent !== newAgent) {
241
+ logger.info('Stopping previous agent...');
242
+ await previousAgent.stop();
243
+ ownedAgents.delete(previousAgent);
244
+ }
218
245
  }
219
- catch (cleanupErr) {
220
- logger.warn(`Failed to cleanup new agent: ${cleanupErr}`);
246
+ catch (err) {
247
+ logger.warn(`Stopping previous agent failed: ${err}`);
248
+ // Don't throw here as the switch was successful
221
249
  }
250
+ return agentInfo;
251
+ }
252
+ catch (error) {
253
+ // Subscribers are shared and may already have moved to the replacement's event bus.
254
+ try {
255
+ if (bridge.webhookSubscriber)
256
+ previousAgent.registerSubscriber(bridge.webhookSubscriber);
257
+ await wireServicesToAgent(previousAgent);
258
+ }
259
+ catch (recoveryError) {
260
+ logger.error('Failed to restore services to previous agent', {
261
+ error: recoveryError,
262
+ });
263
+ }
264
+ throw error;
222
265
  }
223
- throw error;
224
- }
225
- finally {
226
- isSwitchingAgent = false;
227
266
  }
228
- }
229
- async function switchAgentByPath(filePath, bridge) {
230
- if (isSwitchingAgent) {
231
- throw AgentError.switchInProgress();
267
+ async function switchAgentById(agentId, bridge) {
268
+ let newAgent;
269
+ let newAgentConfigPath;
270
+ try {
271
+ // 1. SHUTDOWN OLD TELEMETRY FIRST (before creating new agent)
272
+ logger.info('Shutting down telemetry for agent switch...');
273
+ const { Telemetry } = await import('@dexto/core');
274
+ await Telemetry.shutdownGlobal();
275
+ // 2. Create new agent from registry (will initialize fresh telemetry in createAgentServices)
276
+ const registry = getAgentRegistry();
277
+ newAgentConfigPath = await registry.resolveAgent(agentId, true);
278
+ newAgent = await createAgentFromId(agentId, workspaceRoot);
279
+ // 3. Use common switch logic (register subscribers, start agent, stop previous)
280
+ return await performAgentSwitch(newAgent, agentId, newAgentConfigPath, bridge);
281
+ }
282
+ catch (error) {
283
+ logger.error(`Failed to switch to agent '${agentId}': ${error instanceof Error ? error.message : String(error)}`, { error });
284
+ // Clean up the failed new agent if it was created
285
+ if (newAgent) {
286
+ try {
287
+ await newAgent.stop();
288
+ ownedAgents.delete(newAgent);
289
+ }
290
+ catch (cleanupErr) {
291
+ logger.warn(`Failed to cleanup new agent: ${cleanupErr}`);
292
+ }
293
+ }
294
+ throw error;
295
+ }
232
296
  }
233
- isSwitchingAgent = true;
234
- let newAgent;
235
- try {
236
- // 1. SHUTDOWN OLD TELEMETRY FIRST (before creating new agent)
237
- logger.info('Shutting down telemetry for agent switch...');
238
- const { Telemetry } = await import('@dexto/core');
239
- await Telemetry.shutdownGlobal();
240
- // 2. Load agent configuration from file path
241
- let config = await loadAgentConfig(filePath);
242
- // 2.5. Apply user's LLM preferences to ALL agents
243
- // Three-Layer Resolution: local.llm ?? preferences.llm ?? bundled.llm
244
- if (globalPreferencesExist()) {
245
- try {
246
- const preferences = await loadGlobalPreferences();
247
- if (preferences?.llm?.provider && preferences?.llm?.model) {
248
- config = applyUserPreferences(config, preferences);
249
- logger.debug(`Applied user preferences to agent from ${filePath} (provider=${preferences.llm.provider}, model=${preferences.llm.model})`);
297
+ async function switchAgentByPath(filePath, bridge) {
298
+ let newAgent;
299
+ try {
300
+ // 1. SHUTDOWN OLD TELEMETRY FIRST (before creating new agent)
301
+ logger.info('Shutting down telemetry for agent switch...');
302
+ const { Telemetry } = await import('@dexto/core');
303
+ await Telemetry.shutdownGlobal();
304
+ // 2. Load agent configuration from file path
305
+ let config = await loadAgentConfig(filePath);
306
+ // 2.5. Apply user's LLM preferences to ALL agents
307
+ // Three-Layer Resolution: local.llm ?? preferences.llm ?? bundled.llm
308
+ if (globalPreferencesExist()) {
309
+ try {
310
+ const preferences = await loadGlobalPreferences();
311
+ if (preferences?.llm?.provider && preferences?.llm?.model) {
312
+ config = applyUserPreferences(config, preferences);
313
+ logger.debug(`Applied user preferences to agent from ${filePath} (provider=${preferences.llm.provider}, model=${preferences.llm.model})`);
314
+ }
315
+ }
316
+ catch {
317
+ logger.debug('Could not load preferences, using bundled config');
250
318
  }
251
319
  }
252
- catch {
253
- logger.debug('Could not load preferences, using bundled config');
320
+ // 3. Create new agent instance (will initialize fresh telemetry in createAgentServices)
321
+ newAgent = await createDextoAgentFromConfig({
322
+ config,
323
+ configPath: filePath,
324
+ enrichOptions: {
325
+ logLevel: 'info',
326
+ ...(workspaceRoot ? { workspaceRoot } : {}),
327
+ },
328
+ overrides: { sessionLoggerFactory },
329
+ });
330
+ // 4. Use enriched agentId (derived from config or filename during enrichment)
331
+ const agentId = newAgent.config.agentId;
332
+ // 5. Use common switch logic (register subscribers, start agent, stop previous)
333
+ return await performAgentSwitch(newAgent, agentId, filePath, bridge);
334
+ }
335
+ catch (error) {
336
+ logger.error(`Failed to switch to agent from path '${filePath}': ${error instanceof Error ? error.message : String(error)}`, { error });
337
+ // Clean up the failed new agent if it was created
338
+ if (newAgent) {
339
+ try {
340
+ await newAgent.stop();
341
+ ownedAgents.delete(newAgent);
342
+ }
343
+ catch (cleanupErr) {
344
+ logger.warn(`Failed to cleanup new agent: ${cleanupErr}`);
345
+ }
254
346
  }
347
+ throw error;
255
348
  }
256
- // 3. Create new agent instance (will initialize fresh telemetry in createAgentServices)
257
- newAgent = await createDextoAgentFromConfig({
258
- config,
259
- configPath: filePath,
260
- enrichOptions: {
261
- logLevel: 'info',
262
- ...(workspaceRoot ? { workspaceRoot } : {}),
349
+ }
350
+ // Getter functions for routes (always use current agent)
351
+ // getAgent automatically ensures agent is available before returning it
352
+ const getAgent = () => {
353
+ // CRITICAL: Check agent availability before every access to prevent race conditions
354
+ // during agent switching, stopping, or startup failures
355
+ ensureAgentAvailable();
356
+ return activeAgent;
357
+ };
358
+ const getAgentCard = () => agentCardData;
359
+ const getAgentConfigPath = (_ctx) => activeAgentConfigPath;
360
+ // Create app with agentsContext using closure
361
+ const app = createDextoApp({
362
+ apiPrefix: '/api',
363
+ getAgent,
364
+ getAgentConfigPath,
365
+ getAgentCard,
366
+ approvalCoordinator,
367
+ webhookSubscriber,
368
+ sseSubscriber,
369
+ sessionSseSubscriber,
370
+ ...(webRoot ? { webRoot } : {}),
371
+ ...(webUIConfig ? { webUIConfig } : {}),
372
+ agentsContext: {
373
+ switchAgentById: (id) => {
374
+ if (!bridgeRef)
375
+ throw new Error('Bridge not initialized');
376
+ const bridge = bridgeRef;
377
+ return runAgentSwitch(() => switchAgentById(id, bridge));
263
378
  },
264
- overrides: { sessionLoggerFactory },
265
- });
266
- // 4. Use enriched agentId (derived from config or filename during enrichment)
267
- const agentId = newAgent.config.agentId;
268
- // 5. Use common switch logic (register subscribers, start agent, stop previous)
269
- return await performAgentSwitch(newAgent, agentId, filePath, bridge);
379
+ switchAgentByPath: (filePath) => {
380
+ if (!bridgeRef)
381
+ throw new Error('Bridge not initialized');
382
+ const bridge = bridgeRef;
383
+ return runAgentSwitch(() => switchAgentByPath(filePath, bridge));
384
+ },
385
+ resolveAgentInfo,
386
+ ensureAgentAvailable,
387
+ getActiveAgentId: () => activeAgentId,
388
+ },
389
+ });
390
+ const transportType = process.env.DEXTO_MCP_TRANSPORT_TYPE || 'http';
391
+ try {
392
+ mcpTransport = await createServerMcpTransport(transportType);
270
393
  }
271
394
  catch (error) {
272
- logger.error(`Failed to switch to agent from path '${filePath}': ${error instanceof Error ? error.message : String(error)}`, { error });
273
- // Clean up the failed new agent if it was created
274
- if (newAgent) {
275
- try {
276
- await newAgent.stop();
277
- }
278
- catch (cleanupErr) {
279
- logger.warn(`Failed to cleanup new agent: ${cleanupErr}`);
280
- }
281
- }
282
- throw error;
395
+ const errorMessage = error instanceof Error ? error.message : String(error);
396
+ logger.error(`Failed to create MCP transport: ${errorMessage}`);
397
+ mcpTransport = undefined;
283
398
  }
284
- finally {
285
- isSwitchingAgent = false;
399
+ // Create bridge with app
400
+ bridgeRef = createNodeServer(app, {
401
+ getAgent: () => activeAgent,
402
+ mcpHandlers: mcpTransport ? createMcpHttpHandlers(mcpTransport) : null,
403
+ });
404
+ // Register webhook subscriber for LLM streaming events
405
+ logger.info('Registering webhook subscriber with agent...');
406
+ if (bridgeRef.webhookSubscriber) {
407
+ activeAgent.registerSubscriber(bridgeRef.webhookSubscriber);
286
408
  }
287
- }
288
- // Getter functions for routes (always use current agent)
289
- // getAgent automatically ensures agent is available before returning it
290
- // Accepts Context parameter for compatibility with GetAgentFn type
291
- const getAgent = (_ctx) => {
292
- // CRITICAL: Check agent availability before every access to prevent race conditions
293
- // during agent switching, stopping, or startup failures
294
- ensureAgentAvailable();
295
- return activeAgent;
296
- };
297
- const getAgentCard = () => agentCardData;
298
- const getAgentConfigPath = (_ctx) => activeAgentConfigPath;
299
- // Declare bridge variable that will be set later
300
- let bridgeRef = null;
301
- // Create app with agentsContext using closure
302
- const app = createDextoApp({
303
- apiPrefix: '/api',
304
- getAgent,
305
- getAgentConfigPath,
306
- getAgentCard,
307
- approvalCoordinator,
308
- webhookSubscriber,
309
- sseSubscriber,
310
- sessionSseSubscriber,
311
- ...(webRoot ? { webRoot } : {}),
312
- ...(webUIConfig ? { webUIConfig } : {}),
313
- agentsContext: {
314
- switchAgentById: (id) => {
315
- if (!bridgeRef)
316
- throw new Error('Bridge not initialized');
317
- return switchAgentById(id, bridgeRef);
318
- },
319
- switchAgentByPath: (filePath) => {
320
- if (!bridgeRef)
321
- throw new Error('Bridge not initialized');
322
- return switchAgentByPath(filePath, bridgeRef);
323
- },
409
+ // Update agent card
410
+ agentCardData = createAgentCard({
411
+ defaultName: overrides.name ?? activeAgentId,
412
+ defaultVersion: overrides.version ?? DEFAULT_AGENT_VERSION,
413
+ defaultBaseUrl: baseApiUrl,
414
+ }, overrides);
415
+ // Set approval handler for initial agent if manual mode OR elicitation enabled (before start() for validation)
416
+ const needsHandler = activeAgent.config.permissions.mode === 'manual' ||
417
+ activeAgent.config.elicitation.enabled;
418
+ if (needsHandler) {
419
+ logger.debug('Setting up manual approval handler for initial agent...');
420
+ const handler = createManualApprovalHandler(approvalCoordinator);
421
+ activeAgent.setApprovalHandler(handler);
422
+ }
423
+ // Wire SSE subscribers to initial agent
424
+ logger.info('Wiring SSE subscribers to initial agent...');
425
+ await wireServicesToAgent(activeAgent);
426
+ // Start the initial agent now that approval handler is set and subscribers are wired
427
+ logger.info('Starting initial agent...');
428
+ await activeAgent.start();
429
+ if (workspaceRoot) {
430
+ await applyWorkspaceToAgent(activeAgent, workspaceRoot);
431
+ }
432
+ // Initialize MCP server after agent has started
433
+ if (mcpTransport) {
434
+ try {
435
+ await initializeServerMcpServer(activeAgent, getAgentCard(), mcpTransport, {
436
+ getAgent,
437
+ getAgentCard: () => {
438
+ ensureAgentAvailable();
439
+ return getAgentCard();
440
+ },
441
+ });
442
+ }
443
+ catch (error) {
444
+ const errorMessage = error instanceof Error ? error.message : String(error);
445
+ logger.error(`Failed to initialize MCP server: ${errorMessage}`);
446
+ await mcpTransport.close();
447
+ mcpTransport = undefined;
448
+ }
449
+ }
450
+ disposeShutdown = registerGracefulShutdown(() => ({ stop }));
451
+ const bridge = bridgeRef;
452
+ return {
453
+ stop,
454
+ app,
455
+ server: bridgeRef.server,
456
+ ...(bridgeRef.webhookSubscriber
457
+ ? { webhookSubscriber: bridgeRef.webhookSubscriber }
458
+ : {}),
459
+ agentCard: agentCardData,
460
+ ...(mcpTransport ? { mcpTransport } : {}),
461
+ // Expose switching functions for agent routes
462
+ switchAgentById: (id) => runAgentSwitch(() => switchAgentById(id, bridge)),
463
+ switchAgentByPath: (filePath) => runAgentSwitch(() => switchAgentByPath(filePath, bridge)),
324
464
  resolveAgentInfo,
325
465
  ensureAgentAvailable,
326
466
  getActiveAgentId: () => activeAgentId,
327
- },
328
- });
329
- let mcpTransport;
330
- const transportType = process.env.DEXTO_MCP_TRANSPORT_TYPE || 'http';
331
- try {
332
- mcpTransport = await createServerMcpTransport(transportType);
467
+ };
333
468
  }
334
469
  catch (error) {
335
- const errorMessage = error instanceof Error ? error.message : String(error);
336
- logger.error(`Failed to create MCP transport: ${errorMessage}`);
337
- mcpTransport = undefined;
338
- }
339
- // Create bridge with app
340
- bridgeRef = createNodeServer(app, {
341
- getAgent: () => activeAgent,
342
- mcpHandlers: mcpTransport ? createMcpHttpHandlers(mcpTransport) : null,
343
- });
344
- // Register webhook subscriber for LLM streaming events
345
- logger.info('Registering webhook subscriber with agent...');
346
- if (bridgeRef.webhookSubscriber) {
347
- activeAgent.registerSubscriber(bridgeRef.webhookSubscriber);
348
- }
349
- // Update agent card
350
- agentCardData = createAgentCard({
351
- defaultName: overrides.name ?? activeAgentId,
352
- defaultVersion: overrides.version ?? DEFAULT_AGENT_VERSION,
353
- defaultBaseUrl: baseApiUrl,
354
- }, overrides);
355
- // Set approval handler for initial agent if manual mode OR elicitation enabled (before start() for validation)
356
- const needsHandler = activeAgent.config.permissions.mode === 'manual' || activeAgent.config.elicitation.enabled;
357
- if (needsHandler) {
358
- logger.debug('Setting up manual approval handler for initial agent...');
359
- const handler = createManualApprovalHandler(approvalCoordinator);
360
- activeAgent.setApprovalHandler(handler);
361
- }
362
- // Wire SSE subscribers to initial agent
363
- logger.info('Wiring SSE subscribers to initial agent...');
364
- await wireServicesToAgent(activeAgent);
365
- // Start the initial agent now that approval handler is set and subscribers are wired
366
- logger.info('Starting initial agent...');
367
- await activeAgent.start();
368
- if (workspaceRoot) {
369
- await applyWorkspaceToAgent(activeAgent, workspaceRoot);
370
- }
371
- // Initialize MCP server after agent has started
372
- if (mcpTransport) {
373
470
  try {
374
- await initializeServerMcpServer(activeAgent, getAgentCard(), mcpTransport);
471
+ await stop();
375
472
  }
376
- catch (error) {
377
- const errorMessage = error instanceof Error ? error.message : String(error);
378
- logger.error(`Failed to initialize MCP server: ${errorMessage}`);
379
- mcpTransport = undefined;
473
+ catch (cleanupError) {
474
+ logger.error('Failed to roll back CLI HTTP host', { error: cleanupError });
380
475
  }
476
+ throw error;
381
477
  }
382
- return {
383
- app,
384
- server: bridgeRef.server,
385
- ...(bridgeRef.webhookSubscriber ? { webhookSubscriber: bridgeRef.webhookSubscriber } : {}),
386
- agentCard: agentCardData,
387
- ...(mcpTransport ? { mcpTransport } : {}),
388
- // Expose switching functions for agent routes
389
- switchAgentById: (id) => switchAgentById(id, bridgeRef),
390
- switchAgentByPath: (filePath) => switchAgentByPath(filePath, bridgeRef),
391
- resolveAgentInfo,
392
- ensureAgentAvailable,
393
- getActiveAgentId: () => activeAgentId,
394
- };
395
478
  }
396
479
  export async function startHonoApiServer(agent, port = 3000, agentCardOverride, agentId, configFilePath, workspaceRoot, webRoot, webUIConfig) {
397
- const { server, webhookSubscriber } = await initializeHonoApi(agent, agentCardOverride, port, agentId, configFilePath, workspaceRoot, webRoot, webUIConfig);
398
- server.listen(port, '0.0.0.0', () => {
399
- const networkInterfaces = os.networkInterfaces();
400
- let localIp = 'localhost';
401
- Object.values(networkInterfaces).forEach((ifaceList) => {
402
- ifaceList?.forEach((iface) => {
403
- if (iface.family === 'IPv4' && !iface.internal) {
404
- localIp = iface.address;
405
- }
480
+ const { server, webhookSubscriber, stop } = await initializeHonoApi(agent, agentCardOverride, port, agentId, configFilePath, workspaceRoot, webRoot, webUIConfig);
481
+ try {
482
+ await new Promise((resolve, reject) => {
483
+ const onError = (error) => reject(error);
484
+ server.once('error', onError);
485
+ server.listen(port, '0.0.0.0', () => {
486
+ server.off('error', onError);
487
+ resolve();
406
488
  });
407
489
  });
408
- logger.info(`Hono server started successfully. Accessible at: http://localhost:${port} and http://${localIp}:${port} on your local network.`, null, 'green');
490
+ }
491
+ catch (error) {
492
+ try {
493
+ await stop();
494
+ }
495
+ catch (cleanupError) {
496
+ logger.error('Failed to roll back CLI HTTP listener', { error: cleanupError });
497
+ }
498
+ throw error;
499
+ }
500
+ const networkInterfaces = os.networkInterfaces();
501
+ let localIp = 'localhost';
502
+ Object.values(networkInterfaces).forEach((ifaceList) => {
503
+ ifaceList?.forEach((iface) => {
504
+ if (iface.family === 'IPv4' && !iface.internal) {
505
+ localIp = iface.address;
506
+ }
507
+ });
409
508
  });
509
+ logger.info(`Hono server started successfully. Accessible at: http://localhost:${port} and http://${localIp}:${port} on your local network.`, null, 'green');
410
510
  return {
411
511
  server,
512
+ stop,
412
513
  ...(webhookSubscriber ? { webhookSubscriber } : {}),
413
514
  };
414
515
  }