@agentuity/runtime 1.0.47 → 2.0.0-beta.0

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 (104) hide show
  1. package/dist/_globals.d.ts +58 -0
  2. package/dist/_globals.d.ts.map +1 -0
  3. package/dist/_globals.js +71 -0
  4. package/dist/_globals.js.map +1 -0
  5. package/dist/_metadata.d.ts.map +1 -1
  6. package/dist/_metadata.js +14 -0
  7. package/dist/_metadata.js.map +1 -1
  8. package/dist/_process-protection.d.ts +2 -0
  9. package/dist/_process-protection.d.ts.map +1 -1
  10. package/dist/_process-protection.js +14 -23
  11. package/dist/_process-protection.js.map +1 -1
  12. package/dist/_server.d.ts +4 -0
  13. package/dist/_server.d.ts.map +1 -1
  14. package/dist/_server.js +4 -0
  15. package/dist/_server.js.map +1 -1
  16. package/dist/_services.d.ts +1 -1
  17. package/dist/_services.d.ts.map +1 -1
  18. package/dist/_services.js +5 -1
  19. package/dist/_services.js.map +1 -1
  20. package/dist/_standalone.d.ts.map +1 -1
  21. package/dist/_standalone.js +3 -9
  22. package/dist/_standalone.js.map +1 -1
  23. package/dist/agent.d.ts.map +1 -1
  24. package/dist/agent.js +1 -0
  25. package/dist/agent.js.map +1 -1
  26. package/dist/app.d.ts +149 -71
  27. package/dist/app.d.ts.map +1 -1
  28. package/dist/app.js +121 -156
  29. package/dist/app.js.map +1 -1
  30. package/dist/bootstrap.d.ts +44 -0
  31. package/dist/bootstrap.d.ts.map +1 -0
  32. package/dist/bootstrap.js +256 -0
  33. package/dist/bootstrap.js.map +1 -0
  34. package/dist/dev-patches/aisdk.d.ts.map +1 -1
  35. package/dist/dev-patches/aisdk.js +6 -8
  36. package/dist/dev-patches/aisdk.js.map +1 -1
  37. package/dist/dev-patches/gateway.d.ts.map +1 -1
  38. package/dist/dev-patches/gateway.js +7 -8
  39. package/dist/dev-patches/gateway.js.map +1 -1
  40. package/dist/handlers/_route-meta.d.ts +20 -0
  41. package/dist/handlers/_route-meta.d.ts.map +1 -0
  42. package/dist/handlers/_route-meta.js +25 -0
  43. package/dist/handlers/_route-meta.js.map +1 -0
  44. package/dist/handlers/cron.d.ts.map +1 -1
  45. package/dist/handlers/cron.js +3 -1
  46. package/dist/handlers/cron.js.map +1 -1
  47. package/dist/handlers/sse.d.ts +3 -3
  48. package/dist/handlers/sse.d.ts.map +1 -1
  49. package/dist/handlers/sse.js +4 -16
  50. package/dist/handlers/sse.js.map +1 -1
  51. package/dist/handlers/stream.d.ts.map +1 -1
  52. package/dist/handlers/stream.js +4 -12
  53. package/dist/handlers/stream.js.map +1 -1
  54. package/dist/handlers/websocket.d.ts +3 -1
  55. package/dist/handlers/websocket.d.ts.map +1 -1
  56. package/dist/handlers/websocket.js +6 -37
  57. package/dist/handlers/websocket.js.map +1 -1
  58. package/dist/index.d.ts +1 -1
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +1 -1
  61. package/dist/index.js.map +1 -1
  62. package/dist/middleware.d.ts +1 -8
  63. package/dist/middleware.d.ts.map +1 -1
  64. package/dist/middleware.js +29 -71
  65. package/dist/middleware.js.map +1 -1
  66. package/dist/otel/logger.d.ts.map +1 -1
  67. package/dist/otel/logger.js +4 -7
  68. package/dist/otel/logger.js.map +1 -1
  69. package/dist/otel/otel.d.ts +4 -1
  70. package/dist/otel/otel.d.ts.map +1 -1
  71. package/dist/otel/otel.js +13 -2
  72. package/dist/otel/otel.js.map +1 -1
  73. package/dist/router.d.ts +10 -62
  74. package/dist/router.d.ts.map +1 -1
  75. package/dist/router.js +9 -146
  76. package/dist/router.js.map +1 -1
  77. package/dist/workbench.d.ts +1 -1
  78. package/dist/workbench.d.ts.map +1 -1
  79. package/dist/workbench.js +120 -12
  80. package/dist/workbench.js.map +1 -1
  81. package/package.json +7 -7
  82. package/src/_globals.ts +92 -0
  83. package/src/_metadata.ts +14 -0
  84. package/src/_process-protection.ts +17 -28
  85. package/src/_server.ts +4 -0
  86. package/src/_services.ts +6 -2
  87. package/src/_standalone.ts +4 -9
  88. package/src/agent.ts +1 -0
  89. package/src/app.ts +294 -195
  90. package/src/bootstrap.ts +316 -0
  91. package/src/dev-patches/aisdk.ts +8 -11
  92. package/src/dev-patches/gateway.ts +9 -11
  93. package/src/globals.d.ts +28 -0
  94. package/src/handlers/_route-meta.ts +31 -0
  95. package/src/handlers/cron.ts +4 -1
  96. package/src/handlers/sse.ts +8 -19
  97. package/src/handlers/stream.ts +5 -12
  98. package/src/handlers/websocket.ts +11 -37
  99. package/src/index.ts +2 -3
  100. package/src/middleware.ts +40 -99
  101. package/src/otel/logger.ts +5 -8
  102. package/src/otel/otel.ts +14 -2
  103. package/src/router.ts +12 -216
  104. package/src/workbench.ts +135 -12
@@ -0,0 +1,316 @@
1
+ /**
2
+ * Server lifecycle helpers.
3
+ *
4
+ * These functions are called by createApp() to set up routes, middleware,
5
+ * and the Bun HTTP server. They're kept separate to keep createApp() focused
6
+ * on orchestration while these handle the details.
7
+ */
8
+
9
+ import type { Context } from 'hono';
10
+ import { websocket, serveStatic } from 'hono/bun';
11
+ import { readFileSync, existsSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+ import { mimeTypes } from '@agentuity/server';
14
+
15
+ import { runShutdown } from './app';
16
+ import type { AnalyticsOptions, WorkbenchOptions } from './app';
17
+ import { createRouter } from './router';
18
+ import { createWebSessionMiddleware } from './middleware';
19
+ import { enableProcessExitProtection } from './_process-protection';
20
+ import { hasWaitUntilPending } from './_waituntil';
21
+ import { getOrganizationId, getProjectId, isDevMode as runtimeIsDevMode } from './_config';
22
+ import { BEACON_SCRIPT } from '@agentuity/frontend';
23
+
24
+ // ============================================================================
25
+ // Mode detection
26
+ // ============================================================================
27
+
28
+ /**
29
+ * Runtime mode detection.
30
+ * Dynamic string concatenation prevents Bun.build from inlining NODE_ENV.
31
+ * @see https://github.com/oven-sh/bun/issues/20183
32
+ */
33
+ const getEnv = (key: string) => process.env[key];
34
+ /**
35
+ * Check if running in development mode.
36
+ *
37
+ * The CLI dev server explicitly sets NODE_ENV='development'. In production
38
+ * (cloud deployment, CI integration test running a built app.js), NODE_ENV
39
+ * may be 'production' or unset entirely. When unset, we assume production
40
+ * — the dev server always sets it, so absence means production.
41
+ */
42
+ export const isDevelopment = () => getEnv('NODE' + '_' + 'ENV') === 'development';
43
+
44
+ // ============================================================================
45
+ // Analytics helpers
46
+ // ============================================================================
47
+
48
+ /** Resolve analytics config with defaults */
49
+ export function resolveAnalyticsConfig(
50
+ analytics: boolean | AnalyticsOptions | undefined
51
+ ): AnalyticsOptions & { enabled: boolean } {
52
+ if (analytics === false) {
53
+ return { enabled: false };
54
+ }
55
+ const opts = typeof analytics === 'object' ? analytics : {};
56
+ return {
57
+ enabled: opts.enabled !== false,
58
+ requireConsent: opts.requireConsent ?? false,
59
+ trackClicks: opts.trackClicks ?? true,
60
+ trackScroll: opts.trackScroll ?? true,
61
+ trackOutboundLinks: opts.trackOutboundLinks ?? true,
62
+ trackForms: opts.trackForms ?? false,
63
+ trackWebVitals: opts.trackWebVitals ?? true,
64
+ trackErrors: opts.trackErrors ?? true,
65
+ trackSPANavigation: opts.trackSPANavigation ?? true,
66
+ sampleRate: opts.sampleRate ?? 1,
67
+ excludePatterns: opts.excludePatterns ?? [],
68
+ globalProperties: opts.globalProperties ?? {},
69
+ };
70
+ }
71
+
72
+ /** Resolve workbench config */
73
+ export function resolveWorkbenchConfig(
74
+ workbench: boolean | string | WorkbenchOptions | undefined
75
+ ): { enabled: boolean; route: string; headers: Record<string, string> } {
76
+ if (!workbench) {
77
+ return { enabled: false, route: '/workbench', headers: {} };
78
+ }
79
+ if (workbench === true) {
80
+ return { enabled: true, route: '/workbench', headers: {} };
81
+ }
82
+ if (typeof workbench === 'string') {
83
+ return { enabled: true, route: workbench, headers: {} };
84
+ }
85
+ return {
86
+ enabled: true,
87
+ route: workbench.route ?? '/workbench',
88
+ headers: workbench.headers ?? {},
89
+ };
90
+ }
91
+
92
+ /** Inject analytics scripts into HTML */
93
+ function injectAnalytics(
94
+ html: string,
95
+ analyticsConfig: AnalyticsOptions & { enabled: boolean }
96
+ ): string {
97
+ if (!analyticsConfig.enabled) return html;
98
+
99
+ const orgId = getOrganizationId() || '';
100
+ const projectId = getProjectId() || '';
101
+ const isDevmode = runtimeIsDevMode();
102
+
103
+ const pageConfig = {
104
+ ...analyticsConfig,
105
+ orgId,
106
+ projectId,
107
+ isDevmode,
108
+ };
109
+
110
+ const configScript = `<script>window.__AGENTUITY_ANALYTICS__=${JSON.stringify(pageConfig)};</script>`;
111
+ const sessionScript = '<script src="/_agentuity/webanalytics/session.js" async></script>';
112
+
113
+ // In production, the beacon is already in HTML as a CDN asset (data-agentuity-beacon marker)
114
+ const beaconMarker = '<script data-agentuity-beacon';
115
+ if (html.includes(beaconMarker)) {
116
+ const injection = configScript + sessionScript;
117
+ return html.replace(beaconMarker, injection + beaconMarker);
118
+ }
119
+
120
+ // Development: beacon served from local route
121
+ const beaconScript = '<script src="/_agentuity/webanalytics/analytics.js"></script>';
122
+ const injection = configScript + sessionScript + beaconScript;
123
+
124
+ if (html.includes('</head>')) {
125
+ return html.replace('</head>', injection + '</head>');
126
+ }
127
+ if (html.includes('<body')) {
128
+ return html.replace(/<body([^>]*)>/, `<body$1>${injection}`);
129
+ }
130
+ return injection + html;
131
+ }
132
+
133
+ /** Register analytics routes on the app */
134
+ export function registerAnalyticsRoutes(
135
+ app: ReturnType<typeof createRouter>,
136
+ _analyticsConfig: AnalyticsOptions & { enabled: boolean }
137
+ ): void {
138
+ app.get(
139
+ '/_agentuity/webanalytics/session.js',
140
+ createWebSessionMiddleware(),
141
+ async (c: Context) => {
142
+ const threadId = c.get('_webThreadId') || '';
143
+ const sessionData = JSON.stringify({ threadId });
144
+ const sessionScript = `window.__AGENTUITY_SESSION__=${sessionData};`;
145
+ return new Response(sessionScript, {
146
+ headers: {
147
+ 'Content-Type': 'application/javascript; charset=utf-8',
148
+ 'Cache-Control': 'no-store, no-cache, must-revalidate',
149
+ },
150
+ });
151
+ }
152
+ );
153
+
154
+ if (isDevelopment()) {
155
+ app.get('/_agentuity/webanalytics/analytics.js', async () => {
156
+ return new Response(BEACON_SCRIPT, {
157
+ headers: {
158
+ 'Content-Type': 'application/javascript; charset=utf-8',
159
+ 'Cache-Control': 'no-store, no-cache, must-revalidate',
160
+ },
161
+ });
162
+ });
163
+ }
164
+ }
165
+
166
+ // ============================================================================
167
+ // Health routes
168
+ // ============================================================================
169
+
170
+ export function registerHealthRoutes(app: ReturnType<typeof createRouter>): void {
171
+ if (!isDevelopment()) {
172
+ const healthHandler = (c: Context) => {
173
+ return c.text('OK', 200, { 'Content-Type': 'text/plain; charset=utf-8' });
174
+ };
175
+ const idleHandler = (c: Context) => {
176
+ const server = globalThis.__AGENTUITY_SERVER__;
177
+ if (!server) return c.text('NO', 200, { 'Content-Type': 'text/plain; charset=utf-8' });
178
+ if (hasWaitUntilPending())
179
+ return c.text('NO', 200, { 'Content-Type': 'text/plain; charset=utf-8' });
180
+ if (server.pendingRequests > 1)
181
+ return c.text('NO', 200, { 'Content-Type': 'text/plain; charset=utf-8' });
182
+ if (server.pendingWebSockets > 0)
183
+ return c.text('NO', 200, { 'Content-Type': 'text/plain; charset=utf-8' });
184
+ return c.text('OK', 200, { 'Content-Type': 'text/plain; charset=utf-8' });
185
+ };
186
+ app.get('/_agentuity/health', healthHandler);
187
+ app.get('/_health', healthHandler);
188
+ app.get('/_agentuity/idle', idleHandler);
189
+ app.get('/_idle', idleHandler);
190
+ }
191
+
192
+ if (isDevelopment()) {
193
+ app.get('/_agentuity/ready', (c: Context) => {
194
+ return c.text('OK', 200, { 'Content-Type': 'text/plain; charset=utf-8' });
195
+ });
196
+ }
197
+ }
198
+
199
+ // ============================================================================
200
+ // Web routes (production static serving)
201
+ // ============================================================================
202
+
203
+ export function registerWebRoutes(
204
+ app: ReturnType<typeof createRouter>,
205
+ analyticsConfig: AnalyticsOptions & { enabled: boolean }
206
+ ): void {
207
+ if (isDevelopment()) return;
208
+
209
+ // Resolve client dir relative to the built app.js location (import.meta.dir)
210
+ // rather than process.cwd(), since the app may be run from inside .agentuity/
211
+ const appDir = typeof import.meta.dir === 'string' ? import.meta.dir : process.cwd();
212
+ const clientDir = join(appDir, 'client');
213
+ // Fallback: try process.cwd()/.agentuity/client if appDir/client doesn't exist
214
+ const resolvedClientDir = existsSync(join(clientDir, 'index.html'))
215
+ ? clientDir
216
+ : join(process.cwd(), '.agentuity', 'client');
217
+ const indexHtmlPath = join(resolvedClientDir, 'index.html');
218
+ const baseIndexHtml = existsSync(indexHtmlPath) ? readFileSync(indexHtmlPath, 'utf-8') : '';
219
+
220
+ if (!baseIndexHtml) return;
221
+
222
+ const prodHtmlHandler = (c: Context) => {
223
+ if (analyticsConfig.enabled) {
224
+ const html = injectAnalytics(baseIndexHtml, analyticsConfig);
225
+ return c.html(html);
226
+ }
227
+ return c.html(baseIndexHtml);
228
+ };
229
+
230
+ app.get('/', prodHtmlHandler);
231
+ app.use('/assets/*', serveStatic({ root: resolvedClientDir, mimes: mimeTypes }));
232
+ app.use(
233
+ '/*',
234
+ serveStatic({
235
+ root: resolvedClientDir,
236
+ rewriteRequestPath: (path: string) => path,
237
+ mimes: mimeTypes,
238
+ })
239
+ );
240
+
241
+ app.all('/_agentuity/*', (c: Context) => c.notFound());
242
+ app.all('/api/*', (c: Context) => c.notFound());
243
+
244
+ app.get('*', (c: Context) => {
245
+ const path = c.req.path;
246
+ if (/\.[a-zA-Z0-9]+$/.test(path)) {
247
+ return c.notFound();
248
+ }
249
+ return prodHtmlHandler(c);
250
+ });
251
+ }
252
+
253
+ // ============================================================================
254
+ // Workbench UI route (dev only)
255
+ // ============================================================================
256
+
257
+ export function registerWorkbenchUI(
258
+ app: ReturnType<typeof createRouter>,
259
+ workbenchConfig: { enabled: boolean; route: string }
260
+ ): void {
261
+ if (!workbenchConfig.enabled || !isDevelopment()) return;
262
+
263
+ const workbenchSrcDir = join(process.cwd(), '.agentuity', 'workbench-src');
264
+ const workbenchIndexPath = join(workbenchSrcDir, 'index.html');
265
+
266
+ app.get(workbenchConfig.route, async (c: Context) => {
267
+ const html = await Bun.file(workbenchIndexPath).text();
268
+ const withVite = html
269
+ .replace('src="./main.tsx"', `src="/@fs${workbenchSrcDir}/main.tsx"`)
270
+ .replace('href="./styles.css"', `href="/@fs${workbenchSrcDir}/styles.css"`);
271
+ return c.html(withVite);
272
+ });
273
+ }
274
+
275
+ // ============================================================================
276
+ // Server startup
277
+ // ============================================================================
278
+
279
+ export function startServer(
280
+ app: ReturnType<typeof createRouter>,
281
+ options?: { requestTimeout?: number }
282
+ ): void {
283
+ if (typeof Bun === 'undefined') return;
284
+
285
+ enableProcessExitProtection();
286
+
287
+ const port = parseInt(process.env.PORT || '3500', 10);
288
+ const requestTimeout = options?.requestTimeout ?? 0;
289
+
290
+ const server = Bun.serve({
291
+ fetch: (req, server) => {
292
+ server.timeout(req, requestTimeout);
293
+ return app.fetch(req, server);
294
+ },
295
+ websocket,
296
+ port,
297
+ hostname: '127.0.0.1',
298
+ development: isDevelopment(),
299
+ });
300
+
301
+ globalThis.__AGENTUITY_SERVER__ = server;
302
+
303
+ if (!isDevelopment()) {
304
+ const handleShutdown = async (_signal: string) => {
305
+ try {
306
+ await runShutdown();
307
+ } catch {
308
+ // Ignore shutdown errors
309
+ }
310
+ process.exit(0);
311
+ };
312
+
313
+ process.once('SIGTERM', () => handleShutdown('SIGTERM'));
314
+ process.once('SIGINT', () => handleShutdown('SIGINT'));
315
+ }
316
+ }
@@ -12,19 +12,16 @@
12
12
  function warnMissingKey(envKey: string): void {
13
13
  const isDev =
14
14
  process.env.AGENTUITY_ENVIRONMENT === 'development' || process.env.NODE_ENV !== 'production';
15
+
16
+ // Align no-bundle runtime behavior with historical build-time expectations:
17
+ // in development, don't emit eager startup errors for every provider.
15
18
  if (isDev) {
16
- console.error('[ERROR] No credentials found for this AI provider. To fix this, either:');
17
- console.error(
18
- ' 1. Login to Agentuity Cloud (agentuity auth login) to use the AI Gateway (recommended)'
19
- );
20
- console.error(` 2. Set ${envKey} in your .env file to use the provider directly`);
21
- } else {
22
- console.error(`[ERROR] The environment variable ${envKey} is required. Either:`);
23
- console.error(
24
- ' 1. Use Agentuity Cloud AI Gateway by ensuring AGENTUITY_SDK_KEY is configured'
25
- );
26
- console.error(` 2. Set ${envKey} using "agentuity env set ${envKey}" and redeploy`);
19
+ return;
27
20
  }
21
+
22
+ console.error(`[ERROR] The environment variable ${envKey} is required. Either:`);
23
+ console.error(' 1. Use Agentuity Cloud AI Gateway by ensuring AGENTUITY_SDK_KEY is configured');
24
+ console.error(` 2. Set ${envKey} using "agentuity env set ${envKey}" and redeploy`);
28
25
  }
29
26
 
30
27
  /**
@@ -21,19 +21,17 @@ const GATEWAY_CONFIGS: GatewayConfig[] = [
21
21
  function warnMissingKey(envKey: string): void {
22
22
  const isDev =
23
23
  process.env.AGENTUITY_ENVIRONMENT === 'development' || process.env.NODE_ENV !== 'production';
24
+
25
+ // Align no-bundle runtime behavior with historical build-time expectations:
26
+ // in development, don't emit eager startup errors for every provider.
27
+ // Missing credentials should surface when/if that provider is actually used.
24
28
  if (isDev) {
25
- console.error('[ERROR] No credentials found for this AI provider. To fix this, either:');
26
- console.error(
27
- ' 1. Login to Agentuity Cloud (agentuity auth login) to use the AI Gateway (recommended)'
28
- );
29
- console.error(` 2. Set ${envKey} in your .env file to use the provider directly`);
30
- } else {
31
- console.error(`[ERROR] The environment variable ${envKey} is required. Either:`);
32
- console.error(
33
- ' 1. Use Agentuity Cloud AI Gateway by ensuring AGENTUITY_SDK_KEY is configured'
34
- );
35
- console.error(` 2. Set ${envKey} using "agentuity env set ${envKey}" and redeploy`);
29
+ return;
36
30
  }
31
+
32
+ console.error(`[ERROR] The environment variable ${envKey} is required. Either:`);
33
+ console.error(' 1. Use Agentuity Cloud AI Gateway by ensuring AGENTUITY_SDK_KEY is configured');
34
+ console.error(` 2. Set ${envKey} using "agentuity env set ${envKey}" and redeploy`);
37
35
  }
38
36
 
39
37
  /**
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Type-safe globalThis declarations for Agentuity runtime.
3
+ *
4
+ * String-keyed globals that persist across bun --hot reloads.
5
+ * Symbol-keyed globals are accessed via the typed helpers in _globals.ts.
6
+ */
7
+
8
+ declare global {
9
+ // eslint-disable-next-line no-var
10
+ var __AGENTUITY_SERVER__:
11
+ | {
12
+ stop: (closeActiveConnections?: boolean) => void;
13
+ port: number | undefined;
14
+ pendingRequests: number;
15
+ pendingWebSockets: number;
16
+ }
17
+ | undefined;
18
+
19
+ // eslint-disable-next-line no-var
20
+ var __AGENTUITY_BUN_SUBPROCESS__:
21
+ | {
22
+ kill: (signal?: number | NodeJS.Signals) => void;
23
+ exitCode: number | null;
24
+ }
25
+ | undefined;
26
+ }
27
+
28
+ export {};
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Route metadata tagging for build-time route type discovery.
3
+ *
4
+ * Handler wrappers (websocket, sse, stream, cron) stamp this symbol on
5
+ * the returned middleware so the build tool can detect the route type
6
+ * from `router.routes` without AST parsing.
7
+ */
8
+
9
+ export const ROUTE_META = Symbol.for('agentuity:route-meta');
10
+
11
+ export interface RouteMeta {
12
+ type: 'websocket' | 'sse' | 'stream' | 'cron';
13
+ }
14
+
15
+ /**
16
+ * Tag a handler/middleware with route metadata.
17
+ */
18
+ export function tagRoute<T extends (...args: any[]) => any>(handler: T, meta: RouteMeta): T {
19
+ (handler as any)[ROUTE_META] = meta;
20
+ return handler;
21
+ }
22
+
23
+ /**
24
+ * Read route metadata from a handler/middleware.
25
+ */
26
+ export function getRouteMeta(handler: unknown): RouteMeta | undefined {
27
+ if (typeof handler === 'function') {
28
+ return (handler as any)[ROUTE_META];
29
+ }
30
+ return undefined;
31
+ }
@@ -2,6 +2,7 @@ import type { Context, Handler } from 'hono';
2
2
  import { returnResponse } from '../_util';
3
3
  import type { Env } from '../app';
4
4
  import { verifySignature } from '../signature';
5
+ import { tagRoute } from './_route-meta';
5
6
 
6
7
  /**
7
8
  * Handler function for cron jobs.
@@ -92,7 +93,7 @@ export function cron<E extends Env = Env>(
92
93
  handler = maybeHandler!;
93
94
  }
94
95
 
95
- return async (c: Context<E>) => {
96
+ const cronHandler: Handler<E> = async (c: Context<E>) => {
96
97
  if (c.req.method !== 'POST') {
97
98
  throw new Error(
98
99
  `Cron endpoint must use POST method, but received ${c.req.method}. ` +
@@ -127,6 +128,8 @@ export function cron<E extends Env = Env>(
127
128
 
128
129
  return returnResponse(c, result);
129
130
  };
131
+
132
+ return tagRoute(cronHandler, { type: 'cron' });
130
133
  }
131
134
 
132
135
  /**
@@ -3,8 +3,8 @@ import { stream as honoStream } from 'hono/streaming';
3
3
  import { context as otelContext, ROOT_CONTEXT } from '@opentelemetry/api';
4
4
  import { StructuredError } from '@agentuity/core';
5
5
  import type { Schema } from '@agentuity/schema';
6
- import { getAgentAsyncLocalStorage } from '../_context';
7
6
  import type { Env } from '../app';
7
+ import { tagRoute } from './_route-meta';
8
8
 
9
9
  /**
10
10
  * Error thrown when sse() is called without a handler function.
@@ -83,7 +83,7 @@ export interface SSEOptions<TOutput = unknown> {
83
83
  *
84
84
  * This schema is used for:
85
85
  * - Type inference in generated `routes.ts` registry
86
- * - Automatic typing of `useEventStream` hook's `data` property
86
+ * - Automatic typing of `EventSource/EventStreamManager` hook's `data` property
87
87
  *
88
88
  * The schema is NOT used for runtime validation - SSE messages are sent
89
89
  * as-is through the stream. Use this for TypeScript type safety only.
@@ -187,8 +187,8 @@ function formatSSEMessage(message: SSEMessage): string {
187
187
  * stream.close();
188
188
  * }));
189
189
  *
190
- * // On the frontend, useEventStream will now have typed data:
191
- * // const { data } = useEventStream('/api/stream');
190
+ * // On the frontend, EventSource/EventStreamManager will now have typed data:
191
+ * // const { data } = EventSource/EventStreamManager('/api/stream');
192
192
  * // data.type is 'token' | 'complete' | 'error'
193
193
  * ```
194
194
  *
@@ -225,10 +225,7 @@ export function sse<E extends Env = Env, TOutput = unknown>(
225
225
 
226
226
  // Note: options.output is captured for type inference but not used at runtime
227
227
  // The CLI extracts this during build to generate typed route registries
228
- return (c: Context<E>) => {
229
- const asyncLocalStorage = getAgentAsyncLocalStorage();
230
- const capturedContext = asyncLocalStorage.getStore();
231
-
228
+ const sseHandler: Handler<E> = (c: Context<E>) => {
232
229
  // Track stream completion for deferred session/thread saving
233
230
  // This promise resolves when the stream closes (normally or via abort)
234
231
  let resolveDone: (() => void) | undefined;
@@ -345,10 +342,6 @@ export function sse<E extends Env = Env, TOutput = unknown>(
345
342
  }
346
343
  };
347
344
 
348
- // Run handler with AsyncLocalStorage context propagation.
349
- // honoStream already uses a fire-and-forget pattern internally,
350
- // so we can safely await here - the response is already being sent.
351
- //
352
345
  // IMPORTANT: We run in ROOT_CONTEXT (no active OTEL span) to avoid a Bun bug
353
346
  // where OTEL-instrumented fetch conflicts with streaming responses.
354
347
  // This causes "ReadableStream has already been used" errors when AI SDK's
@@ -357,13 +350,9 @@ export function sse<E extends Env = Env, TOutput = unknown>(
357
350
  // our OTEL fetch wrapper use the original unpatched fetch.
358
351
  // See: https://github.com/agentuity/sdk/issues/471
359
352
  // See: https://github.com/oven-sh/bun/issues/24766
360
- await otelContext.with(ROOT_CONTEXT, async () => {
361
- if (capturedContext) {
362
- await asyncLocalStorage.run(capturedContext, runInContext);
363
- } else {
364
- await runInContext();
365
- }
366
- });
353
+ await otelContext.with(ROOT_CONTEXT, runInContext);
367
354
  });
368
355
  };
356
+
357
+ return tagRoute(sseHandler, { type: 'sse' });
369
358
  }
@@ -1,9 +1,9 @@
1
1
  import type { Context, Handler } from 'hono';
2
2
  import { stream as honoStream } from 'hono/streaming';
3
3
  import { context as otelContext, ROOT_CONTEXT } from '@opentelemetry/api';
4
- import { getAgentAsyncLocalStorage } from '../_context';
5
4
  import type { Env } from '../app';
6
5
  import { STREAM_DONE_PROMISE_KEY, IS_STREAMING_RESPONSE_KEY } from './sse';
6
+ import { tagRoute } from './_route-meta';
7
7
 
8
8
  /**
9
9
  * Handler function for streaming responses.
@@ -57,10 +57,7 @@ export type StreamHandler<E extends Env = Env> = (
57
57
  * @returns Hono handler for streaming response
58
58
  */
59
59
  export function stream<E extends Env = Env>(handler: StreamHandler<E>): Handler<E> {
60
- return (c: Context<E>) => {
61
- const asyncLocalStorage = getAgentAsyncLocalStorage();
62
- const capturedContext = asyncLocalStorage.getStore();
63
-
60
+ const streamHandler: Handler<E> = (c: Context<E>) => {
64
61
  // Track stream completion for deferred session/thread saving
65
62
  // This promise resolves when the stream completes (pipe finishes or errors)
66
63
  let resolveDone: (() => void) | undefined;
@@ -116,13 +113,9 @@ export function stream<E extends Env = Env>(handler: StreamHandler<E>): Handler<
116
113
  // our OTEL fetch wrapper use the original unpatched fetch.
117
114
  // See: https://github.com/agentuity/sdk/issues/471
118
115
  // See: https://github.com/oven-sh/bun/issues/24766
119
- await otelContext.with(ROOT_CONTEXT, async () => {
120
- if (capturedContext) {
121
- await asyncLocalStorage.run(capturedContext, runInContext);
122
- } else {
123
- await runInContext();
124
- }
125
- });
116
+ await otelContext.with(ROOT_CONTEXT, runInContext);
126
117
  });
127
118
  };
119
+
120
+ return tagRoute(streamHandler, { type: 'stream' });
128
121
  }
@@ -1,8 +1,8 @@
1
1
  import type { Context, MiddlewareHandler } from 'hono';
2
2
  import { upgradeWebSocket } from 'hono/bun';
3
3
  import { context as otelContext, ROOT_CONTEXT } from '@opentelemetry/api';
4
- import { getAgentAsyncLocalStorage } from '../_context';
5
4
  import type { Env } from '../app';
5
+ import { tagRoute } from './_route-meta';
6
6
 
7
7
  /**
8
8
  * Context key for WebSocket close promise.
@@ -92,16 +92,15 @@ export type WebSocketHandler<E extends Env = Env> = (
92
92
  * @param handler - Synchronous handler function receiving context and WebSocket connection
93
93
  * @returns Hono middleware handler for WebSocket upgrade
94
94
  */
95
- export function websocket<E extends Env = Env>(handler: WebSocketHandler<E>): MiddlewareHandler<E> {
95
+ export function websocket<E extends Env = Env>(
96
+ handler: WebSocketHandler<E>
97
+ ): MiddlewareHandler<E, string, { outputFormat: 'ws' }> {
96
98
  const wsHandler = upgradeWebSocket((c: Context<E>) => {
97
99
  let openHandler: ((event: Event) => void | Promise<void>) | undefined;
98
100
  let messageHandler: ((event: MessageEvent) => void | Promise<void>) | undefined;
99
101
  let closeHandler: ((event: CloseEvent) => void | Promise<void>) | undefined;
100
102
  let initialized = false;
101
103
 
102
- const asyncLocalStorage = getAgentAsyncLocalStorage();
103
- const capturedContext = asyncLocalStorage.getStore();
104
-
105
104
  // Create done promise for session lifecycle deferral, but ONLY for actual
106
105
  // WebSocket upgrade requests. The factory runs unconditionally for every
107
106
  // request hitting this route (Hono calls createEvents before attempting
@@ -144,11 +143,7 @@ export function websocket<E extends Env = Env>(handler: WebSocketHandler<E>): Mi
144
143
  // See: https://github.com/oven-sh/bun/issues/24766
145
144
  const runHandler = () => {
146
145
  otelContext.with(ROOT_CONTEXT, () => {
147
- if (capturedContext) {
148
- asyncLocalStorage.run(capturedContext, () => handler(c, wsConnection));
149
- } else {
150
- handler(c, wsConnection);
151
- }
146
+ handler(c, wsConnection);
152
147
  });
153
148
  initialized = true;
154
149
  };
@@ -162,14 +157,7 @@ export function websocket<E extends Env = Env>(handler: WebSocketHandler<E>): Mi
162
157
  wsConnection.send = (data) => ws.send(data);
163
158
 
164
159
  if (openHandler) {
165
- const h = openHandler;
166
- await otelContext.with(ROOT_CONTEXT, async () => {
167
- if (capturedContext) {
168
- await asyncLocalStorage.run(capturedContext, () => h(event));
169
- } else {
170
- await h(event);
171
- }
172
- });
160
+ await otelContext.with(ROOT_CONTEXT, () => openHandler!(event));
173
161
  }
174
162
  } catch (err) {
175
163
  c.var.logger?.error('WebSocket onOpen error:', err);
@@ -184,14 +172,7 @@ export function websocket<E extends Env = Env>(handler: WebSocketHandler<E>): Mi
184
172
  runHandler();
185
173
  }
186
174
  if (messageHandler) {
187
- const h = messageHandler;
188
- await otelContext.with(ROOT_CONTEXT, async () => {
189
- if (capturedContext) {
190
- await asyncLocalStorage.run(capturedContext, () => h(event));
191
- } else {
192
- await h(event);
193
- }
194
- });
175
+ await otelContext.with(ROOT_CONTEXT, () => messageHandler!(event));
195
176
  }
196
177
  } catch (err) {
197
178
  c.var.logger?.error('WebSocket onMessage error:', err);
@@ -202,14 +183,7 @@ export function websocket<E extends Env = Env>(handler: WebSocketHandler<E>): Mi
202
183
  onClose: async (event: CloseEvent, _ws: any) => {
203
184
  try {
204
185
  if (closeHandler) {
205
- const h = closeHandler;
206
- await otelContext.with(ROOT_CONTEXT, async () => {
207
- if (capturedContext) {
208
- await asyncLocalStorage.run(capturedContext, () => h(event));
209
- } else {
210
- await h(event);
211
- }
212
- });
186
+ await otelContext.with(ROOT_CONTEXT, () => closeHandler!(event));
213
187
  }
214
188
  } catch (err) {
215
189
  c.var.logger?.error('WebSocket onClose error:', err);
@@ -222,8 +196,8 @@ export function websocket<E extends Env = Env>(handler: WebSocketHandler<E>): Mi
222
196
  };
223
197
  });
224
198
 
225
- const middleware: MiddlewareHandler<E> = (c, next) =>
226
- (wsHandler as unknown as MiddlewareHandler<E>)(c, next);
199
+ const middleware: MiddlewareHandler<E, string, { outputFormat: 'ws' }> = (c, next) =>
200
+ (wsHandler as unknown as MiddlewareHandler<E, string, { outputFormat: 'ws' }>)(c, next);
227
201
 
228
- return middleware;
202
+ return tagRoute(middleware, { type: 'websocket' });
229
203
  }