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