@agentuity/runtime 0.0.105 → 0.0.107

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 (83) hide show
  1. package/dist/_context.d.ts +2 -1
  2. package/dist/_context.d.ts.map +1 -1
  3. package/dist/_context.js +1 -0
  4. package/dist/_context.js.map +1 -1
  5. package/dist/_metadata.d.ts +4 -0
  6. package/dist/_metadata.d.ts.map +1 -1
  7. package/dist/_metadata.js +28 -1
  8. package/dist/_metadata.js.map +1 -1
  9. package/dist/_server.d.ts +1 -1
  10. package/dist/_server.d.ts.map +1 -1
  11. package/dist/_server.js +4 -1
  12. package/dist/_server.js.map +1 -1
  13. package/dist/_services.d.ts +2 -1
  14. package/dist/_services.d.ts.map +1 -1
  15. package/dist/_services.js +11 -2
  16. package/dist/_services.js.map +1 -1
  17. package/dist/_standalone.d.ts +2 -1
  18. package/dist/_standalone.d.ts.map +1 -1
  19. package/dist/_standalone.js +1 -0
  20. package/dist/_standalone.js.map +1 -1
  21. package/dist/_tokens.d.ts +9 -1
  22. package/dist/_tokens.d.ts.map +1 -1
  23. package/dist/_tokens.js +5 -8
  24. package/dist/_tokens.js.map +1 -1
  25. package/dist/agent.d.ts +26 -2
  26. package/dist/agent.d.ts.map +1 -1
  27. package/dist/agent.js +10 -0
  28. package/dist/agent.js.map +1 -1
  29. package/dist/app.d.ts +2 -1
  30. package/dist/app.d.ts.map +1 -1
  31. package/dist/app.js.map +1 -1
  32. package/dist/bun-s3-patch.d.ts +26 -0
  33. package/dist/bun-s3-patch.d.ts.map +1 -0
  34. package/dist/bun-s3-patch.js +65 -0
  35. package/dist/bun-s3-patch.js.map +1 -0
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +2 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/middleware.d.ts +1 -1
  41. package/dist/middleware.d.ts.map +1 -1
  42. package/dist/middleware.js +8 -6
  43. package/dist/middleware.js.map +1 -1
  44. package/dist/services/sandbox/http.d.ts +13 -0
  45. package/dist/services/sandbox/http.d.ts.map +1 -0
  46. package/dist/services/sandbox/http.js +130 -0
  47. package/dist/services/sandbox/http.js.map +1 -0
  48. package/dist/services/sandbox/index.d.ts +2 -0
  49. package/dist/services/sandbox/index.d.ts.map +1 -0
  50. package/dist/services/sandbox/index.js +2 -0
  51. package/dist/services/sandbox/index.js.map +1 -0
  52. package/dist/services/session/http.d.ts +12 -1
  53. package/dist/services/session/http.d.ts.map +1 -1
  54. package/dist/services/session/http.js +31 -1
  55. package/dist/services/session/http.js.map +1 -1
  56. package/dist/services/thread/local.d.ts.map +1 -1
  57. package/dist/services/thread/local.js +7 -6
  58. package/dist/services/thread/local.js.map +1 -1
  59. package/dist/session.d.ts +35 -8
  60. package/dist/session.d.ts.map +1 -1
  61. package/dist/session.js +25 -24
  62. package/dist/session.js.map +1 -1
  63. package/dist/workbench.d.ts.map +1 -1
  64. package/dist/workbench.js +11 -8
  65. package/dist/workbench.js.map +1 -1
  66. package/package.json +5 -5
  67. package/src/_context.ts +2 -0
  68. package/src/_metadata.ts +37 -1
  69. package/src/_server.ts +4 -1
  70. package/src/_services.ts +12 -2
  71. package/src/_standalone.ts +7 -1
  72. package/src/_tokens.ts +5 -9
  73. package/src/agent.ts +40 -1
  74. package/src/app.ts +2 -0
  75. package/src/bun-s3-patch.ts +91 -0
  76. package/src/index.ts +3 -0
  77. package/src/middleware.ts +8 -10
  78. package/src/services/sandbox/http.ts +215 -0
  79. package/src/services/sandbox/index.ts +1 -0
  80. package/src/services/session/http.ts +39 -1
  81. package/src/services/thread/local.ts +8 -5
  82. package/src/session.ts +58 -32
  83. package/src/workbench.ts +11 -12
@@ -0,0 +1,215 @@
1
+ import {
2
+ APIClient,
3
+ sandboxCreate,
4
+ sandboxDestroy,
5
+ sandboxExecute,
6
+ sandboxGet,
7
+ sandboxList,
8
+ sandboxRun,
9
+ sandboxWriteFiles,
10
+ sandboxReadFile,
11
+ } from '@agentuity/server';
12
+ import type {
13
+ SandboxService,
14
+ Sandbox,
15
+ SandboxInfo,
16
+ SandboxCreateOptions,
17
+ SandboxRunOptions,
18
+ SandboxRunResult,
19
+ ListSandboxesParams,
20
+ ListSandboxesResponse,
21
+ ExecuteOptions,
22
+ Execution,
23
+ StreamReader,
24
+ SandboxStatus,
25
+ FileToWrite,
26
+ } from '@agentuity/core';
27
+ import { context, SpanKind, SpanStatusCode, trace } from '@opentelemetry/api';
28
+
29
+ const TRACER_NAME = 'agentuity.sandbox';
30
+
31
+ async function withSpan<T>(
32
+ name: string,
33
+ attributes: Record<string, string | number | boolean>,
34
+ fn: () => Promise<T>
35
+ ): Promise<T> {
36
+ const tracer = trace.getTracer(TRACER_NAME);
37
+ const currentContext = context.active();
38
+ const span = tracer.startSpan(name, { attributes, kind: SpanKind.CLIENT }, currentContext);
39
+ const spanContext = trace.setSpan(currentContext, span);
40
+
41
+ try {
42
+ const result = await context.with(spanContext, fn);
43
+ span.setStatus({ code: SpanStatusCode.OK });
44
+ return result;
45
+ } catch (err) {
46
+ const e = err as Error;
47
+ span.recordException(e);
48
+ span.setStatus({ code: SpanStatusCode.ERROR, message: e?.message ?? String(err) });
49
+ throw err;
50
+ } finally {
51
+ span.end();
52
+ }
53
+ }
54
+
55
+ function createStreamReader(id: string | undefined, baseUrl: string): StreamReader {
56
+ const streamId = id ?? '';
57
+ const url = streamId ? `${baseUrl}/${streamId}` : '';
58
+
59
+ return {
60
+ id: streamId,
61
+ url,
62
+ readonly: true as const,
63
+ getReader(): ReadableStream<Uint8Array> {
64
+ if (!url) {
65
+ return new ReadableStream({
66
+ start(controller) {
67
+ controller.close();
68
+ },
69
+ });
70
+ }
71
+ return new ReadableStream({
72
+ async start(controller) {
73
+ try {
74
+ const response = await fetch(url);
75
+ if (!response.ok || !response.body) {
76
+ controller.close();
77
+ return;
78
+ }
79
+ const reader = response.body.getReader();
80
+ while (true) {
81
+ const { done, value } = await reader.read();
82
+ if (done) break;
83
+ controller.enqueue(value);
84
+ }
85
+ controller.close();
86
+ } catch {
87
+ controller.close();
88
+ }
89
+ },
90
+ });
91
+ },
92
+ };
93
+ }
94
+
95
+ function createSandboxInstance(
96
+ client: APIClient,
97
+ sandboxId: string,
98
+ status: SandboxStatus,
99
+ streamBaseUrl: string,
100
+ stdoutStreamId?: string,
101
+ stderrStreamId?: string
102
+ ): Sandbox {
103
+ const interleaved = !!(stdoutStreamId && stderrStreamId && stdoutStreamId === stderrStreamId);
104
+ return {
105
+ id: sandboxId,
106
+ status,
107
+ stdout: createStreamReader(stdoutStreamId, streamBaseUrl),
108
+ stderr: createStreamReader(stderrStreamId, streamBaseUrl),
109
+ interleaved,
110
+
111
+ async execute(options: ExecuteOptions): Promise<Execution> {
112
+ return withSpan(
113
+ 'agentuity.sandbox.execute',
114
+ {
115
+ 'sandbox.id': sandboxId,
116
+ 'sandbox.command': options.command?.join(' ') ?? '',
117
+ },
118
+ () => sandboxExecute(client, { sandboxId, options, signal: options.signal })
119
+ );
120
+ },
121
+
122
+ async writeFiles(files: FileToWrite[]): Promise<void> {
123
+ await withSpan(
124
+ 'agentuity.sandbox.writeFiles',
125
+ {
126
+ 'sandbox.id': sandboxId,
127
+ 'sandbox.files.count': files.length,
128
+ },
129
+ () => sandboxWriteFiles(client, { sandboxId, files })
130
+ );
131
+ },
132
+
133
+ async readFile(path: string): Promise<ReadableStream<Uint8Array>> {
134
+ return withSpan(
135
+ 'agentuity.sandbox.readFile',
136
+ {
137
+ 'sandbox.id': sandboxId,
138
+ 'sandbox.file.path': path,
139
+ },
140
+ () => sandboxReadFile(client, { sandboxId, path })
141
+ );
142
+ },
143
+
144
+ async destroy(): Promise<void> {
145
+ await withSpan('agentuity.sandbox.destroy', { 'sandbox.id': sandboxId }, () =>
146
+ sandboxDestroy(client, { sandboxId })
147
+ );
148
+ },
149
+ };
150
+ }
151
+
152
+ export class HTTPSandboxService implements SandboxService {
153
+ private client: APIClient;
154
+ private streamBaseUrl: string;
155
+
156
+ constructor(client: APIClient, streamBaseUrl: string) {
157
+ this.client = client;
158
+ this.streamBaseUrl = streamBaseUrl;
159
+ }
160
+
161
+ async run(options: SandboxRunOptions): Promise<SandboxRunResult> {
162
+ return withSpan(
163
+ 'agentuity.sandbox.run',
164
+ {
165
+ 'sandbox.command': options.command?.exec?.join(' ') ?? '',
166
+ 'sandbox.mode': 'oneshot',
167
+ },
168
+ () => sandboxRun(this.client, { options })
169
+ );
170
+ }
171
+
172
+ async create(options?: SandboxCreateOptions): Promise<Sandbox> {
173
+ return withSpan(
174
+ 'agentuity.sandbox.create',
175
+ {
176
+ 'sandbox.network': options?.network?.enabled ?? false,
177
+ 'sandbox.snapshot': options?.snapshot ?? '',
178
+ },
179
+ async () => {
180
+ const response = await sandboxCreate(this.client, { options });
181
+ return createSandboxInstance(
182
+ this.client,
183
+ response.sandboxId,
184
+ response.status,
185
+ this.streamBaseUrl,
186
+ response.stdoutStreamId,
187
+ response.stderrStreamId
188
+ );
189
+ }
190
+ );
191
+ }
192
+
193
+ async get(sandboxId: string): Promise<SandboxInfo> {
194
+ return withSpan('agentuity.sandbox.get', { 'sandbox.id': sandboxId }, () =>
195
+ sandboxGet(this.client, { sandboxId })
196
+ );
197
+ }
198
+
199
+ async list(params?: ListSandboxesParams): Promise<ListSandboxesResponse> {
200
+ return withSpan(
201
+ 'agentuity.sandbox.list',
202
+ {
203
+ 'sandbox.status': params?.status ?? '',
204
+ 'sandbox.limit': params?.limit ?? 50,
205
+ },
206
+ () => sandboxList(this.client, params)
207
+ );
208
+ }
209
+
210
+ async destroy(sandboxId: string): Promise<void> {
211
+ return withSpan('agentuity.sandbox.destroy', { 'sandbox.id': sandboxId }, () =>
212
+ sandboxDestroy(this.client, { sandboxId })
213
+ );
214
+ }
215
+ }
@@ -0,0 +1 @@
1
+ export { HTTPSandboxService } from './http';
@@ -13,7 +13,10 @@ import { internal } from '../../logger/internal';
13
13
  const SessionResponseError = StructuredError('SessionResponseError');
14
14
 
15
15
  /**
16
- * An implementation of the SessionEventProvider which uses HTTP for delivery
16
+ * An implementation of the SessionEventProvider which uses HTTP for delivery.
17
+ *
18
+ * This provider checks that the event has required fields (orgId, projectId for start events)
19
+ * before sending to the backend. If required fields are missing, the event is silently skipped.
17
20
  */
18
21
  export class HTTPSessionEventProvider implements SessionEventProvider {
19
22
  private apiClient: APIClient;
@@ -24,12 +27,33 @@ export class HTTPSessionEventProvider implements SessionEventProvider {
24
27
  this.logger = logger;
25
28
  }
26
29
 
30
+ /**
31
+ * Check if a start event has all required fields for HTTP delivery
32
+ */
33
+ private canSendStartEvent(event: SessionStartEvent): boolean {
34
+ // orgId and projectId are required for the backend
35
+ if (!event.orgId || !event.projectId) {
36
+ internal.info(
37
+ '[session-http] skipping start event - missing required fields: orgId=%s, projectId=%s',
38
+ event.orgId ?? 'missing',
39
+ event.projectId ?? 'missing'
40
+ );
41
+ return false;
42
+ }
43
+ return true;
44
+ }
45
+
27
46
  /**
28
47
  * called when the session starts
29
48
  *
30
49
  * @param event SessionStartEvent
31
50
  */
32
51
  async start(event: SessionStartEvent): Promise<void> {
52
+ // Check required fields before sending
53
+ if (!this.canSendStartEvent(event)) {
54
+ return;
55
+ }
56
+
33
57
  internal.info('[session-http] sending start event: %s', event.id);
34
58
  this.logger.debug('Sending session start event: %s', event.id);
35
59
  const resp = await this.apiClient.post(
@@ -41,18 +65,32 @@ export class HTTPSessionEventProvider implements SessionEventProvider {
41
65
  if (resp.success) {
42
66
  internal.info('[session-http] start event sent successfully: %s', event.id);
43
67
  this.logger.debug('Session start event sent successfully: %s', event.id);
68
+ this.startedSessions.add(event.id);
44
69
  return;
45
70
  }
46
71
  internal.info('[session-http] start event failed: %s - %s', event.id, resp.message);
47
72
  throw new SessionResponseError({ message: resp.message });
48
73
  }
49
74
 
75
+ /**
76
+ * Track session IDs that have been started (to know if we should send complete)
77
+ */
78
+ private startedSessions = new Set<string>();
79
+
50
80
  /**
51
81
  * called when the session completes
52
82
  *
53
83
  * @param event SessionCompleteEvent
54
84
  */
55
85
  async complete(event: SessionCompleteEvent): Promise<void> {
86
+ // Only send complete if we successfully sent a start event
87
+ // This prevents sending orphaned complete events when start was skipped
88
+ if (!this.startedSessions.has(event.id)) {
89
+ internal.info('[session-http] skipping complete event - no matching start: %s', event.id);
90
+ return;
91
+ }
92
+ this.startedSessions.delete(event.id);
93
+
56
94
  internal.info(
57
95
  '[session-http] sending complete event: %s, userData: %s',
58
96
  event.id,
@@ -5,6 +5,7 @@ import type { Env } from '../../app';
5
5
  import {
6
6
  DefaultThread,
7
7
  DefaultThreadIDProvider,
8
+ parseThreadData,
8
9
  validateThreadIdOrThrow,
9
10
  type Thread,
10
11
  type ThreadIDProvider,
@@ -54,15 +55,17 @@ export class LocalThreadProvider implements ThreadProvider {
54
55
  const row = this.db
55
56
  .query<{ state: string }, [string]>('SELECT state FROM threads WHERE id = ?')
56
57
  .get(threadId);
57
- const initialStateJson = row?.state;
58
58
 
59
- // Create thread with restored state
60
- const thread = new DefaultThread(this, threadId, initialStateJson);
59
+ // Parse the stored data, handling both old (flat) and new ({ state, metadata }) formats
60
+ const { flatStateJson, metadata } = parseThreadData(row?.state);
61
+
62
+ // Create thread with restored state and metadata
63
+ const thread = new DefaultThread(this, threadId, flatStateJson, metadata);
61
64
 
62
65
  // Populate thread state from restored data
63
- if (initialStateJson) {
66
+ if (flatStateJson) {
64
67
  try {
65
- const data = JSON.parse(initialStateJson);
68
+ const data = JSON.parse(flatStateJson);
66
69
  for (const [key, value] of Object.entries(data)) {
67
70
  thread.state.set(key, value);
68
71
  }
package/src/session.ts CHANGED
@@ -8,6 +8,39 @@ import { getServiceUrls } from '@agentuity/server';
8
8
  import { internal } from './logger/internal';
9
9
  import { timingSafeEqual } from 'node:crypto';
10
10
 
11
+ /**
12
+ * Result of parsing serialized thread data.
13
+ * @internal
14
+ */
15
+ export interface ParsedThreadData {
16
+ flatStateJson?: string;
17
+ metadata?: Record<string, unknown>;
18
+ }
19
+
20
+ /**
21
+ * Parse serialized thread data, handling both old (flat state) and new ({ state, metadata }) formats.
22
+ * @internal
23
+ */
24
+ export function parseThreadData(raw: string | undefined): ParsedThreadData {
25
+ if (!raw) {
26
+ return {};
27
+ }
28
+
29
+ try {
30
+ const parsed = JSON.parse(raw);
31
+ if (parsed && typeof parsed === 'object' && ('state' in parsed || 'metadata' in parsed)) {
32
+ return {
33
+ flatStateJson: parsed.state ? JSON.stringify(parsed.state) : undefined,
34
+ metadata:
35
+ parsed.metadata && typeof parsed.metadata === 'object' ? parsed.metadata : undefined,
36
+ };
37
+ }
38
+ return { flatStateJson: raw };
39
+ } catch {
40
+ return { flatStateJson: raw };
41
+ }
42
+ }
43
+
11
44
  export type ThreadEventName = 'destroyed';
12
45
  export type SessionEventName = 'completed';
13
46
 
@@ -271,6 +304,10 @@ export interface ThreadIDProvider {
271
304
  * The default implementation (DefaultThreadProvider) stores threads in-memory
272
305
  * with cookie-based identification and 1-hour expiration.
273
306
  *
307
+ * Thread state is serialized using `getSerializedState()` which returns a JSON
308
+ * envelope: `{ "state": {...}, "metadata": {...} }`. Use `parseThreadData()` to
309
+ * correctly parse both old (flat) and new (envelope) formats on restore.
310
+ *
274
311
  * @example
275
312
  * ```typescript
276
313
  * class RedisThreadProvider implements ThreadProvider {
@@ -283,19 +320,29 @@ export interface ThreadIDProvider {
283
320
  * async restore(ctx: Context<Env>): Promise<Thread> {
284
321
  * const threadId = ctx.req.header('x-thread-id') || getCookie(ctx, 'atid') || generateId('thrd');
285
322
  * const data = await this.redis.get(`thread:${threadId}`);
286
- * const thread = new DefaultThread(this, threadId);
287
- * if (data) {
288
- * thread.state = new Map(JSON.parse(data));
323
+ *
324
+ * // Parse stored data, handling both old and new formats
325
+ * const { flatStateJson, metadata } = parseThreadData(data);
326
+ * const thread = new DefaultThread(this, threadId, flatStateJson, metadata);
327
+ *
328
+ * // Populate state from parsed data
329
+ * if (flatStateJson) {
330
+ * const stateObj = JSON.parse(flatStateJson);
331
+ * for (const [key, value] of Object.entries(stateObj)) {
332
+ * thread.state.set(key, value);
333
+ * }
289
334
  * }
290
335
  * return thread;
291
336
  * }
292
337
  *
293
338
  * async save(thread: Thread): Promise<void> {
294
- * await this.redis.setex(
295
- * `thread:${thread.id}`,
296
- * 3600,
297
- * JSON.stringify([...thread.state])
298
- * );
339
+ * if (thread instanceof DefaultThread && thread.isDirty()) {
340
+ * await this.redis.setex(
341
+ * `thread:${thread.id}`,
342
+ * 3600,
343
+ * thread.getSerializedState()
344
+ * );
345
+ * }
299
346
  * }
300
347
  *
301
348
  * async destroy(thread: Thread): Promise<void> {
@@ -1256,31 +1303,10 @@ export class DefaultThreadProvider implements ThreadProvider {
1256
1303
  internal.info('[thread] restoring state from WebSocket');
1257
1304
  const restoredData = await this.wsClient.restore(threadId);
1258
1305
  if (restoredData) {
1259
- initialStateJson = restoredData;
1260
1306
  internal.info('[thread] restored state: %d bytes', restoredData.length);
1261
- // Parse to check if it includes metadata
1262
- try {
1263
- const parsed = JSON.parse(restoredData);
1264
- // New format: { state?: {...}, metadata?: {...} }
1265
- if (
1266
- parsed &&
1267
- typeof parsed === 'object' &&
1268
- ('state' in parsed || 'metadata' in parsed)
1269
- ) {
1270
- if (parsed.metadata) {
1271
- restoredMetadata = parsed.metadata;
1272
- }
1273
- // Update initialStateJson to be just the state part for backwards compatibility
1274
- if (parsed.state) {
1275
- initialStateJson = JSON.stringify(parsed.state);
1276
- } else {
1277
- initialStateJson = undefined;
1278
- }
1279
- }
1280
- // else: Old format (just state object), keep as-is
1281
- } catch {
1282
- // Keep original if parse fails
1283
- }
1307
+ const { flatStateJson, metadata } = parseThreadData(restoredData);
1308
+ initialStateJson = flatStateJson;
1309
+ restoredMetadata = metadata;
1284
1310
  } else {
1285
1311
  internal.info('[thread] no existing state found');
1286
1312
  }
package/src/workbench.ts CHANGED
@@ -112,20 +112,19 @@ export const createWorkbenchExecutionRoute = (): Handler => {
112
112
  ctx.var.logger?.warn('Thread not available in workbench execution route');
113
113
  }
114
114
 
115
- // Handle cases where result might be undefined/null
116
- if (result === undefined || result === null) {
117
- return ctx.json({ success: true, result: null });
118
- }
119
-
120
- return ctx.json(result);
115
+ return ctx.json({ success: true, data: result ?? null });
121
116
  } catch (error) {
122
- return ctx.json(
123
- {
124
- error: 'Internal server error',
125
- message: error instanceof Error ? error.message : String(error),
117
+ const err = error instanceof Error ? error : new Error(String(error));
118
+ // Return 200 with wrapped error so UI can display it properly
119
+ return ctx.json({
120
+ success: false,
121
+ error: {
122
+ message: err.message,
123
+ stack: err.stack,
124
+ code: 'code' in err && typeof err.code === 'string' ? err.code : 'EXECUTION_ERROR',
125
+ cause: err.cause,
126
126
  },
127
- { status: 500 }
128
- );
127
+ });
129
128
  }
130
129
  };
131
130
  };