dexto 1.13.5 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/mcp/http-gateway.d.ts +21 -0
- package/dist/api/mcp/http-gateway.d.ts.map +1 -0
- package/dist/api/mcp/http-gateway.js +110 -0
- package/dist/api/mcp/tool-aggregation-handler.d.ts +9 -6
- package/dist/api/mcp/tool-aggregation-handler.d.ts.map +1 -1
- package/dist/api/mcp/tool-aggregation-handler.js +203 -70
- package/dist/api/server-hono.d.ts +2 -0
- package/dist/api/server-hono.d.ts.map +1 -1
- package/dist/api/server-hono.js +377 -276
- package/dist/cli/approval/headless-approval-handler.d.ts +4 -0
- package/dist/cli/approval/headless-approval-handler.d.ts.map +1 -0
- package/dist/cli/approval/headless-approval-handler.js +19 -0
- package/dist/cli/auth/api-client.js +1 -1
- package/dist/cli/auth/constants.d.ts +1 -0
- package/dist/cli/auth/constants.d.ts.map +1 -1
- package/dist/cli/auth/constants.js +2 -1
- package/dist/cli/auth/dexto-api-key.d.ts.map +1 -1
- package/dist/cli/auth/dexto-api-key.js +3 -0
- package/dist/cli/auth/login-persistence.d.ts +1 -1
- package/dist/cli/auth/login-persistence.d.ts.map +1 -1
- package/dist/cli/auth/login-persistence.js +9 -1
- package/dist/cli/auth/service.d.ts +2 -0
- package/dist/cli/auth/service.d.ts.map +1 -1
- package/dist/cli/auth/service.js +1 -0
- package/dist/cli/commands/agents/register.d.ts.map +1 -1
- package/dist/cli/commands/agents/register.js +0 -1
- package/dist/cli/commands/auth/login.d.ts.map +1 -1
- package/dist/cli/commands/auth/login.js +9 -4
- package/dist/cli/commands/cloud/discovery.d.ts +17 -0
- package/dist/cli/commands/cloud/discovery.d.ts.map +1 -0
- package/dist/cli/commands/cloud/discovery.js +88 -0
- package/dist/cli/commands/cloud/register.d.ts +3 -0
- package/dist/cli/commands/cloud/register.d.ts.map +1 -0
- package/dist/cli/commands/cloud/register.js +27 -0
- package/dist/cli/commands/mcp/register.d.ts +1 -1
- package/dist/cli/commands/mcp/register.d.ts.map +1 -1
- package/dist/cli/commands/mcp/register.js +259 -26
- package/dist/cli/commands/mcp/standalone.d.ts +98 -0
- package/dist/cli/commands/mcp/standalone.d.ts.map +1 -0
- package/dist/cli/commands/mcp/standalone.js +489 -0
- package/dist/cli/commands/run/headless.d.ts +3 -1
- package/dist/cli/commands/run/headless.d.ts.map +1 -1
- package/dist/cli/commands/run/headless.js +13 -1
- package/dist/cli/commands/run/output.d.ts +21 -0
- package/dist/cli/commands/run/output.d.ts.map +1 -0
- package/dist/cli/commands/run/output.js +95 -0
- package/dist/cli/commands/run/register.d.ts.map +1 -1
- package/dist/cli/commands/run/register.js +27 -3
- package/dist/cli/commands/setup.d.ts +2 -2
- package/dist/cli/commands/trace/client.d.ts +2 -2
- package/dist/cli/utils/options.js +3 -1
- package/dist/config/cli-overrides.d.ts +3 -1
- package/dist/config/cli-overrides.d.ts.map +1 -1
- package/dist/config/cli-overrides.js +9 -3
- package/dist/index-main.js +11 -10
- package/dist/utils/env.d.ts +2 -0
- package/dist/utils/env.d.ts.map +1 -1
- package/dist/utils/env.js +6 -0
- package/dist/utils/graceful-shutdown.d.ts +1 -1
- package/dist/utils/graceful-shutdown.d.ts.map +1 -1
- package/dist/utils/graceful-shutdown.js +28 -9
- package/package.json +14 -14
package/dist/api/server-hono.js
CHANGED
|
@@ -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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
240
|
+
if (previousAgent && previousAgent !== newAgent) {
|
|
241
|
+
logger.info('Stopping previous agent...');
|
|
242
|
+
await previousAgent.stop();
|
|
243
|
+
ownedAgents.delete(previousAgent);
|
|
244
|
+
}
|
|
218
245
|
}
|
|
219
|
-
catch (
|
|
220
|
-
logger.warn(`
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
285
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
|
471
|
+
await stop();
|
|
375
472
|
}
|
|
376
|
-
catch (
|
|
377
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
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
|
}
|