@agentuity/runtime 0.0.101 → 0.0.103

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,14 +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';
18
+ import type { Context } from 'hono';
17
19
 
18
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
+ }
19
67
 
20
68
  export interface AppConfig<TAppState = Record<string, never>> {
21
69
  /**
22
70
  * Override the default cors settings
23
71
  */
24
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;
25
90
  /**
26
91
  * Override the default services
27
92
  */
package/src/eval.ts CHANGED
@@ -1,34 +1,40 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  import type { StandardSchemaV1, InferInput, InferOutput } from '@agentuity/core';
3
3
  import type { AgentContext } from './agent';
4
+ import { z } from 'zod';
4
5
 
5
6
  // Eval SDK types
6
7
  export type EvalContext = AgentContext<any, any, any>;
7
8
 
8
9
  export type EvalRunResultMetadata = {
9
- reason: string;
10
10
  // biome-ignore lint/suspicious/noExplicitAny: metadata can contain any type of data
11
11
  [key: string]: any;
12
12
  };
13
13
 
14
- export type EvalRunResultBinary = {
15
- success: true;
16
- passed: boolean;
17
- metadata: EvalRunResultMetadata;
18
- };
14
+ export const EvalHandlerResultSchema = z.object({
15
+ passed: z.boolean(),
16
+ score: z.number().min(0).max(1).optional(),
17
+ metadata: z.record(z.string(), z.any()),
18
+ });
19
19
 
20
- export type EvalRunResultScore = {
21
- success: true;
22
- score: number; // 0-1 range
23
- metadata: EvalRunResultMetadata;
24
- };
20
+ export type EvalHandlerResult = z.infer<typeof EvalHandlerResultSchema>;
21
+
22
+ // Internal types for catalyst (include success field)
23
+ export const EvalRunResultSuccessSchema = z.object({
24
+ success: z.literal(true),
25
+ passed: z.boolean(),
26
+ score: z.number().min(0).max(1).optional(),
27
+ metadata: z.record(z.string(), z.any()),
28
+ });
29
+
30
+ export type EvalRunResultSuccess = z.infer<typeof EvalRunResultSuccessSchema>;
25
31
 
26
32
  export type EvalRunResultError = {
27
33
  success: false;
28
34
  error: string;
29
35
  };
30
36
 
31
- export type EvalRunResult = EvalRunResultBinary | EvalRunResultScore | EvalRunResultError;
37
+ export type EvalRunResult = EvalRunResultSuccess | EvalRunResultError;
32
38
 
33
39
  export type CreateEvalRunRequest = {
34
40
  projectId: string;
@@ -80,11 +86,11 @@ type InferSchemaOutput<T> = T extends StandardSchemaV1 ? InferOutput<T> : any;
80
86
 
81
87
  export type EvalFunction<TInput = any, TOutput = any> = [TInput] extends [undefined]
82
88
  ? [TOutput] extends [undefined]
83
- ? (ctx: EvalContext) => Promise<EvalRunResult>
84
- : (ctx: EvalContext, output: TOutput) => Promise<EvalRunResult>
89
+ ? (ctx: EvalContext) => Promise<EvalHandlerResult>
90
+ : (ctx: EvalContext, output: TOutput) => Promise<EvalHandlerResult>
85
91
  : [TOutput] extends [undefined]
86
- ? (ctx: EvalContext, input: TInput) => Promise<EvalRunResult>
87
- : (ctx: EvalContext, input: TInput, output: TOutput) => Promise<EvalRunResult>;
92
+ ? (ctx: EvalContext, input: TInput) => Promise<EvalHandlerResult>
93
+ : (ctx: EvalContext, input: TInput, output: TOutput) => Promise<EvalHandlerResult>;
88
94
 
89
95
  /**
90
96
  * The Eval handler interface.
package/src/index.ts CHANGED
@@ -28,6 +28,7 @@ export {
28
28
  // app.ts exports (all app-related functionality)
29
29
  export {
30
30
  type AppConfig,
31
+ type CompressionConfig,
31
32
  type Variables,
32
33
  type TriggerType,
33
34
  type PrivateVariables,
@@ -43,7 +44,12 @@ export {
43
44
  export { addEventListener, removeEventListener } from './_events';
44
45
 
45
46
  // middleware.ts exports (Vite-native)
46
- export { createBaseMiddleware, createCorsMiddleware, createOtelMiddleware } from './middleware';
47
+ export {
48
+ createBaseMiddleware,
49
+ createCorsMiddleware,
50
+ createOtelMiddleware,
51
+ createCompressionMiddleware,
52
+ } from './middleware';
47
53
 
48
54
  // Internal exports needed by generated entry files
49
55
  export { register } from './otel/config';
@@ -63,8 +69,8 @@ export { type HonoEnv, type WebSocketConnection, createRouter } from './router';
63
69
  export {
64
70
  type EvalContext,
65
71
  type EvalRunResultMetadata,
66
- type EvalRunResultBinary,
67
- type EvalRunResultScore,
72
+ type EvalHandlerResult,
73
+ type EvalRunResultSuccess,
68
74
  type EvalRunResultError,
69
75
  type EvalRunResult,
70
76
  type CreateEvalRunRequest,
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';
@@ -124,31 +126,77 @@ export function createBaseMiddleware(config: MiddlewareConfig) {
124
126
  }
125
127
 
126
128
  /**
127
- * 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
+ * ```
128
164
  */
129
- export function createCorsMiddleware(corsOptions?: Parameters<typeof cors>[0]) {
130
- return cors({
131
- origin: corsOptions?.origin ?? ((origin) => origin),
132
- allowHeaders: corsOptions?.allowHeaders ?? [
133
- 'Content-Type',
134
- 'Authorization',
135
- 'Accept',
136
- 'Origin',
137
- 'X-Requested-With',
138
- THREAD_HEADER,
139
- ],
140
- allowMethods: ['POST', 'GET', 'OPTIONS', 'HEAD', 'PUT', 'DELETE', 'PATCH'],
141
- exposeHeaders: [
142
- 'Content-Length',
143
- TOKENS_HEADER,
144
- DURATION_HEADER,
145
- THREAD_HEADER,
146
- SESSION_HEADER,
147
- DEPLOYMENT_HEADER,
148
- ],
149
- maxAge: 600,
150
- credentials: true,
151
- ...(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);
152
200
  });
153
201
  }
154
202
 
@@ -321,3 +369,76 @@ export function createOtelMiddleware() {
321
369
  });
322
370
  });
323
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
+ }
@@ -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
  }