@agentuity/runtime 0.0.6

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 (72) hide show
  1. package/AGENTS.md +128 -0
  2. package/README.md +221 -0
  3. package/dist/_config.d.ts +61 -0
  4. package/dist/_config.d.ts.map +1 -0
  5. package/dist/_context.d.ts +33 -0
  6. package/dist/_context.d.ts.map +1 -0
  7. package/dist/_idle.d.ts +7 -0
  8. package/dist/_idle.d.ts.map +1 -0
  9. package/dist/_server.d.ts +17 -0
  10. package/dist/_server.d.ts.map +1 -0
  11. package/dist/_services.d.ts +2 -0
  12. package/dist/_services.d.ts.map +1 -0
  13. package/dist/_util.d.ts +16 -0
  14. package/dist/_util.d.ts.map +1 -0
  15. package/dist/_waituntil.d.ts +20 -0
  16. package/dist/_waituntil.d.ts.map +1 -0
  17. package/dist/agent.d.ts +88 -0
  18. package/dist/agent.d.ts.map +1 -0
  19. package/dist/app.d.ts +24 -0
  20. package/dist/app.d.ts.map +1 -0
  21. package/dist/index.d.ts +6 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/logger/console.d.ts +50 -0
  24. package/dist/logger/console.d.ts.map +1 -0
  25. package/dist/logger/index.d.ts +4 -0
  26. package/dist/logger/index.d.ts.map +1 -0
  27. package/dist/logger/internal.d.ts +79 -0
  28. package/dist/logger/internal.d.ts.map +1 -0
  29. package/dist/logger/logger.d.ts +41 -0
  30. package/dist/logger/logger.d.ts.map +1 -0
  31. package/dist/logger/user.d.ts +8 -0
  32. package/dist/logger/user.d.ts.map +1 -0
  33. package/dist/logger/util.d.ts +11 -0
  34. package/dist/logger/util.d.ts.map +1 -0
  35. package/dist/otel/config.d.ts +17 -0
  36. package/dist/otel/config.d.ts.map +1 -0
  37. package/dist/otel/console.d.ts +26 -0
  38. package/dist/otel/console.d.ts.map +1 -0
  39. package/dist/otel/fetch.d.ts +12 -0
  40. package/dist/otel/fetch.d.ts.map +1 -0
  41. package/dist/otel/http.d.ts +16 -0
  42. package/dist/otel/http.d.ts.map +1 -0
  43. package/dist/otel/logger.d.ts +36 -0
  44. package/dist/otel/logger.d.ts.map +1 -0
  45. package/dist/otel/otel.d.ts +58 -0
  46. package/dist/otel/otel.d.ts.map +1 -0
  47. package/dist/router.d.ts +37 -0
  48. package/dist/router.d.ts.map +1 -0
  49. package/package.json +58 -0
  50. package/src/_config.ts +101 -0
  51. package/src/_context.ts +86 -0
  52. package/src/_idle.ts +26 -0
  53. package/src/_server.ts +279 -0
  54. package/src/_services.ts +164 -0
  55. package/src/_util.ts +63 -0
  56. package/src/_waituntil.ts +246 -0
  57. package/src/agent.ts +287 -0
  58. package/src/app.ts +31 -0
  59. package/src/index.ts +5 -0
  60. package/src/logger/console.ts +111 -0
  61. package/src/logger/index.ts +3 -0
  62. package/src/logger/internal.ts +165 -0
  63. package/src/logger/logger.ts +44 -0
  64. package/src/logger/user.ts +11 -0
  65. package/src/logger/util.ts +80 -0
  66. package/src/otel/config.ts +81 -0
  67. package/src/otel/console.ts +56 -0
  68. package/src/otel/fetch.ts +103 -0
  69. package/src/otel/http.ts +51 -0
  70. package/src/otel/logger.ts +238 -0
  71. package/src/otel/otel.ts +317 -0
  72. package/src/router.ts +302 -0
@@ -0,0 +1,86 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import type { Tracer } from '@opentelemetry/api';
3
+ import type { KeyValueStorage, ObjectStorage, StreamStorage, VectorStorage } from '@agentuity/core';
4
+ import type { AgentContext, AgentName } from './agent';
5
+ import type { Logger } from './logger';
6
+ import WaitUntilHandler from './_waituntil';
7
+ import { registerServices } from './_services';
8
+
9
+ export interface RequestAgentContextArgs<TAgentMap, TAgent> {
10
+ sessionId: string;
11
+ agent: TAgentMap;
12
+ current: TAgent;
13
+ agentName: AgentName;
14
+ logger: Logger;
15
+ tracer: Tracer;
16
+ setHeader: (k: string, v: string) => void;
17
+ }
18
+
19
+ export class RequestAgentContext<TAgentMap, TAgent> implements AgentContext {
20
+ agent: TAgentMap;
21
+ current: TAgent;
22
+ agentName: AgentName;
23
+ logger: Logger;
24
+ sessionId: string;
25
+ tracer: Tracer;
26
+ kv!: KeyValueStorage;
27
+ objectstore!: ObjectStorage;
28
+ stream!: StreamStorage;
29
+ vector!: VectorStorage;
30
+ private waituntilHandler: WaitUntilHandler;
31
+
32
+ constructor(args: RequestAgentContextArgs<TAgentMap, TAgent>) {
33
+ this.agent = args.agent;
34
+ this.current = args.current;
35
+ this.agentName = args.agentName;
36
+ this.logger = args.logger;
37
+ this.sessionId = args.sessionId;
38
+ this.tracer = args.tracer;
39
+ this.waituntilHandler = new WaitUntilHandler(args.setHeader, args.tracer);
40
+ registerServices(this);
41
+ }
42
+
43
+ waitUntil(callback: Promise<void> | (() => void | Promise<void>)): void {
44
+ this.waituntilHandler.waitUntil(callback);
45
+ }
46
+
47
+ waitUntilAll(): Promise<void> {
48
+ return this.waituntilHandler.waitUntilAll(this.logger, this.sessionId);
49
+ }
50
+ }
51
+
52
+ const asyncLocalStorage = new AsyncLocalStorage<AgentContext>();
53
+
54
+ export const inAgentContext = (): boolean => {
55
+ const context = asyncLocalStorage.getStore();
56
+ return !!context;
57
+ };
58
+
59
+ export const getAgentContext = (): AgentContext => {
60
+ const context = asyncLocalStorage.getStore();
61
+ if (!context) {
62
+ throw new Error('AgentContext is not available');
63
+ }
64
+ return context;
65
+ };
66
+
67
+ export const runInAgentContext = <TAgentMap, TAgent>(
68
+ ctxObject: Record<string, unknown>,
69
+ args: RequestAgentContextArgs<TAgentMap, TAgent>,
70
+ next: () => Promise<void>
71
+ ) => {
72
+ const ctx = new RequestAgentContext(args);
73
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
74
+ const _ctx = ctx as any;
75
+ Object.getOwnPropertyNames(ctx).forEach((k) => {
76
+ ctxObject[k] = _ctx[k];
77
+ });
78
+ for (const k of ['waitUntil']) {
79
+ ctxObject[k] = _ctx[k];
80
+ }
81
+ return asyncLocalStorage.run(ctx, async () => {
82
+ return next().then(() => {
83
+ setImmediate(() => ctx.waitUntilAll()); // TODO: move until session
84
+ });
85
+ });
86
+ };
package/src/_idle.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { getServer } from './_server';
2
+ import { hasWaitUntilPending } from './_waituntil';
3
+
4
+ /**
5
+ * returns true if the server is idle (no pending requests, websockets, or waitUntil tasks)
6
+ *
7
+ * @returns true if idle
8
+ */
9
+ export function isIdle() {
10
+ if (hasWaitUntilPending()) {
11
+ return false;
12
+ }
13
+
14
+ const _server = getServer();
15
+ if (_server) {
16
+ // we have to check >1 since the idle request itself will show up as a pending request
17
+ if (_server.pendingRequests > 1) {
18
+ return false;
19
+ }
20
+ if (_server.pendingWebSockets > 0) {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ return true;
26
+ }
package/src/_server.ts ADDED
@@ -0,0 +1,279 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
2
+ import {
3
+ context,
4
+ SpanKind,
5
+ SpanStatusCode,
6
+ type Context,
7
+ type Tracer,
8
+ trace,
9
+ type Attributes,
10
+ } from '@opentelemetry/api';
11
+ import type { Span } from '@opentelemetry/sdk-trace-base';
12
+ import type { SpanProcessor } from '@opentelemetry/sdk-trace-base';
13
+ import { ServiceException } from '@agentuity/core';
14
+ import { createMiddleware } from 'hono/factory';
15
+ import { Hono } from 'hono';
16
+ import { HTTPException } from 'hono/http-exception';
17
+ import type { BunWebSocketData } from 'hono/bun';
18
+ import { websocket } from 'hono/bun';
19
+ import type { AppConfig, Env } from './app';
20
+ import { extractTraceContextFromRequest } from './otel/http';
21
+ import { register } from './otel/config';
22
+ import type { Logger } from './logger';
23
+ import { isIdle } from './_idle';
24
+ import * as runtimeConfig from './_config';
25
+ import { inAgentContext, getAgentContext } from './_context';
26
+
27
+ let globalServerInstance: Bun.Server<BunWebSocketData> | null = null;
28
+
29
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
+ let globalAppInstance: Hono<any> | null = null;
31
+
32
+ let globalLogger: Logger | null = null;
33
+ let globalTracer: Tracer | null = null;
34
+
35
+ export function getServer() {
36
+ return globalServerInstance;
37
+ }
38
+
39
+ export function getApp() {
40
+ return globalAppInstance;
41
+ }
42
+
43
+ export function getLogger() {
44
+ return globalLogger;
45
+ }
46
+
47
+ export function getTracer() {
48
+ return globalTracer;
49
+ }
50
+
51
+ function isDevelopment(): boolean {
52
+ const devmode = runtimeConfig.isDevMode();
53
+ const environment = runtimeConfig.getEnvironment();
54
+ return devmode || environment === 'development';
55
+ }
56
+
57
+ function getPort(): number {
58
+ return Number.parseInt(process.env.AGENTUITY_PORT ?? process.env.PORT ?? '3000') || 3000;
59
+ }
60
+
61
+ const spanProcessors: SpanProcessor[] = [];
62
+
63
+ /**
64
+ * add a custom span processor that will be added to the otel configuration. this method must be
65
+ * called before the createApp is called for it to be added.
66
+ */
67
+ export function addSpanProcessor(processor: SpanProcessor) {
68
+ spanProcessors.push(processor);
69
+ }
70
+
71
+ function registerAgentuitySpanProcessor() {
72
+ const orgId = runtimeConfig.getOrganizationId();
73
+ const projectId = runtimeConfig.getProjectId();
74
+ const deploymentId = runtimeConfig.getDeploymentId();
75
+ const devmode = runtimeConfig.isDevMode();
76
+ const environment = runtimeConfig.getEnvironment();
77
+
78
+ class RegisterAgentSpanProcessor implements SpanProcessor {
79
+ onStart(span: Span, _context: Context) {
80
+ const attrs: Attributes = {
81
+ '@agentuity/orgId': orgId,
82
+ '@agentuity/projectId': projectId,
83
+ '@agentuity/deploymentId': deploymentId,
84
+ '@agentuity/devmode': devmode,
85
+ '@agentuity/environment': environment,
86
+ };
87
+ if (inAgentContext()) {
88
+ const agentCtx = getAgentContext();
89
+ if (agentCtx.current?.metadata) {
90
+ attrs['@agentuity/agentId'] = agentCtx.current.metadata.id;
91
+ attrs['@agentuity/agentName'] = agentCtx.current.metadata.name;
92
+ }
93
+ }
94
+ span.setAttributes(attrs);
95
+ }
96
+
97
+ onEnd(_span: Span) {
98
+ /* */
99
+ }
100
+
101
+ forceFlush() {
102
+ return Promise.resolve();
103
+ }
104
+
105
+ shutdown() {
106
+ return Promise.resolve();
107
+ }
108
+ }
109
+ addSpanProcessor(new RegisterAgentSpanProcessor());
110
+ }
111
+
112
+ export const createServer = <E extends Env>(app: Hono<E>, _config?: AppConfig) => {
113
+ if (globalServerInstance) {
114
+ return globalServerInstance;
115
+ }
116
+
117
+ registerAgentuitySpanProcessor();
118
+
119
+ const server = Bun.serve({
120
+ hostname: '127.0.0.1',
121
+ development: isDevelopment(),
122
+ fetch: app.fetch,
123
+ idleTimeout: 0,
124
+ port: getPort(),
125
+ websocket,
126
+ });
127
+
128
+ const otel = register({ processors: spanProcessors });
129
+
130
+ globalAppInstance = app;
131
+ globalServerInstance = server;
132
+ globalLogger = otel.logger;
133
+ globalTracer = otel.tracer;
134
+
135
+ let isShutdown = false;
136
+
137
+ app.onError((error, _c) => {
138
+ if (error instanceof HTTPException) {
139
+ otel.logger.error('HTTP Error: %s (%d)', error.cause, error.status);
140
+ return error.getResponse();
141
+ }
142
+ if (
143
+ error instanceof ServiceException ||
144
+ ('statusCode' in error && typeof error.statusCode === 'number')
145
+ ) {
146
+ otel.logger.error('Service Exception: %s (%d)', error.message, error.statusCode);
147
+ return new Response(error.message, {
148
+ status: (error.statusCode as number) ?? 500,
149
+ });
150
+ }
151
+ return new Response('Internal Server Error', { status: 500 });
152
+ });
153
+
154
+ app.use(async (c, next) => {
155
+ c.set('logger', otel.logger);
156
+ c.set('tracer', otel.tracer);
157
+ c.set('meter', otel.meter);
158
+ const skipLogging = c.req.path.startsWith('/_agentuity/');
159
+ const started = performance.now();
160
+ if (!skipLogging) {
161
+ otel.logger.debug('%s %s started', c.req.method, c.req.path);
162
+ }
163
+ await next();
164
+ if (!skipLogging) {
165
+ otel.logger.debug(
166
+ '%s %s completed (%d) in %sms',
167
+ c.req.method,
168
+ c.req.path,
169
+ c.res.status,
170
+ Number(performance.now() - started).toFixed(2)
171
+ );
172
+ }
173
+ });
174
+
175
+ app.route('/_agentuity', createAgentuityAPIs());
176
+
177
+ // Attach services to context for API routes
178
+ app.use('/api/*', async (c, next) => {
179
+ const { registerServices } = await import('./_services');
180
+ registerServices(c);
181
+ await next();
182
+ });
183
+
184
+ app.use('/api/*', otelMiddleware);
185
+ app.use('/agent/*', otelMiddleware);
186
+
187
+ const shutdown = async () => {
188
+ if (isShutdown) {
189
+ return;
190
+ }
191
+ otel.logger.info('shutdown started');
192
+ isShutdown = true;
193
+ // Force exit after timeout if cleanup hangs
194
+ setTimeout(() => process.exit(1), 30_000).unref();
195
+ await server.stop();
196
+ await otel.shutdown();
197
+ otel.logger.info('shutdown completed');
198
+ };
199
+
200
+ process.on('beforeExit', async () => await shutdown());
201
+ process.on('exit', async () => await shutdown());
202
+ process.once('SIGINT', async () => {
203
+ await shutdown();
204
+ process.exit(0);
205
+ });
206
+ process.once('SIGTERM', async () => {
207
+ await shutdown();
208
+ process.exit(0);
209
+ });
210
+ process.once('uncaughtException', async (err) => {
211
+ otel.logger.error('An uncaught exception was received: %s', err);
212
+ await shutdown();
213
+ process.exit(1);
214
+ });
215
+ process.once('unhandledRejection', async (reason) => {
216
+ otel.logger.error('An unhandled promise rejection was received: %s', reason);
217
+ await shutdown();
218
+ process.exit(1);
219
+ });
220
+
221
+ return server;
222
+ };
223
+
224
+ const createAgentuityAPIs = () => {
225
+ const router = new Hono<Env>();
226
+ router.get('idle', (c) => {
227
+ if (isIdle()) {
228
+ return new Response('OK', { status: 200 });
229
+ }
230
+ return new Response('NO', { status: 200 });
231
+ });
232
+ return router;
233
+ };
234
+
235
+ const otelMiddleware = createMiddleware<Env>(async (c, next) => {
236
+ // Extract trace context from headers
237
+ const extractedContext = extractTraceContextFromRequest(c.req.raw);
238
+
239
+ const method = c.req.method;
240
+ const url = new URL(c.req.url);
241
+
242
+ // Execute the request handler within the extracted context
243
+ await context.with(extractedContext, async (): Promise<void> => {
244
+ // Create a span for this incoming request
245
+ await trace.getTracer('http-server').startActiveSpan(
246
+ `HTTP ${method}`,
247
+ {
248
+ kind: SpanKind.SERVER,
249
+ attributes: {
250
+ 'http.method': method,
251
+ 'http.host': url.host,
252
+ 'http.user_agent': c.req.header('user-agent') || '',
253
+ 'http.path': url.pathname,
254
+ },
255
+ },
256
+ async (span): Promise<void> => {
257
+ try {
258
+ await next();
259
+ span.setStatus({
260
+ code: SpanStatusCode.OK,
261
+ });
262
+ } catch (ex) {
263
+ if (ex instanceof Error) {
264
+ span.recordException(ex);
265
+ }
266
+ const message = (ex as Error).message ?? String(ex);
267
+ span.setStatus({
268
+ code: SpanStatusCode.ERROR,
269
+ message,
270
+ });
271
+ c.var.logger.error('ERROR: %s', message);
272
+ throw ex;
273
+ } finally {
274
+ span.end();
275
+ }
276
+ }
277
+ );
278
+ });
279
+ });
@@ -0,0 +1,164 @@
1
+ import { context, SpanKind, SpanStatusCode, trace } from '@opentelemetry/api';
2
+ import {
3
+ KeyValueStorageService,
4
+ ObjectStorageService,
5
+ StreamStorageService,
6
+ VectorStorageService,
7
+ type ListStreamsResponse,
8
+ type VectorUpsertResult,
9
+ type VectorSearchResult,
10
+ } from '@agentuity/core';
11
+ import { createServerFetchAdapter, getServiceUrls } from '@agentuity/server';
12
+ import { injectTraceContextToHeaders } from './otel/http';
13
+ import { getLogger, getTracer } from './_server';
14
+ import { getSDKVersion } from './_config';
15
+
16
+ const userAgent = `Agentuity SDK/${getSDKVersion()}`;
17
+ const bearerKey = `Bearer ${process.env.AGENTUITY_SDK_KEY}`;
18
+
19
+ const serviceUrls = getServiceUrls();
20
+ const kvBaseUrl = serviceUrls.keyvalue;
21
+ const streamBaseUrl = serviceUrls.stream;
22
+ const vectorBaseUrl = serviceUrls.vector;
23
+ const objectBaseUrl = serviceUrls.objectstore;
24
+
25
+ const adapter = createServerFetchAdapter({
26
+ headers: {
27
+ Authorization: bearerKey,
28
+ 'User-Agent': userAgent,
29
+ },
30
+ onBefore: async (url, options, callback) => {
31
+ getLogger()?.debug('before request: %s with options: %s', url, options);
32
+ if (!options.telemetry) {
33
+ return callback();
34
+ }
35
+ options.headers = { ...options.headers, ...injectTraceContextToHeaders() };
36
+ const tracer = getTracer() ?? trace.getTracer('agentuity');
37
+ const currentContext = context.active();
38
+ const span = tracer.startSpan(
39
+ options.telemetry.name,
40
+ { attributes: options.telemetry.attributes, kind: SpanKind.CLIENT },
41
+ currentContext
42
+ );
43
+ const spanContext = trace.setSpan(currentContext, span);
44
+ try {
45
+ await context.with(spanContext, callback);
46
+ span.setStatus({ code: SpanStatusCode.OK });
47
+ } catch (err) {
48
+ const e = err as Error;
49
+ span.recordException(e);
50
+ span.setStatus({ code: SpanStatusCode.ERROR, message: e?.message ?? String(err) });
51
+ throw err;
52
+ } finally {
53
+ span.end();
54
+ }
55
+ },
56
+ onAfter: async (url, options, result, err) => {
57
+ getLogger()?.debug('after request: %s (%d) => %s', url, result.response.status, err);
58
+ if (err) {
59
+ return;
60
+ }
61
+ const span = trace.getSpan(context.active());
62
+ switch (options.telemetry?.name) {
63
+ case 'agentuity.keyvalue.get': {
64
+ if (result.response.status === 404) {
65
+ span?.addEvent('miss');
66
+ } else if (result.response.ok) {
67
+ span?.addEvent('hit');
68
+ }
69
+ break;
70
+ }
71
+ case 'agentuity.stream.create': {
72
+ if (result.response.ok) {
73
+ const res = result.data as { id: string };
74
+ span?.setAttributes({
75
+ 'stream.id': res.id,
76
+ 'stream.url': `${streamBaseUrl}/${res.id}`,
77
+ });
78
+ }
79
+ break;
80
+ }
81
+ case 'agentuity.stream.list': {
82
+ const response = result.data as ListStreamsResponse;
83
+ if (response && span) {
84
+ span.setAttributes({
85
+ 'stream.count': response.streams.length,
86
+ 'stream.total': response.total,
87
+ });
88
+ }
89
+ break;
90
+ }
91
+ case 'agentuity.vector.upsert': {
92
+ if (result.response.ok) {
93
+ const data = result.data as VectorUpsertResult[];
94
+ span?.setAttributes({
95
+ 'vector.count': data.length,
96
+ });
97
+ }
98
+ break;
99
+ }
100
+ case 'agentuity.vector.search': {
101
+ if (result.response.ok) {
102
+ const data = result.data as VectorSearchResult[];
103
+ span?.setAttributes({
104
+ 'vector.results': data.length,
105
+ });
106
+ }
107
+ break;
108
+ }
109
+ case 'agentuity.vector.get': {
110
+ if (result.response.status === 404) {
111
+ span?.addEvent('miss');
112
+ } else if (result.response.ok) {
113
+ span?.addEvent('hit');
114
+ }
115
+ break;
116
+ }
117
+ case 'agentuity.objectstore.get': {
118
+ if (result.response.status === 404) {
119
+ span?.addEvent('miss');
120
+ } else if (result.response.ok) {
121
+ span?.addEvent('hit');
122
+ }
123
+ break;
124
+ }
125
+ case 'agentuity.objectstore.delete': {
126
+ if (result.response.status === 404) {
127
+ span?.addEvent('not_found', { deleted: false });
128
+ } else if (result.response.ok) {
129
+ span?.addEvent('deleted', { deleted: true });
130
+ }
131
+ break;
132
+ }
133
+ }
134
+ },
135
+ });
136
+
137
+ const kv = new KeyValueStorageService(kvBaseUrl, adapter);
138
+ const objectStore = new ObjectStorageService(objectBaseUrl, adapter);
139
+ const stream = new StreamStorageService(streamBaseUrl, adapter);
140
+ const vector = new VectorStorageService(vectorBaseUrl, adapter);
141
+
142
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
143
+ export function registerServices(o: any) {
144
+ Object.defineProperty(o, 'kv', {
145
+ get: () => kv,
146
+ enumerable: false,
147
+ configurable: false,
148
+ });
149
+ Object.defineProperty(o, 'objectstore', {
150
+ get: () => objectStore,
151
+ enumerable: false,
152
+ configurable: false,
153
+ });
154
+ Object.defineProperty(o, 'stream', {
155
+ get: () => stream,
156
+ enumerable: false,
157
+ configurable: false,
158
+ });
159
+ Object.defineProperty(o, 'vector', {
160
+ get: () => vector,
161
+ enumerable: false,
162
+ configurable: false,
163
+ });
164
+ }
package/src/_util.ts ADDED
@@ -0,0 +1,63 @@
1
+ import type { Context } from 'hono';
2
+
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
+ export function returnResponse(ctx: Context, result: any) {
5
+ if (result instanceof ReadableStream) return ctx.body(result);
6
+ if (result instanceof Response) return result;
7
+ if (typeof result === 'string') return ctx.text(result);
8
+ if (typeof result === 'number' || typeof result === 'boolean') return ctx.text(String(result));
9
+ return ctx.json(result);
10
+ }
11
+
12
+ /**
13
+ * SHA256 hash of the given values
14
+ *
15
+ * @param val one or more strings to hash
16
+ * @returns hash string in hex format
17
+ */
18
+ export function hash(...val: string[]): string {
19
+ const hasher = new Bun.CryptoHasher('sha256');
20
+ val.forEach((val) => hasher.update(val));
21
+ return hasher.digest().toHex();
22
+ }
23
+
24
+ /**
25
+ * Safely stringify an object to JSON, handling circular references
26
+ * @param obj - The object to stringify
27
+ * @returns JSON string representation
28
+ */
29
+ export function safeStringify(obj: unknown): string {
30
+ const stack: unknown[] = [];
31
+
32
+ function replacer(_key: string, value: unknown): unknown {
33
+ if (typeof value === 'bigint') {
34
+ return value.toString();
35
+ }
36
+
37
+ if (typeof value === 'object' && value !== null) {
38
+ // Check if this object is already in our ancestor chain
39
+ if (stack.includes(value)) {
40
+ return '[Circular]';
41
+ }
42
+
43
+ // Add to stack before processing
44
+ stack.push(value);
45
+
46
+ // Process the object
47
+ const result = Array.isArray(value) ? [] : {};
48
+
49
+ for (const [k, v] of Object.entries(value)) {
50
+ (result as Record<string, unknown>)[k] = replacer(k, v);
51
+ }
52
+
53
+ // Remove from stack after processing
54
+ stack.pop();
55
+
56
+ return result;
57
+ }
58
+
59
+ return value;
60
+ }
61
+
62
+ return JSON.stringify(replacer('', obj));
63
+ }