@agentuity/runtime 0.0.42 → 0.0.44

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 (134) hide show
  1. package/AGENTS.md +11 -9
  2. package/README.md +4 -4
  3. package/dist/_context.d.ts +12 -4
  4. package/dist/_context.d.ts.map +1 -1
  5. package/dist/_server.d.ts +7 -4
  6. package/dist/_server.d.ts.map +1 -1
  7. package/dist/_services.d.ts +13 -2
  8. package/dist/_services.d.ts.map +1 -1
  9. package/dist/_util.d.ts +1 -1
  10. package/dist/_util.d.ts.map +1 -1
  11. package/dist/_waituntil.d.ts +1 -3
  12. package/dist/_waituntil.d.ts.map +1 -1
  13. package/dist/agent.d.ts +41 -14
  14. package/dist/agent.d.ts.map +1 -1
  15. package/dist/app.d.ts +90 -8
  16. package/dist/app.d.ts.map +1 -1
  17. package/dist/eval.d.ts +79 -0
  18. package/dist/eval.d.ts.map +1 -0
  19. package/dist/index.d.ts +6 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/io/email.d.ts +77 -0
  22. package/dist/io/email.d.ts.map +1 -0
  23. package/dist/logger/console.d.ts +21 -1
  24. package/dist/logger/console.d.ts.map +1 -1
  25. package/dist/logger/index.d.ts +0 -1
  26. package/dist/logger/index.d.ts.map +1 -1
  27. package/dist/logger/user.d.ts +2 -2
  28. package/dist/logger/user.d.ts.map +1 -1
  29. package/dist/otel/config.d.ts +3 -1
  30. package/dist/otel/config.d.ts.map +1 -1
  31. package/dist/otel/console.d.ts +2 -1
  32. package/dist/otel/console.d.ts.map +1 -1
  33. package/dist/otel/exporters/index.d.ts +4 -0
  34. package/dist/otel/exporters/index.d.ts.map +1 -0
  35. package/dist/otel/exporters/jsonl-log-exporter.d.ts +36 -0
  36. package/dist/otel/exporters/jsonl-log-exporter.d.ts.map +1 -0
  37. package/dist/otel/exporters/jsonl-metric-exporter.d.ts +40 -0
  38. package/dist/otel/exporters/jsonl-metric-exporter.d.ts.map +1 -0
  39. package/dist/otel/exporters/jsonl-trace-exporter.d.ts +36 -0
  40. package/dist/otel/exporters/jsonl-trace-exporter.d.ts.map +1 -0
  41. package/dist/otel/http.d.ts.map +1 -1
  42. package/dist/otel/logger.d.ts +15 -11
  43. package/dist/otel/logger.d.ts.map +1 -1
  44. package/dist/otel/otel.d.ts +8 -2
  45. package/dist/otel/otel.d.ts.map +1 -1
  46. package/dist/router.d.ts +4 -1
  47. package/dist/router.d.ts.map +1 -1
  48. package/dist/services/evalrun/composite.d.ts +21 -0
  49. package/dist/services/evalrun/composite.d.ts.map +1 -0
  50. package/dist/services/evalrun/http.d.ts +24 -0
  51. package/dist/services/evalrun/http.d.ts.map +1 -0
  52. package/dist/services/evalrun/index.d.ts +5 -0
  53. package/dist/services/evalrun/index.d.ts.map +1 -0
  54. package/dist/services/evalrun/json.d.ts +21 -0
  55. package/dist/services/evalrun/json.d.ts.map +1 -0
  56. package/dist/services/evalrun/local.d.ts +19 -0
  57. package/dist/services/evalrun/local.d.ts.map +1 -0
  58. package/dist/services/local/_db.d.ts +4 -0
  59. package/dist/services/local/_db.d.ts.map +1 -0
  60. package/dist/services/local/_router.d.ts +3 -0
  61. package/dist/services/local/_router.d.ts.map +1 -0
  62. package/dist/services/local/_util.d.ts +18 -0
  63. package/dist/services/local/_util.d.ts.map +1 -0
  64. package/dist/services/local/index.d.ts +8 -0
  65. package/dist/services/local/index.d.ts.map +1 -0
  66. package/dist/services/local/keyvalue.d.ts +10 -0
  67. package/dist/services/local/keyvalue.d.ts.map +1 -0
  68. package/dist/services/local/objectstore.d.ts +11 -0
  69. package/dist/services/local/objectstore.d.ts.map +1 -0
  70. package/dist/services/local/stream.d.ts +10 -0
  71. package/dist/services/local/stream.d.ts.map +1 -0
  72. package/dist/services/local/vector.d.ts +13 -0
  73. package/dist/services/local/vector.d.ts.map +1 -0
  74. package/dist/services/session/composite.d.ts +21 -0
  75. package/dist/services/session/composite.d.ts.map +1 -0
  76. package/dist/services/session/http.d.ts +23 -0
  77. package/dist/services/session/http.d.ts.map +1 -0
  78. package/dist/services/session/index.d.ts +5 -0
  79. package/dist/services/session/index.d.ts.map +1 -0
  80. package/dist/services/session/json.d.ts +22 -0
  81. package/dist/services/session/json.d.ts.map +1 -0
  82. package/dist/services/session/local.d.ts +19 -0
  83. package/dist/services/session/local.d.ts.map +1 -0
  84. package/dist/session.d.ts +70 -0
  85. package/dist/session.d.ts.map +1 -0
  86. package/package.json +10 -6
  87. package/src/_config.ts +1 -1
  88. package/src/_context.ts +19 -16
  89. package/src/_server.ts +284 -42
  90. package/src/_services.ts +147 -34
  91. package/src/_util.ts +2 -3
  92. package/src/_waituntil.ts +5 -153
  93. package/src/agent.ts +667 -65
  94. package/src/app.ts +159 -13
  95. package/src/eval.ts +95 -0
  96. package/src/index.ts +6 -1
  97. package/src/io/email.ts +173 -0
  98. package/src/logger/console.ts +222 -15
  99. package/src/logger/index.ts +0 -1
  100. package/src/logger/user.ts +8 -4
  101. package/src/otel/config.ts +7 -44
  102. package/src/otel/console.ts +9 -4
  103. package/src/otel/exporters/README.md +217 -0
  104. package/src/otel/exporters/index.ts +3 -0
  105. package/src/otel/exporters/jsonl-log-exporter.ts +113 -0
  106. package/src/otel/exporters/jsonl-metric-exporter.ts +120 -0
  107. package/src/otel/exporters/jsonl-trace-exporter.ts +121 -0
  108. package/src/otel/http.ts +3 -1
  109. package/src/otel/logger.ts +106 -41
  110. package/src/otel/otel.ts +43 -22
  111. package/src/router.ts +44 -4
  112. package/src/services/evalrun/composite.ts +34 -0
  113. package/src/services/evalrun/http.ts +112 -0
  114. package/src/services/evalrun/index.ts +4 -0
  115. package/src/services/evalrun/json.ts +46 -0
  116. package/src/services/evalrun/local.ts +28 -0
  117. package/src/services/local/README.md +1576 -0
  118. package/src/services/local/_db.ts +182 -0
  119. package/src/services/local/_router.ts +86 -0
  120. package/src/services/local/_util.ts +49 -0
  121. package/src/services/local/index.ts +7 -0
  122. package/src/services/local/keyvalue.ts +118 -0
  123. package/src/services/local/objectstore.ts +152 -0
  124. package/src/services/local/stream.ts +296 -0
  125. package/src/services/local/vector.ts +264 -0
  126. package/src/services/session/composite.ts +33 -0
  127. package/src/services/session/http.ts +64 -0
  128. package/src/services/session/index.ts +4 -0
  129. package/src/services/session/json.ts +42 -0
  130. package/src/services/session/local.ts +28 -0
  131. package/src/session.ts +284 -0
  132. package/dist/_unauthenticated.d.ts +0 -26
  133. package/dist/_unauthenticated.d.ts.map +0 -1
  134. package/src/_unauthenticated.ts +0 -126
package/src/agent.ts CHANGED
@@ -1,16 +1,44 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  import type {
3
- StandardSchemaV1,
4
3
  KeyValueStorage,
5
4
  ObjectStorage,
5
+ StandardSchemaV1,
6
6
  StreamStorage,
7
7
  VectorStorage,
8
8
  } from '@agentuity/core';
9
- import { trace, type Tracer } from '@opentelemetry/api';
10
-
9
+ import type { Tracer } from '@opentelemetry/api';
10
+ import { SpanStatusCode, context, trace } from '@opentelemetry/api';
11
11
  import type { Context, MiddlewareHandler } from 'hono';
12
12
  import { getAgentContext, runInAgentContext, type RequestAgentContextArgs } from './_context';
13
13
  import type { Logger } from './logger';
14
+ import type {
15
+ Eval,
16
+ EvalContext,
17
+ EvalRunResult,
18
+ EvalMetadata,
19
+ EvalFunction,
20
+ ExternalEvalMetadata,
21
+ } from './eval';
22
+ import { internal } from './logger/internal';
23
+ import { getApp } from './app';
24
+ import type { Thread, Session } from './session';
25
+ import { privateContext } from './_server';
26
+ import { generateId } from './session';
27
+ import { getEvalRunEventProvider } from './_services';
28
+ import * as runtimeConfig from './_config';
29
+ import type { EvalRunStartEvent } from '@agentuity/core';
30
+
31
+ export type AgentEventName = 'started' | 'completed' | 'errored';
32
+
33
+ export type AgentEventCallback<TAgent extends Agent<any, any, any>> =
34
+ | ((eventName: 'started', agent: TAgent, context: AgentContext) => Promise<void> | void)
35
+ | ((eventName: 'completed', agent: TAgent, context: AgentContext) => Promise<void> | void)
36
+ | ((
37
+ eventName: 'errored',
38
+ agent: TAgent,
39
+ context: AgentContext,
40
+ data: Error
41
+ ) => Promise<void> | void);
14
42
 
15
43
  export interface AgentContext {
16
44
  // email: () => Promise<Email | null>;
@@ -19,6 +47,7 @@ export interface AgentContext {
19
47
  waitUntil: (promise: Promise<void> | (() => void | Promise<void>)) => void;
20
48
  agent?: any; // Will be augmented by generated code
21
49
  current?: any; // Will be augmented by generated code
50
+ parent?: any; // Will be augmented by generated code - reference to parent agent for subagents
22
51
  agentName?: AgentName;
23
52
  logger: Logger;
24
53
  sessionId: string;
@@ -27,9 +56,12 @@ export interface AgentContext {
27
56
  objectstore: ObjectStorage;
28
57
  stream: StreamStorage;
29
58
  vector: VectorStorage;
59
+ state: Map<string, unknown>;
60
+ thread: Thread;
61
+ session: Session;
30
62
  }
31
63
 
32
- interface AgentMetadata {
64
+ type InternalAgentMetadata = {
33
65
  /**
34
66
  * the unique identifier for this agent and project
35
67
  */
@@ -38,14 +70,6 @@ interface AgentMetadata {
38
70
  * the folder name for the agent
39
71
  */
40
72
  identifier: string;
41
- /**
42
- * the human readable name for the agent (identifier is used if not specified)
43
- */
44
- name: string;
45
- /**
46
- * the human readable description for the agent (empty if not provided)
47
- */
48
- description: string;
49
73
  /**
50
74
  * the relative path to the agent from the root project directory
51
75
  */
@@ -54,7 +78,32 @@ interface AgentMetadata {
54
78
  * a unique version for the agent. computed as the SHA256 contents of the file.
55
79
  */
56
80
  version: string;
57
- }
81
+ };
82
+
83
+ type ExternalAgentMetadata = {
84
+ /**
85
+ * the human readable name for the agent
86
+ */
87
+ name: string;
88
+ /**
89
+ * the human readable description for the agent (empty if not provided)
90
+ */
91
+ description: string;
92
+ };
93
+
94
+ type AgentMetadata = InternalAgentMetadata & ExternalAgentMetadata;
95
+
96
+ // Type for createEval method
97
+ type CreateEvalMethod<
98
+ TInput extends StandardSchemaV1 | undefined = any,
99
+ TOutput extends StandardSchemaV1 | undefined = any,
100
+ > = (config: {
101
+ metadata?: Partial<ExternalEvalMetadata>;
102
+ handler: EvalFunction<
103
+ TInput extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<TInput> : undefined,
104
+ TOutput extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<TOutput> : undefined
105
+ >;
106
+ }) => Eval<TInput, TOutput>;
58
107
 
59
108
  /**
60
109
  * The Agent handler interface.
@@ -66,6 +115,58 @@ export type Agent<
66
115
  > = {
67
116
  metadata: AgentMetadata;
68
117
  handler: (ctx: AgentContext, ...args: any[]) => any | Promise<any>;
118
+ evals?: Eval[];
119
+ createEval: CreateEvalMethod<TInput, TOutput>;
120
+ addEventListener(
121
+ eventName: 'started',
122
+ callback: (
123
+ eventName: 'started',
124
+ agent: Agent<TInput, TOutput, TStream>,
125
+ context: AgentContext
126
+ ) => Promise<void> | void
127
+ ): void;
128
+ addEventListener(
129
+ eventName: 'completed',
130
+ callback: (
131
+ eventName: 'completed',
132
+ agent: Agent<TInput, TOutput, TStream>,
133
+ context: AgentContext
134
+ ) => Promise<void> | void
135
+ ): void;
136
+ addEventListener(
137
+ eventName: 'errored',
138
+ callback: (
139
+ eventName: 'errored',
140
+ agent: Agent<TInput, TOutput, TStream>,
141
+ context: AgentContext,
142
+ data: Error
143
+ ) => Promise<void> | void
144
+ ): void;
145
+ removeEventListener(
146
+ eventName: 'started',
147
+ callback: (
148
+ eventName: 'started',
149
+ agent: Agent<TInput, TOutput, TStream>,
150
+ context: AgentContext
151
+ ) => Promise<void> | void
152
+ ): void;
153
+ removeEventListener(
154
+ eventName: 'completed',
155
+ callback: (
156
+ eventName: 'completed',
157
+ agent: Agent<TInput, TOutput, TStream>,
158
+ context: AgentContext
159
+ ) => Promise<void> | void
160
+ ): void;
161
+ removeEventListener(
162
+ eventName: 'errored',
163
+ callback: (
164
+ eventName: 'errored',
165
+ agent: Agent<TInput, TOutput, TStream>,
166
+ context: AgentContext,
167
+ data: Error
168
+ ) => Promise<void> | void
169
+ ): void;
69
170
  } & (TInput extends StandardSchemaV1 ? { inputSchema: TInput } : { inputSchema?: never }) &
70
171
  (TOutput extends StandardSchemaV1 ? { outputSchema: TOutput } : { outputSchema?: never }) &
71
172
  (TStream extends true ? { stream: true } : { stream?: false });
@@ -96,6 +197,58 @@ export interface AgentRunner<
96
197
  // Will be populated at runtime with strongly typed agents
97
198
  const agents = new Map<string, Agent>();
98
199
 
200
+ // WeakMap to store event listeners for each agent instance (truly private)
201
+ const agentEventListeners = new WeakMap<
202
+ Agent<any, any, any>,
203
+ Map<AgentEventName, Set<AgentEventCallback<any>>>
204
+ >();
205
+
206
+ // Helper to fire event listeners sequentially, abort on first error
207
+ async function fireAgentEvent(
208
+ agent: Agent<any, any, any>,
209
+ eventName: 'started' | 'completed',
210
+ context: AgentContext
211
+ ): Promise<void>;
212
+ async function fireAgentEvent(
213
+ agent: Agent<any, any, any>,
214
+ eventName: 'errored',
215
+ context: AgentContext,
216
+ data: Error
217
+ ): Promise<void>;
218
+ async function fireAgentEvent(
219
+ agent: Agent<any, any, any>,
220
+ eventName: AgentEventName,
221
+ context: AgentContext,
222
+ data?: Error
223
+ ): Promise<void> {
224
+ // Fire agent-level listeners
225
+ const listeners = agentEventListeners.get(agent);
226
+ if (listeners) {
227
+ const callbacks = listeners.get(eventName);
228
+ if (callbacks && callbacks.size > 0) {
229
+ for (const callback of callbacks) {
230
+ if (eventName === 'errored' && data) {
231
+ await (callback as any)(eventName, agent, context, data);
232
+ } else if (eventName === 'started' || eventName === 'completed') {
233
+ await (callback as any)(eventName, agent, context);
234
+ }
235
+ }
236
+ }
237
+ }
238
+
239
+ // Fire app-level listeners
240
+ const app = getApp();
241
+ if (app) {
242
+ if (eventName === 'errored' && data) {
243
+ await app.fireEvent('agent.errored', agent, context, data);
244
+ } else if (eventName === 'started') {
245
+ await app.fireEvent('agent.started', agent, context);
246
+ } else if (eventName === 'completed') {
247
+ await app.fireEvent('agent.completed', agent, context);
248
+ }
249
+ }
250
+ }
251
+
99
252
  /**
100
253
  * Union type of all registered agent names.
101
254
  * Falls back to `string` when no agents are registered (before augmentation).
@@ -118,7 +271,7 @@ export function createAgent<
118
271
  output?: TOutput;
119
272
  stream?: TStream;
120
273
  };
121
- metadata?: Partial<Omit<AgentMetadata, 'id'>>;
274
+ metadata?: Partial<ExternalAgentMetadata>;
122
275
  handler: TInput extends StandardSchemaV1
123
276
  ? TStream extends true
124
277
  ? TOutput extends StandardSchemaV1
@@ -159,6 +312,10 @@ export function createAgent<
159
312
  const inputSchema = config.schema?.input;
160
313
  const outputSchema = config.schema?.output;
161
314
 
315
+ // Initialize evals array before handler so it can be captured in closure
316
+ // Evals should only be added via agent.createEval() after agent creation
317
+ const evalsArray: Eval[] = [];
318
+
162
319
  const handler = async (_ctx: Context, input?: any) => {
163
320
  let validatedInput: any = undefined;
164
321
 
@@ -174,25 +331,390 @@ export function createAgent<
174
331
 
175
332
  const agentCtx = getAgentContext();
176
333
 
177
- const result = inputSchema
178
- ? await (config.handler as any)(agentCtx, validatedInput)
179
- : await (config.handler as any)(agentCtx);
334
+ // Get the agent instance from the agents Map to fire events
335
+ // The agent will be registered in the agents Map before the handler is called
336
+ const agentName = agentCtx.agentName;
337
+ const registeredAgent = agentName ? agents.get(agentName) : undefined;
338
+
339
+ // Fire 'started' event (only if agent is registered)
340
+ if (registeredAgent) {
341
+ await fireAgentEvent(registeredAgent, 'started', agentCtx);
342
+ }
343
+
344
+ try {
345
+ const result = inputSchema
346
+ ? await (config.handler as any)(agentCtx, validatedInput)
347
+ : await (config.handler as any)(agentCtx);
348
+
349
+ let validatedOutput: any = result;
350
+ if (outputSchema) {
351
+ const outputResult = await outputSchema['~standard'].validate(result);
352
+ if (outputResult.issues) {
353
+ throw new Error(
354
+ `Output validation failed: ${outputResult.issues.map((i: any) => i.message).join(', ')}`
355
+ );
356
+ }
357
+ validatedOutput = outputResult.value;
358
+ }
359
+
360
+ // Store validated input/output in context state for event listeners
361
+ agentCtx.state.set('_evalInput', validatedInput);
362
+ agentCtx.state.set('_evalOutput', validatedOutput);
363
+
364
+ // Fire 'completed' event - evals will run via event listener
365
+ if (registeredAgent) {
366
+ await fireAgentEvent(registeredAgent, 'completed', agentCtx);
367
+ }
368
+
369
+ return validatedOutput;
370
+ } catch (error) {
371
+ // Fire 'errored' event
372
+ if (registeredAgent) {
373
+ await fireAgentEvent(registeredAgent, 'errored', agentCtx, error as Error);
374
+ }
375
+ throw error;
376
+ }
377
+ };
378
+
379
+ // Infer input/output types from agent schema
380
+ type AgentInput = TInput extends StandardSchemaV1
381
+ ? StandardSchemaV1.InferOutput<TInput>
382
+ : undefined;
383
+ type AgentOutput = TOutput extends StandardSchemaV1
384
+ ? StandardSchemaV1.InferOutput<TOutput>
385
+ : undefined;
386
+
387
+ // Create createEval method that infers types from agent and automatically adds to agent
388
+ const createEval = (evalConfig: {
389
+ metadata?: Partial<Omit<EvalMetadata, 'id' | 'version'>>;
390
+ handler: EvalFunction<AgentInput, AgentOutput>;
391
+ }): Eval<TInput, TOutput> => {
392
+ const evalName = evalConfig.metadata?.name || 'unnamed';
393
+ // Trace log to verify evals file is imported
394
+ internal.debug(
395
+ `createEval called for agent "${config?.metadata?.name || 'unknown'}": registering eval "${evalName}"`
396
+ );
397
+
398
+ // Get filename (can be provided via __filename or set by bundler)
399
+ const filename = evalConfig.metadata?.filename || '';
400
+
401
+ // Derive identifier from filename if not provided
402
+ let identifier = evalConfig.metadata?.identifier || '';
403
+ if (!identifier && filename) {
404
+ const pathParts = filename.split(/[/\\]/);
405
+ const basename = pathParts[pathParts.length - 1] || '';
406
+ identifier = basename.replace(/\.(ts|tsx|js|jsx)$/, '') || '';
407
+ }
408
+
409
+ // Use name as identifier fallback
410
+ if (!identifier) {
411
+ identifier = evalName;
412
+ }
413
+
414
+ // Generate eval ID and version at runtime (similar to build-time generation)
415
+ const projectId = runtimeConfig.getProjectId() || '';
416
+ const deploymentId = runtimeConfig.getDeploymentId() || '';
417
+ // Generate version from available metadata (deterministic hash)
418
+ // At build-time, version is hash of file contents; at runtime we use metadata
419
+ const versionHasher = new Bun.CryptoHasher('sha1');
420
+ versionHasher.update(identifier);
421
+ versionHasher.update(evalName);
422
+ versionHasher.update(filename);
423
+ const version = versionHasher.digest('hex');
424
+ // Generate eval ID using same logic as build-time (getEvalId)
425
+ // Format: eval_${hashSHA1(projectId, deploymentId, filename, name, version)}
426
+ const idHasher = new Bun.CryptoHasher('sha1');
427
+ idHasher.update(projectId);
428
+ idHasher.update(deploymentId);
429
+ idHasher.update(filename);
430
+ idHasher.update(evalName);
431
+ idHasher.update(version);
432
+ const evalId = `eval_${idHasher.digest('hex')}`;
433
+
434
+ const evalType: any = {
435
+ metadata: {
436
+ id: evalId,
437
+ version,
438
+ identifier,
439
+ name: evalConfig.metadata?.name || '',
440
+ description: evalConfig.metadata?.description || '',
441
+ filename,
442
+ },
443
+ handler: evalConfig.handler,
444
+ };
445
+
446
+ if (inputSchema) {
447
+ evalType.inputSchema = inputSchema;
448
+ }
180
449
 
181
450
  if (outputSchema) {
182
- const outputResult = await outputSchema['~standard'].validate(result);
183
- if (outputResult.issues) {
184
- throw new Error(
185
- `Output validation failed: ${outputResult.issues.map((i: any) => i.message).join(', ')}`
451
+ evalType.outputSchema = outputSchema;
452
+ }
453
+
454
+ // Automatically add eval to agent's evals array
455
+ evalsArray.push(evalType);
456
+ internal.debug(
457
+ `Added eval "${evalName}" to agent "${config?.metadata?.name || 'unknown'}". Total evals: ${evalsArray.length}`
458
+ );
459
+
460
+ return evalType as Eval<TInput, TOutput>;
461
+ };
462
+
463
+ const agent: any = {
464
+ handler,
465
+ metadata: config.metadata,
466
+ evals: evalsArray,
467
+ createEval,
468
+ };
469
+
470
+ // Add event listener methods
471
+ agent.addEventListener = (eventName: AgentEventName, callback: any): void => {
472
+ const agentForListeners = agent as any as Agent<any, any, any>;
473
+ const callbackForListeners = callback as any as AgentEventCallback<any>;
474
+ let listeners = agentEventListeners.get(agentForListeners);
475
+ if (!listeners) {
476
+ listeners = new Map();
477
+ agentEventListeners.set(agentForListeners, listeners);
478
+ }
479
+ let callbacks = listeners.get(eventName);
480
+ if (!callbacks) {
481
+ callbacks = new Set();
482
+ listeners.set(eventName, callbacks);
483
+ }
484
+ callbacks.add(callbackForListeners);
485
+ };
486
+
487
+ // Automatically add event listener for 'completed' event to run evals
488
+ (agent as Agent).addEventListener('completed', async (_event, _agent, ctx) => {
489
+ // Get the agent instance from the agents Map to access its current evals array
490
+ // This ensures we get evals that were added via agent.createEval() after agent creation
491
+ const agentName = ctx.agentName;
492
+ const registeredAgent = agentName ? agents.get(agentName) : undefined;
493
+ const agentEvals = registeredAgent?.evals || evalsArray;
494
+
495
+ internal.debug(
496
+ `Checking evals: agentName=${agentName}, evalsArray.length=${evalsArray?.length || 0}, agent.evals.length=${registeredAgent?.evals?.length || 0}`
497
+ );
498
+
499
+ if (agentEvals && agentEvals.length > 0) {
500
+ internal.info(`Executing ${agentEvals.length} eval(s) after agent run`);
501
+
502
+ // Get validated input/output from context state
503
+ const validatedInput = ctx.state.get('_evalInput');
504
+ const validatedOutput = ctx.state.get('_evalOutput');
505
+
506
+ // Execute each eval using waitUntil to avoid blocking the response
507
+ for (const evalItem of agentEvals) {
508
+ const evalName = evalItem.metadata.name || 'unnamed';
509
+
510
+ ctx.waitUntil(
511
+ (async () => {
512
+ internal.info(`[EVALRUN] Starting eval run tracking for '${evalName}'`);
513
+ const evalRunId = generateId('evalrun');
514
+ const evalId = evalItem.metadata.id || '';
515
+ const orgId = runtimeConfig.getOrganizationId();
516
+ const projectId = runtimeConfig.getProjectId();
517
+ const devMode = runtimeConfig.isDevMode() ?? false;
518
+ const evalRunEventProvider = getEvalRunEventProvider();
519
+
520
+ // Only send events if we have required context (devmode flag will be set based on devMode)
521
+ const shouldSendEvalRunEvents = orgId && projectId && evalId !== '';
522
+
523
+ internal.info(`[EVALRUN] Checking conditions for eval '${evalName}':`, {
524
+ orgId: orgId,
525
+ projectId: projectId,
526
+ evalId: evalId,
527
+ devMode,
528
+ hasEvalRunEventProvider: !!evalRunEventProvider,
529
+ shouldSendEvalRunEvents,
530
+ });
531
+
532
+ if (!shouldSendEvalRunEvents) {
533
+ const reasons: string[] = [];
534
+ if (!orgId) reasons.push('missing orgId');
535
+ if (!projectId) reasons.push('missing projectId');
536
+ if (!evalId || evalId === '') reasons.push('empty evalId');
537
+ internal.info(
538
+ `[EVALRUN] Skipping eval run events for '${evalName}': ${reasons.join(', ')}`
539
+ );
540
+ }
541
+
542
+ try {
543
+ internal.debug(`Executing eval: ${evalName}`);
544
+
545
+ // Send eval run start event
546
+ if (shouldSendEvalRunEvents && evalRunEventProvider) {
547
+ internal.info(
548
+ `[EVALRUN] Sending start event for eval '${evalName}' (id: ${evalRunId}, evalId: ${evalId})`
549
+ );
550
+ try {
551
+ const startEvent: EvalRunStartEvent = {
552
+ id: evalRunId,
553
+ sessionId: ctx.sessionId,
554
+ evalId: evalId,
555
+ orgId: orgId!,
556
+ projectId: projectId!,
557
+ devmode: Boolean(devMode),
558
+ };
559
+ internal.debug(
560
+ '[EVALRUN] Start event payload: %s',
561
+ JSON.stringify(startEvent, null, 2)
562
+ );
563
+ await evalRunEventProvider.start(startEvent);
564
+ internal.info(
565
+ `[EVALRUN] Start event sent successfully for eval '${evalName}' (id: ${evalRunId})`
566
+ );
567
+ } catch (error) {
568
+ internal.error(
569
+ `[EVALRUN] Error sending eval run start event for '${evalName}' (id: ${evalRunId})`,
570
+ {
571
+ error,
572
+ }
573
+ );
574
+ // Don't throw - continue with eval execution even if start event fails
575
+ }
576
+ } else if (shouldSendEvalRunEvents && !evalRunEventProvider) {
577
+ internal.warn(
578
+ `[EVALRUN] Conditions met but no evalRunEventProvider available for '${evalName}'`
579
+ );
580
+ } else {
581
+ internal.debug(
582
+ `[EVALRUN] Not sending start event for '${evalName}': shouldSendEvalRunEvents=${shouldSendEvalRunEvents}, hasProvider=${!!evalRunEventProvider}`
583
+ );
584
+ }
585
+
586
+ // Validate eval input if schema exists
587
+ let evalValidatedInput: any = validatedInput;
588
+ if (evalItem.inputSchema) {
589
+ const evalInputResult =
590
+ await evalItem.inputSchema['~standard'].validate(validatedInput);
591
+ if (evalInputResult.issues) {
592
+ throw new Error(
593
+ `Eval input validation failed: ${evalInputResult.issues.map((i: any) => i.message).join(', ')}`
594
+ );
595
+ }
596
+ evalValidatedInput = evalInputResult.value;
597
+ }
598
+
599
+ // Validate eval output if schema exists
600
+ let evalValidatedOutput: any = validatedOutput;
601
+ if (evalItem.outputSchema) {
602
+ const evalOutputResult =
603
+ await evalItem.outputSchema['~standard'].validate(validatedOutput);
604
+ if (evalOutputResult.issues) {
605
+ throw new Error(
606
+ `Eval output validation failed: ${evalOutputResult.issues.map((i: any) => i.message).join(', ')}`
607
+ );
608
+ }
609
+ evalValidatedOutput = evalOutputResult.value;
610
+ }
611
+
612
+ // Create EvalContext (just an alias for AgentContext)
613
+ const evalContext: EvalContext = ctx;
614
+
615
+ // Execute the eval handler conditionally based on agent schema
616
+ let result: EvalRunResult;
617
+ if (inputSchema && outputSchema) {
618
+ // Both input and output defined
619
+ result = await (evalItem.handler as any)(
620
+ evalContext,
621
+ evalValidatedInput,
622
+ evalValidatedOutput
623
+ );
624
+ } else if (inputSchema) {
625
+ // Only input defined
626
+ result = await (evalItem.handler as any)(evalContext, evalValidatedInput);
627
+ } else if (outputSchema) {
628
+ // Only output defined
629
+ result = await (evalItem.handler as any)(evalContext, evalValidatedOutput);
630
+ } else {
631
+ // Neither defined
632
+ result = await (evalItem.handler as any)(evalContext);
633
+ }
634
+
635
+ // Process the returned result
636
+ if (result.success) {
637
+ if ('passed' in result) {
638
+ internal.info(
639
+ `Eval '${evalName}' pass: ${result.passed}`,
640
+ result.metadata
641
+ );
642
+ } else if ('score' in result) {
643
+ internal.info(
644
+ `Eval '${evalName}' score: ${result.score}`,
645
+ result.metadata
646
+ );
647
+ }
648
+ } else {
649
+ internal.error(`Eval '${evalName}' failed: ${result.error}`);
650
+ }
651
+
652
+ // Send eval run complete event
653
+ if (shouldSendEvalRunEvents && evalRunEventProvider) {
654
+ internal.info(
655
+ `[EVALRUN] Sending complete event for eval '${evalName}' (id: ${evalRunId})`
656
+ );
657
+ try {
658
+ await evalRunEventProvider.complete({
659
+ id: evalRunId,
660
+ result: result.success ? result : undefined,
661
+ error: result.success ? undefined : result.error,
662
+ });
663
+ internal.info(
664
+ `[EVALRUN] Complete event sent successfully for eval '${evalName}' (id: ${evalRunId})`
665
+ );
666
+ } catch (error) {
667
+ internal.error(
668
+ `[EVALRUN] Error sending eval run complete event for '${evalName}' (id: ${evalRunId})`,
669
+ {
670
+ error,
671
+ }
672
+ );
673
+ }
674
+ }
675
+
676
+ internal.debug(`Eval '${evalName}' completed successfully`);
677
+ } catch (error) {
678
+ const errorMessage = error instanceof Error ? error.message : String(error);
679
+ internal.error(`Error executing eval '${evalName}'`, { error });
680
+
681
+ // Send eval run complete event with error
682
+ if (shouldSendEvalRunEvents && evalRunEventProvider) {
683
+ internal.info(
684
+ `[EVALRUN] Sending complete event (error) for eval '${evalName}' (id: ${evalRunId})`
685
+ );
686
+ try {
687
+ await evalRunEventProvider.complete({
688
+ id: evalRunId,
689
+ error: errorMessage,
690
+ });
691
+ internal.info(
692
+ `[EVALRUN] Complete event (error) sent successfully for eval '${evalName}' (id: ${evalRunId})`
693
+ );
694
+ } catch (eventError) {
695
+ internal.error(
696
+ `[EVALRUN] Error sending eval run complete event (error) for '${evalName}' (id: ${evalRunId})`,
697
+ { error: eventError }
698
+ );
699
+ }
700
+ }
701
+ }
702
+ })()
186
703
  );
187
704
  }
188
- return outputResult.value;
189
705
  }
706
+ });
190
707
 
191
- return result;
708
+ agent.removeEventListener = (eventName: AgentEventName, callback: any): void => {
709
+ const agentForListeners = agent as any as Agent<any, any, any>;
710
+ const callbackForListeners = callback as any as AgentEventCallback<any>;
711
+ const listeners = agentEventListeners.get(agentForListeners);
712
+ if (!listeners) return;
713
+ const callbacks = listeners.get(eventName);
714
+ if (!callbacks) return;
715
+ callbacks.delete(callbackForListeners);
192
716
  };
193
717
 
194
- const agent: any = { handler, metadata: config.metadata };
195
-
196
718
  if (inputSchema) {
197
719
  agent.inputSchema = inputSchema;
198
720
  }
@@ -208,6 +730,35 @@ export function createAgent<
208
730
  return agent as Agent<TInput, TOutput, TStream>;
209
731
  }
210
732
 
733
+ const runWithSpan = async <T>(
734
+ tracer: Tracer,
735
+ agent: Agent<any, any, any>,
736
+ handler: () => Promise<T>
737
+ ): Promise<T> => {
738
+ const currentContext = context.active();
739
+ const span = tracer.startSpan(
740
+ 'agent.run',
741
+ {
742
+ attributes: {
743
+ '@agentuity/agentName': agent.metadata?.name || '',
744
+ '@agentuity/agentId': agent.metadata?.id || '',
745
+ },
746
+ },
747
+ currentContext
748
+ );
749
+
750
+ try {
751
+ const spanContext = trace.setSpan(currentContext, span);
752
+ return await context.with(spanContext, handler);
753
+ } catch (error) {
754
+ span.recordException(error as Error);
755
+ span.setStatus({ code: SpanStatusCode.ERROR });
756
+ throw error;
757
+ } finally {
758
+ span.end();
759
+ }
760
+ };
761
+
211
762
  const createAgentRunner = <
212
763
  TInput extends StandardSchemaV1 | undefined = any,
213
764
  TOutput extends StandardSchemaV1 | undefined = any,
@@ -216,76 +767,127 @@ const createAgentRunner = <
216
767
  agent: Agent<TInput, TOutput, TStream>,
217
768
  ctx: Context
218
769
  ): AgentRunner<TInput, TOutput, TStream> => {
770
+ const tracer = ctx.var.tracer;
771
+
219
772
  if (agent.inputSchema) {
220
773
  return {
221
774
  metadata: agent.metadata,
222
775
  run: async (input: any) => {
223
- return agent.handler(ctx as unknown as AgentContext, input);
776
+ return runWithSpan(tracer, agent as Agent<any, any, any>, () =>
777
+ agent.handler(ctx as unknown as AgentContext, input)
778
+ );
224
779
  },
225
780
  } as AgentRunner<TInput, TOutput, TStream>;
226
781
  } else {
227
782
  return {
228
783
  metadata: agent.metadata,
229
784
  run: async () => {
230
- return agent.handler(ctx as unknown as AgentContext);
785
+ return runWithSpan(tracer, agent as Agent<any, any, any>, () =>
786
+ agent.handler(ctx as unknown as AgentContext)
787
+ );
231
788
  },
232
789
  } as AgentRunner<TInput, TOutput, TStream>;
233
790
  }
234
791
  };
235
792
 
793
+ /**
794
+ * Populate the agents object with all registered agents
795
+ */
796
+ export const populateAgentsRegistry = (ctx: Context): any => {
797
+ const agentsObj: any = {};
798
+
799
+ // Convert kebab-case to camelCase
800
+ const toCamelCase = (str: string): string => {
801
+ return str
802
+ .replace(/[-_\s]+(.)?/g, (_, char) => (char ? char.toUpperCase() : ''))
803
+ .replace(/^(.)/, (char) => char.toLowerCase());
804
+ };
805
+
806
+ // Build nested structure for agents and subagents
807
+ for (const [name, agentFn] of agents) {
808
+ const runner = createAgentRunner(agentFn, ctx);
809
+
810
+ if (name.includes('.')) {
811
+ // Subagent: "parent.child"
812
+ const parts = name.split('.');
813
+ if (parts.length !== 2) {
814
+ internal.warn(`Invalid subagent name format: "${name}". Expected "parent.child".`);
815
+ continue;
816
+ }
817
+ const parentName = parts[0];
818
+ const childName = parts[1];
819
+ if (parentName && childName) {
820
+ if (!agentsObj[parentName]) {
821
+ // Ensure parent exists
822
+ const parentAgent = agents.get(parentName);
823
+ if (parentAgent) {
824
+ agentsObj[parentName] = createAgentRunner(parentAgent, ctx);
825
+ }
826
+ }
827
+ // Attach subagent to parent using camelCase property name
828
+ const camelChildName = toCamelCase(childName);
829
+ if (agentsObj[parentName]) {
830
+ agentsObj[parentName][camelChildName] = runner;
831
+ }
832
+ }
833
+ } else {
834
+ // Parent agent or standalone agent
835
+ agentsObj[name] = runner;
836
+ }
837
+ }
838
+
839
+ return agentsObj;
840
+ };
841
+
236
842
  export const createAgentMiddleware = (agentName: AgentName): MiddlewareHandler => {
237
843
  return async (ctx, next) => {
238
- // Detect websocket upgrade requests
239
- const isWebSocket = ctx.req.header('upgrade')?.toLowerCase() === 'websocket';
240
-
241
844
  // Populate agents object with strongly-typed keys
242
- const agentsObj: any = {};
243
- for (const [name, agentFn] of agents) {
244
- agentsObj[name] = createAgentRunner(agentFn, ctx);
845
+ const agentsObj = populateAgentsRegistry(ctx);
846
+
847
+ // Determine current and parent agents
848
+ let currentAgent: AgentRunner | undefined;
849
+ let parentAgent: AgentRunner | undefined;
850
+
851
+ if (agentName?.includes('.')) {
852
+ // This is a subagent
853
+ const parts = agentName.split('.');
854
+ const parentName = parts[0];
855
+ const childName = parts[1];
856
+ if (parentName && childName) {
857
+ currentAgent = agentsObj[parentName]?.[childName];
858
+ parentAgent = agentsObj[parentName];
859
+ }
860
+ } else if (agentName) {
861
+ // This is a parent or standalone agent
862
+ currentAgent = agentsObj[agentName];
863
+ }
864
+
865
+ const _ctx = privateContext(ctx);
866
+ if (currentAgent?.metadata?.id) {
867
+ _ctx.var.agentIds.add(currentAgent.metadata.id);
245
868
  }
246
869
 
870
+ const sessionId = ctx.var.sessionId;
871
+ const thread = ctx.var.thread;
872
+ const session = ctx.var.session;
873
+
247
874
  const args: Partial<RequestAgentContextArgs<AgentRegistry, any>> = {
248
875
  agent: agentsObj,
249
- current: agentsObj[agentName],
876
+ current: currentAgent,
877
+ parent: parentAgent,
250
878
  agentName,
251
879
  logger: ctx.var.logger.child({ agent: agentName }),
252
880
  tracer: ctx.var.tracer,
253
- setHeader: (k: string, v: string) => ctx.res.headers.set(k, v),
881
+ sessionId,
882
+ session,
883
+ thread,
884
+ handler: ctx.var.waitUntilHandler,
254
885
  };
255
886
 
256
- const span = trace.getActiveSpan();
257
- if (span?.spanContext) {
258
- args.sessionId = span.spanContext().traceId;
259
- } else {
260
- args.sessionId = Bun.randomUUIDv7();
261
- }
262
-
263
887
  return runInAgentContext(
264
888
  ctx as unknown as Record<string, unknown>,
265
889
  args as RequestAgentContextArgs<any, any>,
266
- next,
267
- isWebSocket
890
+ next
268
891
  );
269
-
270
- // FIXME
271
- // ctx.email = async (): Promise<Email> => {
272
- // return {
273
- // address: 'test@example.com',
274
- // name: 'Test User',
275
- // html: '<p>Hello, world!</p>',
276
- // text: 'Hello, world!',
277
- // };
278
- // };
279
- // ctx.sms = async (): Promise<SMS> => {
280
- // return {
281
- // message: 'Hello, world!',
282
- // number: '+1234567890',
283
- // };
284
- // };
285
- // ctx.cron = async (): Promise<Cron> => {
286
- // return {
287
- // schedule: '0 0 * * *',
288
- // };
289
- // };
290
892
  };
291
893
  };