@reminix/runtime 0.0.13 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agent.js CHANGED
@@ -2,17 +2,125 @@
2
2
  * Agent classes for Reminix Runtime.
3
3
  */
4
4
  import { VERSION } from './version.js';
5
- /**
6
- * Default parameters schema for agents.
7
- * Request: { prompt: '...' }
8
- */
9
- const DEFAULT_AGENT_PARAMETERS = {
5
+ /** Default template when none specified and no custom input/output. */
6
+ const DEFAULT_AGENT_TEMPLATE = 'prompt';
7
+ /** JSON schema for a single tool call (OpenAI-style). */
8
+ const TOOL_CALL_ITEM_SCHEMA = {
9
+ type: 'object',
10
+ properties: {
11
+ id: { type: 'string', description: 'Tool call id' },
12
+ type: { type: 'string', enum: ['function'], description: 'Tool call type' },
13
+ function: {
14
+ type: 'object',
15
+ properties: {
16
+ name: { type: 'string', description: 'Function/tool name' },
17
+ arguments: { type: 'string', description: 'JSON string of arguments' },
18
+ },
19
+ required: ['name', 'arguments'],
20
+ },
21
+ },
22
+ required: ['id', 'type', 'function'],
23
+ };
24
+ /** JSON schema for a message item (OpenAI-style; supports tool_calls and tool results). */
25
+ const CHAT_INPUT_MESSAGE_ITEMS = {
10
26
  type: 'object',
11
27
  properties: {
12
- prompt: { type: 'string', description: 'The prompt or task for the agent' },
28
+ role: { type: 'string', description: 'Message role (user, assistant, system, tool)' },
29
+ content: { type: 'string', description: 'Message content', nullable: true },
30
+ tool_calls: {
31
+ type: 'array',
32
+ description: 'Tool calls requested by the model (assistant messages)',
33
+ items: TOOL_CALL_ITEM_SCHEMA,
34
+ },
35
+ tool_call_id: {
36
+ type: 'string',
37
+ description: 'Id of the tool call this message is a result for (tool messages)',
38
+ },
39
+ name: { type: 'string', description: 'Tool name (tool messages)' },
40
+ },
41
+ };
42
+ const AGENT_TEMPLATES = {
43
+ prompt: {
44
+ input: {
45
+ type: 'object',
46
+ properties: {
47
+ prompt: { type: 'string', description: 'The prompt or task for the agent' },
48
+ },
49
+ required: ['prompt'],
50
+ },
51
+ output: { type: 'string' },
52
+ },
53
+ chat: {
54
+ input: {
55
+ type: 'object',
56
+ properties: {
57
+ messages: {
58
+ type: 'array',
59
+ description: 'Chat messages (OpenAI-style)',
60
+ items: CHAT_INPUT_MESSAGE_ITEMS,
61
+ },
62
+ },
63
+ required: ['messages'],
64
+ },
65
+ output: { type: 'string' },
66
+ },
67
+ task: {
68
+ input: {
69
+ type: 'object',
70
+ properties: {
71
+ task: { type: 'string', description: 'Task name or description' },
72
+ },
73
+ required: ['task'],
74
+ additionalProperties: true,
75
+ },
76
+ output: {
77
+ description: 'Structured JSON result (object, array, string, number, boolean, or null)',
78
+ type: 'object',
79
+ additionalProperties: true,
80
+ },
81
+ },
82
+ rag: {
83
+ input: {
84
+ type: 'object',
85
+ properties: {
86
+ query: { type: 'string', description: 'The question to answer from documents' },
87
+ messages: {
88
+ type: 'array',
89
+ description: 'Optional prior conversation (chat-style RAG)',
90
+ items: CHAT_INPUT_MESSAGE_ITEMS,
91
+ },
92
+ collectionIds: {
93
+ type: 'array',
94
+ items: { type: 'string' },
95
+ description: 'Optional knowledge collection IDs to scope the search',
96
+ },
97
+ },
98
+ required: ['query'],
99
+ },
100
+ output: { type: 'string' },
101
+ },
102
+ thread: {
103
+ input: {
104
+ type: 'object',
105
+ properties: {
106
+ messages: {
107
+ type: 'array',
108
+ description: 'Chat messages with tool_calls and tool results (OpenAI-style)',
109
+ items: CHAT_INPUT_MESSAGE_ITEMS,
110
+ },
111
+ },
112
+ required: ['messages'],
113
+ },
114
+ output: {
115
+ type: 'array',
116
+ description: 'Updated message thread (OpenAI-style, may include assistant message and tool_calls)',
117
+ items: CHAT_INPUT_MESSAGE_ITEMS,
118
+ },
13
119
  },
14
- required: ['prompt'],
15
120
  };
121
+ /** Default input/output schemas (same as prompt template). Used by AgentBase and custom agents. */
122
+ const DEFAULT_AGENT_INPUT = AGENT_TEMPLATES[DEFAULT_AGENT_TEMPLATE].input;
123
+ const DEFAULT_AGENT_OUTPUT = AGENT_TEMPLATES[DEFAULT_AGENT_TEMPLATE].output;
16
124
  /**
17
125
  * Abstract base class defining the agent interface.
18
126
  *
@@ -21,29 +129,22 @@ const DEFAULT_AGENT_PARAMETERS = {
21
129
  * `AgentAdapter` for framework adapters.
22
130
  */
23
131
  export class AgentBase {
24
- /**
25
- * Whether execute supports streaming. Override to enable.
26
- */
27
- get streaming() {
28
- return false;
29
- }
30
132
  /**
31
133
  * Return agent metadata for discovery.
32
134
  * Override this to provide custom metadata.
33
135
  */
34
136
  get metadata() {
35
137
  return {
36
- type: 'agent',
37
- parameters: DEFAULT_AGENT_PARAMETERS,
38
- requestKeys: ['prompt'],
39
- responseKeys: ['content'],
138
+ capabilities: { streaming: false },
139
+ input: DEFAULT_AGENT_INPUT,
140
+ output: DEFAULT_AGENT_OUTPUT,
40
141
  };
41
142
  }
42
143
  /**
43
- * Handle a streaming execute request.
144
+ * Handle a streaming invoke request.
44
145
  */
45
146
  // eslint-disable-next-line require-yield
46
- async *executeStream(_request) {
147
+ async *invokeStream(_request) {
47
148
  throw new Error('Streaming not implemented for this agent');
48
149
  }
49
150
  /**
@@ -96,7 +197,6 @@ export class AgentBase {
96
197
  {
97
198
  name: this.name,
98
199
  ...this.metadata,
99
- streaming: this.streaming,
100
200
  },
101
201
  ],
102
202
  }, { headers: corsHeaders });
@@ -106,38 +206,29 @@ export class AgentBase {
106
206
  if (method === 'POST' && invokeMatch) {
107
207
  const agentName = invokeMatch[1];
108
208
  if (agentName !== this.name) {
109
- return Response.json({ error: `Agent '${agentName}' not found` }, { status: 404, headers: corsHeaders });
209
+ return Response.json({ error: { type: 'NotFoundError', message: `Agent '${agentName}' not found` } }, { status: 404, headers: corsHeaders });
110
210
  }
111
211
  const body = (await request.json());
112
- // Get requestKeys from agent metadata (all agents have defaults)
113
- const requestKeys = this.metadata.requestKeys ?? [];
114
- // Extract declared keys from body into input object
115
- // e.g., requestKeys: ['prompt'] with body { prompt: '...' } -> input = { prompt: '...' }
116
- const input = {};
117
- for (const key of requestKeys) {
118
- if (key in body) {
119
- input[key] = body[key];
120
- }
121
- }
122
- const executeRequest = {
123
- input,
212
+ const invokeRequest = {
213
+ input: body.input ?? {},
124
214
  stream: body.stream === true,
125
215
  context: body.context,
126
216
  };
127
217
  // Handle streaming
128
- if (executeRequest.stream) {
218
+ if (invokeRequest.stream) {
129
219
  const stream = new ReadableStream({
130
220
  start: async (controller) => {
131
221
  const encoder = new TextEncoder();
132
222
  try {
133
- for await (const chunk of this.executeStream(executeRequest)) {
134
- controller.enqueue(encoder.encode(`data: ${chunk}\n\n`));
223
+ for await (const chunk of this.invokeStream(invokeRequest)) {
224
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ delta: chunk })}\n\n`));
135
225
  }
136
- controller.enqueue(encoder.encode('data: [DONE]\n\n'));
226
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ done: true })}\n\n`));
137
227
  }
138
228
  catch (error) {
139
229
  const message = error instanceof Error ? error.message : 'Unknown error';
140
- controller.enqueue(encoder.encode(`data: ${JSON.stringify({ error: message })}\n\n`));
230
+ const errorType = error instanceof Error ? error.constructor.name : 'ExecutionError';
231
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ error: { type: errorType, message } })}\n\n`));
141
232
  }
142
233
  controller.close();
143
234
  },
@@ -151,15 +242,16 @@ export class AgentBase {
151
242
  },
152
243
  });
153
244
  }
154
- const response = await this.execute(executeRequest);
245
+ const response = await this.invoke(invokeRequest);
155
246
  return Response.json(response, { headers: corsHeaders });
156
247
  }
157
248
  // Not found
158
- return Response.json({ error: 'Not found' }, { status: 404, headers: corsHeaders });
249
+ return Response.json({ error: { type: 'NotFoundError', message: 'Not found' } }, { status: 404, headers: corsHeaders });
159
250
  }
160
251
  catch (error) {
161
252
  const message = error instanceof Error ? error.message : 'Unknown error';
162
- return Response.json({ error: message }, { status: 500, headers: corsHeaders });
253
+ const errorType = error instanceof Error ? error.constructor.name : 'ExecutionError';
254
+ return Response.json({ error: { type: errorType, message } }, { status: 500, headers: corsHeaders });
163
255
  }
164
256
  };
165
257
  }
@@ -182,8 +274,8 @@ export class AgentBase {
182
274
  export class Agent extends AgentBase {
183
275
  _name;
184
276
  _metadata;
185
- _executeHandler = null;
186
- _executeStreamHandler = null;
277
+ _invokeHandler = null;
278
+ _invokeStreamHandler = null;
187
279
  /**
188
280
  * Create a new agent.
189
281
  *
@@ -206,19 +298,15 @@ export class Agent extends AgentBase {
206
298
  */
207
299
  get metadata() {
208
300
  return {
209
- type: 'agent',
210
- parameters: DEFAULT_AGENT_PARAMETERS,
211
- requestKeys: ['prompt'],
212
- responseKeys: ['content'],
301
+ capabilities: {
302
+ streaming: this._invokeStreamHandler !== null,
303
+ ...this._metadata.capabilities,
304
+ },
305
+ input: this._metadata.input ?? DEFAULT_AGENT_INPUT,
306
+ output: this._metadata.output ?? DEFAULT_AGENT_OUTPUT,
213
307
  ...this._metadata,
214
308
  };
215
309
  }
216
- /**
217
- * Whether execute supports streaming.
218
- */
219
- get streaming() {
220
- return this._executeStreamHandler !== null;
221
- }
222
310
  /**
223
311
  * Register a handler.
224
312
  *
@@ -228,7 +316,7 @@ export class Agent extends AgentBase {
228
316
  * });
229
317
  */
230
318
  handler(fn) {
231
- this._executeHandler = fn;
319
+ this._invokeHandler = fn;
232
320
  return this;
233
321
  }
234
322
  /**
@@ -236,31 +324,31 @@ export class Agent extends AgentBase {
236
324
  *
237
325
  * @example
238
326
  * agent.streamHandler(async function* (request) {
239
- * yield '{"chunk": "Hello"}';
240
- * yield '{"chunk": " world!"}';
327
+ * yield 'Hello';
328
+ * yield ' world!';
241
329
  * });
242
330
  */
243
331
  streamHandler(fn) {
244
- this._executeStreamHandler = fn;
332
+ this._invokeStreamHandler = fn;
245
333
  return this;
246
334
  }
247
335
  /**
248
- * Handle an execute request.
336
+ * Handle an invoke request.
249
337
  */
250
- async execute(request) {
251
- if (this._executeHandler === null) {
252
- throw new Error(`No execute handler registered for agent '${this._name}'`);
338
+ async invoke(request) {
339
+ if (this._invokeHandler === null) {
340
+ throw new Error(`No invoke handler registered for agent '${this._name}'`);
253
341
  }
254
- return this._executeHandler(request);
342
+ return this._invokeHandler(request);
255
343
  }
256
344
  /**
257
- * Handle a streaming execute request.
345
+ * Handle a streaming invoke request.
258
346
  */
259
- async *executeStream(request) {
260
- if (this._executeStreamHandler === null) {
261
- throw new Error(`No streaming execute handler registered for agent '${this._name}'`);
347
+ async *invokeStream(request) {
348
+ if (this._invokeStreamHandler === null) {
349
+ throw new Error(`No streaming invoke handler registered for agent '${this._name}'`);
262
350
  }
263
- yield* this._executeStreamHandler(request);
351
+ yield* this._invokeStreamHandler(request);
264
352
  }
265
353
  }
266
354
  /**
@@ -269,77 +357,38 @@ export class Agent extends AgentBase {
269
357
  function isAsyncGeneratorFunction(fn) {
270
358
  return fn?.constructor?.name === 'AsyncGeneratorFunction';
271
359
  }
272
- /**
273
- * Wrap output schema to match the full response structure based on responseKeys.
274
- *
275
- * If responseKeys = ["output"], wraps the schema as { output: <schema> }
276
- * If responseKeys = ["message"], wraps the schema as { message: <schema> }
277
- * If responseKeys = ["message", "output"], wraps as { message: <schema>, output: <schema> }
278
- *
279
- * @param outputSchema - The schema for the return value (or undefined)
280
- * @param responseKeys - List of top-level response keys
281
- * @returns Wrapped schema describing the full response object, or undefined if outputSchema is undefined
282
- */
283
- function wrapOutputSchemaForResponseKeys(outputSchema, responseKeys) {
284
- if (outputSchema === undefined || responseKeys.length === 0) {
285
- return undefined;
286
- }
287
- // If single response key, wrap the output schema
288
- if (responseKeys.length === 1) {
289
- return {
290
- type: 'object',
291
- properties: { [responseKeys[0]]: outputSchema },
292
- required: responseKeys,
293
- };
294
- }
295
- // Multiple response keys - need to split the output schema
296
- // For now, assume the output schema describes the first key's value
297
- // and other keys are optional/unknown
298
- const properties = { [responseKeys[0]]: outputSchema };
299
- const required = [responseKeys[0]];
300
- // For additional keys, we don't know their schema, so mark as optional
301
- // Users can override via metadata if they need full schema
302
- for (const key of responseKeys.slice(1)) {
303
- properties[key] = { type: 'object' }; // Placeholder - should be overridden
304
- }
305
- return {
306
- type: 'object',
307
- properties,
308
- required,
309
- };
310
- }
311
360
  /**
312
361
  * Create an agent from a configuration object.
313
362
  *
314
- * By default, agents expect `{ prompt: string }` in the request body and
315
- * return `{ output: ... }`. You can customize by providing `parameters`.
363
+ * By default, agents expect `{ input: { prompt: string } }` in the request body and
364
+ * return `{ output: string }`. You can customize by providing `input` schema.
316
365
  *
317
366
  * @example
318
367
  * ```typescript
319
- * // Simple agent with default parameters
320
- * // Request: { prompt: 'Hello world' }
368
+ * // Simple agent with default input/output
369
+ * // Request: { input: { prompt: 'Hello world' } }
321
370
  * // Response: { output: 'You said: Hello world' }
322
371
  * const echo = agent('echo', {
323
372
  * description: 'Echo the prompt',
324
373
  * handler: async ({ prompt }) => `You said: ${prompt}`,
325
374
  * });
326
375
  *
327
- * // Agent with custom parameters
328
- * // Request: { a: 1, b: 2 }
376
+ * // Agent with custom input schema
377
+ * // Request: { input: { a: 1, b: 2 } }
329
378
  * // Response: { output: 3 }
330
379
  * const calculator = agent('calculator', {
331
380
  * description: 'Add two numbers',
332
- * parameters: {
381
+ * input: {
333
382
  * type: 'object',
334
383
  * properties: { a: { type: 'number' }, b: { type: 'number' } },
335
384
  * required: ['a', 'b'],
336
385
  * },
386
+ * output: { type: 'number' },
337
387
  * handler: async ({ a, b }) => (a as number) + (b as number),
338
388
  * });
339
389
  *
340
390
  * // Streaming agent (async generator)
341
- * // Request: { prompt: 'hello world' }
342
- * // Response: { output: 'hello world ' } (streamed)
391
+ * // Request: { input: { prompt: 'hello world' }, stream: true }
343
392
  * const streamer = agent('streamer', {
344
393
  * description: 'Stream text word by word',
345
394
  * handler: async function* ({ prompt }) {
@@ -351,43 +400,25 @@ function wrapOutputSchemaForResponseKeys(outputSchema, responseKeys) {
351
400
  * ```
352
401
  */
353
402
  export function agent(name, options) {
354
- // Use provided parameters or default to { prompt: string }
355
- const parameters = options.parameters ?? DEFAULT_AGENT_PARAMETERS;
356
- // Derive requestKeys from parameters.properties
357
- const requestKeys = Object.keys(parameters.properties);
358
- // Default responseKeys (can be overridden via metadata)
359
- const responseKeys = ['content'];
360
- // Wrap output schema to match responseKeys structure
361
- const wrappedOutput = wrapOutputSchemaForResponseKeys(options.output, responseKeys);
362
- // Build metadata (allow metadata override to change responseKeys)
363
- const baseMetadata = {
364
- type: 'agent',
365
- description: options.description,
366
- parameters,
367
- requestKeys,
368
- responseKeys,
369
- };
370
- // If metadata override includes responseKeys, re-wrap output schema
371
- const finalResponseKeys = options.metadata?.responseKeys ?? responseKeys;
372
- const finalWrappedOutput = options.metadata?.responseKeys !== undefined
373
- ? wrapOutputSchemaForResponseKeys(options.output, finalResponseKeys)
374
- : wrappedOutput;
375
- if (finalWrappedOutput !== undefined) {
376
- baseMetadata.output = finalWrappedOutput;
377
- }
403
+ // Default template is 'prompt' when no template and no custom input/output
404
+ const effectiveTemplate = options.template ??
405
+ (options.input === undefined && options.output === undefined
406
+ ? DEFAULT_AGENT_TEMPLATE
407
+ : undefined);
408
+ const inputSchema = options.input ??
409
+ (effectiveTemplate ? AGENT_TEMPLATES[effectiveTemplate].input : DEFAULT_AGENT_INPUT);
410
+ const outputSchema = options.output ??
411
+ (effectiveTemplate ? AGENT_TEMPLATES[effectiveTemplate].output : DEFAULT_AGENT_OUTPUT);
378
412
  const agentInstance = new Agent(name, {
379
413
  metadata: {
380
- ...baseMetadata,
381
- ...options.metadata,
414
+ description: options.description,
415
+ input: inputSchema,
416
+ output: outputSchema,
417
+ ...(effectiveTemplate !== undefined && { template: effectiveTemplate }),
382
418
  },
383
419
  });
384
420
  // Detect if handler is an async generator function
385
421
  const isStreaming = isAsyncGeneratorFunction(options.handler);
386
- // Get the response keys from the agent's metadata (allows custom override)
387
- const getResponseKeys = () => {
388
- const keys = agentInstance.metadata.responseKeys;
389
- return keys && keys.length > 0 ? keys : ['content'];
390
- };
391
422
  if (isStreaming) {
392
423
  const streamFn = options.handler;
393
424
  // Register streaming handler
@@ -400,194 +431,14 @@ export function agent(name, options) {
400
431
  for await (const chunk of streamFn(request.input, request.context)) {
401
432
  chunks.push(chunk);
402
433
  }
403
- const result = chunks.join('');
404
- // If result is dict, use as-is; otherwise wrap in first responseKey
405
- if (typeof result === 'object' && result !== null && !Array.isArray(result)) {
406
- return result;
407
- }
408
- const responseKeys = getResponseKeys();
409
- return { [responseKeys[0]]: result };
434
+ return { output: chunks.join('') };
410
435
  });
411
436
  }
412
437
  else {
413
438
  const regularHandler = options.handler;
414
439
  agentInstance.handler(async (request) => {
415
440
  const result = await regularHandler(request.input, request.context);
416
- // If result is dict with all responseKeys, use as-is; otherwise wrap in first responseKey
417
- const responseKeys = getResponseKeys();
418
- if (typeof result === 'object' &&
419
- result !== null &&
420
- !Array.isArray(result) &&
421
- responseKeys.every((key) => key in result)) {
422
- return result;
423
- }
424
- return { [responseKeys[0]]: result };
425
- });
426
- }
427
- return agentInstance;
428
- }
429
- /**
430
- * Create a chat agent from a configuration object.
431
- *
432
- * This is a convenience factory that creates an agent with a standard chat
433
- * interface (messages in, messages out).
434
- *
435
- * Request: `{ messages: [...] }`
436
- * Response: `{ messages: [{ role: 'assistant', content: '...' }, ...] }`
437
- *
438
- * @example
439
- * ```typescript
440
- * // Non-streaming chat agent
441
- * // Request: { messages: [{ role: 'user', content: 'hello' }] }
442
- * // Response: { messages: [{ role: 'assistant', content: 'You said: hello' }] }
443
- * const bot = chatAgent('bot', {
444
- * description: 'A simple chatbot',
445
- * handler: async (messages) => {
446
- * const lastMsg = messages.at(-1)?.content ?? '';
447
- * return [{ role: 'assistant', content: `You said: ${lastMsg}` }];
448
- * },
449
- * });
450
- *
451
- * // Streaming chat agent (async generator)
452
- * const streamingBot = chatAgent('streaming-bot', {
453
- * description: 'A streaming chatbot',
454
- * handler: async function* (messages) {
455
- * yield 'Hello';
456
- * yield ' ';
457
- * yield 'world!';
458
- * },
459
- * });
460
- * ```
461
- */
462
- export function chatAgent(name, options) {
463
- // Chat agents have default request/response keys (can be overridden via metadata)
464
- const requestKeys = ['messages'];
465
- const responseKeys = ['messages'];
466
- // Message item schema (shared between parameters and output)
467
- const messageItemSchema = {
468
- type: 'object',
469
- properties: {
470
- role: {
471
- type: 'string',
472
- enum: ['system', 'user', 'assistant', 'tool'],
473
- },
474
- content: {
475
- type: ['string', 'null'],
476
- },
477
- name: {
478
- type: 'string',
479
- },
480
- tool_call_id: {
481
- type: 'string',
482
- },
483
- tool_calls: {
484
- type: 'array',
485
- items: {
486
- type: 'object',
487
- properties: {
488
- id: { type: 'string' },
489
- type: { type: 'string', enum: ['function'] },
490
- function: {
491
- type: 'object',
492
- properties: {
493
- name: { type: 'string' },
494
- arguments: { type: 'string' },
495
- },
496
- required: ['name', 'arguments'],
497
- },
498
- },
499
- required: ['id', 'type', 'function'],
500
- },
501
- },
502
- },
503
- required: ['role'],
504
- };
505
- // Define standard chat agent schemas
506
- const parametersSchema = {
507
- type: 'object',
508
- properties: {
509
- messages: {
510
- type: 'array',
511
- items: messageItemSchema,
512
- },
513
- },
514
- required: ['messages'],
515
- };
516
- // Messages schema (array of messages, the value, not the full response)
517
- const messagesSchema = {
518
- type: 'array',
519
- items: messageItemSchema,
520
- };
521
- // Wrap messages schema to match responseKeys structure
522
- const wrappedOutput = wrapOutputSchemaForResponseKeys(messagesSchema, responseKeys);
523
- // Build metadata (allow metadata override to change responseKeys)
524
- const baseMetadata = {
525
- type: 'chat_agent',
526
- description: options.description,
527
- parameters: parametersSchema,
528
- requestKeys,
529
- responseKeys,
530
- };
531
- // If metadata override includes responseKeys, re-wrap output schema
532
- const finalResponseKeys = options.metadata?.responseKeys ?? responseKeys;
533
- const finalWrappedOutput = options.metadata?.responseKeys !== undefined
534
- ? wrapOutputSchemaForResponseKeys(messagesSchema, finalResponseKeys)
535
- : wrappedOutput;
536
- if (finalWrappedOutput !== undefined) {
537
- baseMetadata.output = finalWrappedOutput;
538
- }
539
- const agentInstance = new Agent(name, {
540
- metadata: {
541
- ...baseMetadata,
542
- ...options.metadata,
543
- },
544
- });
545
- // Detect if handler is an async generator function
546
- const isStreaming = isAsyncGeneratorFunction(options.handler);
547
- // Get the response keys from the agent's metadata (allows custom override)
548
- const getResponseKeys = () => {
549
- const keys = agentInstance.metadata.responseKeys;
550
- return keys && keys.length > 0 ? keys : ['messages'];
551
- };
552
- if (isStreaming) {
553
- const streamFn = options.handler;
554
- // Register streaming handler
555
- agentInstance.streamHandler(async function* (request) {
556
- const rawMessages = (request.input.messages ?? []);
557
- yield* streamFn(rawMessages, request.context);
558
- });
559
- // Also register non-streaming handler that collects chunks
560
- agentInstance.handler(async (request) => {
561
- const rawMessages = (request.input.messages ?? []);
562
- const chunks = [];
563
- for await (const chunk of streamFn(rawMessages, request.context)) {
564
- chunks.push(chunk);
565
- }
566
- const result = [{ role: 'assistant', content: chunks.join('') }];
567
- // For chat agents, always wrap in first responseKey (typically "messages")
568
- const responseKeys = getResponseKeys();
569
- return { [responseKeys[0]]: result };
570
- });
571
- }
572
- else {
573
- const regularHandler = options.handler;
574
- agentInstance.handler(async (request) => {
575
- const rawMessages = (request.input.messages ?? []);
576
- const result = await regularHandler(rawMessages, request.context);
577
- const responseKeys = getResponseKeys();
578
- // Check if result is already a full response dict with all responseKeys
579
- // (This handles cases where responseKeys are overridden and function returns a dict)
580
- if (typeof result === 'object' &&
581
- result !== null &&
582
- !Array.isArray(result) &&
583
- responseKeys.every((key) => key in result)) {
584
- return result;
585
- }
586
- // Convert list of Message objects to list of dicts
587
- const messagesList = Array.isArray(result)
588
- ? result.map((m) => ({ role: m.role, content: m.content }))
589
- : [{ role: result.role, content: result.content }];
590
- return { [responseKeys[0]]: messagesList };
441
+ return { output: result };
591
442
  });
592
443
  }
593
444
  return agentInstance;