@reminix/runtime 0.0.7 → 0.0.9
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 +361 -168
- package/dist/agent-adapter.d.ts +4 -9
- package/dist/agent-adapter.d.ts.map +1 -1
- package/dist/agent-adapter.js +3 -13
- package/dist/agent-adapter.js.map +1 -1
- package/dist/agent.d.ts +149 -80
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +377 -150
- package/dist/agent.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +20 -40
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +8 -13
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -2,24 +2,29 @@
|
|
|
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 = {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
prompt: { type: 'string', description: 'The prompt or task for the agent' },
|
|
13
|
+
},
|
|
14
|
+
required: ['prompt'],
|
|
15
|
+
};
|
|
5
16
|
/**
|
|
6
17
|
* Abstract base class defining the agent interface.
|
|
7
18
|
*
|
|
8
19
|
* This is the core contract that all agents must fulfill.
|
|
9
20
|
* Use `Agent` for callback-based registration or extend
|
|
10
|
-
* `
|
|
21
|
+
* `AgentAdapter` for framework adapters.
|
|
11
22
|
*/
|
|
12
23
|
export class AgentBase {
|
|
13
24
|
/**
|
|
14
|
-
* Whether
|
|
15
|
-
*/
|
|
16
|
-
get invokeStreaming() {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Whether chat supports streaming. Override to enable.
|
|
25
|
+
* Whether execute supports streaming. Override to enable.
|
|
21
26
|
*/
|
|
22
|
-
get
|
|
27
|
+
get streaming() {
|
|
23
28
|
return false;
|
|
24
29
|
}
|
|
25
30
|
/**
|
|
@@ -27,20 +32,18 @@ export class AgentBase {
|
|
|
27
32
|
* Override this to provide custom metadata.
|
|
28
33
|
*/
|
|
29
34
|
get metadata() {
|
|
30
|
-
return {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
async *invokeStream(_request) {
|
|
37
|
-
throw new Error('Streaming not implemented for this agent');
|
|
35
|
+
return {
|
|
36
|
+
type: 'agent',
|
|
37
|
+
parameters: DEFAULT_AGENT_PARAMETERS,
|
|
38
|
+
requestKeys: ['prompt'],
|
|
39
|
+
responseKeys: ['content'],
|
|
40
|
+
};
|
|
38
41
|
}
|
|
39
42
|
/**
|
|
40
|
-
* Handle a streaming
|
|
43
|
+
* Handle a streaming execute request.
|
|
41
44
|
*/
|
|
42
45
|
// eslint-disable-next-line require-yield
|
|
43
|
-
async *
|
|
46
|
+
async *executeStream(_request) {
|
|
44
47
|
throw new Error('Streaming not implemented for this agent');
|
|
45
48
|
}
|
|
46
49
|
/**
|
|
@@ -53,7 +56,7 @@ export class AgentBase {
|
|
|
53
56
|
* ```typescript
|
|
54
57
|
* // Vercel Edge Function
|
|
55
58
|
* const agent = new Agent('my-agent');
|
|
56
|
-
* agent.
|
|
59
|
+
* agent.onExecute(async (req) => ({ output: 'Hello!' }));
|
|
57
60
|
* export default agent.toHandler();
|
|
58
61
|
*
|
|
59
62
|
* // Cloudflare Worker
|
|
@@ -93,30 +96,41 @@ export class AgentBase {
|
|
|
93
96
|
{
|
|
94
97
|
name: this.name,
|
|
95
98
|
...this.metadata,
|
|
96
|
-
|
|
97
|
-
chat: { streaming: this.chatStreaming },
|
|
99
|
+
streaming: this.streaming,
|
|
98
100
|
},
|
|
99
101
|
],
|
|
100
102
|
}, { headers: corsHeaders });
|
|
101
103
|
}
|
|
102
|
-
// POST /agents/{name}/
|
|
103
|
-
const
|
|
104
|
-
if (method === 'POST' &&
|
|
105
|
-
const agentName =
|
|
104
|
+
// POST /agents/{name}/execute
|
|
105
|
+
const executeMatch = path.match(/^\/agents\/([^/]+)\/execute$/);
|
|
106
|
+
if (method === 'POST' && executeMatch) {
|
|
107
|
+
const agentName = executeMatch[1];
|
|
106
108
|
if (agentName !== this.name) {
|
|
107
109
|
return Response.json({ error: `Agent '${agentName}' not found` }, { status: 404, headers: corsHeaders });
|
|
108
110
|
}
|
|
109
111
|
const body = (await request.json());
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
}
|
|
112
121
|
}
|
|
122
|
+
const executeRequest = {
|
|
123
|
+
input,
|
|
124
|
+
stream: body.stream === true,
|
|
125
|
+
context: body.context,
|
|
126
|
+
};
|
|
113
127
|
// Handle streaming
|
|
114
|
-
if (
|
|
128
|
+
if (executeRequest.stream) {
|
|
115
129
|
const stream = new ReadableStream({
|
|
116
130
|
start: async (controller) => {
|
|
117
131
|
const encoder = new TextEncoder();
|
|
118
132
|
try {
|
|
119
|
-
for await (const chunk of this.
|
|
133
|
+
for await (const chunk of this.executeStream(executeRequest)) {
|
|
120
134
|
controller.enqueue(encoder.encode(`data: ${chunk}\n\n`));
|
|
121
135
|
}
|
|
122
136
|
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
|
|
@@ -137,48 +151,7 @@ export class AgentBase {
|
|
|
137
151
|
},
|
|
138
152
|
});
|
|
139
153
|
}
|
|
140
|
-
const response = await this.
|
|
141
|
-
return Response.json(response, { headers: corsHeaders });
|
|
142
|
-
}
|
|
143
|
-
// POST /agents/{name}/chat
|
|
144
|
-
const chatMatch = path.match(/^\/agents\/([^/]+)\/chat$/);
|
|
145
|
-
if (method === 'POST' && chatMatch) {
|
|
146
|
-
const agentName = chatMatch[1];
|
|
147
|
-
if (agentName !== this.name) {
|
|
148
|
-
return Response.json({ error: `Agent '${agentName}' not found` }, { status: 404, headers: corsHeaders });
|
|
149
|
-
}
|
|
150
|
-
const body = (await request.json());
|
|
151
|
-
if (!body.messages || body.messages.length === 0) {
|
|
152
|
-
return Response.json({ error: 'messages is required and must not be empty' }, { status: 400, headers: corsHeaders });
|
|
153
|
-
}
|
|
154
|
-
// Handle streaming
|
|
155
|
-
if (body.stream) {
|
|
156
|
-
const stream = new ReadableStream({
|
|
157
|
-
start: async (controller) => {
|
|
158
|
-
const encoder = new TextEncoder();
|
|
159
|
-
try {
|
|
160
|
-
for await (const chunk of this.chatStream(body)) {
|
|
161
|
-
controller.enqueue(encoder.encode(`data: ${chunk}\n\n`));
|
|
162
|
-
}
|
|
163
|
-
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
|
|
164
|
-
}
|
|
165
|
-
catch (error) {
|
|
166
|
-
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
167
|
-
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ error: message })}\n\n`));
|
|
168
|
-
}
|
|
169
|
-
controller.close();
|
|
170
|
-
},
|
|
171
|
-
});
|
|
172
|
-
return new Response(stream, {
|
|
173
|
-
headers: {
|
|
174
|
-
...corsHeaders,
|
|
175
|
-
'Content-Type': 'text/event-stream',
|
|
176
|
-
'Cache-Control': 'no-cache',
|
|
177
|
-
Connection: 'keep-alive',
|
|
178
|
-
},
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
const response = await this.chat(body);
|
|
154
|
+
const response = await this.execute(executeRequest);
|
|
182
155
|
return Response.json(response, { headers: corsHeaders });
|
|
183
156
|
}
|
|
184
157
|
// Not found
|
|
@@ -199,28 +172,22 @@ export class AgentBase {
|
|
|
199
172
|
* ```typescript
|
|
200
173
|
* const agent = new Agent('my-agent');
|
|
201
174
|
*
|
|
202
|
-
* agent.
|
|
175
|
+
* agent.onExecute(async (request) => {
|
|
203
176
|
* return { output: 'Hello!' };
|
|
204
177
|
* });
|
|
205
178
|
*
|
|
206
|
-
* agent.onChat(async (request) => {
|
|
207
|
-
* return { output: 'Hi!', messages: [...] };
|
|
208
|
-
* });
|
|
209
|
-
*
|
|
210
179
|
* serve({ agents: [agent], port: 8080 });
|
|
211
180
|
* ```
|
|
212
181
|
*/
|
|
213
182
|
export class Agent extends AgentBase {
|
|
214
183
|
_name;
|
|
215
184
|
_metadata;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
_invokeStreamHandler = null;
|
|
219
|
-
_chatStreamHandler = null;
|
|
185
|
+
_executeHandler = null;
|
|
186
|
+
_executeStreamHandler = null;
|
|
220
187
|
/**
|
|
221
188
|
* Create a new agent.
|
|
222
189
|
*
|
|
223
|
-
* @param name - The agent name (used in URLs like /agents/{name}/
|
|
190
|
+
* @param name - The agent name (used in URLs like /agents/{name}/execute)
|
|
224
191
|
* @param options - Optional configuration
|
|
225
192
|
*/
|
|
226
193
|
constructor(name, options) {
|
|
@@ -238,104 +205,364 @@ export class Agent extends AgentBase {
|
|
|
238
205
|
* Return agent metadata for discovery.
|
|
239
206
|
*/
|
|
240
207
|
get metadata() {
|
|
241
|
-
return {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
208
|
+
return {
|
|
209
|
+
type: 'agent',
|
|
210
|
+
parameters: DEFAULT_AGENT_PARAMETERS,
|
|
211
|
+
requestKeys: ['prompt'],
|
|
212
|
+
responseKeys: ['content'],
|
|
213
|
+
...this._metadata,
|
|
214
|
+
};
|
|
248
215
|
}
|
|
249
216
|
/**
|
|
250
|
-
* Whether
|
|
217
|
+
* Whether execute supports streaming.
|
|
251
218
|
*/
|
|
252
|
-
get
|
|
253
|
-
return this.
|
|
219
|
+
get streaming() {
|
|
220
|
+
return this._executeStreamHandler !== null;
|
|
254
221
|
}
|
|
255
222
|
/**
|
|
256
|
-
* Register an
|
|
223
|
+
* Register an execute handler.
|
|
257
224
|
*
|
|
258
225
|
* @example
|
|
259
|
-
* agent.
|
|
226
|
+
* agent.onExecute(async (request) => {
|
|
260
227
|
* return { output: 'Hello!' };
|
|
261
228
|
* });
|
|
262
229
|
*/
|
|
263
|
-
|
|
264
|
-
this.
|
|
265
|
-
return this;
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Register a chat handler.
|
|
269
|
-
*
|
|
270
|
-
* @example
|
|
271
|
-
* agent.onChat(async (request) => {
|
|
272
|
-
* return { output: 'Hi!', messages: [...] };
|
|
273
|
-
* });
|
|
274
|
-
*/
|
|
275
|
-
onChat(handler) {
|
|
276
|
-
this._chatHandler = handler;
|
|
230
|
+
onExecute(handler) {
|
|
231
|
+
this._executeHandler = handler;
|
|
277
232
|
return this;
|
|
278
233
|
}
|
|
279
234
|
/**
|
|
280
|
-
* Register a streaming
|
|
235
|
+
* Register a streaming execute handler.
|
|
281
236
|
*
|
|
282
237
|
* @example
|
|
283
|
-
* agent.
|
|
238
|
+
* agent.onExecuteStream(async function* (request) {
|
|
284
239
|
* yield '{"chunk": "Hello"}';
|
|
285
240
|
* yield '{"chunk": " world!"}';
|
|
286
241
|
* });
|
|
287
242
|
*/
|
|
288
|
-
|
|
289
|
-
this.
|
|
243
|
+
onExecuteStream(handler) {
|
|
244
|
+
this._executeStreamHandler = handler;
|
|
290
245
|
return this;
|
|
291
246
|
}
|
|
292
247
|
/**
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
* @example
|
|
296
|
-
* agent.onChatStream(async function* (request) {
|
|
297
|
-
* yield '{"chunk": "Hi"}';
|
|
298
|
-
* });
|
|
299
|
-
*/
|
|
300
|
-
onChatStream(handler) {
|
|
301
|
-
this._chatStreamHandler = handler;
|
|
302
|
-
return this;
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* Handle an invoke request.
|
|
248
|
+
* Handle an execute request.
|
|
306
249
|
*/
|
|
307
|
-
async
|
|
308
|
-
if (this.
|
|
309
|
-
throw new Error(`No
|
|
250
|
+
async execute(request) {
|
|
251
|
+
if (this._executeHandler === null) {
|
|
252
|
+
throw new Error(`No execute handler registered for agent '${this._name}'`);
|
|
310
253
|
}
|
|
311
|
-
return this.
|
|
254
|
+
return this._executeHandler(request);
|
|
312
255
|
}
|
|
313
256
|
/**
|
|
314
|
-
* Handle a
|
|
257
|
+
* Handle a streaming execute request.
|
|
315
258
|
*/
|
|
316
|
-
async
|
|
317
|
-
if (this.
|
|
318
|
-
throw new Error(`No
|
|
259
|
+
async *executeStream(request) {
|
|
260
|
+
if (this._executeStreamHandler === null) {
|
|
261
|
+
throw new Error(`No streaming execute handler registered for agent '${this._name}'`);
|
|
319
262
|
}
|
|
320
|
-
|
|
263
|
+
yield* this._executeStreamHandler(request);
|
|
321
264
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Detect if a function is an async generator function.
|
|
268
|
+
*/
|
|
269
|
+
function isAsyncGeneratorFunction(fn) {
|
|
270
|
+
return fn?.constructor?.name === 'AsyncGeneratorFunction';
|
|
271
|
+
}
|
|
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;
|
|
330
286
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
|
|
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
|
+
/**
|
|
312
|
+
* Create an agent from a configuration object.
|
|
313
|
+
*
|
|
314
|
+
* By default, agents expect `{ prompt: string }` in the request body and
|
|
315
|
+
* return `{ output: ... }`. You can customize by providing `parameters`.
|
|
316
|
+
*
|
|
317
|
+
* @example
|
|
318
|
+
* ```typescript
|
|
319
|
+
* // Simple agent with default parameters
|
|
320
|
+
* // Request: { prompt: 'Hello world' }
|
|
321
|
+
* // Response: { output: 'You said: Hello world' }
|
|
322
|
+
* const echo = agent('echo', {
|
|
323
|
+
* description: 'Echo the prompt',
|
|
324
|
+
* execute: async ({ prompt }) => `You said: ${prompt}`,
|
|
325
|
+
* });
|
|
326
|
+
*
|
|
327
|
+
* // Agent with custom parameters
|
|
328
|
+
* // Request: { a: 1, b: 2 }
|
|
329
|
+
* // Response: { output: 3 }
|
|
330
|
+
* const calculator = agent('calculator', {
|
|
331
|
+
* description: 'Add two numbers',
|
|
332
|
+
* parameters: {
|
|
333
|
+
* type: 'object',
|
|
334
|
+
* properties: { a: { type: 'number' }, b: { type: 'number' } },
|
|
335
|
+
* required: ['a', 'b'],
|
|
336
|
+
* },
|
|
337
|
+
* execute: async ({ a, b }) => (a as number) + (b as number),
|
|
338
|
+
* });
|
|
339
|
+
*
|
|
340
|
+
* // Streaming agent (async generator)
|
|
341
|
+
* // Request: { prompt: 'hello world' }
|
|
342
|
+
* // Response: { output: 'hello world ' } (streamed)
|
|
343
|
+
* const streamer = agent('streamer', {
|
|
344
|
+
* description: 'Stream text word by word',
|
|
345
|
+
* execute: async function* ({ prompt }) {
|
|
346
|
+
* for (const word of (prompt as string).split(' ')) {
|
|
347
|
+
* yield word + ' ';
|
|
348
|
+
* }
|
|
349
|
+
* },
|
|
350
|
+
* });
|
|
351
|
+
* ```
|
|
352
|
+
*/
|
|
353
|
+
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
|
+
}
|
|
378
|
+
const agentInstance = new Agent(name, {
|
|
379
|
+
metadata: {
|
|
380
|
+
...baseMetadata,
|
|
381
|
+
...options.metadata,
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
// Detect if execute is an async generator function
|
|
385
|
+
const isStreaming = isAsyncGeneratorFunction(options.execute);
|
|
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
|
+
if (isStreaming) {
|
|
392
|
+
const streamExecute = options.execute;
|
|
393
|
+
// Register streaming execute handler
|
|
394
|
+
agentInstance.onExecuteStream(async function* (request) {
|
|
395
|
+
yield* streamExecute(request.input, request.context);
|
|
396
|
+
});
|
|
397
|
+
// Also register non-streaming handler that collects chunks
|
|
398
|
+
agentInstance.onExecute(async (request) => {
|
|
399
|
+
const chunks = [];
|
|
400
|
+
for await (const chunk of streamExecute(request.input, request.context)) {
|
|
401
|
+
chunks.push(chunk);
|
|
402
|
+
}
|
|
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 };
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
const regularExecute = options.execute;
|
|
414
|
+
agentInstance.onExecute(async (request) => {
|
|
415
|
+
const result = await regularExecute(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, message out).
|
|
434
|
+
*
|
|
435
|
+
* Request: `{ messages: [...] }`
|
|
436
|
+
* Response: `{ message: { role: 'assistant', content: '...' } }`
|
|
437
|
+
*
|
|
438
|
+
* @example
|
|
439
|
+
* ```typescript
|
|
440
|
+
* // Non-streaming chat agent
|
|
441
|
+
* // Request: { messages: [{ role: 'user', content: 'hello' }] }
|
|
442
|
+
* // Response: { message: { role: 'assistant', content: 'You said: hello' } }
|
|
443
|
+
* const bot = chatAgent('bot', {
|
|
444
|
+
* description: 'A simple chatbot',
|
|
445
|
+
* execute: 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
|
+
* execute: 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 = ['message'];
|
|
466
|
+
// Define standard chat agent schemas
|
|
467
|
+
const parametersSchema = {
|
|
468
|
+
type: 'object',
|
|
469
|
+
properties: {
|
|
470
|
+
messages: {
|
|
471
|
+
type: 'array',
|
|
472
|
+
items: {
|
|
473
|
+
type: 'object',
|
|
474
|
+
properties: {
|
|
475
|
+
role: { type: 'string' },
|
|
476
|
+
content: { type: 'string' },
|
|
477
|
+
},
|
|
478
|
+
required: ['role', 'content'],
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
required: ['messages'],
|
|
483
|
+
};
|
|
484
|
+
// Message schema (the value, not the full response)
|
|
485
|
+
const messageSchema = {
|
|
486
|
+
type: 'object',
|
|
487
|
+
properties: {
|
|
488
|
+
role: { type: 'string' },
|
|
489
|
+
content: { type: 'string' },
|
|
490
|
+
},
|
|
491
|
+
required: ['role', 'content'],
|
|
492
|
+
};
|
|
493
|
+
// Wrap message schema to match responseKeys structure
|
|
494
|
+
const wrappedOutput = wrapOutputSchemaForResponseKeys(messageSchema, responseKeys);
|
|
495
|
+
// Build metadata (allow metadata override to change responseKeys)
|
|
496
|
+
const baseMetadata = {
|
|
497
|
+
type: 'chat_agent',
|
|
498
|
+
description: options.description,
|
|
499
|
+
parameters: parametersSchema,
|
|
500
|
+
requestKeys,
|
|
501
|
+
responseKeys,
|
|
502
|
+
};
|
|
503
|
+
// If metadata override includes responseKeys, re-wrap output schema
|
|
504
|
+
const finalResponseKeys = options.metadata?.responseKeys ?? responseKeys;
|
|
505
|
+
const finalWrappedOutput = options.metadata?.responseKeys !== undefined
|
|
506
|
+
? wrapOutputSchemaForResponseKeys(messageSchema, finalResponseKeys)
|
|
507
|
+
: wrappedOutput;
|
|
508
|
+
if (finalWrappedOutput !== undefined) {
|
|
509
|
+
baseMetadata.output = finalWrappedOutput;
|
|
510
|
+
}
|
|
511
|
+
const agentInstance = new Agent(name, {
|
|
512
|
+
metadata: {
|
|
513
|
+
...baseMetadata,
|
|
514
|
+
...options.metadata,
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
// Detect if execute is an async generator function
|
|
518
|
+
const isStreaming = isAsyncGeneratorFunction(options.execute);
|
|
519
|
+
// Get the response keys from the agent's metadata (allows custom override)
|
|
520
|
+
const getResponseKeys = () => {
|
|
521
|
+
const keys = agentInstance.metadata.responseKeys;
|
|
522
|
+
return keys && keys.length > 0 ? keys : ['message'];
|
|
523
|
+
};
|
|
524
|
+
if (isStreaming) {
|
|
525
|
+
const streamExecute = options.execute;
|
|
526
|
+
// Register streaming execute handler
|
|
527
|
+
agentInstance.onExecuteStream(async function* (request) {
|
|
528
|
+
const rawMessages = (request.input.messages ?? []);
|
|
529
|
+
yield* streamExecute(rawMessages, request.context);
|
|
530
|
+
});
|
|
531
|
+
// Also register non-streaming handler that collects chunks
|
|
532
|
+
agentInstance.onExecute(async (request) => {
|
|
533
|
+
const rawMessages = (request.input.messages ?? []);
|
|
534
|
+
const chunks = [];
|
|
535
|
+
for await (const chunk of streamExecute(rawMessages, request.context)) {
|
|
536
|
+
chunks.push(chunk);
|
|
537
|
+
}
|
|
538
|
+
const result = { role: 'assistant', content: chunks.join('') };
|
|
539
|
+
// For chat agents, always wrap in first responseKey (typically "message")
|
|
540
|
+
const responseKeys = getResponseKeys();
|
|
541
|
+
return { [responseKeys[0]]: result };
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
const regularExecute = options.execute;
|
|
546
|
+
agentInstance.onExecute(async (request) => {
|
|
547
|
+
const rawMessages = (request.input.messages ?? []);
|
|
548
|
+
const result = await regularExecute(rawMessages, request.context);
|
|
549
|
+
const responseKeys = getResponseKeys();
|
|
550
|
+
// Check if result is already a full response dict with all responseKeys
|
|
551
|
+
// (This handles cases where responseKeys are overridden and function returns a dict)
|
|
552
|
+
if (typeof result === 'object' &&
|
|
553
|
+
result !== null &&
|
|
554
|
+
!Array.isArray(result) &&
|
|
555
|
+
'role' in result === false && // Not a Message (Message has 'role' property)
|
|
556
|
+
responseKeys.every((key) => key in result)) {
|
|
557
|
+
return result;
|
|
558
|
+
}
|
|
559
|
+
// Convert Message to dict if needed, then wrap in first responseKey
|
|
560
|
+
const messageDict = typeof result === 'object' && result !== null && 'role' in result && 'content' in result
|
|
561
|
+
? { role: result.role, content: result.content }
|
|
562
|
+
: result;
|
|
563
|
+
return { [responseKeys[0]]: messageDict };
|
|
564
|
+
});
|
|
339
565
|
}
|
|
566
|
+
return agentInstance;
|
|
340
567
|
}
|
|
341
568
|
//# sourceMappingURL=agent.js.map
|