@agentuity/runtime 0.0.100 → 0.0.102

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 (68) hide show
  1. package/AGENTS.md +34 -212
  2. package/dist/_metadata.d.ts +107 -0
  3. package/dist/_metadata.d.ts.map +1 -0
  4. package/dist/_metadata.js +179 -0
  5. package/dist/_metadata.js.map +1 -0
  6. package/dist/_process-protection.d.ts.map +1 -1
  7. package/dist/_process-protection.js +4 -0
  8. package/dist/_process-protection.js.map +1 -1
  9. package/dist/_services.d.ts.map +1 -1
  10. package/dist/_services.js +18 -17
  11. package/dist/_services.js.map +1 -1
  12. package/dist/_standalone.d.ts.map +1 -1
  13. package/dist/_standalone.js +17 -0
  14. package/dist/_standalone.js.map +1 -1
  15. package/dist/agent.d.ts.map +1 -1
  16. package/dist/agent.js +53 -12
  17. package/dist/agent.js.map +1 -1
  18. package/dist/app.d.ts +61 -10
  19. package/dist/app.d.ts.map +1 -1
  20. package/dist/app.js.map +1 -1
  21. package/dist/devmode.d.ts.map +1 -1
  22. package/dist/devmode.js +13 -5
  23. package/dist/devmode.js.map +1 -1
  24. package/dist/index.d.ts +3 -2
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +5 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/middleware.d.ts +61 -5
  29. package/dist/middleware.d.ts.map +1 -1
  30. package/dist/middleware.js +192 -25
  31. package/dist/middleware.js.map +1 -1
  32. package/dist/services/evalrun/http.d.ts.map +1 -1
  33. package/dist/services/evalrun/http.js +14 -4
  34. package/dist/services/evalrun/http.js.map +1 -1
  35. package/dist/services/local/vector.d.ts +5 -1
  36. package/dist/services/local/vector.d.ts.map +1 -1
  37. package/dist/services/local/vector.js +112 -0
  38. package/dist/services/local/vector.js.map +1 -1
  39. package/dist/services/session/http.d.ts.map +1 -1
  40. package/dist/services/session/http.js +7 -0
  41. package/dist/services/session/http.js.map +1 -1
  42. package/dist/services/session/local.d.ts +2 -2
  43. package/dist/services/session/local.d.ts.map +1 -1
  44. package/dist/services/session/local.js +5 -4
  45. package/dist/services/session/local.js.map +1 -1
  46. package/dist/session.d.ts +30 -4
  47. package/dist/session.d.ts.map +1 -1
  48. package/dist/session.js +90 -13
  49. package/dist/session.js.map +1 -1
  50. package/dist/workbench.d.ts.map +1 -1
  51. package/dist/workbench.js +13 -20
  52. package/dist/workbench.js.map +1 -1
  53. package/package.json +5 -5
  54. package/src/_metadata.ts +307 -0
  55. package/src/_process-protection.ts +6 -0
  56. package/src/_services.ts +23 -21
  57. package/src/_standalone.ts +22 -0
  58. package/src/agent.ts +63 -12
  59. package/src/app.ts +65 -9
  60. package/src/devmode.ts +16 -5
  61. package/src/index.ts +12 -2
  62. package/src/middleware.ts +221 -29
  63. package/src/services/evalrun/http.ts +15 -4
  64. package/src/services/local/vector.ts +160 -0
  65. package/src/services/session/http.ts +11 -0
  66. package/src/services/session/local.ts +9 -4
  67. package/src/session.ts +142 -13
  68. package/src/workbench.ts +13 -26
package/src/app.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  import { type Env as HonoEnv } from 'hono';
3
3
  import type { cors } from 'hono/cors';
4
+ import type { compress } from 'hono/compress';
4
5
  import type { Logger } from './logger';
5
6
  import type { Meter, Tracer } from '@opentelemetry/api';
6
7
  import type {
@@ -14,19 +15,78 @@ import type {
14
15
  import type { Email } from './io/email';
15
16
  import type { ThreadProvider, SessionProvider, Session, Thread } from './session';
16
17
  import type WaitUntilHandler from './_waituntil';
17
-
18
- // TODO: This should be imported from workbench package, but causes circular dependency
19
- export interface WorkbenchInstance {
20
- config: { route?: string; headers?: Record<string, string> };
21
- }
18
+ import type { Context } from 'hono';
22
19
 
23
20
  type CorsOptions = Parameters<typeof cors>[0];
21
+ type HonoCompressOptions = Parameters<typeof compress>[0];
22
+
23
+ /**
24
+ * Configuration options for response compression middleware.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * const app = await createApp({
29
+ * compression: {
30
+ * enabled: true,
31
+ * threshold: 1024,
32
+ * }
33
+ * });
34
+ * ```
35
+ */
36
+ export interface CompressionConfig {
37
+ /**
38
+ * Enable or disable compression globally.
39
+ * @default true
40
+ */
41
+ enabled?: boolean;
42
+
43
+ /**
44
+ * Minimum response body size in bytes before compression is attempted.
45
+ * Responses smaller than this threshold will not be compressed.
46
+ * @default 1024
47
+ */
48
+ threshold?: number;
49
+
50
+ /**
51
+ * Optional filter function to skip compression for specific requests.
52
+ * Return false to skip compression for the request.
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * filter: (c) => !c.req.path.startsWith('/internal')
57
+ * ```
58
+ */
59
+ filter?: (c: Context) => boolean;
60
+
61
+ /**
62
+ * Raw options passed through to Hono's compress middleware.
63
+ * These are merged with Agentuity's defaults.
64
+ */
65
+ honoOptions?: HonoCompressOptions;
66
+ }
24
67
 
25
68
  export interface AppConfig<TAppState = Record<string, never>> {
26
69
  /**
27
70
  * Override the default cors settings
28
71
  */
29
72
  cors?: CorsOptions;
73
+ /**
74
+ * Configure response compression.
75
+ * Set to `false` to disable compression entirely.
76
+ *
77
+ * @example
78
+ * ```typescript
79
+ * const app = await createApp({
80
+ * compression: {
81
+ * threshold: 2048,
82
+ * }
83
+ * });
84
+ *
85
+ * // Or disable compression:
86
+ * const app = await createApp({ compression: false });
87
+ * ```
88
+ */
89
+ compression?: CompressionConfig | false;
30
90
  /**
31
91
  * Override the default services
32
92
  */
@@ -63,10 +123,6 @@ export interface AppConfig<TAppState = Record<string, never>> {
63
123
  * the EvalRunEventProvider to override instead of the default
64
124
  */
65
125
  evalRunEvent?: EvalRunEventProvider;
66
- /**
67
- * the Workbench to override instead of the default
68
- */
69
- workbench?: WorkbenchInstance;
70
126
  };
71
127
  /**
72
128
  * Optional setup function called before server starts
package/src/devmode.ts CHANGED
@@ -78,12 +78,23 @@ const overlay = `
78
78
  </style>
79
79
  `;
80
80
 
81
+ // Global controller to avoid registering multiple SIGINT listeners
82
+ let globalController: AbortController | undefined;
83
+ let globalSigintHandler: (() => void) | undefined;
84
+
81
85
  export function registerDevModeRoutes(router: Hono) {
82
- const controller = new AbortController();
83
- const signal = controller.signal;
84
- process.on('SIGINT', () => {
85
- controller.abort();
86
- });
86
+ // Reuse existing controller or create new one
87
+ if (!globalController) {
88
+ globalController = new AbortController();
89
+
90
+ // Only register SIGINT handler once
91
+ globalSigintHandler = () => {
92
+ globalController?.abort();
93
+ };
94
+ process.on('SIGINT', globalSigintHandler);
95
+ }
96
+
97
+ const signal = globalController.signal;
87
98
  router.get('/__dev__/reload', () => {
88
99
  const stream = new ReadableStream({
89
100
  start(controller): void {
package/src/index.ts CHANGED
@@ -27,8 +27,8 @@ export {
27
27
 
28
28
  // app.ts exports (all app-related functionality)
29
29
  export {
30
- type WorkbenchInstance,
31
30
  type AppConfig,
31
+ type CompressionConfig,
32
32
  type Variables,
33
33
  type TriggerType,
34
34
  type PrivateVariables,
@@ -44,7 +44,12 @@ export {
44
44
  export { addEventListener, removeEventListener } from './_events';
45
45
 
46
46
  // middleware.ts exports (Vite-native)
47
- export { createBaseMiddleware, createCorsMiddleware, createOtelMiddleware } from './middleware';
47
+ export {
48
+ createBaseMiddleware,
49
+ createCorsMiddleware,
50
+ createOtelMiddleware,
51
+ createCompressionMiddleware,
52
+ } from './middleware';
48
53
 
49
54
  // Internal exports needed by generated entry files
50
55
  export { register } from './otel/config';
@@ -193,3 +198,8 @@ export type { RouteSchema, GetRouteSchema } from './_validation';
193
198
  */
194
199
  // eslint-disable-next-line @typescript-eslint/no-empty-object-type
195
200
  export interface AppState {}
201
+
202
+ // Re-export bootstrapRuntimeEnv from @agentuity/server for convenience
203
+ // This allows generated code to import from @agentuity/runtime instead of having
204
+ // a direct dependency on @agentuity/server
205
+ export { bootstrapRuntimeEnv, type RuntimeBootstrapOptions } from '@agentuity/server';
package/src/middleware.ts CHANGED
@@ -5,8 +5,10 @@
5
5
 
6
6
  import { createMiddleware } from 'hono/factory';
7
7
  import { cors } from 'hono/cors';
8
- import type { Env } from './app';
8
+ import { compress } from 'hono/compress';
9
+ import type { Env, CompressionConfig } from './app';
9
10
  import type { Logger } from './logger';
11
+ import { getAppConfig } from './app';
10
12
  import { generateId } from './session';
11
13
  import { runInHTTPContext } from './_context';
12
14
  import { DURATION_HEADER, TOKENS_HEADER } from './_tokens';
@@ -22,6 +24,8 @@ import {
22
24
  } from '@opentelemetry/api';
23
25
  import { TraceState } from '@opentelemetry/core';
24
26
  import * as runtimeConfig from './_config';
27
+ import { getSessionEventProvider } from './_services';
28
+ import { internal } from './logger/internal';
25
29
 
26
30
  const SESSION_HEADER = 'x-session-id';
27
31
  const THREAD_HEADER = 'x-thread-id';
@@ -74,7 +78,8 @@ export interface MiddlewareConfig {
74
78
  * Create base middleware that sets up context variables
75
79
  */
76
80
  export function createBaseMiddleware(config: MiddlewareConfig) {
77
- return createMiddleware<Env>(async (c, next) => {
81
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
82
+ return createMiddleware<Env<any>>(async (c, next) => {
78
83
  c.set('logger', config.logger);
79
84
  c.set('tracer', config.tracer);
80
85
  c.set('meter', config.meter);
@@ -121,31 +126,77 @@ export function createBaseMiddleware(config: MiddlewareConfig) {
121
126
  }
122
127
 
123
128
  /**
124
- * Create CORS middleware
129
+ * Create CORS middleware with lazy config resolution.
130
+ *
131
+ * Handles Cross-Origin Resource Sharing (CORS) headers for API routes.
132
+ * Config is resolved at request time, allowing it to be set via createApp().
133
+ * Static options passed here take precedence over app config.
134
+ *
135
+ * Default behavior:
136
+ * - Reflects the request origin (allows any origin)
137
+ * - Allows common headers: Content-Type, Authorization, Accept, Origin, X-Requested-With
138
+ * - Allows all standard HTTP methods
139
+ * - Enables credentials
140
+ * - Sets max-age to 600 seconds (10 minutes)
141
+ *
142
+ * @param staticOptions - Optional static CORS options that override app config
143
+ *
144
+ * @example
145
+ * ```typescript
146
+ * // Use with default settings
147
+ * app.use('/api/*', createCorsMiddleware());
148
+ *
149
+ * // Or configure via createApp
150
+ * const app = await createApp({
151
+ * cors: {
152
+ * origin: 'https://example.com',
153
+ * allowHeaders: ['Content-Type', 'Authorization', 'X-Custom-Header'],
154
+ * maxAge: 3600,
155
+ * }
156
+ * });
157
+ *
158
+ * // Or pass static options directly (overrides app config)
159
+ * app.use('/api/*', createCorsMiddleware({
160
+ * origin: ['https://app.example.com', 'https://admin.example.com'],
161
+ * credentials: true,
162
+ * }));
163
+ * ```
125
164
  */
126
- export function createCorsMiddleware(corsOptions?: Parameters<typeof cors>[0]) {
127
- return cors({
128
- origin: corsOptions?.origin ?? ((origin) => origin),
129
- allowHeaders: corsOptions?.allowHeaders ?? [
130
- 'Content-Type',
131
- 'Authorization',
132
- 'Accept',
133
- 'Origin',
134
- 'X-Requested-With',
135
- THREAD_HEADER,
136
- ],
137
- allowMethods: ['POST', 'GET', 'OPTIONS', 'HEAD', 'PUT', 'DELETE', 'PATCH'],
138
- exposeHeaders: [
139
- 'Content-Length',
140
- TOKENS_HEADER,
141
- DURATION_HEADER,
142
- THREAD_HEADER,
143
- SESSION_HEADER,
144
- DEPLOYMENT_HEADER,
145
- ],
146
- maxAge: 600,
147
- credentials: true,
148
- ...(corsOptions ?? {}),
165
+ export function createCorsMiddleware(staticOptions?: Parameters<typeof cors>[0]) {
166
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
167
+ return createMiddleware<Env<any>>(async (c, next) => {
168
+ // Lazy resolve: merge app config with static options
169
+ const appConfig = getAppConfig();
170
+ const corsOptions = {
171
+ ...appConfig?.cors,
172
+ ...staticOptions,
173
+ };
174
+
175
+ const corsMiddleware = cors({
176
+ origin: corsOptions?.origin ?? ((origin: string) => origin),
177
+ allowHeaders: corsOptions?.allowHeaders ?? [
178
+ 'Content-Type',
179
+ 'Authorization',
180
+ 'Accept',
181
+ 'Origin',
182
+ 'X-Requested-With',
183
+ THREAD_HEADER,
184
+ ],
185
+ allowMethods: ['POST', 'GET', 'OPTIONS', 'HEAD', 'PUT', 'DELETE', 'PATCH'],
186
+ exposeHeaders: [
187
+ 'Content-Length',
188
+ TOKENS_HEADER,
189
+ DURATION_HEADER,
190
+ THREAD_HEADER,
191
+ SESSION_HEADER,
192
+ DEPLOYMENT_HEADER,
193
+ ],
194
+ maxAge: 600,
195
+ credentials: true,
196
+ ...(corsOptions ?? {}),
197
+ });
198
+
199
+ return corsMiddleware(c, next);
149
200
  });
150
201
  }
151
202
 
@@ -154,7 +205,8 @@ export function createCorsMiddleware(corsOptions?: Parameters<typeof cors>[0]) {
154
205
  * This is the critical middleware that creates AgentContext
155
206
  */
156
207
  export function createOtelMiddleware() {
157
- return createMiddleware<Env>(async (c, next) => {
208
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
209
+ return createMiddleware<Env<any>>(async (c, next) => {
158
210
  // Import providers dynamically to avoid circular deps
159
211
  const { getThreadProvider, getSessionProvider } = await import('./_services');
160
212
  const WaitUntilHandler = (await import('./_waituntil')).default;
@@ -188,6 +240,14 @@ export function createOtelMiddleware() {
188
240
  const deploymentId = runtimeConfig.getDeploymentId();
189
241
  const isDevMode = runtimeConfig.isDevMode();
190
242
 
243
+ internal.info(
244
+ '[session] config: orgId=%s, projectId=%s, deploymentId=%s, isDevMode=%s',
245
+ orgId ?? 'NOT SET (AGENTUITY_CLOUD_ORG_ID)',
246
+ projectId ?? 'NOT SET (AGENTUITY_CLOUD_PROJECT_ID)',
247
+ deploymentId ?? 'none',
248
+ isDevMode
249
+ );
250
+
191
251
  if (projectId) traceState = traceState.set('pid', projectId);
192
252
  if (orgId) traceState = traceState.set('oid', orgId);
193
253
  if (isDevMode) traceState = traceState.set('d', '1');
@@ -211,17 +271,45 @@ export function createOtelMiddleware() {
211
271
  c.set('session', session);
212
272
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
213
273
  (c as any).set('waitUntilHandler', handler);
274
+ const agentIds = new Set<string>();
214
275
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
215
- (c as any).set('agentIds', new Set<string>());
276
+ (c as any).set('agentIds', agentIds);
216
277
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
217
278
  (c as any).set('trigger', 'api');
218
279
 
280
+ // Send session start event (so evalruns can reference this session)
281
+ const sessionEventProvider = getSessionEventProvider();
282
+ const shouldSendSession = !!(orgId && projectId);
283
+ if (shouldSendSession && sessionEventProvider) {
284
+ try {
285
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
286
+ const routeId = (c as any).var?.routeId || '';
287
+ await sessionEventProvider.start({
288
+ id: sessionId,
289
+ threadId: thread.id,
290
+ orgId,
291
+ projectId,
292
+ deploymentId: deploymentId || undefined,
293
+ devmode: isDevMode,
294
+ trigger: 'api',
295
+ routeId,
296
+ environment: runtimeConfig.getEnvironment(),
297
+ url: c.req.path,
298
+ method: c.req.method,
299
+ });
300
+ } catch (_ex) {
301
+ // Silently ignore session start errors - don't block request
302
+ }
303
+ }
304
+
219
305
  try {
220
306
  await next();
221
-
222
307
  // Save session/thread and send events
308
+ internal.info('[session] saving session %s (thread: %s)', sessionId, thread.id);
223
309
  await sessionProvider.save(session);
310
+ internal.info('[session] session saved, now saving thread');
224
311
  await threadProvider.save(thread);
312
+ internal.info('[session] thread saved');
225
313
  span.setStatus({ code: SpanStatusCode.OK });
226
314
  } catch (ex) {
227
315
  if (ex instanceof Error) {
@@ -233,6 +321,37 @@ export function createOtelMiddleware() {
233
321
  });
234
322
  throw ex;
235
323
  } finally {
324
+ // Send session complete event
325
+ internal.info(
326
+ '[session] shouldSendSession: %s, hasSessionEventProvider: %s',
327
+ shouldSendSession,
328
+ !!sessionEventProvider
329
+ );
330
+ if (shouldSendSession && sessionEventProvider) {
331
+ try {
332
+ const userData = session.serializeUserData();
333
+ internal.info(
334
+ '[session] sending session complete event, userData: %s',
335
+ userData ? `${userData.length} bytes` : 'none'
336
+ );
337
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
338
+ const agentIdsSet = (c as any).get('agentIds') as Set<string> | undefined;
339
+ const agentIds = agentIdsSet ? [...agentIdsSet].filter(Boolean) : undefined;
340
+ internal.info('[session] agentIds: %o', agentIds);
341
+ await sessionEventProvider.complete({
342
+ id: sessionId,
343
+ threadId: thread.empty() ? null : thread.id,
344
+ statusCode: c.res?.status ?? 200,
345
+ agentIds: agentIds?.length ? agentIds : undefined,
346
+ userData,
347
+ });
348
+ internal.info('[session] session complete event sent');
349
+ } catch (ex) {
350
+ internal.info('[session] session complete event failed: %s', ex);
351
+ // Silently ignore session complete errors - don't block response
352
+ }
353
+ }
354
+
236
355
  const headers: Record<string, string> = {};
237
356
  propagation.inject(context.active(), headers);
238
357
  for (const key of Object.keys(headers)) {
@@ -250,3 +369,76 @@ export function createOtelMiddleware() {
250
369
  });
251
370
  });
252
371
  }
372
+
373
+ /**
374
+ * Create compression middleware with lazy config resolution.
375
+ *
376
+ * Compresses response bodies using gzip or deflate based on the Accept-Encoding header.
377
+ * Config is resolved at request time, allowing it to be set via createApp().
378
+ *
379
+ * @param staticConfig - Optional static config that overrides app config
380
+ *
381
+ * @example
382
+ * ```typescript
383
+ * // Use with default settings
384
+ * app.use('*', createCompressionMiddleware());
385
+ *
386
+ * // Or configure via createApp
387
+ * const app = await createApp({
388
+ * compression: {
389
+ * threshold: 2048,
390
+ * }
391
+ * });
392
+ * ```
393
+ */
394
+ export function createCompressionMiddleware(staticConfig?: CompressionConfig) {
395
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
396
+ return createMiddleware<Env<any>>(async (c, next) => {
397
+ // Lazy resolve: merge app config with static config
398
+ const appConfig = getAppConfig();
399
+ const appCompressionConfig = appConfig?.compression;
400
+
401
+ // Check if compression is explicitly disabled
402
+ if (appCompressionConfig === false || staticConfig?.enabled === false) {
403
+ return next();
404
+ }
405
+
406
+ // Merge configs: static config takes precedence over app config
407
+ const config: CompressionConfig = {
408
+ ...(typeof appCompressionConfig === 'object' ? appCompressionConfig : {}),
409
+ ...staticConfig,
410
+ };
411
+
412
+ const { enabled = true, threshold = 1024, filter, honoOptions } = config;
413
+
414
+ // Skip if explicitly disabled
415
+ if (!enabled) {
416
+ return next();
417
+ }
418
+
419
+ // Skip WebSocket upgrade requests
420
+ const upgrade = c.req.header('upgrade');
421
+ if (upgrade && upgrade.toLowerCase() === 'websocket') {
422
+ return next();
423
+ }
424
+
425
+ // Skip if no Accept-Encoding header
426
+ const acceptEncoding = c.req.header('accept-encoding');
427
+ if (!acceptEncoding) {
428
+ return next();
429
+ }
430
+
431
+ // Check custom filter
432
+ if (filter && !filter(c)) {
433
+ return next();
434
+ }
435
+
436
+ // Create and run the Hono compress middleware
437
+ const compressMiddleware = compress({
438
+ threshold,
439
+ ...honoOptions,
440
+ });
441
+
442
+ await compressMiddleware(c, next);
443
+ });
444
+ }
@@ -13,6 +13,7 @@ import {
13
13
  type Logger,
14
14
  StructuredError,
15
15
  } from '@agentuity/core';
16
+ import { internal } from '../../logger/internal';
16
17
 
17
18
  const EvalRunResponseError = StructuredError('EvalRunResponseError');
18
19
 
@@ -38,12 +39,22 @@ export class HTTPEvalRunEventProvider implements EvalRunEventProvider {
38
39
  async start(event: EvalRunStartEvent): Promise<void> {
39
40
  const endpoint = '/evalrun/2025-03-17';
40
41
  const fullUrl = `${this.baseUrl}${endpoint}`;
41
- this.logger.debug('[EVALRUN HTTP] Sending eval run start event: %s', event.id);
42
- this.logger.debug('[EVALRUN HTTP] URL: %s %s', 'POST', fullUrl);
43
- this.logger.debug('[EVALRUN HTTP] Base URL: %s', this.baseUrl);
44
42
 
45
43
  const payload = { ...event, timestamp: Date.now() };
46
- this.logger.debug('[EVALRUN HTTP] Start event payload: %s', JSON.stringify(payload, null, 2));
44
+
45
+ // Log full payload using internal logger
46
+ internal.info('[EVALRUN HTTP] ========== START PAYLOAD ==========');
47
+ internal.info('[EVALRUN HTTP] id: %s', payload.id);
48
+ internal.info('[EVALRUN HTTP] evalId: %s', payload.evalId);
49
+ internal.info('[EVALRUN HTTP] evalIdentifier: %s', payload.evalIdentifier);
50
+ internal.info('[EVALRUN HTTP] sessionId: %s', payload.sessionId);
51
+ internal.info('[EVALRUN HTTP] orgId: %s', payload.orgId);
52
+ internal.info('[EVALRUN HTTP] projectId: %s', payload.projectId);
53
+ internal.info('[EVALRUN HTTP] devmode: %s', payload.devmode);
54
+ internal.info('[EVALRUN HTTP] deploymentId: %s', payload.deploymentId);
55
+ internal.info('[EVALRUN HTTP] spanId: %s', payload.spanId);
56
+ internal.info('[EVALRUN HTTP] URL: POST %s', fullUrl);
57
+ internal.info('[EVALRUN HTTP] ============================================');
47
58
 
48
59
  try {
49
60
  const resp = await this.apiClient.post(
@@ -8,6 +8,8 @@ import type {
8
8
  VectorSearchResultWithDocument,
9
9
  VectorSearchParams,
10
10
  VectorSearchResult,
11
+ VectorNamespaceStats,
12
+ VectorNamespaceStatsWithSamples,
11
13
  } from '@agentuity/core';
12
14
  import { randomUUID } from 'node:crypto';
13
15
  import { simpleEmbedding, cosineSimilarity, now } from './_util';
@@ -271,4 +273,162 @@ export class LocalVectorStorage implements VectorStorage {
271
273
  const { count } = query.get(this.#projectPath, name) as { count: number };
272
274
  return count > 0;
273
275
  }
276
+
277
+ async getStats(name: string): Promise<VectorNamespaceStatsWithSamples> {
278
+ if (!name?.trim()) {
279
+ throw new Error('Vector storage name is required');
280
+ }
281
+
282
+ const countQuery = this.#db.query(`
283
+ SELECT COUNT(*) as count,
284
+ MIN(created_at) as created_at, MAX(updated_at) as last_used
285
+ FROM vector_storage
286
+ WHERE project_path = ? AND name = ?
287
+ `);
288
+
289
+ const stats = countQuery.get(this.#projectPath, name) as {
290
+ count: number;
291
+ created_at: number | null;
292
+ last_used: number | null;
293
+ };
294
+
295
+ if (stats.count === 0) {
296
+ return { sum: 0, count: 0 };
297
+ }
298
+
299
+ const sampleQuery = this.#db.query(`
300
+ SELECT key, embedding, document, metadata, created_at, updated_at
301
+ FROM vector_storage
302
+ WHERE project_path = ? AND name = ?
303
+ LIMIT 20
304
+ `);
305
+
306
+ const samples = sampleQuery.all(this.#projectPath, name) as Array<{
307
+ key: string;
308
+ embedding: string;
309
+ document: string | null;
310
+ metadata: string | null;
311
+ created_at: number;
312
+ updated_at: number;
313
+ }>;
314
+
315
+ const encoder = new TextEncoder();
316
+ let totalSum = 0;
317
+ const sampledResults: VectorNamespaceStatsWithSamples['sampledResults'] = {};
318
+ for (const sample of samples) {
319
+ const embeddingBytes = encoder.encode(sample.embedding).length;
320
+ const documentBytes = sample.document ? encoder.encode(sample.document).length : 0;
321
+ const size = embeddingBytes + documentBytes;
322
+ totalSum += size;
323
+ sampledResults![sample.key] = {
324
+ embedding: JSON.parse(sample.embedding),
325
+ document: sample.document || undefined,
326
+ size,
327
+ metadata: sample.metadata ? JSON.parse(sample.metadata) : undefined,
328
+ firstUsed: sample.created_at,
329
+ lastUsed: sample.updated_at,
330
+ };
331
+ }
332
+
333
+ // Estimate total size based on sampled average if we have more records than samples
334
+ const estimatedSum =
335
+ stats.count <= samples.length
336
+ ? totalSum
337
+ : Math.round((totalSum / samples.length) * stats.count);
338
+
339
+ return {
340
+ sum: estimatedSum,
341
+ count: stats.count,
342
+ createdAt: stats.created_at || undefined,
343
+ lastUsed: stats.last_used || undefined,
344
+ sampledResults,
345
+ };
346
+ }
347
+
348
+ async getAllStats(): Promise<Record<string, VectorNamespaceStats>> {
349
+ const query = this.#db.query(`
350
+ SELECT name, embedding, document
351
+ FROM vector_storage
352
+ WHERE project_path = ?
353
+ `);
354
+
355
+ const rows = query.all(this.#projectPath) as Array<{
356
+ name: string;
357
+ embedding: string;
358
+ document: string | null;
359
+ }>;
360
+
361
+ const encoder = new TextEncoder();
362
+ const namespaceStats = new Map<
363
+ string,
364
+ { sum: number; count: number; createdAt?: number; lastUsed?: number }
365
+ >();
366
+
367
+ for (const row of rows) {
368
+ const embeddingBytes = encoder.encode(row.embedding).length;
369
+ const documentBytes = row.document ? encoder.encode(row.document).length : 0;
370
+ const size = embeddingBytes + documentBytes;
371
+
372
+ const existing = namespaceStats.get(row.name);
373
+ if (existing) {
374
+ existing.sum += size;
375
+ existing.count += 1;
376
+ } else {
377
+ namespaceStats.set(row.name, { sum: size, count: 1 });
378
+ }
379
+ }
380
+
381
+ // Get timestamps in a separate query
382
+ const timestampQuery = this.#db.query(`
383
+ SELECT name, MIN(created_at) as created_at, MAX(updated_at) as last_used
384
+ FROM vector_storage
385
+ WHERE project_path = ?
386
+ GROUP BY name
387
+ `);
388
+
389
+ const timestamps = timestampQuery.all(this.#projectPath) as Array<{
390
+ name: string;
391
+ created_at: number | null;
392
+ last_used: number | null;
393
+ }>;
394
+
395
+ for (const ts of timestamps) {
396
+ const stats = namespaceStats.get(ts.name);
397
+ if (stats) {
398
+ stats.createdAt = ts.created_at || undefined;
399
+ stats.lastUsed = ts.last_used || undefined;
400
+ }
401
+ }
402
+
403
+ const results: Record<string, VectorNamespaceStats> = {};
404
+ for (const [name, stats] of namespaceStats) {
405
+ results[name] = stats;
406
+ }
407
+
408
+ return results;
409
+ }
410
+
411
+ async getNamespaces(): Promise<string[]> {
412
+ const query = this.#db.query(`
413
+ SELECT DISTINCT name
414
+ FROM vector_storage
415
+ WHERE project_path = ?
416
+ `);
417
+
418
+ const rows = query.all(this.#projectPath) as Array<{ name: string }>;
419
+ return rows.map((row) => row.name);
420
+ }
421
+
422
+ async deleteNamespace(name: string): Promise<void> {
423
+ if (!name?.trim()) {
424
+ throw new Error('Vector storage name is required');
425
+ }
426
+
427
+ const stmt = this.#db.prepare(`
428
+ DELETE FROM vector_storage
429
+ WHERE project_path = ? AND name = ?
430
+ `);
431
+
432
+ stmt.run(this.#projectPath, name);
433
+ }
274
434
  }