@objectstack/service-ai 4.0.3 → 4.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +293 -0
  2. package/dist/index.cjs +1176 -135
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +1225 -430
  5. package/dist/index.d.ts +1225 -430
  6. package/dist/index.js +1160 -128
  7. package/dist/index.js.map +1 -1
  8. package/package.json +35 -8
  9. package/.turbo/turbo-build.log +0 -22
  10. package/CHANGELOG.md +0 -53
  11. package/src/__tests__/ai-service.test.ts +0 -964
  12. package/src/__tests__/auth-and-toolcalling.test.ts +0 -677
  13. package/src/__tests__/chatbot-features.test.ts +0 -1116
  14. package/src/__tests__/metadata-tools.test.ts +0 -970
  15. package/src/__tests__/objectql-conversation-service.test.ts +0 -382
  16. package/src/__tests__/tool-routes.test.ts +0 -191
  17. package/src/__tests__/vercel-stream-encoder.test.ts +0 -310
  18. package/src/adapters/index.ts +0 -6
  19. package/src/adapters/memory-adapter.ts +0 -72
  20. package/src/adapters/types.ts +0 -3
  21. package/src/adapters/vercel-adapter.ts +0 -148
  22. package/src/agent-runtime.ts +0 -154
  23. package/src/agents/data-chat-agent.ts +0 -79
  24. package/src/agents/index.ts +0 -4
  25. package/src/agents/metadata-assistant-agent.ts +0 -87
  26. package/src/ai-service.ts +0 -364
  27. package/src/conversation/in-memory-conversation-service.ts +0 -103
  28. package/src/conversation/index.ts +0 -4
  29. package/src/conversation/objectql-conversation-service.ts +0 -301
  30. package/src/index.ts +0 -60
  31. package/src/objects/ai-conversation.object.ts +0 -86
  32. package/src/objects/ai-message.object.ts +0 -86
  33. package/src/objects/index.ts +0 -10
  34. package/src/plugin.ts +0 -391
  35. package/src/routes/agent-routes.ts +0 -190
  36. package/src/routes/ai-routes.ts +0 -439
  37. package/src/routes/index.ts +0 -5
  38. package/src/routes/message-utils.ts +0 -90
  39. package/src/routes/tool-routes.ts +0 -142
  40. package/src/stream/index.ts +0 -3
  41. package/src/stream/vercel-stream-encoder.ts +0 -153
  42. package/src/tools/add-field.tool.ts +0 -70
  43. package/src/tools/create-object.tool.ts +0 -66
  44. package/src/tools/data-tools.ts +0 -293
  45. package/src/tools/delete-field.tool.ts +0 -38
  46. package/src/tools/describe-object.tool.ts +0 -31
  47. package/src/tools/index.ts +0 -18
  48. package/src/tools/list-objects.tool.ts +0 -34
  49. package/src/tools/metadata-tools.ts +0 -430
  50. package/src/tools/modify-field.tool.ts +0 -44
  51. package/src/tools/tool-registry.ts +0 -132
  52. package/tsconfig.json +0 -17
package/src/plugin.ts DELETED
@@ -1,391 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import type { Plugin, PluginContext } from '@objectstack/core';
4
- import type { IAIService, IAIConversationService, IDataEngine, IMetadataService, LLMAdapter } from '@objectstack/spec/contracts';
5
- import { AIService } from './ai-service.js';
6
- import type { AIServiceConfig } from './ai-service.js';
7
- import { buildAIRoutes } from './routes/ai-routes.js';
8
- import { buildAgentRoutes } from './routes/agent-routes.js';
9
- import { buildToolRoutes } from './routes/tool-routes.js';
10
- import { ObjectQLConversationService } from './conversation/objectql-conversation-service.js';
11
- import { AiConversationObject, AiMessageObject } from './objects/index.js';
12
- import { registerDataTools } from './tools/data-tools.js';
13
- import { registerMetadataTools } from './tools/metadata-tools.js';
14
- import { AgentRuntime } from './agent-runtime.js';
15
- import { DATA_CHAT_AGENT, METADATA_ASSISTANT_AGENT } from './agents/index.js';
16
- import { VercelLLMAdapter } from './adapters/vercel-adapter.js';
17
- import { MemoryLLMAdapter } from './adapters/memory-adapter.js';
18
-
19
- /**
20
- * Configuration options for the AIServicePlugin.
21
- */
22
- export interface AIServicePluginOptions {
23
- /** LLM adapter to use (defaults to MemoryLLMAdapter). */
24
- adapter?: LLMAdapter;
25
- /** Enable debug logging. */
26
- debug?: boolean;
27
- /** Explicit conversation service override. When set, auto-detection is skipped. */
28
- conversationService?: IAIConversationService;
29
- }
30
-
31
- /**
32
- * AIServicePlugin — Kernel plugin for the unified AI capability service.
33
- *
34
- * Lifecycle:
35
- * 1. **init** — Creates {@link AIService}, registers as `'ai'` service.
36
- * If an existing AI service is already registered, it is replaced.
37
- * 2. **start** — Triggers `'ai:ready'` hook so other plugins can register
38
- * tools or extend the service. Registers REST/SSE routes.
39
- * 3. **destroy** — Cleans up references.
40
- *
41
- * @example
42
- * ```ts
43
- * import { LiteKernel } from '@objectstack/core';
44
- * import { AIServicePlugin } from '@objectstack/service-ai';
45
- *
46
- * const kernel = new LiteKernel();
47
- * kernel.use(new AIServicePlugin());
48
- * await kernel.bootstrap();
49
- *
50
- * const ai = kernel.getService<IAIService>('ai');
51
- * const result = await ai.chat([{ role: 'user', content: 'Hello' }]);
52
- * ```
53
- */
54
- export class AIServicePlugin implements Plugin {
55
- name = 'com.objectstack.service-ai';
56
- version = '1.0.0';
57
- type = 'standard' as const;
58
- dependencies: string[] = ['com.objectstack.engine.objectql']; // manifest service required
59
-
60
- private service?: AIService;
61
- private readonly options: AIServicePluginOptions;
62
-
63
- constructor(options: AIServicePluginOptions = {}) {
64
- this.options = options;
65
- }
66
-
67
- /**
68
- * Auto-detect LLM provider from environment variables.
69
- *
70
- * Priority order:
71
- * 1. AI_GATEWAY_MODEL → Vercel AI Gateway
72
- * 2. OPENAI_API_KEY → OpenAI
73
- * 3. ANTHROPIC_API_KEY → Anthropic
74
- * 4. GOOGLE_GENERATIVE_AI_API_KEY → Google
75
- * 5. Fallback → MemoryLLMAdapter
76
- *
77
- * Returns the adapter and a description for logging.
78
- */
79
- private async detectAdapter(ctx: PluginContext): Promise<{ adapter: LLMAdapter; description: string }> {
80
- // 1. Vercel AI Gateway — works with any provider via gateway('provider/model')
81
- const gatewayModel = process.env.AI_GATEWAY_MODEL;
82
- if (gatewayModel) {
83
- try {
84
- const gatewayPkg = '@ai-sdk/gateway';
85
- const { gateway } = await import(/* webpackIgnore: true */ gatewayPkg);
86
- const adapter = new VercelLLMAdapter({ model: gateway(gatewayModel) });
87
- return { adapter, description: `Vercel AI Gateway (model: ${gatewayModel})` };
88
- } catch (err) {
89
- ctx.logger.warn(
90
- `[AI] Failed to load @ai-sdk/gateway for AI_GATEWAY_MODEL=${gatewayModel}, trying next provider`,
91
- err instanceof Error ? { error: err.message } : undefined
92
- );
93
- }
94
- }
95
-
96
- // 2. Direct provider SDKs
97
- const providerConfigs: Array<{
98
- envKey: string;
99
- pkg: string;
100
- factory: string;
101
- defaultModel: string;
102
- displayName: string;
103
- }> = [
104
- {
105
- envKey: 'OPENAI_API_KEY',
106
- pkg: '@ai-sdk/openai',
107
- factory: 'openai',
108
- defaultModel: 'gpt-4o',
109
- displayName: 'OpenAI'
110
- },
111
- {
112
- envKey: 'ANTHROPIC_API_KEY',
113
- pkg: '@ai-sdk/anthropic',
114
- factory: 'anthropic',
115
- defaultModel: 'claude-sonnet-4-20250514',
116
- displayName: 'Anthropic'
117
- },
118
- {
119
- envKey: 'GOOGLE_GENERATIVE_AI_API_KEY',
120
- pkg: '@ai-sdk/google',
121
- factory: 'google',
122
- defaultModel: 'gemini-2.0-flash',
123
- displayName: 'Google'
124
- },
125
- ];
126
-
127
- for (const { envKey, pkg, factory, defaultModel, displayName } of providerConfigs) {
128
- if (process.env[envKey]) {
129
- try {
130
- const mod = await import(/* webpackIgnore: true */ pkg);
131
- const createModel = mod[factory] ?? mod.default;
132
- if (typeof createModel === 'function') {
133
- const modelId = process.env.AI_MODEL ?? defaultModel;
134
- const adapter = new VercelLLMAdapter({ model: createModel(modelId) });
135
- return { adapter, description: `${displayName} (model: ${modelId})` };
136
- }
137
- } catch (err) {
138
- ctx.logger.warn(
139
- `[AI] Failed to load ${pkg} for ${envKey}, trying next provider`,
140
- err instanceof Error ? { error: err.message } : undefined
141
- );
142
- }
143
- }
144
- }
145
-
146
- // 3. Fallback to MemoryLLMAdapter
147
- ctx.logger.warn('[AI] No LLM provider configured via environment variables. Falling back to MemoryLLMAdapter (echo mode). Set AI_GATEWAY_MODEL, OPENAI_API_KEY, ANTHROPIC_API_KEY, or GOOGLE_GENERATIVE_AI_API_KEY to use a real LLM.');
148
- return { adapter: new MemoryLLMAdapter(), description: 'MemoryLLMAdapter (echo mode - for testing only)' };
149
- }
150
-
151
- async init(ctx: PluginContext): Promise<void> {
152
- // Check if there is an existing AI service (e.g. from dev-plugin)
153
- let hasExisting = false;
154
- try {
155
- const existing = ctx.getService<IAIService>('ai');
156
- if (existing && typeof existing.chat === 'function') {
157
- hasExisting = true;
158
- ctx.logger.debug('[AI] Found existing AI service, replacing');
159
- }
160
- } catch {
161
- // No existing service — that's fine
162
- }
163
-
164
- // Determine conversation service: explicit > auto-detect IDataEngine > InMemory fallback
165
- let conversationService: IAIConversationService | undefined = this.options.conversationService;
166
- if (!conversationService) {
167
- try {
168
- const engine = ctx.getService<IDataEngine>('data');
169
- if (engine && typeof engine.find === 'function') {
170
- conversationService = new ObjectQLConversationService(engine);
171
- ctx.logger.info('[AI] Using ObjectQLConversationService (IDataEngine detected)');
172
- }
173
- } catch {
174
- // No data engine — fall back to InMemory
175
- }
176
- }
177
-
178
- // Determine LLM adapter: explicit > auto-detect from env > MemoryLLMAdapter fallback
179
- let adapter: LLMAdapter;
180
- let adapterDescription: string;
181
-
182
- if (this.options.adapter) {
183
- // User provided an explicit adapter
184
- adapter = this.options.adapter;
185
- adapterDescription = `${adapter.name} (explicitly configured)`;
186
- } else {
187
- // Auto-detect from environment variables
188
- const detected = await this.detectAdapter(ctx);
189
- adapter = detected.adapter;
190
- adapterDescription = detected.description;
191
- }
192
-
193
- // Log the selected adapter
194
- ctx.logger.info(`[AI] Using LLM adapter: ${adapterDescription}`);
195
-
196
- const config: AIServiceConfig = {
197
- adapter,
198
- logger: ctx.logger,
199
- conversationService,
200
- };
201
-
202
- this.service = new AIService(config);
203
-
204
- // Register or replace the AI service
205
- if (hasExisting) {
206
- ctx.replaceService('ai', this.service);
207
- } else {
208
- ctx.registerService('ai', this.service);
209
- }
210
-
211
- // Register AI system objects via the manifest service.
212
- ctx.getService<{ register(m: any): void }>('manifest').register({
213
- id: 'com.objectstack.service-ai',
214
- name: 'AI Service',
215
- version: '1.0.0',
216
- type: 'plugin',
217
- namespace: 'ai',
218
- objects: [AiConversationObject, AiMessageObject],
219
- });
220
-
221
- if (this.options.debug) {
222
- ctx.hook('ai:beforeChat', async (messages: unknown) => {
223
- ctx.logger.debug('[AI] Before chat', { messages });
224
- });
225
- }
226
-
227
- // Contribute navigation items to the Setup App (if SetupPlugin is loaded).
228
- try {
229
- const setupNav = ctx.getService<{ contribute(c: any): void }>('setupNav');
230
- if (setupNav) {
231
- setupNav.contribute({
232
- areaId: 'area_ai',
233
- items: [
234
- { id: 'nav_ai_conversations', type: 'object', label: { key: 'setup.nav.ai_conversations', defaultValue: 'Conversations' }, objectName: 'conversations', icon: 'message-square', order: 10 },
235
- { id: 'nav_ai_messages', type: 'object', label: { key: 'setup.nav.ai_messages', defaultValue: 'Messages' }, objectName: 'messages', icon: 'messages-square', order: 20 },
236
- ],
237
- });
238
- ctx.logger.info('[AI] Navigation items contributed to Setup App');
239
- }
240
- } catch {
241
- // SetupPlugin not loaded — skip silently
242
- }
243
-
244
- ctx.logger.info('[AI] Service initialized');
245
- }
246
-
247
- async start(ctx: PluginContext): Promise<void> {
248
- if (!this.service) return;
249
-
250
- // ── Auto-register built-in tools & agents when services are available ──
251
- let metadataService: IMetadataService | undefined;
252
- try {
253
- metadataService = ctx.getService<IMetadataService>('metadata');
254
- console.log('[AI Plugin] Retrieved metadata service:', !!metadataService, 'has getRegisteredTypes:', typeof (metadataService as any)?.getRegisteredTypes);
255
- } catch (e: any) {
256
- console.log('[AI] Metadata service not available:', e.message);
257
- ctx.logger.debug('[AI] Metadata service not available');
258
- }
259
-
260
- // Data tools require only the data engine
261
- try {
262
- const dataEngine = ctx.getService<IDataEngine>('data');
263
- if (dataEngine) {
264
- registerDataTools(this.service.toolRegistry, { dataEngine });
265
- ctx.logger.info('[AI] Built-in data tools registered');
266
-
267
- // Register data tools as metadata (for Studio visibility)
268
- if (metadataService) {
269
- const { DATA_TOOL_DEFINITIONS } = await import('./tools/data-tools.js');
270
- for (const toolDef of DATA_TOOL_DEFINITIONS) {
271
- const toolExists =
272
- typeof metadataService.exists === 'function'
273
- ? await metadataService.exists('tool', toolDef.name)
274
- : false;
275
-
276
- if (!toolExists) {
277
- await metadataService.register('tool', toolDef.name, toolDef);
278
- }
279
- }
280
- ctx.logger.info(`[AI] ${DATA_TOOL_DEFINITIONS.length} data tools registered as metadata`);
281
- }
282
-
283
- // Register the built-in data_chat agent (requires metadata service)
284
- if (metadataService) {
285
- try {
286
- const agentExists =
287
- typeof metadataService.exists === 'function'
288
- ? await metadataService.exists('agent', DATA_CHAT_AGENT.name)
289
- : false;
290
-
291
- if (!agentExists) {
292
- await metadataService.register('agent', DATA_CHAT_AGENT.name, DATA_CHAT_AGENT);
293
- console.log('[AI] Registered data_chat agent to metadataService');
294
- ctx.logger.info('[AI] data_chat agent registered');
295
- } else {
296
- console.log('[AI] data_chat agent already exists, skipping');
297
- ctx.logger.debug('[AI] data_chat agent already exists, skipping auto-registration');
298
- }
299
- } catch (err) {
300
- ctx.logger.warn('[AI] Failed to register data_chat agent', err instanceof Error ? { error: err.message, stack: err.stack } : { error: String(err) });
301
- }
302
- }
303
- }
304
- } catch {
305
- ctx.logger.debug('[AI] Data engine not available, skipping data tools');
306
- }
307
-
308
- // Metadata tools require only the metadata service
309
- if (metadataService) {
310
- try {
311
- registerMetadataTools(this.service.toolRegistry, { metadataService });
312
- ctx.logger.info('[AI] Built-in metadata tools registered');
313
-
314
- // Register metadata tools as metadata (for Studio visibility)
315
- const { METADATA_TOOL_DEFINITIONS } = await import('./tools/metadata-tools.js');
316
- for (const toolDef of METADATA_TOOL_DEFINITIONS) {
317
- const toolExists =
318
- typeof metadataService.exists === 'function'
319
- ? await metadataService.exists('tool', toolDef.name)
320
- : false;
321
-
322
- if (!toolExists) {
323
- await metadataService.register('tool', toolDef.name, toolDef);
324
- }
325
- }
326
- ctx.logger.info(`[AI] ${METADATA_TOOL_DEFINITIONS.length} metadata tools registered as metadata`);
327
-
328
- // Register the built-in metadata_assistant agent
329
- try {
330
- const agentExists =
331
- typeof metadataService.exists === 'function'
332
- ? await metadataService.exists('agent', METADATA_ASSISTANT_AGENT.name)
333
- : false;
334
-
335
- if (!agentExists) {
336
- await metadataService.register('agent', METADATA_ASSISTANT_AGENT.name, METADATA_ASSISTANT_AGENT);
337
- console.log('[AI] Registered metadata_assistant agent to metadataService');
338
- ctx.logger.info('[AI] metadata_assistant agent registered');
339
- } else {
340
- console.log('[AI] metadata_assistant agent already exists, skipping');
341
- ctx.logger.debug('[AI] metadata_assistant agent already exists, skipping auto-registration');
342
- }
343
- } catch (err) {
344
- ctx.logger.warn('[AI] Failed to register metadata_assistant agent', err instanceof Error ? { error: err.message, stack: err.stack } : { error: String(err) });
345
- }
346
- } catch (err) {
347
- ctx.logger.debug('[AI] Failed to register metadata tools', err instanceof Error ? err : undefined);
348
- }
349
- }
350
-
351
- // Trigger hook to notify AI service is ready — other plugins can register tools
352
- await ctx.trigger('ai:ready', this.service);
353
-
354
- // Build and expose route definitions
355
- const routes = buildAIRoutes(this.service, this.service.conversationService, ctx.logger);
356
-
357
- // Build tool routes
358
- const toolRoutes = buildToolRoutes(this.service, ctx.logger);
359
- routes.push(...toolRoutes);
360
- ctx.logger.info(`[AI] Tool routes registered (${toolRoutes.length} routes)`);
361
-
362
- // Build agent routes if metadata service is available
363
- if (metadataService) {
364
- const agentRuntime = new AgentRuntime(metadataService);
365
- const agentRoutes = buildAgentRoutes(this.service, agentRuntime, ctx.logger);
366
- routes.push(...agentRoutes);
367
- ctx.logger.info(`[AI] Agent routes registered (${agentRoutes.length} routes)`);
368
- } else {
369
- ctx.logger.debug('[AI] Metadata service not available, skipping agent routes');
370
- }
371
-
372
- // Trigger hook so HTTP server plugins can mount these routes
373
- await ctx.trigger('ai:routes', routes);
374
-
375
- // Cache routes on the kernel so HttpDispatcher can find them
376
- const kernel = ctx.getKernel();
377
- if (kernel) {
378
- (kernel as any).__aiRoutes = routes;
379
- }
380
-
381
- ctx.logger.info(
382
- `[AI] Service started — adapter="${this.service.adapterName}", ` +
383
- `tools=${this.service.toolRegistry.size}, ` +
384
- `routes=${routes.length}`,
385
- );
386
- }
387
-
388
- async destroy(): Promise<void> {
389
- this.service = undefined;
390
- }
391
- }
@@ -1,190 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import type { ModelMessage } from '@objectstack/spec/contracts';
4
- import type { Logger } from '@objectstack/spec/contracts';
5
- import type { AIService } from '../ai-service.js';
6
- import type { AgentRuntime, AgentChatContext } from '../agent-runtime.js';
7
- import type { RouteDefinition } from './ai-routes.js';
8
- import { normalizeMessage, validateMessageContent } from './message-utils.js';
9
- import { encodeVercelDataStream } from '../stream/vercel-stream-encoder.js';
10
-
11
- /**
12
- * Allowed message roles for the agent chat endpoint.
13
- *
14
- * Only `user` and `assistant` are accepted from clients.
15
- * `system` messages are injected server-side from agent instructions,
16
- * and `tool` messages are produced by the tool-call loop — accepting
17
- * either from the client would allow callers to override agent
18
- * guardrails or inject fabricated tool results.
19
- */
20
- const ALLOWED_AGENT_ROLES = new Set<string>(['user', 'assistant']);
21
-
22
- function validateAgentMessage(raw: unknown): string | null {
23
- if (typeof raw !== 'object' || raw === null) {
24
- return 'each message must be an object';
25
- }
26
- const msg = raw as Record<string, unknown>;
27
- if (typeof msg.role !== 'string' || !ALLOWED_AGENT_ROLES.has(msg.role)) {
28
- return `message.role must be one of ${[...ALLOWED_AGENT_ROLES].map(r => `"${r}"`).join(', ')} for agent chat`;
29
- }
30
-
31
- // Assistant messages may legitimately have empty content (e.g. tool-call-only)
32
- const allowEmpty = msg.role === 'assistant';
33
- return validateMessageContent(msg, { allowEmptyContent: allowEmpty });
34
- }
35
-
36
- /**
37
- * Build agent-specific REST routes.
38
- *
39
- * | Method | Path | Description |
40
- * |:---|:---|:---|
41
- * | GET | /api/v1/ai/agents | List all active agents |
42
- * | POST | /api/v1/ai/agents/:agentName/chat | Chat with a specific agent |
43
- */
44
- export function buildAgentRoutes(
45
- aiService: AIService,
46
- agentRuntime: AgentRuntime,
47
- logger: Logger,
48
- ): RouteDefinition[] {
49
- return [
50
- // ── List active agents ──────────────────────────────────────
51
- {
52
- method: 'GET',
53
- path: '/api/v1/ai/agents',
54
- description: 'List all active AI agents',
55
- auth: true,
56
- permissions: ['ai:chat'],
57
- handler: async () => {
58
- try {
59
- const agents = await agentRuntime.listAgents();
60
- return { status: 200, body: { agents } };
61
- } catch (err) {
62
- logger.error(
63
- '[AI Route] /agents list error',
64
- err instanceof Error ? err : undefined,
65
- );
66
- return { status: 500, body: { error: 'Internal AI service error' } };
67
- }
68
- },
69
- },
70
-
71
- // ── Chat with a specific agent ──────────────────────────────
72
- //
73
- // Dual-mode endpoint matching the general chat route behaviour:
74
- // • `stream !== false` → Vercel Data Stream Protocol (SSE)
75
- // • `stream === false` → JSON response (legacy)
76
- //
77
- {
78
- method: 'POST',
79
- path: '/api/v1/ai/agents/:agentName/chat',
80
- description: 'Chat with a specific AI agent (supports Vercel AI Data Stream Protocol)',
81
- auth: true,
82
- permissions: ['ai:chat', 'ai:agents'],
83
- handler: async (req) => {
84
- const agentName = req.params?.agentName;
85
- if (!agentName) {
86
- return { status: 400, body: { error: 'agentName parameter is required' } };
87
- }
88
-
89
- // Parse request body
90
- const body = (req.body ?? {}) as Record<string, unknown>;
91
- const {
92
- messages: rawMessages,
93
- context: chatContext,
94
- options: extraOptions,
95
- } = body as {
96
- messages?: unknown[];
97
- context?: AgentChatContext;
98
- options?: Record<string, unknown>;
99
- };
100
-
101
- if (!Array.isArray(rawMessages) || rawMessages.length === 0) {
102
- return { status: 400, body: { error: 'messages array is required' } };
103
- }
104
-
105
- for (const msg of rawMessages) {
106
- const err = validateAgentMessage(msg);
107
- if (err) return { status: 400, body: { error: err } };
108
- }
109
-
110
- // Load agent definition
111
- const agent = await agentRuntime.loadAgent(agentName);
112
- if (!agent) {
113
- return { status: 404, body: { error: `Agent "${agentName}" not found` } };
114
- }
115
- if (!agent.active) {
116
- return { status: 403, body: { error: `Agent "${agentName}" is not active` } };
117
- }
118
-
119
- try {
120
- // Build system messages from agent instructions + UI context
121
- const systemMessages = agentRuntime.buildSystemMessages(agent, chatContext);
122
-
123
- // Resolve agent model/tools → request options
124
- const agentOptions = agentRuntime.buildRequestOptions(
125
- agent,
126
- aiService.toolRegistry.getAll(),
127
- );
128
-
129
- // Whitelist only safe caller overrides — block tools/toolChoice/model
130
- // to prevent tool-definition injection or DoS via unregistered tools.
131
- const safeOverrides: Record<string, unknown> = {};
132
- if (extraOptions) {
133
- const ALLOWED_KEYS = new Set(['temperature', 'maxTokens', 'stop']);
134
- for (const key of Object.keys(extraOptions)) {
135
- if (ALLOWED_KEYS.has(key)) {
136
- safeOverrides[key] = extraOptions[key];
137
- }
138
- }
139
- }
140
- const mergedOptions = { ...agentOptions, ...safeOverrides };
141
-
142
- // Prepend system messages then user conversation
143
- const fullMessages: ModelMessage[] = [
144
- ...systemMessages,
145
- ...rawMessages.map(m => normalizeMessage(m as Record<string, unknown>)),
146
- ];
147
-
148
- const chatWithToolsOptions = {
149
- ...mergedOptions,
150
- maxIterations: agent.planning?.maxIterations,
151
- };
152
-
153
- // ── Choose response mode ─────────────────────────────
154
- const wantStream = body.stream !== false;
155
-
156
- if (wantStream) {
157
- // Vercel Data Stream Protocol (SSE) — matches general chat behaviour
158
- if (!aiService.streamChatWithTools) {
159
- return { status: 501, body: { error: 'Streaming is not supported by the configured AI service' } };
160
- }
161
- const events = aiService.streamChatWithTools(fullMessages, chatWithToolsOptions);
162
- return {
163
- status: 200,
164
- stream: true,
165
- vercelDataStream: true,
166
- contentType: 'text/event-stream',
167
- headers: {
168
- 'Content-Type': 'text/event-stream',
169
- 'Cache-Control': 'no-cache',
170
- 'Connection': 'keep-alive',
171
- 'x-vercel-ai-ui-message-stream': 'v1',
172
- },
173
- events: encodeVercelDataStream(events),
174
- };
175
- }
176
-
177
- // JSON response (non-streaming / legacy)
178
- const result = await aiService.chatWithTools(fullMessages, chatWithToolsOptions);
179
- return { status: 200, body: result };
180
- } catch (err) {
181
- logger.error(
182
- '[AI Route] /agents/:agentName/chat error',
183
- err instanceof Error ? err : undefined,
184
- );
185
- return { status: 500, body: { error: 'Internal AI service error' } };
186
- }
187
- },
188
- },
189
- ];
190
- }