@agentuity/runtime 0.0.68 → 0.0.70

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 (94) hide show
  1. package/AGENTS.md +130 -10
  2. package/README.md +6 -8
  3. package/dist/_config.d.ts +16 -0
  4. package/dist/_config.d.ts.map +1 -1
  5. package/dist/_config.js +16 -0
  6. package/dist/_config.js.map +1 -1
  7. package/dist/_context.d.ts +17 -15
  8. package/dist/_context.d.ts.map +1 -1
  9. package/dist/_context.js +17 -8
  10. package/dist/_context.js.map +1 -1
  11. package/dist/_server.d.ts.map +1 -1
  12. package/dist/_server.js +27 -14
  13. package/dist/_server.js.map +1 -1
  14. package/dist/_services.d.ts.map +1 -1
  15. package/dist/_services.js +2 -29
  16. package/dist/_services.js.map +1 -1
  17. package/dist/_validation.d.ts +89 -0
  18. package/dist/_validation.d.ts.map +1 -0
  19. package/dist/_validation.js +29 -0
  20. package/dist/_validation.js.map +1 -0
  21. package/dist/agent.d.ts +476 -104
  22. package/dist/agent.d.ts.map +1 -1
  23. package/dist/agent.js +293 -97
  24. package/dist/agent.js.map +1 -1
  25. package/dist/app.d.ts +6 -18
  26. package/dist/app.d.ts.map +1 -1
  27. package/dist/app.js +1 -1
  28. package/dist/app.js.map +1 -1
  29. package/dist/eval.d.ts +4 -4
  30. package/dist/eval.d.ts.map +1 -1
  31. package/dist/index.d.ts +3 -1
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +1 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/otel/config.d.ts.map +1 -1
  36. package/dist/otel/config.js +5 -2
  37. package/dist/otel/config.js.map +1 -1
  38. package/dist/otel/console.d.ts +10 -6
  39. package/dist/otel/console.d.ts.map +1 -1
  40. package/dist/otel/console.js +31 -13
  41. package/dist/otel/console.js.map +1 -1
  42. package/dist/otel/logger.d.ts.map +1 -1
  43. package/dist/otel/logger.js +0 -19
  44. package/dist/otel/logger.js.map +1 -1
  45. package/dist/otel/otel.d.ts +2 -1
  46. package/dist/otel/otel.d.ts.map +1 -1
  47. package/dist/otel/otel.js +28 -15
  48. package/dist/otel/otel.js.map +1 -1
  49. package/dist/services/local/_db.d.ts.map +1 -1
  50. package/dist/services/local/_db.js +1 -22
  51. package/dist/services/local/_db.js.map +1 -1
  52. package/dist/services/local/_router.d.ts.map +1 -1
  53. package/dist/services/local/_router.js +0 -32
  54. package/dist/services/local/_router.js.map +1 -1
  55. package/dist/services/local/index.d.ts +0 -1
  56. package/dist/services/local/index.d.ts.map +1 -1
  57. package/dist/services/local/index.js +0 -1
  58. package/dist/services/local/index.js.map +1 -1
  59. package/dist/session.d.ts +18 -2
  60. package/dist/session.d.ts.map +1 -1
  61. package/dist/session.js +6 -0
  62. package/dist/session.js.map +1 -1
  63. package/dist/validator.d.ts +140 -0
  64. package/dist/validator.d.ts.map +1 -0
  65. package/dist/validator.js +146 -0
  66. package/dist/validator.js.map +1 -0
  67. package/dist/workbench.d.ts.map +1 -1
  68. package/dist/workbench.js +38 -29
  69. package/dist/workbench.js.map +1 -1
  70. package/package.json +6 -6
  71. package/src/_config.ts +19 -0
  72. package/src/_context.ts +25 -31
  73. package/src/_server.ts +30 -14
  74. package/src/_services.ts +0 -28
  75. package/src/_validation.ts +119 -0
  76. package/src/agent.ts +872 -272
  77. package/src/app.ts +5 -18
  78. package/src/eval.ts +6 -6
  79. package/src/index.ts +3 -1
  80. package/src/otel/config.ts +5 -2
  81. package/src/otel/console.ts +34 -20
  82. package/src/otel/logger.ts +0 -18
  83. package/src/otel/otel.ts +43 -29
  84. package/src/services/local/_db.ts +1 -27
  85. package/src/services/local/_router.ts +0 -46
  86. package/src/services/local/index.ts +0 -1
  87. package/src/session.ts +22 -2
  88. package/src/validator.ts +277 -0
  89. package/src/workbench.ts +38 -32
  90. package/dist/services/local/objectstore.d.ts +0 -19
  91. package/dist/services/local/objectstore.d.ts.map +0 -1
  92. package/dist/services/local/objectstore.js +0 -117
  93. package/dist/services/local/objectstore.js.map +0 -1
  94. package/src/services/local/objectstore.ts +0 -177
package/src/agent.ts CHANGED
@@ -2,24 +2,24 @@
2
2
  import {
3
3
  StructuredError,
4
4
  type KeyValueStorage,
5
- type ObjectStorage,
6
5
  type StandardSchemaV1,
7
6
  type StreamStorage,
8
7
  type VectorStorage,
8
+ type InferOutput,
9
9
  toCamelCase,
10
10
  } from '@agentuity/core';
11
11
  import { context, SpanStatusCode, type Tracer, trace } from '@opentelemetry/api';
12
12
  import type { Context, MiddlewareHandler } from 'hono';
13
- import { getAgentContext, runInAgentContext, type RequestAgentContextArgs } from './_context';
13
+ import type { Handler } from 'hono/types';
14
+ import { validator } from 'hono/validator';
15
+ import { AGENT_RUNTIME, INTERNAL_AGENT, CURRENT_AGENT } from './_config';
16
+ import {
17
+ getAgentContext,
18
+ setupRequestAgentContext,
19
+ type RequestAgentContextArgs,
20
+ } from './_context';
14
21
  import type { Logger } from './logger';
15
- import type {
16
- Eval,
17
- EvalContext,
18
- EvalRunResult,
19
- EvalMetadata,
20
- EvalFunction,
21
- ExternalEvalMetadata,
22
- } from './eval';
22
+ import type { Eval, EvalContext, EvalRunResult, EvalFunction } from './eval';
23
23
  import { internal } from './logger/internal';
24
24
  import { getApp } from './app';
25
25
  import type { Thread, Session } from './session';
@@ -29,6 +29,7 @@ import { getEvalRunEventProvider } from './_services';
29
29
  import * as runtimeConfig from './_config';
30
30
  import type { EvalRunStartEvent } from '@agentuity/core';
31
31
  import type { AppState } from './index';
32
+ import { validateSchema, formatValidationIssues } from './_validation';
32
33
 
33
34
  export type AgentEventName = 'started' | 'completed' | 'errored';
34
35
 
@@ -50,24 +51,36 @@ export type AgentEventCallback<TAgent extends Agent<any, any, any>> =
50
51
  data: Error
51
52
  ) => Promise<void> | void);
52
53
 
54
+ /**
55
+ * Runtime state container for agents and event listeners.
56
+ * Isolates global state into context for better testing.
57
+ */
58
+ export interface AgentRuntimeState {
59
+ agents: Map<string, Agent<any, any, any, any, any>>;
60
+ agentConfigs: Map<string, unknown>;
61
+ agentEventListeners: WeakMap<
62
+ Agent<any, any, any, any, any>,
63
+ Map<AgentEventName, Set<AgentEventCallback<any>>>
64
+ >;
65
+ }
66
+
53
67
  /**
54
68
  * Context object passed to every agent handler providing access to runtime services and state.
55
69
  *
56
70
  * @template TAgentRegistry - Registry of all available agents (auto-generated, strongly-typed)
57
- * @template TCurrent - Current agent runner type
58
- * @template TParent - Parent agent runner type (if called from another agent)
59
71
  * @template TConfig - Agent-specific configuration type from setup function
60
72
  * @template TAppState - Application-wide state type from createApp
61
73
  *
62
74
  * @example
63
75
  * ```typescript
64
- * const agent = createAgent({
76
+ * const agent = createAgent('my-agent', {
65
77
  * handler: async (ctx, input) => {
66
78
  * // Logging
67
79
  * ctx.logger.info('Processing request', { input });
68
80
  *
69
- * // Call another agent
70
- * const result = await ctx.agent.otherAgent.run({ data: input });
81
+ * // Call another agent (import it directly)
82
+ * import otherAgent from './other-agent';
83
+ * const result = await otherAgent.run({ data: input });
71
84
  *
72
85
  * // Store data
73
86
  * await ctx.kv.set('key', { value: result });
@@ -86,12 +99,17 @@ export type AgentEventCallback<TAgent extends Agent<any, any, any>> =
86
99
  * ```
87
100
  */
88
101
  export interface AgentContext<
89
- TAgentRegistry extends AgentRegistry = AgentRegistry,
90
- TCurrent extends AgentRunner<any, any, any> | undefined = AgentRunner<any, any, any> | undefined,
91
- TParent extends AgentRunner<any, any, any> | undefined = AgentRunner<any, any, any> | undefined,
102
+ _TAgentRegistry extends AgentRegistry = AgentRegistry,
92
103
  TConfig = unknown,
93
104
  TAppState = Record<string, never>,
94
105
  > {
106
+ /**
107
+ * Internal runtime state (agents, configs, event listeners).
108
+ * Stored with Symbol key to prevent accidental access.
109
+ * Use getAgentRuntime(ctx) to access.
110
+ * @internal
111
+ */
112
+ [AGENT_RUNTIME]: AgentRuntimeState;
95
113
  /**
96
114
  * Schedule a background task that continues after the response is sent.
97
115
  * Useful for cleanup, logging, or async operations that don't block the response.
@@ -108,34 +126,6 @@ export interface AgentContext<
108
126
  */
109
127
  waitUntil: (promise: Promise<void> | (() => void | Promise<void>)) => void;
110
128
 
111
- /**
112
- * Registry of all agents in the application. Strongly-typed and auto-generated.
113
- * Use to call other agents from within your handler.
114
- *
115
- * @example
116
- * ```typescript
117
- * const emailResult = await ctx.agent.email.run({ to: 'user@example.com' });
118
- * const smsResult = await ctx.agent.sms.run({ phone: '+1234567890' });
119
- * ```
120
- */
121
- agent: TAgentRegistry;
122
-
123
- /**
124
- * Information about the currently executing agent.
125
- */
126
- current: TCurrent;
127
-
128
- /**
129
- * Information about the parent agent (if this agent was called by another agent).
130
- * Use ctx.agent.parentName for strongly-typed access.
131
- */
132
- parent: TParent;
133
-
134
- /**
135
- * Name of the current agent being executed.
136
- */
137
- agentName: AgentName;
138
-
139
129
  /**
140
130
  * Structured logger with OpenTelemetry integration.
141
131
  * Logs are automatically correlated with traces.
@@ -184,19 +174,6 @@ export interface AgentContext<
184
174
  */
185
175
  kv: KeyValueStorage;
186
176
 
187
- /**
188
- * Object storage for files and blobs (S3-compatible).
189
- *
190
- * @example
191
- * ```typescript
192
- * await ctx.objectstore.put('images/photo.jpg', buffer);
193
- * const file = await ctx.objectstore.get('images/photo.jpg');
194
- * await ctx.objectstore.delete('images/photo.jpg');
195
- * const objects = await ctx.objectstore.list('images/');
196
- * ```
197
- */
198
- objectstore: ObjectStorage;
199
-
200
177
  /**
201
178
  * Stream storage for real-time data streams and logs.
202
179
  *
@@ -283,13 +260,13 @@ export interface AgentContext<
283
260
 
284
261
  type InternalAgentMetadata = {
285
262
  /**
286
- * the unique identifier for this project, agent and deployment.
263
+ * the unique name for the agent (user-provided).
287
264
  */
288
- id: string;
265
+ name: string;
289
266
  /**
290
- * the unique identifier for this project and agent across multiple deployments.
267
+ * the unique identifier for this project, agent and deployment.
291
268
  */
292
- identifier: string;
269
+ id: string;
293
270
  /**
294
271
  * the unique identifier for this agent across multiple deployments
295
272
  */
@@ -315,10 +292,6 @@ type InternalAgentMetadata = {
315
292
  };
316
293
 
317
294
  type ExternalAgentMetadata = {
318
- /**
319
- * the human readable name for the agent.
320
- */
321
- name: string;
322
295
  /**
323
296
  * the human readable description for the agent
324
297
  */
@@ -330,25 +303,22 @@ type AgentMetadata = InternalAgentMetadata & ExternalAgentMetadata;
330
303
  /**
331
304
  * Configuration object for creating an agent evaluation function.
332
305
  *
333
- * @template TInput - Input schema type from the parent agent
334
- * @template TOutput - Output schema type from the parent agent
306
+ * @template TInput - Input schema type from the agent
307
+ * @template TOutput - Output schema type from the agent
335
308
  */
336
309
  export interface CreateEvalConfig<
337
310
  TInput extends StandardSchemaV1 | undefined = any,
338
311
  TOutput extends StandardSchemaV1 | undefined = any,
339
312
  > {
340
313
  /**
341
- * Optional metadata for the evaluation function.
314
+ * Optional description of what this evaluation does.
342
315
  *
343
316
  * @example
344
317
  * ```typescript
345
- * metadata: {
346
- * name: 'Validate positive output',
347
- * description: 'Ensures output is greater than zero'
348
- * }
318
+ * description: 'Ensures output is greater than zero'
349
319
  * ```
350
320
  */
351
- metadata?: Partial<ExternalEvalMetadata>;
321
+ description?: string;
352
322
 
353
323
  /**
354
324
  * Evaluation handler function that tests the agent's behavior.
@@ -383,8 +353,8 @@ export interface CreateEvalConfig<
383
353
  * ```
384
354
  */
385
355
  handler: EvalFunction<
386
- TInput extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<TInput> : undefined,
387
- TOutput extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<TOutput> : undefined
356
+ TInput extends StandardSchemaV1 ? InferOutput<TInput> : undefined,
357
+ TOutput extends StandardSchemaV1 ? InferOutput<TOutput> : undefined
388
358
  >;
389
359
  }
390
360
 
@@ -392,7 +362,148 @@ export interface CreateEvalConfig<
392
362
  type CreateEvalMethod<
393
363
  TInput extends StandardSchemaV1 | undefined = any,
394
364
  TOutput extends StandardSchemaV1 | undefined = any,
395
- > = (config: CreateEvalConfig<TInput, TOutput>) => Eval<TInput, TOutput>;
365
+ > = (name: string, config: CreateEvalConfig<TInput, TOutput>) => Eval<TInput, TOutput>;
366
+
367
+ /**
368
+ * Validator function type with method overloads for different validation scenarios.
369
+ * Provides type-safe validation middleware that integrates with Hono's type system.
370
+ *
371
+ * This validator automatically validates incoming JSON request bodies using StandardSchema-compatible
372
+ * schemas (Zod, Valibot, ArkType, etc.) and provides full TypeScript type inference for validated data
373
+ * accessible via `c.req.valid('json')`.
374
+ *
375
+ * The validator returns 400 Bad Request with validation error details if validation fails.
376
+ *
377
+ * @template TInput - Agent's input schema type (StandardSchemaV1 or undefined)
378
+ * @template _TOutput - Agent's output schema type (reserved for future output validation)
379
+ *
380
+ * @example Basic usage with agent's schema
381
+ * ```typescript
382
+ * router.post('/', agent.validator(), async (c) => {
383
+ * const data = c.req.valid('json'); // Fully typed from agent's input schema
384
+ * return c.json(data);
385
+ * });
386
+ * ```
387
+ *
388
+ * @example Override with custom input schema
389
+ * ```typescript
390
+ * router.post('/custom', agent.validator({ input: z.object({ id: z.string() }) }), async (c) => {
391
+ * const data = c.req.valid('json'); // Typed as { id: string }
392
+ * return c.json(data);
393
+ * });
394
+ * ```
395
+ */
396
+ export interface AgentValidator<
397
+ TInput extends StandardSchemaV1 | undefined,
398
+ _TOutput extends StandardSchemaV1 | undefined,
399
+ > {
400
+ /**
401
+ * Validates using the agent's input schema (no override).
402
+ * Returns Hono middleware handler that validates JSON request body.
403
+ *
404
+ * @returns Middleware handler with type inference for validated data
405
+ *
406
+ * @example
407
+ * ```typescript
408
+ * // Agent has schema: { input: z.object({ name: z.string() }) }
409
+ * router.post('/', agent.validator(), async (c) => {
410
+ * const data = c.req.valid('json'); // { name: string }
411
+ * return c.json({ received: data.name });
412
+ * });
413
+ * ```
414
+ */
415
+ (): TInput extends StandardSchemaV1
416
+ ? Handler<
417
+ any,
418
+ any,
419
+ {
420
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
421
+ in: {};
422
+ out: { json: InferOutput<TInput> };
423
+ }
424
+ >
425
+ : Handler<any, any, any>;
426
+
427
+ /**
428
+ * Output-only validation override.
429
+ * Validates only the response body (no input validation).
430
+ *
431
+ * Useful for GET routes or routes where input validation is handled elsewhere.
432
+ * The middleware validates the JSON response body and throws 500 Internal Server Error
433
+ * if validation fails.
434
+ *
435
+ * @template TOverrideOutput - Custom output schema type
436
+ * @param override - Object containing output schema
437
+ * @returns Middleware handler that validates response output
438
+ *
439
+ * @example GET route with output validation
440
+ * ```typescript
441
+ * router.get('/', agent.validator({ output: z.array(z.object({ id: z.string() })) }), async (c) => {
442
+ * // Returns array of objects - validated against schema
443
+ * return c.json([{ id: '123' }, { id: '456' }]);
444
+ * });
445
+ * ```
446
+ */
447
+ <TOverrideOutput extends StandardSchemaV1>(override: {
448
+ output: TOverrideOutput;
449
+ }): Handler<
450
+ any,
451
+ any,
452
+ {
453
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
454
+ in: {};
455
+ out: { json: InferOutput<TOverrideOutput> };
456
+ }
457
+ >;
458
+
459
+ /**
460
+ * Validates with custom input and optional output schemas (POST/PUT/PATCH/DELETE).
461
+ * Overrides the agent's schema for this specific route.
462
+ *
463
+ * @template TOverrideInput - Custom input schema type
464
+ * @template TOverrideOutput - Optional custom output schema type
465
+ * @param override - Object containing input (required) and output (optional) schemas
466
+ * @returns Middleware handler with type inference from custom schemas
467
+ *
468
+ * @example Custom input schema
469
+ * ```typescript
470
+ * router.post('/users', agent.validator({
471
+ * input: z.object({ email: z.string().email(), name: z.string() })
472
+ * }), async (c) => {
473
+ * const data = c.req.valid('json'); // { email: string, name: string }
474
+ * return c.json({ id: '123', ...data });
475
+ * });
476
+ * ```
477
+ *
478
+ * @example Custom input and output schemas
479
+ * ```typescript
480
+ * router.post('/convert', agent.validator({
481
+ * input: z.string(),
482
+ * output: z.number()
483
+ * }), async (c) => {
484
+ * const data = c.req.valid('json'); // string
485
+ * return c.json(123);
486
+ * });
487
+ * ```
488
+ */
489
+ <
490
+ TOverrideInput extends StandardSchemaV1,
491
+ TOverrideOutput extends StandardSchemaV1 | undefined = undefined,
492
+ >(override: {
493
+ input: TOverrideInput;
494
+ output?: TOverrideOutput;
495
+ }): Handler<
496
+ any,
497
+ any,
498
+ {
499
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
500
+ in: {};
501
+ out: {
502
+ json: InferOutput<TOverrideInput>;
503
+ };
504
+ }
505
+ >;
506
+ }
396
507
 
397
508
  /**
398
509
  * Agent instance type returned by createAgent().
@@ -421,7 +532,8 @@ type CreateEvalMethod<
421
532
  * });
422
533
  *
423
534
  * // Create evals for testing
424
- * const eval1 = agent.createEval({
535
+ * const eval1 = agent.createEval('check-positive', {
536
+ * description: 'Ensures result is greater than 5',
425
537
  * handler: async (run, result) => {
426
538
  * return result > 5; // Assert output is greater than 5
427
539
  * }
@@ -442,12 +554,79 @@ export type Agent<
442
554
 
443
555
  /**
444
556
  * The main handler function that processes agent requests.
445
- * Receives AgentContext and validated input, returns output or stream.
557
+ * Gets AgentContext from AsyncLocalStorage, receives validated input, returns output or stream.
446
558
  */
447
- handler: (
448
- ctx: AgentContext<any, any, any, TConfig, TAppState>,
449
- ...args: any[]
450
- ) => any | Promise<any>;
559
+ handler: (...args: any[]) => any | Promise<any>;
560
+
561
+ /**
562
+ * Creates a type-safe validation middleware for routes using StandardSchema validation.
563
+ *
564
+ * This method validates incoming JSON request bodies against the agent's **input schema**
565
+ * and optionally validates outgoing JSON responses against the **output schema**.
566
+ * Provides full TypeScript type inference for validated input data accessible via `c.req.valid('json')`.
567
+ *
568
+ * **Validation behavior:**
569
+ * - **Input**: Validates request JSON body, returns 400 Bad Request on failure
570
+ * - **Output**: Validates response JSON body (if output schema provided), throws 500 on failure
571
+ * - Passes validated input data to handler via `c.req.valid('json')`
572
+ * - Full TypeScript type inference for validated input data
573
+ *
574
+ * **Supported schema libraries:**
575
+ * - Zod (z.object, z.string, etc.)
576
+ * - Valibot (v.object, v.string, etc.)
577
+ * - ArkType (type({ ... }))
578
+ * - Any StandardSchema-compatible library
579
+ *
580
+ * **Method overloads:**
581
+ * - `agent.validator()` - Validates using agent's input/output schemas
582
+ * - `agent.validator({ output: schema })` - Output-only validation (no input validation)
583
+ * - `agent.validator({ input: schema })` - Custom input schema override
584
+ * - `agent.validator({ input: schema, output: schema })` - Both input and output validated
585
+ *
586
+ * @returns Hono middleware handler with proper type inference
587
+ *
588
+ * @example Automatic validation using agent's schema
589
+ * ```typescript
590
+ * // Agent defined with: schema: { input: z.object({ name: z.string(), age: z.number() }) }
591
+ * router.post('/', agent.validator(), async (c) => {
592
+ * const data = c.req.valid('json'); // Fully typed: { name: string, age: number }
593
+ * return c.json({ greeting: `Hello ${data.name}, age ${data.age}` });
594
+ * });
595
+ * ```
596
+ *
597
+ * @example Override with custom schema per-route
598
+ * ```typescript
599
+ * router.post('/email', agent.validator({
600
+ * input: z.object({ email: z.string().email() })
601
+ * }), async (c) => {
602
+ * const data = c.req.valid('json'); // Typed as { email: string }
603
+ * return c.json({ sent: data.email });
604
+ * });
605
+ * ```
606
+ *
607
+ * @example Works with any StandardSchema library
608
+ * ```typescript
609
+ * import * as v from 'valibot';
610
+ *
611
+ * router.post('/valibot', agent.validator({
612
+ * input: v.object({ count: v.number() })
613
+ * }), async (c) => {
614
+ * const data = c.req.valid('json'); // Typed correctly
615
+ * return c.json({ count: data.count });
616
+ * });
617
+ * ```
618
+ *
619
+ * @example Validation error response (400)
620
+ * ```typescript
621
+ * // Request: { "name": "Bob" } (missing 'age')
622
+ * // Response: {
623
+ * // "error": "Validation failed",
624
+ * // "message": "age: Invalid input: expected number, received undefined",
625
+ * // "issues": [{ "message": "...", "path": ["age"] }]
626
+ * // }
627
+ * ```
628
+ */
629
+ validator: AgentValidator<TInput, TOutput>;
451
630
 
452
631
  /**
453
632
  * Array of evaluation functions created via agent.createEval().
@@ -471,8 +650,8 @@ export type Agent<
471
650
  * });
472
651
  *
473
652
  * // Create eval to validate output
474
- * agent.createEval({
475
- * metadata: { name: 'Check positive output' },
653
+ * agent.createEval('check-positive', {
654
+ * description: 'Ensures output is a positive number',
476
655
  * handler: async (run, result) => {
477
656
  * return result > 0; // Assert output is positive
478
657
  * }
@@ -511,7 +690,7 @@ export type Agent<
511
690
  callback: (
512
691
  eventName: 'started',
513
692
  agent: Agent<TInput, TOutput, TStream, TConfig, TAppState>,
514
- context: AgentContext<any, any, any, TConfig, TAppState>
693
+ context: AgentContext<any, TConfig, TAppState>
515
694
  ) => Promise<void> | void
516
695
  ): void;
517
696
 
@@ -533,7 +712,7 @@ export type Agent<
533
712
  callback: (
534
713
  eventName: 'completed',
535
714
  agent: Agent<TInput, TOutput, TStream, TConfig, TAppState>,
536
- context: AgentContext<any, any, any, TConfig, TAppState>
715
+ context: AgentContext<any, TConfig, TAppState>
537
716
  ) => Promise<void> | void
538
717
  ): void;
539
718
 
@@ -555,7 +734,7 @@ export type Agent<
555
734
  callback: (
556
735
  eventName: 'errored',
557
736
  agent: Agent<TInput, TOutput, TStream, TConfig, TAppState>,
558
- context: AgentContext<any, any, any, TConfig, TAppState>,
737
+ context: AgentContext<any, TConfig, TAppState>,
559
738
  data: Error
560
739
  ) => Promise<void> | void
561
740
  ): void;
@@ -571,7 +750,7 @@ export type Agent<
571
750
  callback: (
572
751
  eventName: 'started',
573
752
  agent: Agent<TInput, TOutput, TStream, TConfig, TAppState>,
574
- context: AgentContext<any, any, any, TConfig, TAppState>
753
+ context: AgentContext<any, TConfig, TAppState>
575
754
  ) => Promise<void> | void
576
755
  ): void;
577
756
 
@@ -586,7 +765,7 @@ export type Agent<
586
765
  callback: (
587
766
  eventName: 'completed',
588
767
  agent: Agent<TInput, TOutput, TStream, TConfig, TAppState>,
589
- context: AgentContext<any, any, any, TConfig, TAppState>
768
+ context: AgentContext<any, TConfig, TAppState>
590
769
  ) => Promise<void> | void
591
770
  ): void;
592
771
 
@@ -601,7 +780,7 @@ export type Agent<
601
780
  callback: (
602
781
  eventName: 'errored',
603
782
  agent: Agent<TInput, TOutput, TStream, TConfig, TAppState>,
604
- context: AgentContext<any, any, any, TConfig, TAppState>,
783
+ context: AgentContext<any, TConfig, TAppState>,
605
784
  data: Error
606
785
  ) => Promise<void> | void
607
786
  ): void;
@@ -609,14 +788,14 @@ export type Agent<
609
788
  (TOutput extends StandardSchemaV1 ? { outputSchema: TOutput } : { outputSchema?: never }) &
610
789
  (TStream extends true ? { stream: true } : { stream?: false });
611
790
 
612
- type InferSchemaInput<T> = T extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<T> : never;
791
+ type InferSchemaInput<T> = T extends StandardSchemaV1 ? InferOutput<T> : never;
613
792
 
614
793
  type InferStreamOutput<TOutput, TStream extends boolean> = TStream extends true
615
794
  ? TOutput extends StandardSchemaV1
616
- ? ReadableStream<StandardSchemaV1.InferOutput<TOutput>>
795
+ ? ReadableStream<InferOutput<TOutput>>
617
796
  : ReadableStream<unknown>
618
797
  : TOutput extends StandardSchemaV1
619
- ? StandardSchemaV1.InferOutput<TOutput>
798
+ ? InferOutput<TOutput>
620
799
  : void;
621
800
 
622
801
  type SchemaInput<TSchema> = TSchema extends { input: infer I } ? I : undefined;
@@ -630,10 +809,10 @@ type SchemaStream<TSchema> = TSchema extends { stream: infer S }
630
809
  type SchemaHandlerReturn<TSchema> =
631
810
  SchemaStream<TSchema> extends true
632
811
  ? SchemaOutput<TSchema> extends StandardSchemaV1
633
- ? ReadableStream<StandardSchemaV1.InferOutput<SchemaOutput<TSchema>>>
812
+ ? ReadableStream<InferOutput<SchemaOutput<TSchema>>>
634
813
  : ReadableStream<unknown>
635
814
  : SchemaOutput<TSchema> extends StandardSchemaV1
636
- ? StandardSchemaV1.InferOutput<SchemaOutput<TSchema>>
815
+ ? InferOutput<SchemaOutput<TSchema>>
637
816
  : void;
638
817
 
639
818
  // Handler signature based on schema + setup result (no self-reference)
@@ -641,21 +820,37 @@ type AgentHandlerFromConfig<TSchema, TSetupReturn, TAppState = AppState> =
641
820
  SchemaInput<TSchema> extends infer I
642
821
  ? I extends StandardSchemaV1
643
822
  ? (
644
- ctx: AgentContext<any, any, any, TSetupReturn, TAppState>,
645
- input: StandardSchemaV1.InferOutput<I>
823
+ ctx: AgentContext<any, TSetupReturn, TAppState>,
824
+ input: InferOutput<I>
646
825
  ) => Promise<SchemaHandlerReturn<TSchema>> | SchemaHandlerReturn<TSchema>
647
826
  : (
648
- ctx: AgentContext<any, any, any, TSetupReturn, TAppState>
827
+ ctx: AgentContext<any, TSetupReturn, TAppState>
649
828
  ) => Promise<SchemaHandlerReturn<TSchema>> | SchemaHandlerReturn<TSchema>
650
829
  : (
651
- ctx: AgentContext<any, any, any, TSetupReturn, TAppState>
830
+ ctx: AgentContext<any, TSetupReturn, TAppState>
652
831
  ) => Promise<SchemaHandlerReturn<TSchema>> | SchemaHandlerReturn<TSchema>;
653
832
 
654
833
  /**
655
834
  * Configuration object for creating an agent with automatic type inference.
656
835
  *
836
+ * Passed as the second parameter to createAgent(name, config).
837
+ *
657
838
  * @template TSchema - Schema definition object containing optional input, output, and stream properties
658
839
  * @template TConfig - Function type that returns agent-specific configuration from setup
840
+ *
841
+ * @example
842
+ * ```typescript
843
+ * const agent = createAgent('greeting', {
844
+ * description: 'Generates personalized greetings',
845
+ * schema: {
846
+ * input: z.object({ name: z.string(), age: z.number() }),
847
+ * output: z.string()
848
+ * },
849
+ * handler: async (ctx, { name, age }) => {
850
+ * return `Hello, ${name}! You are ${age} years old.`;
851
+ * }
852
+ * });
853
+ * ```
659
854
  */
660
855
  export interface CreateAgentConfig<
661
856
  TSchema extends
@@ -682,17 +877,22 @@ export interface CreateAgentConfig<
682
877
  schema?: TSchema;
683
878
 
684
879
  /**
685
- * Agent metadata visible in the Agentuity platform.
880
+ * Optional description of what this agent does, visible in the Agentuity platform.
686
881
  *
687
882
  * @example
688
883
  * ```typescript
689
- * metadata: {
690
- * name: 'Greeting Agent',
691
- * description: 'Returns personalized greetings'
692
- * }
884
+ * description: 'Returns personalized greetings'
693
885
  * ```
694
886
  */
695
- metadata: ExternalAgentMetadata;
887
+ description?: string;
888
+
889
+ /**
890
+ * Optional metadata object (typically injected by build plugin during compilation).
891
+ * Contains agent identification and versioning information.
892
+ *
893
+ * @internal - Usually populated by build tooling, not manually set
894
+ */
895
+ metadata?: Partial<AgentMetadata>;
696
896
 
697
897
  /**
698
898
  * Optional async function called once on app startup to initialize agent-specific resources.
@@ -756,17 +956,187 @@ export interface CreateAgentConfig<
756
956
  ) => Promise<void> | void;
757
957
  }
758
958
 
959
+ /**
960
+ * The public interface returned by createAgent().
961
+ * Provides methods to run the agent, create evaluations, and manage event listeners.
962
+ *
963
+ * @template TInput - Input schema type (StandardSchemaV1 or undefined if no input)
964
+ * @template TOutput - Output schema type (StandardSchemaV1 or undefined if no output)
965
+ * @template TStream - Whether the agent returns a stream (true/false)
966
+ *
967
+ * @example
968
+ * ```typescript
969
+ * const agent = createAgent('greeting', {
970
+ * schema: {
971
+ * input: z.object({ name: z.string() }),
972
+ * output: z.string()
973
+ * },
974
+ * handler: async (ctx, { name }) => `Hello, ${name}!`
975
+ * });
976
+ *
977
+ * // Run the agent
978
+ * const result = await agent.run({ name: 'Alice' });
979
+ *
980
+ * // Create evaluation
981
+ * const evalDef = agent.createEval('greeting-accuracy', {
982
+ * description: 'Checks if greeting includes the user name',
983
+ * handler: async (ctx, input, output) => {
984
+ * return { score: output.includes(input.name) ? 1 : 0 };
985
+ * }
986
+ * });
987
+ *
988
+ * // Listen to events
989
+ * agent.addEventListener('completed', async (eventName, agent, context) => {
990
+ * console.log('Agent completed successfully');
991
+ * });
992
+ * ```
993
+ */
759
994
  export interface AgentRunner<
760
995
  TInput extends StandardSchemaV1 | undefined = any,
761
996
  TOutput extends StandardSchemaV1 | undefined = any,
762
997
  TStream extends boolean = false,
763
998
  > {
999
+ /** Agent metadata (id, name, description, etc.) */
764
1000
  metadata: AgentMetadata;
1001
+
1002
+ /**
1003
+ * Execute the agent with validated input.
1004
+ * If agent has no input schema, call with no arguments.
1005
+ * If agent has input schema, pass validated input object.
1006
+ *
1007
+ * @example
1008
+ * ```typescript
1009
+ * // Agent with input
1010
+ * const result = await agent.run({ name: 'Alice' });
1011
+ *
1012
+ * // Agent without input
1013
+ * const result = await agent.run();
1014
+ * ```
1015
+ */
765
1016
  run: undefined extends TInput
766
1017
  ? () => Promise<InferStreamOutput<Exclude<TOutput, undefined>, TStream>>
767
1018
  : (
768
1019
  input: InferSchemaInput<Exclude<TInput, undefined>>
769
1020
  ) => Promise<InferStreamOutput<Exclude<TOutput, undefined>, TStream>>;
1021
+
1022
+ /**
1023
+ * Create Hono validator middleware for this agent.
1024
+ * Automatically validates request input against the agent's schema.
1025
+ *
1026
+ * @example
1027
+ * ```typescript
1028
+ * import myAgent from './my-agent';
1029
+ * router.post('/', myAgent.validator(), async (c) => {
1030
+ * const data = c.req.valid('json'); // Fully typed!
1031
+ * return c.json(await myAgent.run(data));
1032
+ * });
1033
+ * ```
1034
+ */
1035
+ validator: AgentValidator<TInput, TOutput>;
1036
+
1037
+ /** Input schema (if defined) */
1038
+ inputSchema?: TInput;
1039
+
1040
+ /** Output schema (if defined) */
1041
+ outputSchema?: TOutput;
1042
+
1043
+ /** Whether agent returns a stream */
1044
+ stream?: TStream;
1045
+
1046
+ /**
1047
+ * Create an evaluation for this agent.
1048
+ * Evaluations run automatically after the agent completes.
1049
+ *
1050
+ * @example
1051
+ * ```typescript
1052
+ * const accuracyEval = agent.createEval('accuracy', {
1053
+ * description: 'Validates output length is non-zero',
1054
+ * handler: async (ctx, input, output) => ({
1055
+ * score: output.length > 0 ? 1 : 0,
1056
+ * metadata: { outputLength: output.length }
1057
+ * })
1058
+ * });
1059
+ * ```
1060
+ */
1061
+ createEval: CreateEvalMethod<TInput, TOutput>;
1062
+
1063
+ /**
1064
+ * Add event listener for 'started' or 'completed' events.
1065
+ * Listeners fire sequentially in the order they were added.
1066
+ *
1067
+ * @param eventName - 'started' or 'completed'
1068
+ * @param callback - Function to call when event fires
1069
+ *
1070
+ * @example
1071
+ * ```typescript
1072
+ * agent.addEventListener('started', async (eventName, agent, context) => {
1073
+ * context.logger.info('Agent execution started');
1074
+ * });
1075
+ * ```
1076
+ */
1077
+ addEventListener(
1078
+ eventName: 'started' | 'completed',
1079
+ callback: (
1080
+ eventName: 'started' | 'completed',
1081
+ agent: Agent<TInput, TOutput, TStream, any, any>,
1082
+ context: AgentContext<any, any, any>
1083
+ ) => Promise<void> | void
1084
+ ): void;
1085
+
1086
+ /**
1087
+ * Add event listener for 'errored' event.
1088
+ * Fires when agent handler throws an error.
1089
+ *
1090
+ * @param eventName - 'errored'
1091
+ * @param callback - Function to call when error occurs
1092
+ *
1093
+ * @example
1094
+ * ```typescript
1095
+ * agent.addEventListener('errored', async (eventName, agent, context, error) => {
1096
+ * context.logger.error('Agent failed', { error: error.message });
1097
+ * });
1098
+ * ```
1099
+ */
1100
+ addEventListener(
1101
+ eventName: 'errored',
1102
+ callback: (
1103
+ eventName: 'errored',
1104
+ agent: Agent<TInput, TOutput, TStream, any, any>,
1105
+ context: AgentContext<any, any, any>,
1106
+ error: Error
1107
+ ) => Promise<void> | void
1108
+ ): void;
1109
+
1110
+ /**
1111
+ * Remove event listener for 'started' or 'completed' events.
1112
+ *
1113
+ * @param eventName - 'started' or 'completed'
1114
+ * @param callback - The same callback function that was added
1115
+ */
1116
+ removeEventListener(
1117
+ eventName: 'started' | 'completed',
1118
+ callback: (
1119
+ eventName: 'started' | 'completed',
1120
+ agent: Agent<TInput, TOutput, TStream, any, any>,
1121
+ context: AgentContext<any, any, any>
1122
+ ) => Promise<void> | void
1123
+ ): void;
1124
+
1125
+ /**
1126
+ * Remove event listener for 'errored' event.
1127
+ *
1128
+ * @param eventName - 'errored'
1129
+ * @param callback - The same callback function that was added
1130
+ */
1131
+ removeEventListener(
1132
+ eventName: 'errored',
1133
+ callback: (
1134
+ eventName: 'errored',
1135
+ agent: Agent<TInput, TOutput, TStream, any, any>,
1136
+ context: AgentContext<any, any, any>,
1137
+ error: Error
1138
+ ) => Promise<void> | void
1139
+ ): void;
770
1140
  }
771
1141
 
772
1142
  // Will be populated at runtime with strongly typed agents
@@ -781,26 +1151,49 @@ const agentEventListeners = new WeakMap<
781
1151
  // Map to store agent configs returned from setup (keyed by agent name)
782
1152
  const agentConfigs = new Map<string, unknown>();
783
1153
 
1154
+ /**
1155
+ * Get the global runtime state (for production use).
1156
+ * In tests, use TestAgentContext which has isolated runtime state.
1157
+ */
1158
+ export function getGlobalRuntimeState(): AgentRuntimeState {
1159
+ return {
1160
+ agents,
1161
+ agentConfigs,
1162
+ agentEventListeners,
1163
+ };
1164
+ }
1165
+
1166
+ /**
1167
+ * Get the runtime state from an AgentContext.
1168
+ * @internal
1169
+ */
1170
+ export function getAgentRuntime(ctx: AgentContext<any, any, any>): AgentRuntimeState {
1171
+ return ctx[AGENT_RUNTIME];
1172
+ }
1173
+
784
1174
  // Helper to fire event listeners sequentially, abort on first error
785
1175
  async function fireAgentEvent(
1176
+ runtime: AgentRuntimeState,
786
1177
  agent: Agent<any, any, any, any, any>,
787
1178
  eventName: 'started' | 'completed',
788
- context: AgentContext<any, any, any, any, any>
1179
+ context: AgentContext<any, any, any>
789
1180
  ): Promise<void>;
790
1181
  async function fireAgentEvent(
1182
+ runtime: AgentRuntimeState,
791
1183
  agent: Agent<any, any, any, any, any>,
792
1184
  eventName: 'errored',
793
- context: AgentContext<any, any, any, any, any>,
1185
+ context: AgentContext<any, any, any>,
794
1186
  data: Error
795
1187
  ): Promise<void>;
796
1188
  async function fireAgentEvent(
1189
+ runtime: AgentRuntimeState,
797
1190
  agent: Agent<any, any, any, any, any>,
798
1191
  eventName: AgentEventName,
799
- context: AgentContext<any, any, any, any, any>,
1192
+ context: AgentContext<any, any, any>,
800
1193
  data?: Error
801
1194
  ): Promise<void> {
802
1195
  // Fire agent-level listeners
803
- const listeners = agentEventListeners.get(agent);
1196
+ const listeners = runtime.agentEventListeners.get(agent);
804
1197
  if (listeners) {
805
1198
  const callbacks = listeners.get(eventName);
806
1199
  if (callbacks && callbacks.size > 0) {
@@ -895,17 +1288,22 @@ export interface CreateAgentConfigExplicit<
895
1288
  };
896
1289
 
897
1290
  /**
898
- * Agent metadata.
1291
+ * Optional description of what this agent does.
899
1292
  *
900
1293
  * @example
901
1294
  * ```typescript
902
- * metadata: {
903
- * name: 'My Agent',
904
- * description: 'Does something useful'
905
- * }
1295
+ * description: 'Does something useful'
906
1296
  * ```
907
1297
  */
908
- metadata: ExternalAgentMetadata;
1298
+ description?: string;
1299
+
1300
+ /**
1301
+ * Optional metadata object (typically injected by build plugin during compilation).
1302
+ * Contains agent identification and versioning information.
1303
+ *
1304
+ * @internal - Usually populated by build tooling, not manually set
1305
+ */
1306
+ metadata?: Partial<AgentMetadata>;
909
1307
 
910
1308
  /**
911
1309
  * Optional setup function receiving app state, returns agent config.
@@ -955,43 +1353,39 @@ export interface CreateAgentConfigExplicit<
955
1353
  ? TStream extends true
956
1354
  ? TOutput extends StandardSchemaV1
957
1355
  ? (
958
- c: AgentContext<any, any, any, TConfig, TAppState>,
959
- input: StandardSchemaV1.InferOutput<TInput>
1356
+ c: AgentContext<any, TConfig, TAppState>,
1357
+ input: InferOutput<TInput>
960
1358
  ) =>
961
- | Promise<ReadableStream<StandardSchemaV1.InferOutput<TOutput>>>
962
- | ReadableStream<StandardSchemaV1.InferOutput<TOutput>>
1359
+ | Promise<ReadableStream<InferOutput<TOutput>>>
1360
+ | ReadableStream<InferOutput<TOutput>>
963
1361
  : (
964
- c: AgentContext<any, any, any, TConfig, TAppState>,
965
- input: StandardSchemaV1.InferOutput<TInput>
1362
+ c: AgentContext<any, TConfig, TAppState>,
1363
+ input: InferOutput<TInput>
966
1364
  ) => Promise<ReadableStream<unknown>> | ReadableStream<unknown>
967
1365
  : TOutput extends StandardSchemaV1
968
1366
  ? (
969
- c: AgentContext<any, any, any, TConfig, TAppState>,
970
- input: StandardSchemaV1.InferOutput<TInput>
971
- ) =>
972
- | Promise<StandardSchemaV1.InferOutput<TOutput>>
973
- | StandardSchemaV1.InferOutput<TOutput>
1367
+ c: AgentContext<any, TConfig, TAppState>,
1368
+ input: InferOutput<TInput>
1369
+ ) => Promise<InferOutput<TOutput>> | InferOutput<TOutput>
974
1370
  : (
975
- c: AgentContext<any, any, any, TConfig, TAppState>,
976
- input: StandardSchemaV1.InferOutput<TInput>
1371
+ c: AgentContext<any, TConfig, TAppState>,
1372
+ input: InferOutput<TInput>
977
1373
  ) => Promise<void> | void
978
1374
  : TStream extends true
979
1375
  ? TOutput extends StandardSchemaV1
980
1376
  ? (
981
- c: AgentContext<any, any, any, TConfig, TAppState>
1377
+ c: AgentContext<any, TConfig, TAppState>
982
1378
  ) =>
983
- | Promise<ReadableStream<StandardSchemaV1.InferOutput<TOutput>>>
984
- | ReadableStream<StandardSchemaV1.InferOutput<TOutput>>
1379
+ | Promise<ReadableStream<InferOutput<TOutput>>>
1380
+ | ReadableStream<InferOutput<TOutput>>
985
1381
  : (
986
- c: AgentContext<any, any, any, TConfig, TAppState>
1382
+ c: AgentContext<any, TConfig, TAppState>
987
1383
  ) => Promise<ReadableStream<unknown>> | ReadableStream<unknown>
988
1384
  : TOutput extends StandardSchemaV1
989
1385
  ? (
990
- c: AgentContext<any, any, any, TConfig, TAppState>
991
- ) =>
992
- | Promise<StandardSchemaV1.InferOutput<TOutput>>
993
- | StandardSchemaV1.InferOutput<TOutput>
994
- : (c: AgentContext<any, any, any, TConfig, TAppState>) => Promise<void> | void;
1386
+ c: AgentContext<any, TConfig, TAppState>
1387
+ ) => Promise<InferOutput<TOutput>> | InferOutput<TOutput>
1388
+ : (c: AgentContext<any, TConfig, TAppState>) => Promise<void> | void;
995
1389
  }
996
1390
 
997
1391
  /**
@@ -1002,17 +1396,15 @@ export interface CreateAgentConfigExplicit<
1002
1396
  * @template TSchema - Schema definition object containing optional input, output, and stream properties
1003
1397
  * @template TConfig - Function type that returns agent-specific configuration from setup
1004
1398
  *
1399
+ * @param name - Unique agent name (must be unique within the project)
1005
1400
  * @param config - Agent configuration object
1006
1401
  *
1007
- * @returns Agent instance that can be registered with the runtime
1402
+ * @returns AgentRunner with a run method for executing the agent
1008
1403
  *
1009
1404
  * @example
1010
1405
  * ```typescript
1011
- * const agent = createAgent({
1012
- * metadata: {
1013
- * name: 'Greeting Agent',
1014
- * description: 'Returns personalized greetings'
1015
- * },
1406
+ * const agent = createAgent('greeting-agent', {
1407
+ * description: 'Returns personalized greetings',
1016
1408
  * schema: {
1017
1409
  * input: z.object({ name: z.string(), age: z.number() }),
1018
1410
  * output: z.string()
@@ -1022,6 +1414,9 @@ export interface CreateAgentConfigExplicit<
1022
1414
  * return `Hello, ${name}! You are ${age} years old.`;
1023
1415
  * }
1024
1416
  * });
1417
+ *
1418
+ * // Call the agent directly
1419
+ * const result = await agent.run({ name: 'Alice', age: 30 });
1025
1420
  * ```
1026
1421
  */
1027
1422
  export function createAgent<
@@ -1034,14 +1429,9 @@ export function createAgent<
1034
1429
  | undefined = undefined,
1035
1430
  TConfig extends (app: AppState) => any = any,
1036
1431
  >(
1432
+ name: string,
1037
1433
  config: CreateAgentConfig<TSchema, TConfig>
1038
- ): Agent<
1039
- SchemaInput<TSchema>,
1040
- SchemaOutput<TSchema>,
1041
- SchemaStream<TSchema>,
1042
- TConfig extends (app: AppState) => infer R ? Awaited<R> : undefined,
1043
- AppState
1044
- >;
1434
+ ): AgentRunner<SchemaInput<TSchema>, SchemaOutput<TSchema>, SchemaStream<TSchema>>;
1045
1435
 
1046
1436
  /**
1047
1437
  * Creates an agent with explicit generic type parameters.
@@ -1054,9 +1444,10 @@ export function createAgent<
1054
1444
  * @template TConfig - Type returned by setup function
1055
1445
  * @template TAppState - Custom app state type from createApp
1056
1446
  *
1447
+ * @param name - Unique agent name (must be unique within the project)
1057
1448
  * @param config - Agent configuration object
1058
1449
  *
1059
- * @returns Agent instance with explicit types
1450
+ * @returns AgentRunner with explicit types and a run method
1060
1451
  *
1061
1452
  * @example
1062
1453
  * ```typescript
@@ -1066,11 +1457,8 @@ export function createAgent<
1066
1457
  * const agent = createAgent<
1067
1458
  * z.ZodObject<any>, // TInput
1068
1459
  * z.ZodString, // TOutput
1069
- * false, // TStream
1070
- * MyConfig, // TConfig
1071
- * MyAppState // TAppState
1072
- * >({
1073
- * metadata: { name: 'Custom Agent' },
1460
+ * false // TStream
1461
+ * >('custom-agent', {
1074
1462
  * setup: async (app) => ({ cache: new Map() }),
1075
1463
  * handler: async (ctx, input) => {
1076
1464
  * const db = ctx.app.db;
@@ -1087,8 +1475,9 @@ export function createAgent<
1087
1475
  TConfig = unknown,
1088
1476
  TAppState = AppState,
1089
1477
  >(
1478
+ name: string,
1090
1479
  config: CreateAgentConfigExplicit<TInput, TOutput, TStream, TConfig, TAppState>
1091
- ): Agent<TInput, TOutput, TStream, TConfig, TAppState>;
1480
+ ): AgentRunner<TInput, TOutput, TStream>;
1092
1481
 
1093
1482
  // Implementation
1094
1483
  export function createAgent<
@@ -1098,8 +1487,9 @@ export function createAgent<
1098
1487
  TConfig = unknown,
1099
1488
  TAppState = AppState,
1100
1489
  >(
1490
+ name: string,
1101
1491
  config: CreateAgentConfigExplicit<TInput, TOutput, TStream, TConfig, TAppState>
1102
- ): Agent<TInput, TOutput, TStream, TConfig, TAppState> {
1492
+ ): AgentRunner<TInput, TOutput, TStream> {
1103
1493
  const inputSchema = config.schema?.input;
1104
1494
  const outputSchema = config.schema?.output;
1105
1495
 
@@ -1107,7 +1497,7 @@ export function createAgent<
1107
1497
  // Evals should only be added via agent.createEval() after agent creation
1108
1498
  const evalsArray: Eval[] = [];
1109
1499
 
1110
- const handler = async (_ctx: Context, input?: any) => {
1500
+ const handler = async (input?: any) => {
1111
1501
  let validatedInput: any = undefined;
1112
1502
 
1113
1503
  if (inputSchema) {
@@ -1121,17 +1511,16 @@ export function createAgent<
1121
1511
  validatedInput = inputResult.value;
1122
1512
  }
1123
1513
 
1124
- const agentCtx = getAgentContext() as AgentContext<any, any, any, TConfig, TAppState>;
1514
+ const agentCtx = getAgentContext() as AgentContext<any, TConfig, TAppState>;
1125
1515
 
1126
- // Get the agent instance from the agents Map to fire events
1127
- // The agent will be registered in the agents Map before the handler is called
1128
- const agentName = agentCtx.agentName;
1129
- const registeredAgent = agentName ? agents.get(agentName) : undefined;
1516
+ // Store current agent for telemetry (using Symbol to keep it internal)
1517
+ (agentCtx as any)[CURRENT_AGENT] = agent;
1130
1518
 
1131
- // Fire 'started' event (only if agent is registered)
1132
- if (registeredAgent) {
1133
- await fireAgentEvent(registeredAgent, 'started', agentCtx);
1134
- }
1519
+ // Get the agent instance from the runtime state to fire events
1520
+ const runtime = getAgentRuntime(agentCtx);
1521
+
1522
+ // Fire 'started' event
1523
+ await fireAgentEvent(runtime, agent as Agent, 'started', agentCtx);
1135
1524
 
1136
1525
  try {
1137
1526
  const result = inputSchema
@@ -1139,7 +1528,8 @@ export function createAgent<
1139
1528
  : await (config.handler as any)(agentCtx);
1140
1529
 
1141
1530
  let validatedOutput: any = result;
1142
- if (outputSchema) {
1531
+ // Skip output validation for streaming agents (they return ReadableStream)
1532
+ if (outputSchema && !config.schema?.stream) {
1143
1533
  const outputResult = await outputSchema['~standard'].validate(result);
1144
1534
  if (outputResult.issues) {
1145
1535
  throw new ValidationError({
@@ -1155,50 +1545,44 @@ export function createAgent<
1155
1545
  agentCtx.state.set('_evalOutput', validatedOutput);
1156
1546
 
1157
1547
  // Fire 'completed' event - evals will run via event listener
1158
- if (registeredAgent) {
1159
- await fireAgentEvent(registeredAgent, 'completed', agentCtx);
1160
- }
1548
+ await fireAgentEvent(runtime, agent as Agent, 'completed', agentCtx);
1161
1549
 
1162
1550
  return validatedOutput;
1163
1551
  } catch (error) {
1164
1552
  // Fire 'errored' event
1165
- if (registeredAgent) {
1166
- await fireAgentEvent(registeredAgent, 'errored', agentCtx, error as Error);
1167
- }
1553
+ await fireAgentEvent(runtime, agent as Agent, 'errored', agentCtx, error as Error);
1168
1554
  throw error;
1169
1555
  }
1170
1556
  };
1171
1557
 
1172
1558
  // Infer input/output types from agent schema
1173
- type AgentInput = TInput extends StandardSchemaV1
1174
- ? StandardSchemaV1.InferOutput<TInput>
1175
- : undefined;
1176
- type AgentOutput = TOutput extends StandardSchemaV1
1177
- ? StandardSchemaV1.InferOutput<TOutput>
1178
- : undefined;
1559
+ type AgentInput = TInput extends StandardSchemaV1 ? InferOutput<TInput> : undefined;
1560
+ type AgentOutput = TOutput extends StandardSchemaV1 ? InferOutput<TOutput> : undefined;
1179
1561
 
1180
1562
  // Create createEval method that infers types from agent and automatically adds to agent
1181
- const createEval = (evalConfig: {
1182
- metadata?: Partial<EvalMetadata>;
1183
- handler: EvalFunction<AgentInput, AgentOutput>;
1184
- }): Eval<TInput, TOutput> => {
1185
- const evalName = evalConfig.metadata?.name || 'unnamed';
1563
+ const createEval = (
1564
+ evalName: string,
1565
+ evalConfig: {
1566
+ description?: string;
1567
+ handler: EvalFunction<AgentInput, AgentOutput>;
1568
+ }
1569
+ ): Eval<TInput, TOutput> => {
1186
1570
  // Trace log to verify evals file is imported
1187
1571
  internal.debug(
1188
- `createEval called for agent "${config?.metadata?.name || 'unknown'}": registering eval "${evalName}"`
1572
+ `createEval called for agent "${name || 'unknown'}": registering eval "${evalName}"`
1189
1573
  );
1190
1574
 
1191
- // Get filename (can be provided via __filename or set by bundler)
1192
- const filename = evalConfig.metadata?.filename || '';
1575
+ // Get filename (set by bundler)
1576
+ const filename = '';
1193
1577
 
1194
1578
  // Use name as identifier for consistency (same as agents)
1195
1579
  const identifier = evalName;
1196
1580
 
1197
1581
  // Use build-time injected id/version if available, otherwise generate at runtime
1198
1582
  // Build-time injection happens via bundler AST transformation
1199
- let evalId = evalConfig.metadata?.id;
1200
- let stableEvalId = evalConfig.metadata?.evalId;
1201
- let version = evalConfig.metadata?.version;
1583
+ let evalId: string | undefined;
1584
+ let stableEvalId: string | undefined;
1585
+ let version: string | undefined;
1202
1586
 
1203
1587
  // Generate version from available metadata if not provided (deterministic hash)
1204
1588
  // At build-time, version is hash of file contents; at runtime we use metadata
@@ -1239,8 +1623,8 @@ export function createAgent<
1239
1623
  evalId: stableEvalId,
1240
1624
  version,
1241
1625
  identifier,
1242
- name: evalConfig.metadata?.name || '',
1243
- description: evalConfig.metadata?.description || '',
1626
+ name: evalName,
1627
+ description: evalConfig.description || '',
1244
1628
  filename,
1245
1629
  },
1246
1630
  handler: evalConfig.handler,
@@ -1257,15 +1641,31 @@ export function createAgent<
1257
1641
  // Automatically add eval to agent's evals array
1258
1642
  evalsArray.push(evalType);
1259
1643
  internal.debug(
1260
- `Added eval "${evalName}" to agent "${config?.metadata?.name || 'unknown'}". Total evals: ${evalsArray.length}`
1644
+ `Added eval "${evalName}" to agent "${name || 'unknown'}". Total evals: ${evalsArray.length}`
1261
1645
  );
1262
1646
 
1263
1647
  return evalType as Eval<TInput, TOutput>;
1264
1648
  };
1265
1649
 
1650
+ // Build metadata - merge user-provided metadata with defaults
1651
+ // The build plugin injects metadata via config.metadata during AST transformation
1652
+ const metadata: Partial<AgentMetadata> = {
1653
+ // Defaults (used when running without build, e.g., dev mode)
1654
+ name,
1655
+ description: config.description,
1656
+ id: '',
1657
+ agentId: '',
1658
+ filename: '',
1659
+ version: '',
1660
+ inputSchemaCode: '',
1661
+ outputSchemaCode: '',
1662
+ // Merge in build-time injected metadata (overrides defaults)
1663
+ ...config.metadata,
1664
+ };
1665
+
1266
1666
  const agent: any = {
1267
1667
  handler,
1268
- metadata: config.metadata,
1668
+ metadata,
1269
1669
  evals: evalsArray,
1270
1670
  createEval,
1271
1671
  setup: config.setup,
@@ -1291,14 +1691,12 @@ export function createAgent<
1291
1691
 
1292
1692
  // Automatically add event listener for 'completed' event to run evals
1293
1693
  (agent as Agent).addEventListener('completed', async (_event, _agent, ctx) => {
1294
- // Get the agent instance from the agents Map to access its current evals array
1694
+ // Use the agent instance passed to event listener to access its evals array
1295
1695
  // This ensures we get evals that were added via agent.createEval() after agent creation
1296
- const agentName = ctx.agentName;
1297
- const registeredAgent = agentName ? agents.get(agentName) : undefined;
1298
- const agentEvals = registeredAgent?.evals || evalsArray;
1696
+ const agentEvals = _agent?.evals || evalsArray;
1299
1697
 
1300
1698
  internal.debug(
1301
- `Checking evals: agentName=${agentName}, evalsArray.length=${evalsArray?.length || 0}, agent.evals.length=${registeredAgent?.evals?.length || 0}`
1699
+ `Checking evals: agent=${_agent.metadata?.name}, evalsArray.length=${evalsArray?.length || 0}, agent.evals.length=${_agent?.evals?.length || 0}`
1302
1700
  );
1303
1701
 
1304
1702
  if (agentEvals && agentEvals.length > 0) {
@@ -1536,7 +1934,159 @@ export function createAgent<
1536
1934
  agent.stream = config.schema.stream;
1537
1935
  }
1538
1936
 
1539
- return agent as Agent<TInput, TOutput, TStream, TConfig, TAppState>;
1937
+ // Add validator method with overloads
1938
+ agent.validator = ((override?: any) => {
1939
+ const effectiveInputSchema = override?.input ?? inputSchema;
1940
+ const effectiveOutputSchema = override?.output ?? outputSchema;
1941
+
1942
+ // Helper to build the standard Hono input validator so types flow
1943
+ const buildInputValidator = (schema?: StandardSchemaV1) =>
1944
+ validator('json', async (value, c) => {
1945
+ if (schema) {
1946
+ const result = await validateSchema(schema, value);
1947
+ if (!result.success) {
1948
+ return c.json(
1949
+ {
1950
+ error: 'Validation failed',
1951
+ message: formatValidationIssues(result.issues),
1952
+ issues: result.issues,
1953
+ },
1954
+ 400
1955
+ );
1956
+ }
1957
+ return result.data;
1958
+ }
1959
+ return value;
1960
+ });
1961
+
1962
+ // If no output schema, preserve existing behavior: pure input validation
1963
+ if (!effectiveOutputSchema) {
1964
+ return buildInputValidator(effectiveInputSchema);
1965
+ }
1966
+
1967
+ // Output validation middleware (runs after handler)
1968
+ const outputValidator: MiddlewareHandler = async (c, next) => {
1969
+ await next();
1970
+
1971
+ const res = c.res;
1972
+ if (!res) return;
1973
+
1974
+ // Skip output validation for streaming agents
1975
+ if (config.schema?.stream) {
1976
+ return;
1977
+ }
1978
+
1979
+ // Only validate JSON responses
1980
+ const contentType = res.headers.get('Content-Type') ?? '';
1981
+ if (!contentType.toLowerCase().includes('application/json')) {
1982
+ return;
1983
+ }
1984
+
1985
+ // Clone so we don't consume the body that will be sent
1986
+ let responseBody: unknown;
1987
+ try {
1988
+ const cloned = res.clone();
1989
+ responseBody = await cloned.json();
1990
+ } catch {
1991
+ const OutputValidationError = StructuredError('OutputValidationError')<{
1992
+ issues: any[];
1993
+ }>();
1994
+ throw new OutputValidationError({
1995
+ message: 'Output validation failed: response is not valid JSON',
1996
+ issues: [],
1997
+ });
1998
+ }
1999
+
2000
+ const result = await validateSchema(effectiveOutputSchema, responseBody);
2001
+ if (!result.success) {
2002
+ const OutputValidationError = StructuredError('OutputValidationError')<{
2003
+ issues: any[];
2004
+ }>();
2005
+ throw new OutputValidationError({
2006
+ message: `Output validation failed: ${formatValidationIssues(result.issues)}`,
2007
+ issues: result.issues,
2008
+ });
2009
+ }
2010
+
2011
+ // Replace response with validated/sanitized JSON
2012
+ c.res = new Response(JSON.stringify(result.data), {
2013
+ status: res.status,
2014
+ headers: res.headers,
2015
+ });
2016
+ };
2017
+
2018
+ // If we have no input schema, we only do output validation
2019
+ if (!effectiveInputSchema) {
2020
+ return outputValidator as unknown as Handler;
2021
+ }
2022
+
2023
+ // Compose: input validator → output validator
2024
+ const inputMiddleware = buildInputValidator(effectiveInputSchema);
2025
+
2026
+ const composed: MiddlewareHandler = async (c, next) => {
2027
+ // Run the validator first; its next() runs the output validator,
2028
+ // whose next() runs the actual handler(s)
2029
+ const result = await inputMiddleware(c, async () => {
2030
+ await outputValidator(c, next);
2031
+ });
2032
+ // If inputMiddleware returned early (validation failed), return that response
2033
+ return result;
2034
+ };
2035
+
2036
+ return composed as unknown as Handler;
2037
+ }) as AgentValidator<TInput, TOutput>;
2038
+
2039
+ // Register the agent for runtime use
2040
+ // @ts-expect-error - metadata might be incomplete until build plugin injects InternalAgentMetadata
2041
+ agents.set(name, agent as Agent<TInput, TOutput, TStream, TConfig, TAppState>);
2042
+
2043
+ // Create and return AgentRunner
2044
+ const runner: any = {
2045
+ metadata: metadata as AgentMetadata,
2046
+ validator: agent.validator,
2047
+ inputSchema: inputSchema as TInput,
2048
+ outputSchema: outputSchema as TOutput,
2049
+ stream: (config.schema?.stream as TStream) || (false as TStream),
2050
+ createEval,
2051
+ addEventListener: agent.addEventListener,
2052
+ removeEventListener: agent.removeEventListener,
2053
+ run: inputSchema
2054
+ ? async (input: InferSchemaInput<Exclude<TInput, undefined>>) => {
2055
+ // Get tracer from AsyncLocalStorage context if available
2056
+ try {
2057
+ const agentCtx = getAgentContext();
2058
+ if (agentCtx?.tracer) {
2059
+ return runWithSpan<any, TInput, TOutput, TStream>(
2060
+ agentCtx.tracer,
2061
+ agent as Agent<TInput, TOutput, TStream>,
2062
+ async () => await agent.handler(input)
2063
+ );
2064
+ }
2065
+ } catch {
2066
+ // Context not available, skip span creation
2067
+ }
2068
+ return await agent.handler(input);
2069
+ }
2070
+ : async () => {
2071
+ // Get tracer from AsyncLocalStorage context if available
2072
+ try {
2073
+ const agentCtx = getAgentContext();
2074
+ if (agentCtx?.tracer) {
2075
+ return runWithSpan<any, TInput, TOutput, TStream>(
2076
+ agentCtx.tracer,
2077
+ agent as Agent<TInput, TOutput, TStream>,
2078
+ async () => await agent.handler()
2079
+ );
2080
+ }
2081
+ } catch {
2082
+ // Context not available, skip span creation
2083
+ }
2084
+ return await agent.handler();
2085
+ },
2086
+ [INTERNAL_AGENT]: agent, // Store reference to internal agent for testing
2087
+ };
2088
+
2089
+ return runner as AgentRunner<TInput, TOutput, TStream>;
1540
2090
  }
1541
2091
 
1542
2092
  const runWithSpan = async <
@@ -1581,7 +2131,7 @@ const createAgentRunner = <
1581
2131
  return runWithSpan<any, TInput, TOutput, TStream>(
1582
2132
  tracer,
1583
2133
  agent,
1584
- async () => await agent.handler(ctx as unknown as AgentContext<any, any, any>, input)
2134
+ async () => await agent.handler(input)
1585
2135
  );
1586
2136
  },
1587
2137
  } as AgentRunner<TInput, TOutput, TStream>;
@@ -1592,7 +2142,7 @@ const createAgentRunner = <
1592
2142
  return runWithSpan<any, TInput, TOutput, TStream>(
1593
2143
  tracer,
1594
2144
  agent,
1595
- async () => await agent.handler(ctx as unknown as AgentContext<any, any, any>)
2145
+ async () => await agent.handler()
1596
2146
  );
1597
2147
  },
1598
2148
  } as AgentRunner<TInput, TOutput, TStream>;
@@ -1601,41 +2151,36 @@ const createAgentRunner = <
1601
2151
 
1602
2152
  /**
1603
2153
  * Populate the agents object with all registered agents
2154
+ * Keys are converted to camelCase to match the generated TypeScript types
1604
2155
  */
1605
2156
  export const populateAgentsRegistry = (ctx: Context): any => {
1606
2157
  const agentsObj: any = {};
2158
+ // Track ownership of camelCase keys to detect collisions between different raw names
2159
+ const ownershipMap = new Map<string, string>();
1607
2160
 
1608
- // Build nested structure for agents and subagents
2161
+ // Build flat registry of agents
1609
2162
  for (const [name, agentFn] of agents) {
1610
2163
  const runner = createAgentRunner(agentFn, ctx);
2164
+ const key = toCamelCase(name);
1611
2165
 
1612
- if (name.includes('.')) {
1613
- // Subagent: "parent.child"
1614
- const parts = name.split('.');
1615
- if (parts.length !== 2) {
1616
- internal.warn(`Invalid subagent name format: "${name}". Expected "parent.child".`);
1617
- continue;
1618
- }
1619
- const parentName = parts[0];
1620
- const childName = parts[1];
1621
- if (parentName && childName) {
1622
- if (!agentsObj[parentName]) {
1623
- // Ensure parent exists
1624
- const parentAgent = agents.get(parentName);
1625
- if (parentAgent) {
1626
- agentsObj[parentName] = createAgentRunner(parentAgent, ctx);
1627
- }
1628
- }
1629
- // Attach subagent to parent using camelCase property name
1630
- const camelChildName = toCamelCase(childName);
1631
- if (agentsObj[parentName]) {
1632
- agentsObj[parentName][camelChildName] = runner;
1633
- }
1634
- }
1635
- } else {
1636
- // Parent agent or standalone agent
1637
- agentsObj[name] = runner;
2166
+ // Validate key is non-empty
2167
+ if (!key) {
2168
+ internal.warn(`Agent name "${name}" converts to empty camelCase key. Skipping.`);
2169
+ continue;
2170
+ }
2171
+
2172
+ // Detect collision on key - check ownership
2173
+ const existingOwner = ownershipMap.get(key);
2174
+ if (existingOwner && existingOwner !== name) {
2175
+ internal.error(
2176
+ `Agent registry collision: "${name}" conflicts with "${existingOwner}" (both map to camelCase key "${key}")`
2177
+ );
2178
+ throw new Error(`Agent registry collision detected for key "${key}"`);
1638
2179
  }
2180
+
2181
+ agentsObj[key] = runner;
2182
+ // Record ownership
2183
+ ownershipMap.set(key, name);
1639
2184
  }
1640
2185
 
1641
2186
  return agentsObj;
@@ -1646,32 +2191,16 @@ export const createAgentMiddleware = (agentName: AgentName | ''): MiddlewareHand
1646
2191
  // Populate agents object with strongly-typed keys
1647
2192
  const agentsObj = populateAgentsRegistry(ctx);
1648
2193
 
1649
- // Set agent registry on context for access via c.var.agent
1650
- ctx.set('agent', agentsObj);
1651
-
1652
- // Determine current and parent agents
1653
- let currentAgent: AgentRunner | undefined;
1654
- let parentAgent: AgentRunner | undefined;
1655
-
1656
- if (agentName?.includes('.')) {
1657
- // This is a subagent
1658
- const parts = agentName.split('.');
1659
- const parentName = parts[0];
1660
- const childName = parts[1];
1661
- if (parentName && childName) {
1662
- currentAgent = agentsObj[parentName]?.[childName];
1663
- parentAgent = agentsObj[parentName];
2194
+ // Track agent ID for session telemetry
2195
+ if (agentName) {
2196
+ const agentKey = toCamelCase(agentName);
2197
+ const agent = agentsObj[agentKey];
2198
+ const _ctx = privateContext(ctx);
2199
+ if (agent?.metadata?.id) {
2200
+ // we add both so that you can query by either
2201
+ _ctx.var.agentIds.add(agent.metadata.id);
2202
+ _ctx.var.agentIds.add(agent.metadata.agentId);
1664
2203
  }
1665
- } else if (agentName) {
1666
- // This is a parent or standalone agent
1667
- currentAgent = agentsObj[agentName];
1668
- }
1669
-
1670
- const _ctx = privateContext(ctx);
1671
- if (currentAgent?.metadata?.id) {
1672
- // we add both so that you can query by either
1673
- _ctx.var.agentIds.add(currentAgent.metadata.id);
1674
- _ctx.var.agentIds.add(currentAgent.metadata.agentId);
1675
2204
  }
1676
2205
 
1677
2206
  const sessionId = ctx.var.sessionId;
@@ -1680,18 +2209,9 @@ export const createAgentMiddleware = (agentName: AgentName | ''): MiddlewareHand
1680
2209
  const config = agentName ? getAgentConfig(agentName as AgentName) : undefined;
1681
2210
  const app = ctx.var.app;
1682
2211
 
1683
- const args: RequestAgentContextArgs<
1684
- AgentRegistry,
1685
- AgentRunner | undefined,
1686
- AgentRunner | undefined,
1687
- unknown,
1688
- unknown
1689
- > = {
2212
+ const args: RequestAgentContextArgs<AgentRegistry, unknown, unknown> = {
1690
2213
  agent: agentsObj,
1691
- current: currentAgent,
1692
- parent: parentAgent,
1693
- agentName: agentName as AgentName,
1694
- logger: ctx.var.logger.child({ agent: agentName }),
2214
+ logger: ctx.var.logger,
1695
2215
  tracer: ctx.var.tracer,
1696
2216
  sessionId,
1697
2217
  session,
@@ -1699,9 +2219,10 @@ export const createAgentMiddleware = (agentName: AgentName | ''): MiddlewareHand
1699
2219
  handler: ctx.var.waitUntilHandler,
1700
2220
  config: config || {},
1701
2221
  app: app || {},
2222
+ runtime: getGlobalRuntimeState(),
1702
2223
  };
1703
2224
 
1704
- return runInAgentContext(ctx as unknown as Record<string, unknown>, args, next);
2225
+ return setupRequestAgentContext(ctx as unknown as Record<string, unknown>, args, next);
1705
2226
  };
1706
2227
  };
1707
2228
 
@@ -1718,10 +2239,89 @@ export const runAgentSetups = async (appState: AppState): Promise<void> => {
1718
2239
  };
1719
2240
 
1720
2241
  export const runAgentShutdowns = async (appState: AppState): Promise<void> => {
1721
- for (const [name, agent] of agents.entries()) {
2242
+ const runtime = getGlobalRuntimeState();
2243
+ for (const [name, agent] of runtime.agents.entries()) {
1722
2244
  if (agent.shutdown) {
1723
- const config = getAgentConfig(name as AgentName);
2245
+ const config = runtime.agentConfigs.get(name) as any;
1724
2246
  await agent.shutdown(appState, config);
1725
2247
  }
1726
2248
  }
1727
2249
  };
2250
+
2251
+ /**
2252
+ * Run an agent within a specific AgentContext.
2253
+ * Sets up AsyncLocalStorage with the provided context and executes the agent.
2254
+ *
2255
+ * This is the recommended way to test agents in unit tests. It automatically:
2256
+ * - Registers the agent in the runtime state so event listeners fire
2257
+ * - Sets up AsyncLocalStorage so getAgentContext() works inside the agent
2258
+ * - Handles both agents with input and agents without input
2259
+ *
2260
+ * **Use cases:**
2261
+ * - Unit testing agents with TestAgentContext
2262
+ * - Running agents outside HTTP request flow
2263
+ * - Custom agent execution environments
2264
+ * - Testing event listeners and evaluations
2265
+ *
2266
+ * @template TInput - Type of the input parameter
2267
+ * @template TOutput - Type of the return value
2268
+ *
2269
+ * @param ctx - The AgentContext to use (typically TestAgentContext in tests)
2270
+ * @param agent - The AgentRunner to execute (returned from createAgent)
2271
+ * @param input - Input data (required if agent has input schema, omit otherwise)
2272
+ *
2273
+ * @returns Promise resolving to the agent's output
2274
+ *
2275
+ * @example
2276
+ * ```typescript
2277
+ * import { runInAgentContext, TestAgentContext } from '@agentuity/runtime/test';
2278
+ *
2279
+ * test('greeting agent', async () => {
2280
+ * const ctx = new TestAgentContext();
2281
+ * const result = await runInAgentContext(ctx, greetingAgent, {
2282
+ * name: 'Alice',
2283
+ * age: 30
2284
+ * });
2285
+ * expect(result).toBe('Hello, Alice! You are 30 years old.');
2286
+ * });
2287
+ *
2288
+ * test('no-input agent', async () => {
2289
+ * const ctx = new TestAgentContext();
2290
+ * const result = await runInAgentContext(ctx, statusAgent);
2291
+ * expect(result).toEqual({ status: 'ok' });
2292
+ * });
2293
+ * ```
2294
+ */
2295
+ export async function runInAgentContext<TInput, TOutput>(
2296
+ ctx: AgentContext<any, any, any>,
2297
+ agent: AgentRunner<any, any, any>,
2298
+ input?: TInput
2299
+ ): Promise<TOutput> {
2300
+ const { getAgentAsyncLocalStorage } = await import('./_context');
2301
+ const storage = getAgentAsyncLocalStorage();
2302
+
2303
+ // Register agent in runtime state so events fire (lookup by metadata.name)
2304
+ const agentName = agent.metadata.name;
2305
+ const runtime = getAgentRuntime(ctx);
2306
+
2307
+ // Get internal agent from runner (stored via symbol) or global registry
2308
+ const internalAgent = (agent as any)[INTERNAL_AGENT] || agents.get(agentName);
2309
+
2310
+ if (internalAgent && agentName) {
2311
+ runtime.agents.set(agentName, internalAgent);
2312
+
2313
+ // Copy event listeners from global to context runtime
2314
+ const globalListeners = agentEventListeners.get(internalAgent);
2315
+ if (globalListeners) {
2316
+ runtime.agentEventListeners.set(internalAgent, globalListeners);
2317
+ }
2318
+ }
2319
+
2320
+ return storage.run(ctx, async () => {
2321
+ if (input !== undefined) {
2322
+ return await (agent.run as any)(input);
2323
+ } else {
2324
+ return await (agent.run as any)();
2325
+ }
2326
+ });
2327
+ }