@agentuity/runtime 0.0.107 → 0.0.109

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 (47) 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.map +1 -1
  6. package/dist/_metadata.js +7 -0
  7. package/dist/_metadata.js.map +1 -1
  8. package/dist/_standalone.d.ts +2 -1
  9. package/dist/_standalone.d.ts.map +1 -1
  10. package/dist/_standalone.js +26 -9
  11. package/dist/_standalone.js.map +1 -1
  12. package/dist/agent.d.ts +18 -1
  13. package/dist/agent.d.ts.map +1 -1
  14. package/dist/agent.js +2 -0
  15. package/dist/agent.js.map +1 -1
  16. package/dist/bun-s3-patch.d.ts +13 -2
  17. package/dist/bun-s3-patch.d.ts.map +1 -1
  18. package/dist/bun-s3-patch.js +82 -8
  19. package/dist/bun-s3-patch.js.map +1 -1
  20. package/dist/index.d.ts +2 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +3 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/middleware.d.ts.map +1 -1
  25. package/dist/middleware.js +7 -4
  26. package/dist/middleware.js.map +1 -1
  27. package/dist/services/thread/local.d.ts.map +1 -1
  28. package/dist/services/thread/local.js +106 -25
  29. package/dist/services/thread/local.js.map +1 -1
  30. package/dist/session.d.ts +206 -27
  31. package/dist/session.d.ts.map +1 -1
  32. package/dist/session.js +386 -69
  33. package/dist/session.js.map +1 -1
  34. package/dist/workbench.d.ts.map +1 -1
  35. package/dist/workbench.js +15 -9
  36. package/dist/workbench.js.map +1 -1
  37. package/package.json +5 -5
  38. package/src/_context.ts +2 -1
  39. package/src/_metadata.ts +11 -0
  40. package/src/_standalone.ts +27 -10
  41. package/src/agent.ts +22 -1
  42. package/src/bun-s3-patch.ts +138 -10
  43. package/src/index.ts +6 -0
  44. package/src/middleware.ts +8 -4
  45. package/src/services/thread/local.ts +119 -30
  46. package/src/session.ts +599 -90
  47. package/src/workbench.ts +19 -10
@@ -4,8 +4,19 @@
4
4
  * Agentuity storage uses virtual-hosted-style URLs (e.g., ag-{id}.t3.storage.dev).
5
5
  * Bun's default s3 export uses path-style addressing, causing bucket path mismatch.
6
6
  *
7
- * This module patches Bun.S3Client.prototype.file to automatically set
7
+ * This module patches Bun.S3Client.prototype methods to automatically set
8
8
  * virtualHostedStyle: true when S3_ENDPOINT matches *.storage.dev
9
+ *
10
+ * Patched methods:
11
+ * - file(path, options?) - S3Options
12
+ * - presign(path, options?) - S3FilePresignOptions
13
+ * - write(path, data, options?) - S3Options
14
+ * - delete(path, options?) - S3Options
15
+ * - exists(path, options?) - S3Options
16
+ * - stat(path, options?) - S3Options
17
+ * - size(path, options?) - S3Options
18
+ * - unlink(path, options?) - S3Options
19
+ * - list(input?, options?) - options type doesn't include virtualHostedStyle but we inject it anyway
9
20
  */
10
21
 
11
22
  const PATCHED_SYMBOL = Symbol.for('agentuity.s3.patched');
@@ -27,13 +38,23 @@ export function isAgentuityStorageEndpoint(raw: string): boolean {
27
38
  return host === 'storage.dev' || host.endsWith('.storage.dev');
28
39
  }
29
40
 
41
+ /**
42
+ * Helper to inject virtualHostedStyle into options if not already set
43
+ */
44
+ function injectVirtualHostedStyle(options?: Record<string, unknown>): Record<string, unknown> {
45
+ if (!options || typeof options.virtualHostedStyle === 'undefined') {
46
+ return { ...options, virtualHostedStyle: true };
47
+ }
48
+ return options;
49
+ }
50
+
30
51
  /**
31
52
  * Patch Bun's S3Client to automatically use virtualHostedStyle for storage.dev endpoints
32
53
  *
33
54
  * This function:
34
55
  * 1. Checks if we're running in Bun with S3 support
35
56
  * 2. Checks if S3_ENDPOINT (or AWS_ENDPOINT) points to *.storage.dev
36
- * 3. Patches S3Client.prototype.file to inject virtualHostedStyle: true
57
+ * 3. Patches S3Client.prototype methods to inject virtualHostedStyle: true
37
58
  *
38
59
  * Safe to call in non-Bun environments (will no-op).
39
60
  * Idempotent (safe to call multiple times).
@@ -45,6 +66,21 @@ export function patchBunS3ForStorageDev(): void {
45
66
  S3Client?: {
46
67
  prototype: {
47
68
  file?: (path: string, options?: Record<string, unknown>) => unknown;
69
+ presign?: (path: string, options?: Record<string, unknown>) => unknown;
70
+ write?: (
71
+ path: string,
72
+ data: unknown,
73
+ options?: Record<string, unknown>
74
+ ) => unknown;
75
+ delete?: (path: string, options?: Record<string, unknown>) => unknown;
76
+ exists?: (path: string, options?: Record<string, unknown>) => unknown;
77
+ stat?: (path: string, options?: Record<string, unknown>) => unknown;
78
+ size?: (path: string, options?: Record<string, unknown>) => unknown;
79
+ unlink?: (path: string, options?: Record<string, unknown>) => unknown;
80
+ list?: (
81
+ input?: Record<string, unknown> | null,
82
+ options?: Record<string, unknown>
83
+ ) => unknown;
48
84
  [PATCHED_SYMBOL]?: boolean;
49
85
  };
50
86
  };
@@ -70,22 +106,114 @@ export function patchBunS3ForStorageDev(): void {
70
106
  return;
71
107
  }
72
108
 
109
+ // Patch file(path, options?)
73
110
  const originalFile = S3ClientProto.file!;
74
-
75
111
  S3ClientProto.file = function patchedFile(
76
112
  this: unknown,
77
113
  path: string,
78
114
  options?: Record<string, unknown>
79
115
  ): unknown {
80
- let nextOptions = options;
116
+ return originalFile.call(this, path, injectVirtualHostedStyle(options));
117
+ };
118
+
119
+ // Patch presign(path, options?)
120
+ if (S3ClientProto.presign) {
121
+ const originalPresign = S3ClientProto.presign;
122
+ S3ClientProto.presign = function patchedPresign(
123
+ this: unknown,
124
+ path: string,
125
+ options?: Record<string, unknown>
126
+ ): unknown {
127
+ return originalPresign.call(this, path, injectVirtualHostedStyle(options));
128
+ };
129
+ }
81
130
 
82
- // Apply virtualHostedStyle to all S3Client instances when endpoint is storage.dev
83
- if (!nextOptions || typeof nextOptions.virtualHostedStyle === 'undefined') {
84
- nextOptions = { ...nextOptions, virtualHostedStyle: true };
85
- }
131
+ // Patch write(path, data, options?)
132
+ if (S3ClientProto.write) {
133
+ const originalWrite = S3ClientProto.write;
134
+ S3ClientProto.write = function patchedWrite(
135
+ this: unknown,
136
+ path: string,
137
+ data: unknown,
138
+ options?: Record<string, unknown>
139
+ ): unknown {
140
+ return originalWrite.call(this, path, data, injectVirtualHostedStyle(options));
141
+ };
142
+ }
86
143
 
87
- return originalFile.call(this, path, nextOptions);
88
- };
144
+ // Patch delete(path, options?)
145
+ if (S3ClientProto.delete) {
146
+ const originalDelete = S3ClientProto.delete;
147
+ S3ClientProto.delete = function patchedDelete(
148
+ this: unknown,
149
+ path: string,
150
+ options?: Record<string, unknown>
151
+ ): unknown {
152
+ return originalDelete.call(this, path, injectVirtualHostedStyle(options));
153
+ };
154
+ }
155
+
156
+ // Patch exists(path, options?)
157
+ if (S3ClientProto.exists) {
158
+ const originalExists = S3ClientProto.exists;
159
+ S3ClientProto.exists = function patchedExists(
160
+ this: unknown,
161
+ path: string,
162
+ options?: Record<string, unknown>
163
+ ): unknown {
164
+ return originalExists.call(this, path, injectVirtualHostedStyle(options));
165
+ };
166
+ }
167
+
168
+ // Patch stat(path, options?)
169
+ if (S3ClientProto.stat) {
170
+ const originalStat = S3ClientProto.stat;
171
+ S3ClientProto.stat = function patchedStat(
172
+ this: unknown,
173
+ path: string,
174
+ options?: Record<string, unknown>
175
+ ): unknown {
176
+ return originalStat.call(this, path, injectVirtualHostedStyle(options));
177
+ };
178
+ }
179
+
180
+ // Patch size(path, options?)
181
+ if (S3ClientProto.size) {
182
+ const originalSize = S3ClientProto.size;
183
+ S3ClientProto.size = function patchedSize(
184
+ this: unknown,
185
+ path: string,
186
+ options?: Record<string, unknown>
187
+ ): unknown {
188
+ return originalSize.call(this, path, injectVirtualHostedStyle(options));
189
+ };
190
+ }
191
+
192
+ // Patch unlink(path, options?)
193
+ if (S3ClientProto.unlink) {
194
+ const originalUnlink = S3ClientProto.unlink;
195
+ S3ClientProto.unlink = function patchedUnlink(
196
+ this: unknown,
197
+ path: string,
198
+ options?: Record<string, unknown>
199
+ ): unknown {
200
+ return originalUnlink.call(this, path, injectVirtualHostedStyle(options));
201
+ };
202
+ }
203
+
204
+ // Patch list(input?, options?)
205
+ // Note: The TypeScript type for list's options doesn't include virtualHostedStyle,
206
+ // but we inject it anyway as the underlying implementation may still use it
207
+ if (S3ClientProto.list) {
208
+ const originalList = S3ClientProto.list;
209
+ S3ClientProto.list = function patchedList(
210
+ this: unknown,
211
+ input?: Record<string, unknown> | null,
212
+ options?: Record<string, unknown>
213
+ ): unknown {
214
+ return originalList.call(this, input, injectVirtualHostedStyle(options));
215
+ };
216
+ }
89
217
 
90
218
  S3ClientProto[PATCHED_SYMBOL] = true;
91
219
  }
package/src/index.ts CHANGED
@@ -100,6 +100,8 @@ export {
100
100
  export {
101
101
  type ThreadEventName,
102
102
  type SessionEventName,
103
+ type ThreadState,
104
+ type MergeOperation,
103
105
  type Thread,
104
106
  type Session,
105
107
  type ThreadIDProvider,
@@ -108,6 +110,7 @@ export {
108
110
  generateId,
109
111
  DefaultThreadIDProvider,
110
112
  DefaultThread,
113
+ LazyThreadState,
111
114
  } from './session';
112
115
 
113
116
  // services/thread/local exports
@@ -166,6 +169,9 @@ export {
166
169
  CompositeEvalRunEventProvider,
167
170
  } from './services/evalrun';
168
171
 
172
+ // for loading metadata
173
+ export { loadBuildMetadata } from './_metadata';
174
+
169
175
  // _services.ts exports
170
176
  export { getEvalRunEventProvider, getThreadProvider, getSessionProvider } from './_services';
171
177
 
package/src/middleware.ts CHANGED
@@ -113,6 +113,12 @@ export function createBaseMiddleware(config: MiddlewareConfig) {
113
113
  const endTime = performance.now();
114
114
  const duration = ((endTime - started) / 1000).toFixed(1);
115
115
  c.header(DURATION_HEADER, `${duration}s`);
116
+
117
+ // Set deployment header for all routes
118
+ const deploymentId = runtimeConfig.getDeploymentId();
119
+ if (deploymentId) {
120
+ c.header(DEPLOYMENT_HEADER, deploymentId);
121
+ }
116
122
  }
117
123
 
118
124
  if (!skipLogging && !isWebSocket) {
@@ -336,9 +342,10 @@ export function createOtelMiddleware() {
336
342
  const agentIdsSet = (c as any).get('agentIds') as Set<string> | undefined;
337
343
  const agentIds = agentIdsSet ? [...agentIdsSet].filter(Boolean) : undefined;
338
344
  internal.info('[session] agentIds: %o', agentIds);
345
+ const isEmpty = await thread.empty();
339
346
  await sessionEventProvider.complete({
340
347
  id: sessionId,
341
- threadId: thread.empty() ? null : thread.id,
348
+ threadId: isEmpty ? null : thread.id,
342
349
  statusCode: c.res?.status ?? 200,
343
350
  agentIds: agentIds?.length ? agentIds : undefined,
344
351
  userData,
@@ -357,9 +364,6 @@ export function createOtelMiddleware() {
357
364
  }
358
365
  const traceId = sctx?.traceId || sessionId.replace(/^sess_/, '');
359
366
  c.header(SESSION_HEADER, `sess_${traceId}`);
360
- if (deploymentId) {
361
- c.header(DEPLOYMENT_HEADER, deploymentId);
362
- }
363
367
  span.end();
364
368
  }
365
369
  }
@@ -51,30 +51,37 @@ export class LocalThreadProvider implements ThreadProvider {
51
51
  const threadId = await this.threadIDProvider.getThreadId(this.appState, ctx);
52
52
  validateThreadIdOrThrow(threadId);
53
53
 
54
- // Try to restore state from DB
55
- const row = this.db
56
- .query<{ state: string }, [string]>('SELECT state FROM threads WHERE id = ?')
57
- .get(threadId);
58
-
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);
64
-
65
- // Populate thread state from restored data
66
- if (flatStateJson) {
67
- try {
68
- const data = JSON.parse(flatStateJson);
69
- for (const [key, value] of Object.entries(data)) {
70
- thread.state.set(key, value);
54
+ // Create a restore function for lazy loading
55
+ const restoreFn = async (): Promise<{
56
+ state: Map<string, unknown>;
57
+ metadata: Record<string, unknown>;
58
+ }> => {
59
+ if (!this.db) {
60
+ return { state: new Map(), metadata: {} };
61
+ }
62
+
63
+ const row = this.db
64
+ .query<{ state: string }, [string]>('SELECT state FROM threads WHERE id = ?')
65
+ .get(threadId);
66
+
67
+ const { flatStateJson, metadata } = parseThreadData(row?.state);
68
+
69
+ const state = new Map<string, unknown>();
70
+ if (flatStateJson) {
71
+ try {
72
+ const data = JSON.parse(flatStateJson);
73
+ for (const [key, value] of Object.entries(data)) {
74
+ state.set(key, value);
75
+ }
76
+ } catch {
77
+ // Continue with empty state if parsing fails
71
78
  }
72
- } catch {
73
- // Continue with empty state if parsing fails
74
79
  }
75
- }
76
80
 
77
- return thread;
81
+ return { state, metadata: metadata || {} };
82
+ };
83
+
84
+ return new DefaultThread(this, threadId, restoreFn);
78
85
  }
79
86
 
80
87
  async save(thread: Thread): Promise<void> {
@@ -82,20 +89,102 @@ export class LocalThreadProvider implements ThreadProvider {
82
89
  return;
83
90
  }
84
91
 
85
- // Only save if state was modified
86
- if (!thread.isDirty()) {
92
+ const saveMode = thread.getSaveMode();
93
+ if (saveMode === 'none') {
87
94
  return;
88
95
  }
89
96
 
90
- const stateJson = thread.getSerializedState();
91
97
  const now = Date.now();
92
98
 
93
- // Upsert thread state
94
- this.db.run(
95
- `INSERT INTO threads (id, state, updated_at) VALUES (?, ?, ?)
96
- ON CONFLICT(id) DO UPDATE SET state = ?, updated_at = ?`,
97
- [thread.id, stateJson, now, stateJson, now]
98
- );
99
+ if (saveMode === 'merge') {
100
+ // For merge, we need to load existing state, apply operations, then save
101
+ const operations = thread.getPendingOperations();
102
+ const metadata = thread.getMetadataForSave();
103
+
104
+ // Load existing state
105
+ const row = this.db
106
+ .query<{ state: string }, [string]>('SELECT state FROM threads WHERE id = ?')
107
+ .get(thread.id);
108
+
109
+ const { flatStateJson, metadata: existingMetadata } = parseThreadData(row?.state);
110
+
111
+ const state: Record<string, unknown> = {};
112
+ if (flatStateJson) {
113
+ try {
114
+ Object.assign(state, JSON.parse(flatStateJson));
115
+ } catch {
116
+ // Continue with empty state if parsing fails
117
+ }
118
+ }
119
+
120
+ // Apply operations
121
+ for (const op of operations) {
122
+ switch (op.op) {
123
+ case 'clear':
124
+ for (const key of Object.keys(state)) {
125
+ delete state[key];
126
+ }
127
+ break;
128
+ case 'set':
129
+ if (op.key !== undefined) {
130
+ state[op.key] = op.value;
131
+ }
132
+ break;
133
+ case 'delete':
134
+ if (op.key !== undefined) {
135
+ delete state[op.key];
136
+ }
137
+ break;
138
+ case 'push':
139
+ if (op.key !== undefined) {
140
+ const existing = state[op.key];
141
+ let arr: unknown[];
142
+ if (Array.isArray(existing)) {
143
+ existing.push(op.value);
144
+ arr = existing;
145
+ } else if (existing === undefined) {
146
+ arr = [op.value];
147
+ state[op.key] = arr;
148
+ } else {
149
+ // If non-array, silently skip
150
+ continue;
151
+ }
152
+ // Apply maxRecords limit
153
+ if (op.maxRecords !== undefined && arr.length > op.maxRecords) {
154
+ state[op.key] = arr.slice(arr.length - op.maxRecords);
155
+ }
156
+ }
157
+ break;
158
+ }
159
+ }
160
+
161
+ // Build final data
162
+ const finalMetadata = metadata || existingMetadata || {};
163
+ const hasState = Object.keys(state).length > 0;
164
+ const hasMetadata = Object.keys(finalMetadata).length > 0;
165
+
166
+ let stateJson = '';
167
+ if (hasState || hasMetadata) {
168
+ const data: { state?: Record<string, unknown>; metadata?: Record<string, unknown> } = {};
169
+ if (hasState) data.state = state;
170
+ if (hasMetadata) data.metadata = finalMetadata;
171
+ stateJson = JSON.stringify(data);
172
+ }
173
+
174
+ this.db.run(
175
+ `INSERT INTO threads (id, state, updated_at) VALUES (?, ?, ?)
176
+ ON CONFLICT(id) DO UPDATE SET state = ?, updated_at = ?`,
177
+ [thread.id, stateJson, now, stateJson, now]
178
+ );
179
+ } else {
180
+ // Full save
181
+ const stateJson = await thread.getSerializedState();
182
+ this.db.run(
183
+ `INSERT INTO threads (id, state, updated_at) VALUES (?, ?, ?)
184
+ ON CONFLICT(id) DO UPDATE SET state = ?, updated_at = ?`,
185
+ [thread.id, stateJson, now, stateJson, now]
186
+ );
187
+ }
99
188
  }
100
189
 
101
190
  async destroy(thread: Thread): Promise<void> {