@agentuity/runtime 0.1.13 → 0.1.14

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 (272) hide show
  1. package/package.json +8 -8
  2. package/src/_config.ts +13 -0
  3. package/src/_standalone.ts +167 -19
  4. package/src/index.ts +1 -0
  5. package/dist/_config.d.ts +0 -90
  6. package/dist/_config.d.ts.map +0 -1
  7. package/dist/_config.js +0 -135
  8. package/dist/_config.js.map +0 -1
  9. package/dist/_context.d.ts +0 -76
  10. package/dist/_context.d.ts.map +0 -1
  11. package/dist/_context.js +0 -147
  12. package/dist/_context.js.map +0 -1
  13. package/dist/_events.d.ts +0 -64
  14. package/dist/_events.d.ts.map +0 -1
  15. package/dist/_events.js +0 -92
  16. package/dist/_events.js.map +0 -1
  17. package/dist/_idle.d.ts +0 -7
  18. package/dist/_idle.d.ts.map +0 -1
  19. package/dist/_idle.js +0 -10
  20. package/dist/_idle.js.map +0 -1
  21. package/dist/_metadata.d.ts +0 -117
  22. package/dist/_metadata.d.ts.map +0 -1
  23. package/dist/_metadata.js +0 -246
  24. package/dist/_metadata.js.map +0 -1
  25. package/dist/_process-protection.d.ts +0 -25
  26. package/dist/_process-protection.d.ts.map +0 -1
  27. package/dist/_process-protection.js +0 -65
  28. package/dist/_process-protection.js.map +0 -1
  29. package/dist/_server.d.ts +0 -46
  30. package/dist/_server.d.ts.map +0 -1
  31. package/dist/_server.js +0 -85
  32. package/dist/_server.js.map +0 -1
  33. package/dist/_services.d.ts +0 -21
  34. package/dist/_services.d.ts.map +0 -1
  35. package/dist/_services.js +0 -248
  36. package/dist/_services.js.map +0 -1
  37. package/dist/_standalone.d.ts +0 -168
  38. package/dist/_standalone.d.ts.map +0 -1
  39. package/dist/_standalone.js +0 -441
  40. package/dist/_standalone.js.map +0 -1
  41. package/dist/_tokens.d.ts +0 -12
  42. package/dist/_tokens.d.ts.map +0 -1
  43. package/dist/_tokens.js +0 -96
  44. package/dist/_tokens.js.map +0 -1
  45. package/dist/_util.d.ts +0 -16
  46. package/dist/_util.d.ts.map +0 -1
  47. package/dist/_util.js +0 -54
  48. package/dist/_util.js.map +0 -1
  49. package/dist/_validation.d.ts +0 -89
  50. package/dist/_validation.d.ts.map +0 -1
  51. package/dist/_validation.js +0 -29
  52. package/dist/_validation.js.map +0 -1
  53. package/dist/_waituntil.d.ts +0 -18
  54. package/dist/_waituntil.d.ts.map +0 -1
  55. package/dist/_waituntil.js +0 -97
  56. package/dist/_waituntil.js.map +0 -1
  57. package/dist/agent.d.ts +0 -1210
  58. package/dist/agent.d.ts.map +0 -1
  59. package/dist/agent.js +0 -903
  60. package/dist/agent.js.map +0 -1
  61. package/dist/app.d.ts +0 -322
  62. package/dist/app.d.ts.map +0 -1
  63. package/dist/app.js +0 -160
  64. package/dist/app.js.map +0 -1
  65. package/dist/bun-s3-patch.d.ts +0 -37
  66. package/dist/bun-s3-patch.d.ts.map +0 -1
  67. package/dist/bun-s3-patch.js +0 -139
  68. package/dist/bun-s3-patch.js.map +0 -1
  69. package/dist/cors.d.ts +0 -42
  70. package/dist/cors.d.ts.map +0 -1
  71. package/dist/cors.js +0 -117
  72. package/dist/cors.js.map +0 -1
  73. package/dist/devmode.d.ts +0 -3
  74. package/dist/devmode.d.ts.map +0 -1
  75. package/dist/devmode.js +0 -167
  76. package/dist/devmode.js.map +0 -1
  77. package/dist/eval.d.ts +0 -91
  78. package/dist/eval.d.ts.map +0 -1
  79. package/dist/eval.js +0 -16
  80. package/dist/eval.js.map +0 -1
  81. package/dist/handlers/cron.d.ts +0 -47
  82. package/dist/handlers/cron.d.ts.map +0 -1
  83. package/dist/handlers/cron.js +0 -49
  84. package/dist/handlers/cron.js.map +0 -1
  85. package/dist/handlers/index.d.ts +0 -5
  86. package/dist/handlers/index.d.ts.map +0 -1
  87. package/dist/handlers/index.js +0 -5
  88. package/dist/handlers/index.js.map +0 -1
  89. package/dist/handlers/sse.d.ts +0 -91
  90. package/dist/handlers/sse.d.ts.map +0 -1
  91. package/dist/handlers/sse.js +0 -213
  92. package/dist/handlers/sse.js.map +0 -1
  93. package/dist/handlers/stream.d.ts +0 -52
  94. package/dist/handlers/stream.d.ts.map +0 -1
  95. package/dist/handlers/stream.js +0 -116
  96. package/dist/handlers/stream.js.map +0 -1
  97. package/dist/handlers/websocket.d.ts +0 -49
  98. package/dist/handlers/websocket.d.ts.map +0 -1
  99. package/dist/handlers/websocket.js +0 -143
  100. package/dist/handlers/websocket.js.map +0 -1
  101. package/dist/index.d.ts +0 -71
  102. package/dist/index.d.ts.map +0 -1
  103. package/dist/index.js +0 -58
  104. package/dist/index.js.map +0 -1
  105. package/dist/logger/console.d.ts +0 -70
  106. package/dist/logger/console.d.ts.map +0 -1
  107. package/dist/logger/console.js +0 -274
  108. package/dist/logger/console.js.map +0 -1
  109. package/dist/logger/index.d.ts +0 -3
  110. package/dist/logger/index.d.ts.map +0 -1
  111. package/dist/logger/index.js +0 -3
  112. package/dist/logger/index.js.map +0 -1
  113. package/dist/logger/internal.d.ts +0 -79
  114. package/dist/logger/internal.d.ts.map +0 -1
  115. package/dist/logger/internal.js +0 -133
  116. package/dist/logger/internal.js.map +0 -1
  117. package/dist/logger/logger.d.ts +0 -41
  118. package/dist/logger/logger.d.ts.map +0 -1
  119. package/dist/logger/logger.js +0 -2
  120. package/dist/logger/logger.js.map +0 -1
  121. package/dist/logger/user.d.ts +0 -8
  122. package/dist/logger/user.d.ts.map +0 -1
  123. package/dist/logger/user.js +0 -7
  124. package/dist/logger/user.js.map +0 -1
  125. package/dist/logger/util.d.ts +0 -11
  126. package/dist/logger/util.d.ts.map +0 -1
  127. package/dist/logger/util.js +0 -77
  128. package/dist/logger/util.js.map +0 -1
  129. package/dist/middleware.d.ts +0 -112
  130. package/dist/middleware.d.ts.map +0 -1
  131. package/dist/middleware.js +0 -507
  132. package/dist/middleware.js.map +0 -1
  133. package/dist/otel/config.d.ts +0 -19
  134. package/dist/otel/config.d.ts.map +0 -1
  135. package/dist/otel/config.js +0 -26
  136. package/dist/otel/config.js.map +0 -1
  137. package/dist/otel/console.d.ts +0 -33
  138. package/dist/otel/console.d.ts.map +0 -1
  139. package/dist/otel/console.js +0 -86
  140. package/dist/otel/console.js.map +0 -1
  141. package/dist/otel/exporters/index.d.ts +0 -4
  142. package/dist/otel/exporters/index.d.ts.map +0 -1
  143. package/dist/otel/exporters/index.js +0 -4
  144. package/dist/otel/exporters/index.js.map +0 -1
  145. package/dist/otel/exporters/jsonl-log-exporter.d.ts +0 -36
  146. package/dist/otel/exporters/jsonl-log-exporter.d.ts.map +0 -1
  147. package/dist/otel/exporters/jsonl-log-exporter.js +0 -103
  148. package/dist/otel/exporters/jsonl-log-exporter.js.map +0 -1
  149. package/dist/otel/exporters/jsonl-metric-exporter.d.ts +0 -40
  150. package/dist/otel/exporters/jsonl-metric-exporter.d.ts.map +0 -1
  151. package/dist/otel/exporters/jsonl-metric-exporter.js +0 -104
  152. package/dist/otel/exporters/jsonl-metric-exporter.js.map +0 -1
  153. package/dist/otel/exporters/jsonl-trace-exporter.d.ts +0 -36
  154. package/dist/otel/exporters/jsonl-trace-exporter.d.ts.map +0 -1
  155. package/dist/otel/exporters/jsonl-trace-exporter.js +0 -111
  156. package/dist/otel/exporters/jsonl-trace-exporter.js.map +0 -1
  157. package/dist/otel/fetch.d.ts +0 -12
  158. package/dist/otel/fetch.d.ts.map +0 -1
  159. package/dist/otel/fetch.js +0 -82
  160. package/dist/otel/fetch.js.map +0 -1
  161. package/dist/otel/http.d.ts +0 -16
  162. package/dist/otel/http.d.ts.map +0 -1
  163. package/dist/otel/http.js +0 -44
  164. package/dist/otel/http.js.map +0 -1
  165. package/dist/otel/logger.d.ts +0 -37
  166. package/dist/otel/logger.d.ts.map +0 -1
  167. package/dist/otel/logger.js +0 -268
  168. package/dist/otel/logger.js.map +0 -1
  169. package/dist/otel/otel.d.ts +0 -65
  170. package/dist/otel/otel.d.ts.map +0 -1
  171. package/dist/otel/otel.js +0 -261
  172. package/dist/otel/otel.js.map +0 -1
  173. package/dist/router.d.ts +0 -100
  174. package/dist/router.d.ts.map +0 -1
  175. package/dist/router.js +0 -163
  176. package/dist/router.js.map +0 -1
  177. package/dist/services/evalrun/composite.d.ts +0 -21
  178. package/dist/services/evalrun/composite.d.ts.map +0 -1
  179. package/dist/services/evalrun/composite.js +0 -26
  180. package/dist/services/evalrun/composite.js.map +0 -1
  181. package/dist/services/evalrun/http.d.ts +0 -24
  182. package/dist/services/evalrun/http.d.ts.map +0 -1
  183. package/dist/services/evalrun/http.js +0 -86
  184. package/dist/services/evalrun/http.js.map +0 -1
  185. package/dist/services/evalrun/index.d.ts +0 -5
  186. package/dist/services/evalrun/index.d.ts.map +0 -1
  187. package/dist/services/evalrun/index.js +0 -5
  188. package/dist/services/evalrun/index.js.map +0 -1
  189. package/dist/services/evalrun/json.d.ts +0 -21
  190. package/dist/services/evalrun/json.d.ts.map +0 -1
  191. package/dist/services/evalrun/json.js +0 -38
  192. package/dist/services/evalrun/json.js.map +0 -1
  193. package/dist/services/evalrun/local.d.ts +0 -19
  194. package/dist/services/evalrun/local.d.ts.map +0 -1
  195. package/dist/services/evalrun/local.js +0 -22
  196. package/dist/services/evalrun/local.js.map +0 -1
  197. package/dist/services/local/_db.d.ts +0 -4
  198. package/dist/services/local/_db.d.ts.map +0 -1
  199. package/dist/services/local/_db.js +0 -123
  200. package/dist/services/local/_db.js.map +0 -1
  201. package/dist/services/local/_router.d.ts +0 -3
  202. package/dist/services/local/_router.d.ts.map +0 -1
  203. package/dist/services/local/_router.js +0 -28
  204. package/dist/services/local/_router.js.map +0 -1
  205. package/dist/services/local/_util.d.ts +0 -18
  206. package/dist/services/local/_util.d.ts.map +0 -1
  207. package/dist/services/local/_util.js +0 -44
  208. package/dist/services/local/_util.js.map +0 -1
  209. package/dist/services/local/index.d.ts +0 -7
  210. package/dist/services/local/index.d.ts.map +0 -1
  211. package/dist/services/local/index.js +0 -7
  212. package/dist/services/local/index.js.map +0 -1
  213. package/dist/services/local/keyvalue.d.ts +0 -17
  214. package/dist/services/local/keyvalue.d.ts.map +0 -1
  215. package/dist/services/local/keyvalue.js +0 -125
  216. package/dist/services/local/keyvalue.js.map +0 -1
  217. package/dist/services/local/stream.d.ts +0 -12
  218. package/dist/services/local/stream.d.ts.map +0 -1
  219. package/dist/services/local/stream.js +0 -262
  220. package/dist/services/local/stream.js.map +0 -1
  221. package/dist/services/local/vector.d.ts +0 -17
  222. package/dist/services/local/vector.d.ts.map +0 -1
  223. package/dist/services/local/vector.js +0 -303
  224. package/dist/services/local/vector.js.map +0 -1
  225. package/dist/services/sandbox/http.d.ts +0 -13
  226. package/dist/services/sandbox/http.d.ts.map +0 -1
  227. package/dist/services/sandbox/http.js +0 -130
  228. package/dist/services/sandbox/http.js.map +0 -1
  229. package/dist/services/sandbox/index.d.ts +0 -2
  230. package/dist/services/sandbox/index.d.ts.map +0 -1
  231. package/dist/services/sandbox/index.js +0 -2
  232. package/dist/services/sandbox/index.js.map +0 -1
  233. package/dist/services/session/composite.d.ts +0 -21
  234. package/dist/services/session/composite.d.ts.map +0 -1
  235. package/dist/services/session/composite.js +0 -26
  236. package/dist/services/session/composite.js.map +0 -1
  237. package/dist/services/session/http.d.ts +0 -34
  238. package/dist/services/session/http.d.ts.map +0 -1
  239. package/dist/services/session/http.js +0 -80
  240. package/dist/services/session/http.js.map +0 -1
  241. package/dist/services/session/index.d.ts +0 -5
  242. package/dist/services/session/index.d.ts.map +0 -1
  243. package/dist/services/session/index.js +0 -5
  244. package/dist/services/session/index.js.map +0 -1
  245. package/dist/services/session/json.d.ts +0 -22
  246. package/dist/services/session/json.d.ts.map +0 -1
  247. package/dist/services/session/json.js +0 -35
  248. package/dist/services/session/json.js.map +0 -1
  249. package/dist/services/session/local.d.ts +0 -19
  250. package/dist/services/session/local.d.ts.map +0 -1
  251. package/dist/services/session/local.js +0 -23
  252. package/dist/services/session/local.js.map +0 -1
  253. package/dist/services/thread/local.d.ts +0 -20
  254. package/dist/services/thread/local.d.ts.map +0 -1
  255. package/dist/services/thread/local.js +0 -158
  256. package/dist/services/thread/local.js.map +0 -1
  257. package/dist/session.d.ts +0 -734
  258. package/dist/session.d.ts.map +0 -1
  259. package/dist/session.js +0 -1139
  260. package/dist/session.js.map +0 -1
  261. package/dist/validator.d.ts +0 -142
  262. package/dist/validator.d.ts.map +0 -1
  263. package/dist/validator.js +0 -149
  264. package/dist/validator.js.map +0 -1
  265. package/dist/web.d.ts +0 -8
  266. package/dist/web.d.ts.map +0 -1
  267. package/dist/web.js +0 -66
  268. package/dist/web.js.map +0 -1
  269. package/dist/workbench.d.ts +0 -17
  270. package/dist/workbench.d.ts.map +0 -1
  271. package/dist/workbench.js +0 -507
  272. package/dist/workbench.js.map +0 -1
package/dist/session.js DELETED
@@ -1,1139 +0,0 @@
1
- import { getSignedCookie, setSignedCookie } from 'hono/cookie';
2
- import { fireEvent } from './app';
3
- import { getServiceUrls } from '@agentuity/server';
4
- import { internal } from './logger/internal';
5
- import { timingSafeEqual } from 'node:crypto';
6
- /**
7
- * Parse serialized thread data, handling both old (flat state) and new ({ state, metadata }) formats.
8
- * @internal
9
- */
10
- export function parseThreadData(raw) {
11
- if (!raw) {
12
- return {};
13
- }
14
- try {
15
- const parsed = JSON.parse(raw);
16
- if (parsed && typeof parsed === 'object' && ('state' in parsed || 'metadata' in parsed)) {
17
- return {
18
- flatStateJson: parsed.state ? JSON.stringify(parsed.state) : undefined,
19
- metadata: parsed.metadata && typeof parsed.metadata === 'object' ? parsed.metadata : undefined,
20
- };
21
- }
22
- return { flatStateJson: raw };
23
- }
24
- catch {
25
- return { flatStateJson: raw };
26
- }
27
- }
28
- // WeakMap to store event listeners for Thread and Session instances
29
- const threadEventListeners = new WeakMap();
30
- const sessionEventListeners = new WeakMap();
31
- // Helper to fire thread event listeners
32
- async function fireThreadEvent(thread, eventName) {
33
- const listeners = threadEventListeners.get(thread);
34
- if (!listeners)
35
- return;
36
- const callbacks = listeners.get(eventName);
37
- if (!callbacks || callbacks.size === 0)
38
- return;
39
- for (const callback of callbacks) {
40
- try {
41
- await callback(eventName, thread);
42
- }
43
- catch (error) {
44
- // Log but don't re-throw - event listener errors should not crash the server
45
- internal.error(`Error in thread event listener for '${eventName}':`, error);
46
- }
47
- }
48
- }
49
- // Helper to fire session event listeners
50
- async function fireSessionEvent(session, eventName) {
51
- const listeners = sessionEventListeners.get(session);
52
- if (!listeners)
53
- return;
54
- const callbacks = listeners.get(eventName);
55
- if (!callbacks || callbacks.size === 0)
56
- return;
57
- for (const callback of callbacks) {
58
- try {
59
- await callback(eventName, session);
60
- }
61
- catch (error) {
62
- // Log but don't re-throw - event listener errors should not crash the server
63
- internal.error(`Error in session event listener for '${eventName}':`, error);
64
- }
65
- }
66
- }
67
- // Generate thread or session ID
68
- export function generateId(prefix) {
69
- const arr = new Uint8Array(16);
70
- crypto.getRandomValues(arr);
71
- return `${prefix}${prefix ? '_' : ''}${arr.toHex()}`;
72
- }
73
- /**
74
- * Validates a thread ID against runtime constraints:
75
- * - Must start with 'thrd_'
76
- * - Must be at least 32 characters long (including prefix)
77
- * - Must be less than 64 characters long
78
- * - Must contain only [a-zA-Z0-9] after 'thrd_' prefix (no dashes for maximum randomness)
79
- */
80
- export function isValidThreadId(threadId) {
81
- if (!threadId.startsWith('thrd_')) {
82
- return false;
83
- }
84
- if (threadId.length < 32 || threadId.length > 64) {
85
- return false;
86
- }
87
- const validThreadIdCharacters = /^[a-zA-Z0-9]+$/;
88
- if (!validThreadIdCharacters.test(threadId.substring(5))) {
89
- return false;
90
- }
91
- return true;
92
- }
93
- /**
94
- * Validates a thread ID and throws detailed error messages for debugging.
95
- * @param threadId The thread ID to validate
96
- * @throws Error with detailed message if validation fails
97
- */
98
- export function validateThreadIdOrThrow(threadId) {
99
- if (!threadId) {
100
- throw new Error(`the ThreadIDProvider returned an empty thread id for getThreadId`);
101
- }
102
- if (!threadId.startsWith('thrd_')) {
103
- throw new Error(`the ThreadIDProvider returned an invalid thread id (${threadId}) for getThreadId. The thread id must start with the prefix 'thrd_'.`);
104
- }
105
- if (threadId.length > 64) {
106
- throw new Error(`the ThreadIDProvider returned an invalid thread id (${threadId}) for getThreadId. The thread id must be less than 64 characters long.`);
107
- }
108
- if (threadId.length < 32) {
109
- throw new Error(`the ThreadIDProvider returned an invalid thread id (${threadId}) for getThreadId. The thread id must be at least 32 characters long.`);
110
- }
111
- const validThreadIdCharacters = /^[a-zA-Z0-9]+$/;
112
- if (!validThreadIdCharacters.test(threadId.substring(5))) {
113
- throw new Error(`the ThreadIDProvider returned an invalid thread id (${threadId}) for getThreadId. The thread id must contain only characters that match the regular expression [a-zA-Z0-9].`);
114
- }
115
- }
116
- /**
117
- * Determines if the connection is secure (HTTPS) by checking the request protocol
118
- * and x-forwarded-proto header (for reverse proxy scenarios).
119
- * Defaults to false (HTTP) if unable to determine.
120
- */
121
- export function isSecureConnection(ctx) {
122
- // Check x-forwarded-proto header first (reverse proxy)
123
- const forwardedProto = ctx.req.header('x-forwarded-proto');
124
- if (forwardedProto) {
125
- return forwardedProto === 'https';
126
- }
127
- // Check the request URL protocol if available
128
- try {
129
- if (ctx.req.url) {
130
- const url = new URL(ctx.req.url);
131
- return url.protocol === 'https:';
132
- }
133
- }
134
- catch {
135
- // Fall through to default
136
- }
137
- // Default to HTTP (e.g., for localhost development)
138
- return false;
139
- }
140
- /**
141
- * Signs a thread ID using HMAC SHA-256 and returns it in the format: threadId;signature
142
- * Format: thrd_abc123;base64signature
143
- */
144
- export async function signThreadId(threadId, secret) {
145
- const hasher = new Bun.CryptoHasher('sha256', secret);
146
- hasher.update(threadId);
147
- const signatureBase64 = hasher.digest('base64');
148
- return `${threadId};${signatureBase64}`;
149
- }
150
- /**
151
- * Verifies a signed thread ID header and returns the thread ID if valid, or undefined if invalid.
152
- * Expected format: thrd_abc123;base64signature
153
- */
154
- export async function verifySignedThreadId(signedValue, secret) {
155
- const parts = signedValue.split(';');
156
- if (parts.length !== 2) {
157
- return undefined;
158
- }
159
- const [threadId, providedSignature] = parts;
160
- // Validate both parts exist
161
- if (!threadId || !providedSignature) {
162
- return undefined;
163
- }
164
- // Validate thread ID format before verifying signature
165
- if (!isValidThreadId(threadId)) {
166
- return undefined;
167
- }
168
- // Re-sign the thread ID and compare signatures
169
- const expectedSigned = await signThreadId(threadId, secret);
170
- const expectedSignature = expectedSigned.split(';')[1];
171
- // Validate signature exists
172
- if (!expectedSignature) {
173
- return undefined;
174
- }
175
- // Constant-time comparison to prevent timing attacks
176
- // Check lengths match first (fail fast if different lengths)
177
- if (providedSignature.length !== expectedSignature.length) {
178
- return undefined;
179
- }
180
- try {
181
- // Convert to Buffers for constant-time comparison
182
- const providedBuffer = Buffer.from(providedSignature, 'base64');
183
- const expectedBuffer = Buffer.from(expectedSignature, 'base64');
184
- if (timingSafeEqual(providedBuffer, expectedBuffer)) {
185
- return threadId;
186
- }
187
- }
188
- catch {
189
- // Comparison failed or buffer conversion error
190
- return undefined;
191
- }
192
- return undefined;
193
- }
194
- /**
195
- * DefaultThreadIDProvider will look for an HTTP header `x-thread-id` first,
196
- * then fall back to a signed cookie named `atid`, and use that as the thread id.
197
- * If not found, generate a new one. Validates incoming thread IDs against
198
- * runtime constraints. Uses AGENTUITY_SDK_KEY for signing, falls back to 'agentuity'.
199
- */
200
- export class DefaultThreadIDProvider {
201
- getSecret() {
202
- return process.env.AGENTUITY_SDK_KEY || 'agentuity';
203
- }
204
- async getThreadId(_appState, ctx) {
205
- let threadId;
206
- const secret = this.getSecret();
207
- // Check signed header first
208
- const headerValue = ctx.req.header('x-thread-id');
209
- if (headerValue) {
210
- const verifiedThreadId = await verifySignedThreadId(headerValue, secret);
211
- if (verifiedThreadId) {
212
- threadId = verifiedThreadId;
213
- }
214
- }
215
- // Fall back to signed cookie
216
- if (!threadId) {
217
- const cookieValue = await getSignedCookie(ctx, secret, 'atid');
218
- if (cookieValue && typeof cookieValue === 'string' && isValidThreadId(cookieValue)) {
219
- threadId = cookieValue;
220
- }
221
- }
222
- threadId = threadId || generateId('thrd');
223
- await setSignedCookie(ctx, 'atid', threadId, secret, {
224
- httpOnly: true,
225
- secure: isSecureConnection(ctx),
226
- sameSite: 'Lax',
227
- path: '/',
228
- maxAge: 604800, // 1 week in seconds
229
- });
230
- // Set signed header in response
231
- const signedHeader = await signThreadId(threadId, secret);
232
- ctx.header('x-thread-id', signedHeader);
233
- return threadId;
234
- }
235
- }
236
- export class LazyThreadState {
237
- #status = 'idle';
238
- #state = new Map();
239
- #pendingOperations = [];
240
- #initialStateJson;
241
- #restoreFn;
242
- #loadingPromise = null;
243
- constructor(restoreFn) {
244
- this.#restoreFn = restoreFn;
245
- }
246
- get loaded() {
247
- return this.#status === 'loaded';
248
- }
249
- get dirty() {
250
- if (this.#status === 'pending-writes') {
251
- return this.#pendingOperations.length > 0;
252
- }
253
- if (this.#status === 'loaded') {
254
- const currentJson = JSON.stringify(Object.fromEntries(this.#state));
255
- return currentJson !== this.#initialStateJson;
256
- }
257
- return false;
258
- }
259
- async ensureLoaded() {
260
- if (this.#status === 'loaded') {
261
- return;
262
- }
263
- if (this.#loadingPromise) {
264
- await this.#loadingPromise;
265
- return;
266
- }
267
- this.#loadingPromise = (async () => {
268
- try {
269
- await this.doLoad();
270
- }
271
- finally {
272
- this.#loadingPromise = null;
273
- }
274
- })();
275
- await this.#loadingPromise;
276
- }
277
- async doLoad() {
278
- const { state } = await this.#restoreFn();
279
- // Initialize state from restored data
280
- this.#state = new Map(state);
281
- this.#initialStateJson = JSON.stringify(Object.fromEntries(this.#state));
282
- // Apply any pending operations
283
- for (const op of this.#pendingOperations) {
284
- switch (op.op) {
285
- case 'clear':
286
- this.#state.clear();
287
- break;
288
- case 'set':
289
- if (op.key !== undefined) {
290
- this.#state.set(op.key, op.value);
291
- }
292
- break;
293
- case 'delete':
294
- if (op.key !== undefined) {
295
- this.#state.delete(op.key);
296
- }
297
- break;
298
- case 'push':
299
- if (op.key !== undefined) {
300
- const existing = this.#state.get(op.key);
301
- if (Array.isArray(existing)) {
302
- existing.push(op.value);
303
- // Apply maxRecords limit
304
- if (op.maxRecords !== undefined && existing.length > op.maxRecords) {
305
- existing.splice(0, existing.length - op.maxRecords);
306
- }
307
- }
308
- else if (existing === undefined) {
309
- this.#state.set(op.key, [op.value]);
310
- }
311
- // If existing is non-array, silently skip (error would have been thrown if loaded)
312
- }
313
- break;
314
- }
315
- }
316
- this.#pendingOperations = [];
317
- this.#status = 'loaded';
318
- }
319
- async get(key) {
320
- await this.ensureLoaded();
321
- return this.#state.get(key);
322
- }
323
- async set(key, value) {
324
- if (this.#status === 'loaded') {
325
- this.#state.set(key, value);
326
- }
327
- else {
328
- this.#pendingOperations.push({ op: 'set', key, value });
329
- if (this.#status === 'idle') {
330
- this.#status = 'pending-writes';
331
- }
332
- }
333
- }
334
- async has(key) {
335
- await this.ensureLoaded();
336
- return this.#state.has(key);
337
- }
338
- async delete(key) {
339
- if (this.#status === 'loaded') {
340
- this.#state.delete(key);
341
- }
342
- else {
343
- this.#pendingOperations.push({ op: 'delete', key });
344
- if (this.#status === 'idle') {
345
- this.#status = 'pending-writes';
346
- }
347
- }
348
- }
349
- async clear() {
350
- if (this.#status === 'loaded') {
351
- this.#state.clear();
352
- }
353
- else {
354
- // Clear replaces all previous pending operations
355
- this.#pendingOperations = [{ op: 'clear' }];
356
- if (this.#status === 'idle') {
357
- this.#status = 'pending-writes';
358
- }
359
- }
360
- }
361
- async entries() {
362
- await this.ensureLoaded();
363
- return Array.from(this.#state.entries());
364
- }
365
- async keys() {
366
- await this.ensureLoaded();
367
- return Array.from(this.#state.keys());
368
- }
369
- async values() {
370
- await this.ensureLoaded();
371
- return Array.from(this.#state.values());
372
- }
373
- async size() {
374
- await this.ensureLoaded();
375
- return this.#state.size;
376
- }
377
- async push(key, value, maxRecords) {
378
- if (this.#status === 'loaded') {
379
- // When loaded, push to local array
380
- const existing = this.#state.get(key);
381
- if (Array.isArray(existing)) {
382
- existing.push(value);
383
- // Apply maxRecords limit
384
- if (maxRecords !== undefined && existing.length > maxRecords) {
385
- existing.splice(0, existing.length - maxRecords);
386
- }
387
- }
388
- else if (existing === undefined) {
389
- this.#state.set(key, [value]);
390
- }
391
- else {
392
- throw new Error(`Cannot push to non-array value at key "${key}"`);
393
- }
394
- }
395
- else {
396
- // Queue push operation for merge
397
- const op = { op: 'push', key, value };
398
- if (maxRecords !== undefined) {
399
- op.maxRecords = maxRecords;
400
- }
401
- this.#pendingOperations.push(op);
402
- if (this.#status === 'idle') {
403
- this.#status = 'pending-writes';
404
- }
405
- }
406
- }
407
- /**
408
- * Get the current status for save logic
409
- * @internal
410
- */
411
- getStatus() {
412
- return this.#status;
413
- }
414
- /**
415
- * Get pending operations for merge command
416
- * @internal
417
- */
418
- getPendingOperations() {
419
- return [...this.#pendingOperations];
420
- }
421
- /**
422
- * Get serialized state for full save.
423
- * Ensures state is loaded before serializing.
424
- * @internal
425
- */
426
- async getSerializedState() {
427
- await this.ensureLoaded();
428
- return Object.fromEntries(this.#state);
429
- }
430
- }
431
- export class DefaultThread {
432
- id;
433
- state;
434
- #metadata = null;
435
- #metadataDirty = false;
436
- #metadataLoadPromise = null;
437
- provider;
438
- #restoreFn;
439
- #restoredMetadata;
440
- constructor(provider, id, restoreFn, initialMetadata) {
441
- this.provider = provider;
442
- this.id = id;
443
- this.#restoreFn = restoreFn;
444
- this.#restoredMetadata = initialMetadata;
445
- this.state = new LazyThreadState(restoreFn);
446
- }
447
- async ensureMetadataLoaded() {
448
- if (this.#metadata !== null) {
449
- return;
450
- }
451
- // If we have initial metadata from thread creation, use it
452
- if (this.#restoredMetadata !== undefined) {
453
- this.#metadata = this.#restoredMetadata;
454
- return;
455
- }
456
- if (this.#metadataLoadPromise) {
457
- await this.#metadataLoadPromise;
458
- return;
459
- }
460
- this.#metadataLoadPromise = (async () => {
461
- try {
462
- await this.doLoadMetadata();
463
- }
464
- finally {
465
- this.#metadataLoadPromise = null;
466
- }
467
- })();
468
- await this.#metadataLoadPromise;
469
- }
470
- async doLoadMetadata() {
471
- const { metadata } = await this.#restoreFn();
472
- this.#metadata = metadata;
473
- }
474
- async getMetadata() {
475
- await this.ensureMetadataLoaded();
476
- return { ...this.#metadata };
477
- }
478
- async setMetadata(metadata) {
479
- this.#metadata = metadata;
480
- this.#metadataDirty = true;
481
- }
482
- addEventListener(eventName, callback) {
483
- let listeners = threadEventListeners.get(this);
484
- if (!listeners) {
485
- listeners = new Map();
486
- threadEventListeners.set(this, listeners);
487
- }
488
- let callbacks = listeners.get(eventName);
489
- if (!callbacks) {
490
- callbacks = new Set();
491
- listeners.set(eventName, callbacks);
492
- }
493
- callbacks.add(callback);
494
- }
495
- removeEventListener(eventName, callback) {
496
- const listeners = threadEventListeners.get(this);
497
- if (!listeners)
498
- return;
499
- const callbacks = listeners.get(eventName);
500
- if (!callbacks)
501
- return;
502
- callbacks.delete(callback);
503
- }
504
- async fireEvent(eventName) {
505
- await fireThreadEvent(this, eventName);
506
- }
507
- async destroy() {
508
- await this.provider.destroy(this);
509
- }
510
- /**
511
- * Check if thread has any data (state or metadata)
512
- */
513
- async empty() {
514
- const stateSize = await this.state.size();
515
- // Check both loaded metadata and initial metadata from constructor
516
- const meta = this.#metadata ?? this.#restoredMetadata ?? {};
517
- return stateSize === 0 && Object.keys(meta).length === 0;
518
- }
519
- /**
520
- * Check if thread needs saving
521
- * @internal
522
- */
523
- needsSave() {
524
- return this.state.dirty || this.#metadataDirty;
525
- }
526
- /**
527
- * Get the save mode for this thread
528
- * @internal
529
- */
530
- getSaveMode() {
531
- const stateStatus = this.state.getStatus();
532
- if (stateStatus === 'idle' && !this.#metadataDirty) {
533
- return 'none';
534
- }
535
- if (stateStatus === 'pending-writes') {
536
- return 'merge';
537
- }
538
- if (stateStatus === 'loaded' && (this.state.dirty || this.#metadataDirty)) {
539
- return 'full';
540
- }
541
- // Only metadata was changed without loading state
542
- if (this.#metadataDirty) {
543
- return 'merge';
544
- }
545
- return 'none';
546
- }
547
- /**
548
- * Get pending operations for merge command
549
- * @internal
550
- */
551
- getPendingOperations() {
552
- return this.state.getPendingOperations();
553
- }
554
- /**
555
- * Get metadata for saving (returns null if not loaded/modified)
556
- * @internal
557
- */
558
- getMetadataForSave() {
559
- if (this.#metadataDirty && this.#metadata) {
560
- return this.#metadata;
561
- }
562
- return undefined;
563
- }
564
- /**
565
- * Get serialized state for full save.
566
- * Ensures state is loaded before serializing.
567
- * @internal
568
- */
569
- async getSerializedState() {
570
- const state = await this.state.getSerializedState();
571
- // Also ensure metadata is loaded
572
- const meta = this.#metadata ?? this.#restoredMetadata ?? {};
573
- const hasState = Object.keys(state).length > 0;
574
- const hasMetadata = Object.keys(meta).length > 0;
575
- if (!hasState && !hasMetadata) {
576
- return '';
577
- }
578
- const data = {};
579
- if (hasState) {
580
- data.state = state;
581
- }
582
- if (hasMetadata) {
583
- data.metadata = meta;
584
- }
585
- return JSON.stringify(data);
586
- }
587
- }
588
- export class DefaultSession {
589
- id;
590
- thread;
591
- state;
592
- metadata;
593
- constructor(thread, id, metadata) {
594
- this.id = id;
595
- this.thread = thread;
596
- this.state = new Map();
597
- this.metadata = metadata || {};
598
- }
599
- addEventListener(eventName, callback) {
600
- let listeners = sessionEventListeners.get(this);
601
- if (!listeners) {
602
- listeners = new Map();
603
- sessionEventListeners.set(this, listeners);
604
- }
605
- let callbacks = listeners.get(eventName);
606
- if (!callbacks) {
607
- callbacks = new Set();
608
- listeners.set(eventName, callbacks);
609
- }
610
- callbacks.add(callback);
611
- }
612
- removeEventListener(eventName, callback) {
613
- const listeners = sessionEventListeners.get(this);
614
- if (!listeners)
615
- return;
616
- const callbacks = listeners.get(eventName);
617
- if (!callbacks)
618
- return;
619
- callbacks.delete(callback);
620
- }
621
- async fireEvent(eventName) {
622
- await fireSessionEvent(this, eventName);
623
- }
624
- /**
625
- * Serialize session state to JSON string for persistence.
626
- * Returns undefined if state is empty or exceeds 1MB limit.
627
- * @internal
628
- */
629
- serializeUserData() {
630
- if (this.state.size === 0) {
631
- return undefined;
632
- }
633
- try {
634
- const obj = Object.fromEntries(this.state);
635
- const json = JSON.stringify(obj);
636
- // Check 1MB limit (1,048,576 bytes)
637
- const sizeInBytes = new TextEncoder().encode(json).length;
638
- if (sizeInBytes > 1048576) {
639
- console.error(`Session ${this.id} user_data exceeds 1MB limit (${sizeInBytes} bytes), data will not be persisted`);
640
- return undefined;
641
- }
642
- return json;
643
- }
644
- catch (err) {
645
- console.error(`Failed to serialize session ${this.id} user_data:`, err);
646
- return undefined;
647
- }
648
- }
649
- }
650
- export class ThreadWebSocketClient {
651
- ws = null;
652
- authenticated = false;
653
- pendingRequests = new Map();
654
- reconnectAttempts = 0;
655
- maxReconnectAttempts;
656
- apiKey;
657
- wsUrl;
658
- wsConnecting = null;
659
- reconnectTimer = null;
660
- isDisposed = false;
661
- initialConnectResolve = null;
662
- initialConnectReject = null;
663
- connectionTimeoutMs;
664
- requestTimeoutMs;
665
- reconnectBaseDelayMs;
666
- reconnectMaxDelayMs;
667
- constructor(apiKey, wsUrl, options = {}) {
668
- this.apiKey = apiKey;
669
- this.wsUrl = wsUrl;
670
- this.connectionTimeoutMs = options.connectionTimeoutMs ?? 10_000;
671
- this.requestTimeoutMs = options.requestTimeoutMs ?? 10_000;
672
- this.reconnectBaseDelayMs = options.reconnectBaseDelayMs ?? 1_000;
673
- this.reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? 30_000;
674
- this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
675
- }
676
- async connect() {
677
- return new Promise((resolve, reject) => {
678
- // Store the initial connect promise callbacks if this is the first attempt
679
- if (this.reconnectAttempts === 0) {
680
- this.initialConnectResolve = resolve;
681
- this.initialConnectReject = reject;
682
- }
683
- // Set connection timeout
684
- const connectionTimeout = setTimeout(() => {
685
- this.cleanup();
686
- const rejectFn = this.initialConnectReject || reject;
687
- this.initialConnectResolve = null;
688
- this.initialConnectReject = null;
689
- rejectFn(new Error(`WebSocket connection timeout (${this.connectionTimeoutMs}ms)`));
690
- }, this.connectionTimeoutMs);
691
- try {
692
- this.ws = new WebSocket(this.wsUrl);
693
- this.ws.addEventListener('open', () => {
694
- // Send authentication (do NOT clear timeout yet - wait for auth response)
695
- this.ws?.send(JSON.stringify({ authorization: this.apiKey }));
696
- });
697
- this.ws.addEventListener('message', (event) => {
698
- try {
699
- const message = JSON.parse(event.data);
700
- // Handle auth response
701
- if ('success' in message && !this.authenticated) {
702
- clearTimeout(connectionTimeout);
703
- if (message.success) {
704
- this.authenticated = true;
705
- this.reconnectAttempts = 0;
706
- // Resolve both the current promise and the initial connect promise
707
- const resolveFn = this.initialConnectResolve || resolve;
708
- this.initialConnectResolve = null;
709
- this.initialConnectReject = null;
710
- resolveFn();
711
- }
712
- else {
713
- const err = new Error(`WebSocket authentication failed: ${message.error || 'Unknown error'}`);
714
- this.cleanup();
715
- const rejectFn = this.initialConnectReject || reject;
716
- this.initialConnectResolve = null;
717
- this.initialConnectReject = null;
718
- rejectFn(err);
719
- }
720
- return;
721
- }
722
- // Handle action response
723
- if ('id' in message && this.pendingRequests.has(message.id)) {
724
- const pending = this.pendingRequests.get(message.id);
725
- this.pendingRequests.delete(message.id);
726
- if (message.success) {
727
- pending.resolve(message.data);
728
- }
729
- else {
730
- pending.reject(new Error(message.error || 'Request failed'));
731
- }
732
- }
733
- }
734
- catch {
735
- // Ignore parse errors
736
- }
737
- });
738
- this.ws.addEventListener('error', (_event) => {
739
- clearTimeout(connectionTimeout);
740
- if (!this.authenticated) {
741
- // Don't reject immediately if we'll attempt reconnection
742
- if (this.reconnectAttempts >= this.maxReconnectAttempts || this.isDisposed) {
743
- const rejectFn = this.initialConnectReject || reject;
744
- this.initialConnectResolve = null;
745
- this.initialConnectReject = null;
746
- rejectFn(new Error(`WebSocket error`));
747
- }
748
- }
749
- });
750
- this.ws.addEventListener('close', () => {
751
- clearTimeout(connectionTimeout);
752
- const wasAuthenticated = this.authenticated;
753
- this.authenticated = false;
754
- // Reject all pending requests
755
- for (const [id, pending] of this.pendingRequests) {
756
- pending.reject(new Error('WebSocket connection closed'));
757
- this.pendingRequests.delete(id);
758
- }
759
- // Don't attempt reconnection if disposed
760
- if (this.isDisposed) {
761
- // Reject initial connect if still pending
762
- if (!wasAuthenticated && this.initialConnectReject) {
763
- this.initialConnectReject(new Error('WebSocket closed before authentication'));
764
- this.initialConnectResolve = null;
765
- this.initialConnectReject = null;
766
- }
767
- return;
768
- }
769
- // Attempt reconnection if within retry limits (even if auth didn't complete)
770
- // This handles server rollouts where connection closes before auth finishes
771
- if (this.reconnectAttempts < this.maxReconnectAttempts) {
772
- this.reconnectAttempts++;
773
- const delay = Math.min(this.reconnectBaseDelayMs * Math.pow(2, this.reconnectAttempts), this.reconnectMaxDelayMs);
774
- internal.info(`WebSocket disconnected, attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`);
775
- // Schedule reconnection with backoff delay
776
- this.reconnectTimer = setTimeout(() => {
777
- this.reconnectTimer = null;
778
- // Create new connection promise for reconnection
779
- this.wsConnecting = this.connect().catch(() => {
780
- // Reconnection failed, reset
781
- this.wsConnecting = null;
782
- });
783
- }, delay);
784
- }
785
- else {
786
- internal.error(`WebSocket disconnected after ${this.reconnectAttempts} attempts, giving up`);
787
- // Reject initial connect if still pending (all attempts exhausted)
788
- if (!wasAuthenticated && this.initialConnectReject) {
789
- this.initialConnectReject(new Error(`WebSocket closed before authentication after ${this.reconnectAttempts} attempts`));
790
- this.initialConnectResolve = null;
791
- this.initialConnectReject = null;
792
- }
793
- }
794
- });
795
- }
796
- catch (err) {
797
- clearTimeout(connectionTimeout);
798
- const rejectFn = this.initialConnectReject || reject;
799
- this.initialConnectResolve = null;
800
- this.initialConnectReject = null;
801
- rejectFn(err);
802
- }
803
- });
804
- }
805
- async restore(threadId) {
806
- // Wait for connection/reconnection if in progress
807
- if (this.wsConnecting) {
808
- await this.wsConnecting;
809
- }
810
- if (!this.authenticated || !this.ws) {
811
- throw new Error('WebSocket not connected or authenticated');
812
- }
813
- return new Promise((resolve, reject) => {
814
- const requestId = crypto.randomUUID();
815
- this.pendingRequests.set(requestId, { resolve, reject });
816
- const message = {
817
- id: requestId,
818
- action: 'restore',
819
- data: { thread_id: threadId },
820
- };
821
- this.ws.send(JSON.stringify(message));
822
- // Timeout after configured duration
823
- setTimeout(() => {
824
- if (this.pendingRequests.has(requestId)) {
825
- this.pendingRequests.delete(requestId);
826
- reject(new Error('Request timeout'));
827
- }
828
- }, this.requestTimeoutMs);
829
- });
830
- }
831
- async save(threadId, userData, threadMetadata) {
832
- // Wait for connection/reconnection if in progress
833
- if (this.wsConnecting) {
834
- await this.wsConnecting;
835
- }
836
- if (!this.authenticated || !this.ws) {
837
- throw new Error('WebSocket not connected or authenticated');
838
- }
839
- // Check 1MB limit
840
- const sizeInBytes = new TextEncoder().encode(userData).length;
841
- if (sizeInBytes > 1048576) {
842
- console.error(`Thread ${threadId} user_data exceeds 1MB limit (${sizeInBytes} bytes), data will not be persisted`);
843
- return;
844
- }
845
- return new Promise((resolve, reject) => {
846
- const requestId = crypto.randomUUID();
847
- this.pendingRequests.set(requestId, {
848
- resolve: () => resolve(),
849
- reject,
850
- });
851
- const data = {
852
- thread_id: threadId,
853
- user_data: userData,
854
- };
855
- if (threadMetadata && Object.keys(threadMetadata).length > 0) {
856
- data.metadata = threadMetadata;
857
- }
858
- const message = {
859
- id: requestId,
860
- action: 'save',
861
- data,
862
- };
863
- this.ws.send(JSON.stringify(message));
864
- // Timeout after configured duration
865
- setTimeout(() => {
866
- if (this.pendingRequests.has(requestId)) {
867
- this.pendingRequests.delete(requestId);
868
- reject(new Error('Request timeout'));
869
- }
870
- }, this.requestTimeoutMs);
871
- });
872
- }
873
- async delete(threadId) {
874
- // Wait for connection/reconnection if in progress
875
- if (this.wsConnecting) {
876
- await this.wsConnecting;
877
- }
878
- if (!this.authenticated || !this.ws) {
879
- throw new Error('WebSocket not connected or authenticated');
880
- }
881
- return new Promise((resolve, reject) => {
882
- const requestId = crypto.randomUUID();
883
- this.pendingRequests.set(requestId, {
884
- resolve: () => resolve(),
885
- reject,
886
- });
887
- const message = {
888
- id: requestId,
889
- action: 'delete',
890
- data: { thread_id: threadId },
891
- };
892
- this.ws.send(JSON.stringify(message));
893
- // Timeout after configured duration
894
- setTimeout(() => {
895
- if (this.pendingRequests.has(requestId)) {
896
- this.pendingRequests.delete(requestId);
897
- reject(new Error('Request timeout'));
898
- }
899
- }, this.requestTimeoutMs);
900
- });
901
- }
902
- async merge(threadId, operations, metadata) {
903
- // Wait for connection/reconnection if in progress
904
- if (this.wsConnecting) {
905
- await this.wsConnecting;
906
- }
907
- if (!this.authenticated || !this.ws) {
908
- throw new Error('WebSocket not connected or authenticated');
909
- }
910
- return new Promise((resolve, reject) => {
911
- const requestId = crypto.randomUUID();
912
- this.pendingRequests.set(requestId, {
913
- resolve: () => resolve(),
914
- reject,
915
- });
916
- const data = {
917
- thread_id: threadId,
918
- operations,
919
- };
920
- if (metadata && Object.keys(metadata).length > 0) {
921
- data.metadata = metadata;
922
- }
923
- const message = {
924
- id: requestId,
925
- action: 'merge',
926
- data,
927
- };
928
- this.ws.send(JSON.stringify(message));
929
- // Timeout after configured duration
930
- setTimeout(() => {
931
- if (this.pendingRequests.has(requestId)) {
932
- this.pendingRequests.delete(requestId);
933
- reject(new Error('Request timeout'));
934
- }
935
- }, this.requestTimeoutMs);
936
- });
937
- }
938
- cleanup() {
939
- // Mark as disposed to prevent new reconnection attempts
940
- this.isDisposed = true;
941
- // Cancel any pending reconnection timer
942
- if (this.reconnectTimer) {
943
- clearTimeout(this.reconnectTimer);
944
- this.reconnectTimer = null;
945
- }
946
- if (this.ws) {
947
- this.ws.close();
948
- this.ws = null;
949
- }
950
- this.authenticated = false;
951
- this.pendingRequests.clear();
952
- this.reconnectAttempts = 0;
953
- this.wsConnecting = null;
954
- this.initialConnectResolve = null;
955
- this.initialConnectReject = null;
956
- }
957
- }
958
- export class DefaultThreadProvider {
959
- appState = null;
960
- wsClient = null;
961
- wsConnecting = null;
962
- threadIDProvider = null;
963
- async initialize(appState) {
964
- this.appState = appState;
965
- this.threadIDProvider = new DefaultThreadIDProvider();
966
- // Initialize WebSocket connection for thread persistence (async, non-blocking)
967
- const apiKey = process.env.AGENTUITY_SDK_KEY;
968
- if (apiKey) {
969
- const serviceUrls = getServiceUrls(process.env.AGENTUITY_REGION ?? 'usc');
970
- const catalystUrl = serviceUrls.catalyst;
971
- const wsUrl = new URL('/thread/ws', catalystUrl.replace(/^http/, 'ws'));
972
- internal.debug('connecting to %s', wsUrl);
973
- this.wsClient = new ThreadWebSocketClient(apiKey, wsUrl.toString());
974
- // Connect in background, don't block initialization
975
- this.wsConnecting = this.wsClient
976
- .connect()
977
- .then(() => {
978
- this.wsConnecting = null;
979
- })
980
- .catch((err) => {
981
- internal.error('Failed to connect to thread WebSocket:', err);
982
- this.wsClient = null;
983
- this.wsConnecting = null;
984
- });
985
- }
986
- }
987
- setThreadIDProvider(provider) {
988
- this.threadIDProvider = provider;
989
- }
990
- async restore(ctx) {
991
- const threadId = await this.threadIDProvider.getThreadId(this.appState, ctx);
992
- validateThreadIdOrThrow(threadId);
993
- internal.info('[thread] creating lazy thread %s (no eager restore)', threadId);
994
- // Create a restore function that will be called lazily when state/metadata is accessed
995
- const restoreFn = async () => {
996
- internal.info('[thread] lazy loading state for thread %s', threadId);
997
- // Wait for WebSocket connection if still connecting
998
- if (this.wsConnecting) {
999
- internal.info('[thread] waiting for WebSocket connection');
1000
- await this.wsConnecting;
1001
- }
1002
- if (!this.wsClient) {
1003
- internal.info('[thread] no WebSocket client available, returning empty state');
1004
- return { state: new Map(), metadata: {} };
1005
- }
1006
- try {
1007
- const restoredData = await this.wsClient.restore(threadId);
1008
- if (restoredData) {
1009
- internal.info('[thread] restored state: %d bytes', restoredData.length);
1010
- const { flatStateJson, metadata } = parseThreadData(restoredData);
1011
- const state = new Map();
1012
- if (flatStateJson) {
1013
- try {
1014
- const data = JSON.parse(flatStateJson);
1015
- for (const [key, value] of Object.entries(data)) {
1016
- state.set(key, value);
1017
- }
1018
- }
1019
- catch {
1020
- internal.info('[thread] failed to parse state JSON');
1021
- }
1022
- }
1023
- return { state, metadata: metadata || {} };
1024
- }
1025
- internal.info('[thread] no existing state found');
1026
- return { state: new Map(), metadata: {} };
1027
- }
1028
- catch (err) {
1029
- internal.info('[thread] WebSocket restore failed: %s', err);
1030
- return { state: new Map(), metadata: {} };
1031
- }
1032
- };
1033
- const thread = new DefaultThread(this, threadId, restoreFn);
1034
- await fireEvent('thread.created', thread);
1035
- return thread;
1036
- }
1037
- async save(thread) {
1038
- if (thread instanceof DefaultThread) {
1039
- const saveMode = thread.getSaveMode();
1040
- internal.info('[thread] DefaultThreadProvider.save() - thread %s, saveMode: %s, hasWsClient: %s', thread.id, saveMode, !!this.wsClient);
1041
- if (saveMode === 'none') {
1042
- internal.info('[thread] skipping save - no changes');
1043
- return;
1044
- }
1045
- // Wait for WebSocket connection if still connecting
1046
- if (this.wsConnecting) {
1047
- internal.info('[thread] waiting for WebSocket connection');
1048
- await this.wsConnecting;
1049
- }
1050
- if (!this.wsClient) {
1051
- internal.info('[thread] no WebSocket client available, skipping save');
1052
- return;
1053
- }
1054
- try {
1055
- if (saveMode === 'merge') {
1056
- const operations = thread.getPendingOperations();
1057
- const metadata = thread.getMetadataForSave();
1058
- internal.info('[thread] sending merge command with %d operations', operations.length);
1059
- await this.wsClient.merge(thread.id, operations, metadata);
1060
- internal.info('[thread] WebSocket merge completed');
1061
- }
1062
- else if (saveMode === 'full') {
1063
- const serialized = await thread.getSerializedState();
1064
- internal.info('[thread] saving to WebSocket, serialized length: %d', serialized.length);
1065
- const metadata = thread.getMetadataForSave();
1066
- await this.wsClient.save(thread.id, serialized, metadata);
1067
- internal.info('[thread] WebSocket save completed');
1068
- }
1069
- }
1070
- catch (err) {
1071
- internal.info('[thread] WebSocket save/merge failed: %s', err);
1072
- // Don't throw - allow request to complete even if save fails
1073
- }
1074
- }
1075
- }
1076
- async destroy(thread) {
1077
- if (thread instanceof DefaultThread) {
1078
- try {
1079
- // Wait for WebSocket connection if still connecting
1080
- if (this.wsConnecting) {
1081
- await this.wsConnecting;
1082
- }
1083
- // Delete thread from remote storage
1084
- if (this.wsClient) {
1085
- try {
1086
- await this.wsClient.delete(thread.id);
1087
- }
1088
- catch {
1089
- // Thread might not exist in remote storage if it was never persisted
1090
- // This is normal for ephemeral threads, so just log at debug level
1091
- internal.debug(`Thread ${thread.id} not found in remote storage (already deleted or never persisted)`);
1092
- // Continue with local cleanup even if remote delete fails
1093
- }
1094
- }
1095
- await thread.fireEvent('destroyed');
1096
- await fireEvent('thread.destroyed', thread);
1097
- }
1098
- finally {
1099
- threadEventListeners.delete(thread);
1100
- }
1101
- }
1102
- }
1103
- }
1104
- export class DefaultSessionProvider {
1105
- sessions = new Map();
1106
- async initialize(_appState) {
1107
- // No initialization needed for in-memory provider
1108
- }
1109
- async restore(thread, sessionId) {
1110
- internal.info('[session] restoring session %s for thread %s', sessionId, thread.id);
1111
- let session = this.sessions.get(sessionId);
1112
- if (!session) {
1113
- session = new DefaultSession(thread, sessionId);
1114
- this.sessions.set(sessionId, session);
1115
- internal.info('[session] created new session, firing session.started');
1116
- await fireEvent('session.started', session);
1117
- }
1118
- else {
1119
- internal.info('[session] found existing session');
1120
- }
1121
- return session;
1122
- }
1123
- async save(session) {
1124
- if (session instanceof DefaultSession) {
1125
- internal.info('[session] DefaultSessionProvider.save() - firing completed event for session %s', session.id);
1126
- try {
1127
- await session.fireEvent('completed');
1128
- internal.info('[session] session.fireEvent completed, firing app event');
1129
- await fireEvent('session.completed', session);
1130
- internal.info('[session] session.completed app event fired');
1131
- }
1132
- finally {
1133
- this.sessions.delete(session.id);
1134
- sessionEventListeners.delete(session);
1135
- }
1136
- }
1137
- }
1138
- }
1139
- //# sourceMappingURL=session.js.map