@agentuity/runtime 0.0.60 → 0.0.61

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 (61) hide show
  1. package/dist/_context.d.ts +11 -7
  2. package/dist/_context.d.ts.map +1 -1
  3. package/dist/_context.js +9 -2
  4. package/dist/_context.js.map +1 -1
  5. package/dist/_server.d.ts +4 -2
  6. package/dist/_server.d.ts.map +1 -1
  7. package/dist/_server.js +71 -31
  8. package/dist/_server.js.map +1 -1
  9. package/dist/_services.d.ts +1 -1
  10. package/dist/_services.d.ts.map +1 -1
  11. package/dist/_services.js +4 -2
  12. package/dist/_services.js.map +1 -1
  13. package/dist/_waituntil.d.ts.map +1 -1
  14. package/dist/_waituntil.js +5 -2
  15. package/dist/_waituntil.js.map +1 -1
  16. package/dist/agent.d.ts +647 -19
  17. package/dist/agent.d.ts.map +1 -1
  18. package/dist/agent.js +55 -6
  19. package/dist/agent.js.map +1 -1
  20. package/dist/app.d.ts +205 -28
  21. package/dist/app.d.ts.map +1 -1
  22. package/dist/app.js +181 -13
  23. package/dist/app.js.map +1 -1
  24. package/dist/index.d.ts +41 -2
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +2 -2
  27. package/dist/index.js.map +1 -1
  28. package/dist/io/email.d.ts.map +1 -1
  29. package/dist/io/email.js +11 -3
  30. package/dist/io/email.js.map +1 -1
  31. package/dist/router.d.ts +282 -32
  32. package/dist/router.d.ts.map +1 -1
  33. package/dist/router.js +110 -35
  34. package/dist/router.js.map +1 -1
  35. package/dist/services/evalrun/http.d.ts.map +1 -1
  36. package/dist/services/evalrun/http.js +7 -5
  37. package/dist/services/evalrun/http.js.map +1 -1
  38. package/dist/services/local/_util.d.ts.map +1 -1
  39. package/dist/services/local/_util.js +3 -1
  40. package/dist/services/local/_util.js.map +1 -1
  41. package/dist/services/session/http.d.ts.map +1 -1
  42. package/dist/services/session/http.js +4 -3
  43. package/dist/services/session/http.js.map +1 -1
  44. package/dist/session.d.ts +284 -4
  45. package/dist/session.d.ts.map +1 -1
  46. package/dist/session.js +2 -2
  47. package/dist/session.js.map +1 -1
  48. package/package.json +5 -4
  49. package/src/_context.ts +37 -9
  50. package/src/_server.ts +88 -36
  51. package/src/_services.ts +9 -2
  52. package/src/_waituntil.ts +13 -2
  53. package/src/agent.ts +856 -68
  54. package/src/app.ts +238 -38
  55. package/src/index.ts +42 -2
  56. package/src/io/email.ts +23 -5
  57. package/src/router.ts +359 -83
  58. package/src/services/evalrun/http.ts +15 -4
  59. package/src/services/local/_util.ts +7 -1
  60. package/src/services/session/http.ts +5 -2
  61. package/src/session.ts +297 -4
package/src/app.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  /** biome-ignore-all lint/suspicious/noExplicitAny: any are ok */
3
3
  import { type Env as HonoEnv, Hono } from 'hono';
4
4
  import type { cors } from 'hono/cors';
5
+ import type { BunWebSocketData } from 'hono/bun';
5
6
  import type { Logger } from './logger';
6
7
  import { createServer, getLogger } from './_server';
7
8
  import type { Meter, Tracer } from '@opentelemetry/api';
@@ -26,7 +27,7 @@ export interface WorkbenchInstance {
26
27
 
27
28
  type CorsOptions = Parameters<typeof cors>[0];
28
29
 
29
- export interface AppConfig {
30
+ export interface AppConfig<TAppState = Record<string, never>> {
30
31
  /**
31
32
  * Override the default cors settings
32
33
  */
@@ -76,9 +77,19 @@ export interface AppConfig {
76
77
  */
77
78
  workbench?: WorkbenchInstance;
78
79
  };
80
+ /**
81
+ * Optional setup function called before server starts
82
+ * Returns app state that will be available in all agents and routes
83
+ */
84
+ setup?: () => Promise<TAppState> | TAppState;
85
+ /**
86
+ * Optional shutdown function called when server is stopping
87
+ * Receives the app state returned from setup
88
+ */
89
+ shutdown?: (state: TAppState) => Promise<void> | void;
79
90
  }
80
91
 
81
- export interface Variables {
92
+ export interface Variables<TAppState = Record<string, never>> {
82
93
  logger: Logger;
83
94
  meter: Meter;
84
95
  tracer: Tracer;
@@ -91,6 +102,7 @@ export interface Variables {
91
102
  objectstore: ObjectStorage;
92
103
  stream: StreamStorage;
93
104
  vector: VectorStorage;
105
+ app: TAppState;
94
106
  }
95
107
 
96
108
  export type TriggerType = SessionStartEvent['trigger'];
@@ -102,51 +114,128 @@ export interface PrivateVariables {
102
114
  trigger: TriggerType;
103
115
  }
104
116
 
105
- export interface Env extends HonoEnv {
106
- Variables: Variables;
117
+ export interface Env<TAppState = Record<string, never>> extends HonoEnv {
118
+ Variables: Variables<TAppState>;
107
119
  }
108
120
 
109
- type AppEventMap = {
110
- 'agent.started': [Agent<any, any, any>, AgentContext];
111
- 'agent.completed': [Agent<any, any, any>, AgentContext];
112
- 'agent.errored': [Agent<any, any, any>, AgentContext, Error];
121
+ type AppEventMap<TAppState = Record<string, never>> = {
122
+ 'agent.started': [
123
+ Agent<any, any, any, any, TAppState>,
124
+ AgentContext<any, any, any, any, TAppState>,
125
+ ];
126
+ 'agent.completed': [
127
+ Agent<any, any, any, any, TAppState>,
128
+ AgentContext<any, any, any, any, TAppState>,
129
+ ];
130
+ 'agent.errored': [
131
+ Agent<any, any, any, any, TAppState>,
132
+ AgentContext<any, any, any, any, TAppState>,
133
+ Error,
134
+ ];
113
135
  'session.started': [Session];
114
136
  'session.completed': [Session];
115
137
  'thread.created': [Thread];
116
138
  'thread.destroyed': [Thread];
117
139
  };
118
140
 
119
- type AppEventCallback<K extends keyof AppEventMap> = (
141
+ type AppEventCallback<K extends keyof AppEventMap<any>, TAppState = Record<string, never>> = (
120
142
  eventName: K,
121
- ...args: AppEventMap[K]
143
+ ...args: AppEventMap<TAppState>[K]
122
144
  ) => void | Promise<void>;
123
145
 
124
- export class App {
146
+ /**
147
+ * Main application instance created by createApp().
148
+ * Provides access to the router, server, logger, and app state.
149
+ *
150
+ * @template TAppState - The type of application state returned from setup function
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * const app = await createApp({
155
+ * setup: async () => ({ db: await connectDB() })
156
+ * });
157
+ *
158
+ * // Access state
159
+ * console.log(app.state.db);
160
+ *
161
+ * // Add routes
162
+ * app.router.get('/health', (c) => c.text('OK'));
163
+ *
164
+ * // Listen to events
165
+ * app.addEventListener('agent.started', (eventName, agent, ctx) => {
166
+ * console.log(`Agent ${agent.metadata.name} started`);
167
+ * });
168
+ * ```
169
+ */
170
+ export class App<TAppState = Record<string, never>> {
125
171
  /**
126
- * the router instance
172
+ * The Hono router instance for defining routes.
127
173
  */
128
- readonly router: Hono<Env>;
174
+ readonly router: Hono<Env<TAppState>>;
129
175
  /**
130
- * the server instance
176
+ * The Bun server instance.
131
177
  */
132
- readonly server: ReturnType<typeof createServer>;
178
+ readonly server: Bun.Server<BunWebSocketData>;
133
179
  /**
134
- * the logger instance
180
+ * The application logger instance.
135
181
  */
136
182
  readonly logger: Logger;
183
+ /**
184
+ * The application state returned from the setup function.
185
+ * Available in all agents via ctx.app.
186
+ */
187
+ readonly state: TAppState;
137
188
 
138
- private eventListeners = new Map<keyof AppEventMap, Set<AppEventCallback<any>>>();
189
+ private eventListeners = new Map<
190
+ keyof AppEventMap<TAppState>,
191
+ Set<AppEventCallback<any, TAppState>>
192
+ >();
139
193
 
140
- constructor(config?: AppConfig) {
141
- this.router = new Hono<Env>();
142
- this.server = createServer(this.router, config);
194
+ constructor(
195
+ state: TAppState,
196
+ router: Hono<Env<TAppState>>,
197
+ server: Bun.Server<BunWebSocketData>
198
+ ) {
199
+ this.state = state;
200
+ this.router = router;
201
+ this.server = server;
143
202
  this.logger = getLogger() as Logger;
144
203
  setGlobalApp(this);
145
204
  }
146
205
 
147
- addEventListener<K extends keyof AppEventMap>(
206
+ /**
207
+ * Register an event listener for application lifecycle events.
208
+ *
209
+ * Available events:
210
+ * - `agent.started` - Fired when an agent begins execution
211
+ * - `agent.completed` - Fired when an agent completes successfully
212
+ * - `agent.errored` - Fired when an agent throws an error
213
+ * - `session.started` - Fired when a new session starts
214
+ * - `session.completed` - Fired when a session completes
215
+ * - `thread.created` - Fired when a thread is created
216
+ * - `thread.destroyed` - Fired when a thread is destroyed
217
+ *
218
+ * @param eventName - The event name to listen for
219
+ * @param callback - The callback function to execute when the event fires
220
+ *
221
+ * @example
222
+ * ```typescript
223
+ * app.addEventListener('agent.started', (eventName, agent, ctx) => {
224
+ * console.log(`${agent.metadata.name} started for session ${ctx.sessionId}`);
225
+ * });
226
+ *
227
+ * app.addEventListener('agent.errored', (eventName, agent, ctx, error) => {
228
+ * console.error(`${agent.metadata.name} failed:`, error.message);
229
+ * });
230
+ *
231
+ * app.addEventListener('session.started', (eventName, session) => {
232
+ * console.log(`New session: ${session.id}`);
233
+ * });
234
+ * ```
235
+ */
236
+ addEventListener<K extends keyof AppEventMap<TAppState>>(
148
237
  eventName: K,
149
- callback: AppEventCallback<K>
238
+ callback: AppEventCallback<K, TAppState>
150
239
  ): void {
151
240
  let callbacks = this.eventListeners.get(eventName);
152
241
  if (!callbacks) {
@@ -156,18 +245,48 @@ export class App {
156
245
  callbacks.add(callback);
157
246
  }
158
247
 
159
- removeEventListener<K extends keyof AppEventMap>(
248
+ /**
249
+ * Remove a previously registered event listener.
250
+ *
251
+ * @param eventName - The event name to stop listening for
252
+ * @param callback - The callback function to remove
253
+ *
254
+ * @example
255
+ * ```typescript
256
+ * const handler = (eventName, agent, ctx) => {
257
+ * console.log('Agent started:', agent.metadata.name);
258
+ * };
259
+ *
260
+ * app.addEventListener('agent.started', handler);
261
+ * // Later...
262
+ * app.removeEventListener('agent.started', handler);
263
+ * ```
264
+ */
265
+ removeEventListener<K extends keyof AppEventMap<TAppState>>(
160
266
  eventName: K,
161
- callback: AppEventCallback<K>
267
+ callback: AppEventCallback<K, TAppState>
162
268
  ): void {
163
269
  const callbacks = this.eventListeners.get(eventName);
164
270
  if (!callbacks) return;
165
271
  callbacks.delete(callback);
166
272
  }
167
273
 
168
- async fireEvent<K extends keyof AppEventMap>(
274
+ /**
275
+ * Manually fire an application event.
276
+ * Typically used internally by the runtime, but can be used for custom events.
277
+ *
278
+ * @param eventName - The event name to fire
279
+ * @param args - The arguments to pass to event listeners
280
+ *
281
+ * @example
282
+ * ```typescript
283
+ * // Fire a session completed event
284
+ * await app.fireEvent('session.completed', session);
285
+ * ```
286
+ */
287
+ async fireEvent<K extends keyof AppEventMap<TAppState>>(
169
288
  eventName: K,
170
- ...args: AppEventMap[K]
289
+ ...args: AppEventMap<TAppState>[K]
171
290
  ): Promise<void> {
172
291
  const callbacks = this.eventListeners.get(eventName);
173
292
  if (!callbacks || callbacks.size === 0) return;
@@ -178,34 +297,115 @@ export class App {
178
297
  }
179
298
  }
180
299
 
181
- let globalApp: App | null = null;
300
+ let globalApp: App<any> | null = null;
182
301
 
183
- function setGlobalApp(app: App): void {
302
+ function setGlobalApp(app: App<any>): void {
184
303
  globalApp = app;
185
304
  }
186
305
 
187
- export function getApp(): App | null {
306
+ /**
307
+ * Get the global app instance.
308
+ * Returns null if createApp() has not been called yet.
309
+ *
310
+ * @returns The global App instance or null
311
+ *
312
+ * @example
313
+ * ```typescript
314
+ * const app = getApp();
315
+ * if (app) {
316
+ * console.log('Server running on port:', app.server.port);
317
+ * }
318
+ * ```
319
+ */
320
+ export function getApp(): App<any> | null {
188
321
  return globalApp;
189
322
  }
190
323
 
191
324
  /**
192
- * create a new app instance
325
+ * Creates a new Agentuity application with optional lifecycle hooks and service configuration.
326
+ *
327
+ * This is the main entry point for creating an Agentuity app. The app will:
328
+ * 1. Run the setup function (if provided) to initialize app state
329
+ * 2. Start the Bun server
330
+ * 3. Make the app state available in all agents via ctx.app
331
+ *
332
+ * @template TAppState - The type of application state returned from setup function
333
+ *
334
+ * @param config - Optional application configuration
335
+ * @param config.setup - Function to initialize app state, runs before server starts
336
+ * @param config.shutdown - Function to clean up resources when server stops
337
+ * @param config.cors - CORS configuration for HTTP routes
338
+ * @param config.services - Override default storage and service providers
339
+ *
340
+ * @returns Promise resolving to App instance with running server
193
341
  *
194
- * @returns App instance
342
+ * @example
343
+ * ```typescript
344
+ * // Simple app with no state
345
+ * const app = await createApp();
346
+ *
347
+ * // App with database connection
348
+ * const app = await createApp({
349
+ * setup: async () => {
350
+ * const db = await connectDatabase();
351
+ * return { db };
352
+ * },
353
+ * shutdown: async (state) => {
354
+ * await state.db.close();
355
+ * }
356
+ * });
357
+ *
358
+ * // Access state in agents
359
+ * const agent = createAgent({
360
+ * handler: async (ctx, input) => {
361
+ * const db = ctx.app.db; // Strongly typed!
362
+ * return db.query('SELECT * FROM users');
363
+ * }
364
+ * });
365
+ *
366
+ * // App with custom services
367
+ * const app = await createApp({
368
+ * services: {
369
+ * useLocal: true, // Use local in-memory storage for development
370
+ * }
371
+ * });
372
+ * ```
195
373
  */
196
- export function createApp(config?: AppConfig): App {
197
- return new App(config);
374
+ export async function createApp<TAppState = Record<string, never>>(
375
+ config?: AppConfig<TAppState>
376
+ ): Promise<App<TAppState>> {
377
+ const initializer = async (): Promise<TAppState> => {
378
+ // Run setup if provided
379
+ if (config?.setup) {
380
+ return config.setup();
381
+ } else {
382
+ return {} as TAppState;
383
+ }
384
+ };
385
+
386
+ const router = new Hono<Env<TAppState>>();
387
+ const [server, state] = await createServer<TAppState>(router, initializer, config);
388
+
389
+ return new App(state, router, server);
198
390
  }
199
391
 
200
392
  /**
201
- * fire a global event
393
+ * Fire a global application event.
394
+ * Convenience function that calls fireEvent on the global app instance.
395
+ *
396
+ * @param eventName - The event name to fire
397
+ * @param args - The arguments to pass to event listeners
202
398
  *
203
- * @param eventName
204
- * @param args
399
+ * @example
400
+ * ```typescript
401
+ * // Fire from anywhere in your app
402
+ * await fireEvent('session.started', session);
403
+ * await fireEvent('agent.completed', agent, ctx);
404
+ * ```
205
405
  */
206
- export async function fireEvent<K extends keyof AppEventMap>(
406
+ export async function fireEvent<K extends keyof AppEventMap<any>>(
207
407
  eventName: K,
208
- ...args: AppEventMap[K]
408
+ ...args: AppEventMap<any>[K]
209
409
  ) {
210
410
  await globalApp?.fireEvent(eventName, ...args);
211
411
  }
package/src/index.ts CHANGED
@@ -6,7 +6,47 @@ export * from './eval';
6
6
  export * from './session';
7
7
  export * from './workbench';
8
8
  export type { Logger } from './logger';
9
- export { getRouter } from './_server';
9
+ export { getRouter, getAppState } from './_server';
10
10
  export { Email, parseEmail } from './io/email';
11
11
  export * from './services/evalrun';
12
- export { getEvalRunEventProvider } from './_services';
12
+ export { getEvalRunEventProvider, getThreadProvider, getSessionProvider } from './_services';
13
+
14
+ /**
15
+ * Application state interface that gets automatically augmented based on your createApp setup function.
16
+ *
17
+ * This interface is empty by default but gets populated with strongly-typed properties
18
+ * when you define a setup function in createApp(). The Agentuity build tool automatically
19
+ * generates type augmentations in `.agentuity/.agentuity_runtime.ts`.
20
+ *
21
+ * **How it works:**
22
+ * 1. You define setup() in createApp() that returns an object
23
+ * 2. The build tool generates module augmentation for this interface
24
+ * 3. All agents get strongly-typed access to app state via `ctx.app`
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * // In your app.ts:
29
+ * const app = await createApp({
30
+ * setup: async () => {
31
+ * const db = await connectDatabase();
32
+ * const redis = await connectRedis();
33
+ * return { db, redis };
34
+ * }
35
+ * });
36
+ *
37
+ * // In your agent:
38
+ * const agent = createAgent({
39
+ * handler: async (ctx, input) => {
40
+ * // ctx.app is strongly typed with { db, redis }!
41
+ * const user = await ctx.app.db.query('SELECT * FROM users');
42
+ * await ctx.app.redis.set('key', 'value');
43
+ * return user;
44
+ * }
45
+ * });
46
+ * ```
47
+ *
48
+ * **Note:** If you're not seeing type hints for `ctx.app`, make sure you've run `bun run build`
49
+ * to generate the type augmentations.
50
+ */
51
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
52
+ export interface AppState {}
package/src/io/email.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { StructuredError } from '@agentuity/core';
1
2
  import { type ParsedMail, type Headers, simpleParser, type AddressObject } from 'mailparser';
2
3
 
3
4
  /**
@@ -143,6 +144,21 @@ export class Email {
143
144
  }
144
145
  }
145
146
 
147
+ const InvalidEmailEmptyError = StructuredError(
148
+ 'InvalidEmailEmptyError',
149
+ 'Failed to parse email: empty buffer'
150
+ );
151
+
152
+ const InvalidEmailHeadersError = StructuredError(
153
+ 'InvalidEmailHeadersError',
154
+ 'Failed to parse email: missing headers'
155
+ );
156
+
157
+ const InvalidEmailParseError = StructuredError(
158
+ 'InvalidEmailParseError',
159
+ 'Failed to parse email: body was invalid'
160
+ )<{ bufferSize: number; preview?: string }>();
161
+
146
162
  /**
147
163
  * Parse an email from a buffer and return an Email object.
148
164
  *
@@ -152,22 +168,24 @@ export class Email {
152
168
  */
153
169
  export async function parseEmail(data: Buffer): Promise<Email> {
154
170
  if (data.length === 0) {
155
- throw new Error('Failed to parse email: empty buffer');
171
+ throw new InvalidEmailEmptyError();
156
172
  }
157
173
 
158
174
  const first16KB = data.slice(0, 16384).toString('utf-8', 0, Math.min(data.length, 16384));
159
175
  const hasHeaders = /(^|\r?\n)[!-9;-~]+:\s/.test(first16KB);
160
176
 
161
177
  if (!hasHeaders) {
162
- throw new Error('Failed to parse email: missing headers');
178
+ throw new InvalidEmailHeadersError();
163
179
  }
164
180
 
165
181
  try {
166
182
  const message = await simpleParser(data);
167
183
  return new Email(message);
168
184
  } catch (error) {
169
- throw new Error(
170
- `Failed to parse email: ${error instanceof Error ? error.message : 'Unknown error'}`
171
- );
185
+ throw new InvalidEmailParseError({
186
+ bufferSize: data.length,
187
+ preview: data.subarray(0, 50).toString('hex'),
188
+ cause: error,
189
+ });
172
190
  }
173
191
  }