@agentuity/runtime 0.0.99 → 0.0.101
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.
- package/dist/_metadata.d.ts +107 -0
- package/dist/_metadata.d.ts.map +1 -0
- package/dist/_metadata.js +179 -0
- package/dist/_metadata.js.map +1 -0
- package/dist/_process-protection.d.ts.map +1 -1
- package/dist/_process-protection.js +4 -0
- package/dist/_process-protection.js.map +1 -1
- package/dist/_services.d.ts.map +1 -1
- package/dist/_services.js +18 -17
- package/dist/_services.js.map +1 -1
- package/dist/_standalone.d.ts.map +1 -1
- package/dist/_standalone.js +17 -0
- package/dist/_standalone.js.map +1 -1
- package/dist/agent.d.ts +3 -3
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +53 -12
- package/dist/agent.js.map +1 -1
- package/dist/app.d.ts +0 -10
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js.map +1 -1
- package/dist/devmode.d.ts.map +1 -1
- package/dist/devmode.js +13 -5
- package/dist/devmode.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +2 -2
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +59 -1
- package/dist/middleware.js.map +1 -1
- package/dist/services/evalrun/http.d.ts.map +1 -1
- package/dist/services/evalrun/http.js +14 -4
- package/dist/services/evalrun/http.js.map +1 -1
- package/dist/services/session/http.d.ts.map +1 -1
- package/dist/services/session/http.js +7 -0
- package/dist/services/session/http.js.map +1 -1
- package/dist/services/session/local.d.ts +2 -2
- package/dist/services/session/local.d.ts.map +1 -1
- package/dist/services/session/local.js +5 -4
- package/dist/services/session/local.js.map +1 -1
- package/dist/session.d.ts +30 -4
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +90 -13
- package/dist/session.js.map +1 -1
- package/dist/workbench.d.ts.map +1 -1
- package/dist/workbench.js +13 -20
- package/dist/workbench.js.map +1 -1
- package/package.json +5 -5
- package/src/_metadata.ts +307 -0
- package/src/_process-protection.ts +6 -0
- package/src/_services.ts +23 -21
- package/src/_standalone.ts +22 -0
- package/src/agent.ts +66 -14
- package/src/app.ts +0 -9
- package/src/devmode.ts +16 -5
- package/src/index.ts +5 -1
- package/src/middleware.ts +75 -4
- package/src/services/evalrun/http.ts +15 -4
- package/src/services/session/http.ts +11 -0
- package/src/services/session/local.ts +9 -4
- package/src/session.ts +142 -13
- package/src/workbench.ts +13 -26
package/src/app.ts
CHANGED
|
@@ -15,11 +15,6 @@ import type { Email } from './io/email';
|
|
|
15
15
|
import type { ThreadProvider, SessionProvider, Session, Thread } from './session';
|
|
16
16
|
import type WaitUntilHandler from './_waituntil';
|
|
17
17
|
|
|
18
|
-
// TODO: This should be imported from workbench package, but causes circular dependency
|
|
19
|
-
export interface WorkbenchInstance {
|
|
20
|
-
config: { route?: string; headers?: Record<string, string> };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
18
|
type CorsOptions = Parameters<typeof cors>[0];
|
|
24
19
|
|
|
25
20
|
export interface AppConfig<TAppState = Record<string, never>> {
|
|
@@ -63,10 +58,6 @@ export interface AppConfig<TAppState = Record<string, never>> {
|
|
|
63
58
|
* the EvalRunEventProvider to override instead of the default
|
|
64
59
|
*/
|
|
65
60
|
evalRunEvent?: EvalRunEventProvider;
|
|
66
|
-
/**
|
|
67
|
-
* the Workbench to override instead of the default
|
|
68
|
-
*/
|
|
69
|
-
workbench?: WorkbenchInstance;
|
|
70
61
|
};
|
|
71
62
|
/**
|
|
72
63
|
* Optional setup function called before server starts
|
package/src/devmode.ts
CHANGED
|
@@ -78,12 +78,23 @@ const overlay = `
|
|
|
78
78
|
</style>
|
|
79
79
|
`;
|
|
80
80
|
|
|
81
|
+
// Global controller to avoid registering multiple SIGINT listeners
|
|
82
|
+
let globalController: AbortController | undefined;
|
|
83
|
+
let globalSigintHandler: (() => void) | undefined;
|
|
84
|
+
|
|
81
85
|
export function registerDevModeRoutes(router: Hono) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
// Reuse existing controller or create new one
|
|
87
|
+
if (!globalController) {
|
|
88
|
+
globalController = new AbortController();
|
|
89
|
+
|
|
90
|
+
// Only register SIGINT handler once
|
|
91
|
+
globalSigintHandler = () => {
|
|
92
|
+
globalController?.abort();
|
|
93
|
+
};
|
|
94
|
+
process.on('SIGINT', globalSigintHandler);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const signal = globalController.signal;
|
|
87
98
|
router.get('/__dev__/reload', () => {
|
|
88
99
|
const stream = new ReadableStream({
|
|
89
100
|
start(controller): void {
|
package/src/index.ts
CHANGED
|
@@ -27,7 +27,6 @@ export {
|
|
|
27
27
|
|
|
28
28
|
// app.ts exports (all app-related functionality)
|
|
29
29
|
export {
|
|
30
|
-
type WorkbenchInstance,
|
|
31
30
|
type AppConfig,
|
|
32
31
|
type Variables,
|
|
33
32
|
type TriggerType,
|
|
@@ -193,3 +192,8 @@ export type { RouteSchema, GetRouteSchema } from './_validation';
|
|
|
193
192
|
*/
|
|
194
193
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
195
194
|
export interface AppState {}
|
|
195
|
+
|
|
196
|
+
// Re-export bootstrapRuntimeEnv from @agentuity/server for convenience
|
|
197
|
+
// This allows generated code to import from @agentuity/runtime instead of having
|
|
198
|
+
// a direct dependency on @agentuity/server
|
|
199
|
+
export { bootstrapRuntimeEnv, type RuntimeBootstrapOptions } from '@agentuity/server';
|
package/src/middleware.ts
CHANGED
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
} from '@opentelemetry/api';
|
|
23
23
|
import { TraceState } from '@opentelemetry/core';
|
|
24
24
|
import * as runtimeConfig from './_config';
|
|
25
|
+
import { getSessionEventProvider } from './_services';
|
|
26
|
+
import { internal } from './logger/internal';
|
|
25
27
|
|
|
26
28
|
const SESSION_HEADER = 'x-session-id';
|
|
27
29
|
const THREAD_HEADER = 'x-thread-id';
|
|
@@ -74,7 +76,8 @@ export interface MiddlewareConfig {
|
|
|
74
76
|
* Create base middleware that sets up context variables
|
|
75
77
|
*/
|
|
76
78
|
export function createBaseMiddleware(config: MiddlewareConfig) {
|
|
77
|
-
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
80
|
+
return createMiddleware<Env<any>>(async (c, next) => {
|
|
78
81
|
c.set('logger', config.logger);
|
|
79
82
|
c.set('tracer', config.tracer);
|
|
80
83
|
c.set('meter', config.meter);
|
|
@@ -154,7 +157,8 @@ export function createCorsMiddleware(corsOptions?: Parameters<typeof cors>[0]) {
|
|
|
154
157
|
* This is the critical middleware that creates AgentContext
|
|
155
158
|
*/
|
|
156
159
|
export function createOtelMiddleware() {
|
|
157
|
-
|
|
160
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
161
|
+
return createMiddleware<Env<any>>(async (c, next) => {
|
|
158
162
|
// Import providers dynamically to avoid circular deps
|
|
159
163
|
const { getThreadProvider, getSessionProvider } = await import('./_services');
|
|
160
164
|
const WaitUntilHandler = (await import('./_waituntil')).default;
|
|
@@ -188,6 +192,14 @@ export function createOtelMiddleware() {
|
|
|
188
192
|
const deploymentId = runtimeConfig.getDeploymentId();
|
|
189
193
|
const isDevMode = runtimeConfig.isDevMode();
|
|
190
194
|
|
|
195
|
+
internal.info(
|
|
196
|
+
'[session] config: orgId=%s, projectId=%s, deploymentId=%s, isDevMode=%s',
|
|
197
|
+
orgId ?? 'NOT SET (AGENTUITY_CLOUD_ORG_ID)',
|
|
198
|
+
projectId ?? 'NOT SET (AGENTUITY_CLOUD_PROJECT_ID)',
|
|
199
|
+
deploymentId ?? 'none',
|
|
200
|
+
isDevMode
|
|
201
|
+
);
|
|
202
|
+
|
|
191
203
|
if (projectId) traceState = traceState.set('pid', projectId);
|
|
192
204
|
if (orgId) traceState = traceState.set('oid', orgId);
|
|
193
205
|
if (isDevMode) traceState = traceState.set('d', '1');
|
|
@@ -211,17 +223,45 @@ export function createOtelMiddleware() {
|
|
|
211
223
|
c.set('session', session);
|
|
212
224
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
213
225
|
(c as any).set('waitUntilHandler', handler);
|
|
226
|
+
const agentIds = new Set<string>();
|
|
214
227
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
215
|
-
(c as any).set('agentIds',
|
|
228
|
+
(c as any).set('agentIds', agentIds);
|
|
216
229
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
217
230
|
(c as any).set('trigger', 'api');
|
|
218
231
|
|
|
232
|
+
// Send session start event (so evalruns can reference this session)
|
|
233
|
+
const sessionEventProvider = getSessionEventProvider();
|
|
234
|
+
const shouldSendSession = !!(orgId && projectId);
|
|
235
|
+
if (shouldSendSession && sessionEventProvider) {
|
|
236
|
+
try {
|
|
237
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
238
|
+
const routeId = (c as any).var?.routeId || '';
|
|
239
|
+
await sessionEventProvider.start({
|
|
240
|
+
id: sessionId,
|
|
241
|
+
threadId: thread.id,
|
|
242
|
+
orgId,
|
|
243
|
+
projectId,
|
|
244
|
+
deploymentId: deploymentId || undefined,
|
|
245
|
+
devmode: isDevMode,
|
|
246
|
+
trigger: 'api',
|
|
247
|
+
routeId,
|
|
248
|
+
environment: runtimeConfig.getEnvironment(),
|
|
249
|
+
url: c.req.path,
|
|
250
|
+
method: c.req.method,
|
|
251
|
+
});
|
|
252
|
+
} catch (_ex) {
|
|
253
|
+
// Silently ignore session start errors - don't block request
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
219
257
|
try {
|
|
220
258
|
await next();
|
|
221
|
-
|
|
222
259
|
// Save session/thread and send events
|
|
260
|
+
internal.info('[session] saving session %s (thread: %s)', sessionId, thread.id);
|
|
223
261
|
await sessionProvider.save(session);
|
|
262
|
+
internal.info('[session] session saved, now saving thread');
|
|
224
263
|
await threadProvider.save(thread);
|
|
264
|
+
internal.info('[session] thread saved');
|
|
225
265
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
226
266
|
} catch (ex) {
|
|
227
267
|
if (ex instanceof Error) {
|
|
@@ -233,6 +273,37 @@ export function createOtelMiddleware() {
|
|
|
233
273
|
});
|
|
234
274
|
throw ex;
|
|
235
275
|
} finally {
|
|
276
|
+
// Send session complete event
|
|
277
|
+
internal.info(
|
|
278
|
+
'[session] shouldSendSession: %s, hasSessionEventProvider: %s',
|
|
279
|
+
shouldSendSession,
|
|
280
|
+
!!sessionEventProvider
|
|
281
|
+
);
|
|
282
|
+
if (shouldSendSession && sessionEventProvider) {
|
|
283
|
+
try {
|
|
284
|
+
const userData = session.serializeUserData();
|
|
285
|
+
internal.info(
|
|
286
|
+
'[session] sending session complete event, userData: %s',
|
|
287
|
+
userData ? `${userData.length} bytes` : 'none'
|
|
288
|
+
);
|
|
289
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
290
|
+
const agentIdsSet = (c as any).get('agentIds') as Set<string> | undefined;
|
|
291
|
+
const agentIds = agentIdsSet ? [...agentIdsSet].filter(Boolean) : undefined;
|
|
292
|
+
internal.info('[session] agentIds: %o', agentIds);
|
|
293
|
+
await sessionEventProvider.complete({
|
|
294
|
+
id: sessionId,
|
|
295
|
+
threadId: thread.empty() ? null : thread.id,
|
|
296
|
+
statusCode: c.res?.status ?? 200,
|
|
297
|
+
agentIds: agentIds?.length ? agentIds : undefined,
|
|
298
|
+
userData,
|
|
299
|
+
});
|
|
300
|
+
internal.info('[session] session complete event sent');
|
|
301
|
+
} catch (ex) {
|
|
302
|
+
internal.info('[session] session complete event failed: %s', ex);
|
|
303
|
+
// Silently ignore session complete errors - don't block response
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
236
307
|
const headers: Record<string, string> = {};
|
|
237
308
|
propagation.inject(context.active(), headers);
|
|
238
309
|
for (const key of Object.keys(headers)) {
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type Logger,
|
|
14
14
|
StructuredError,
|
|
15
15
|
} from '@agentuity/core';
|
|
16
|
+
import { internal } from '../../logger/internal';
|
|
16
17
|
|
|
17
18
|
const EvalRunResponseError = StructuredError('EvalRunResponseError');
|
|
18
19
|
|
|
@@ -38,12 +39,22 @@ export class HTTPEvalRunEventProvider implements EvalRunEventProvider {
|
|
|
38
39
|
async start(event: EvalRunStartEvent): Promise<void> {
|
|
39
40
|
const endpoint = '/evalrun/2025-03-17';
|
|
40
41
|
const fullUrl = `${this.baseUrl}${endpoint}`;
|
|
41
|
-
this.logger.debug('[EVALRUN HTTP] Sending eval run start event: %s', event.id);
|
|
42
|
-
this.logger.debug('[EVALRUN HTTP] URL: %s %s', 'POST', fullUrl);
|
|
43
|
-
this.logger.debug('[EVALRUN HTTP] Base URL: %s', this.baseUrl);
|
|
44
42
|
|
|
45
43
|
const payload = { ...event, timestamp: Date.now() };
|
|
46
|
-
|
|
44
|
+
|
|
45
|
+
// Log full payload using internal logger
|
|
46
|
+
internal.info('[EVALRUN HTTP] ========== START PAYLOAD ==========');
|
|
47
|
+
internal.info('[EVALRUN HTTP] id: %s', payload.id);
|
|
48
|
+
internal.info('[EVALRUN HTTP] evalId: %s', payload.evalId);
|
|
49
|
+
internal.info('[EVALRUN HTTP] evalIdentifier: %s', payload.evalIdentifier);
|
|
50
|
+
internal.info('[EVALRUN HTTP] sessionId: %s', payload.sessionId);
|
|
51
|
+
internal.info('[EVALRUN HTTP] orgId: %s', payload.orgId);
|
|
52
|
+
internal.info('[EVALRUN HTTP] projectId: %s', payload.projectId);
|
|
53
|
+
internal.info('[EVALRUN HTTP] devmode: %s', payload.devmode);
|
|
54
|
+
internal.info('[EVALRUN HTTP] deploymentId: %s', payload.deploymentId);
|
|
55
|
+
internal.info('[EVALRUN HTTP] spanId: %s', payload.spanId);
|
|
56
|
+
internal.info('[EVALRUN HTTP] URL: POST %s', fullUrl);
|
|
57
|
+
internal.info('[EVALRUN HTTP] ============================================');
|
|
47
58
|
|
|
48
59
|
try {
|
|
49
60
|
const resp = await this.apiClient.post(
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type Logger,
|
|
9
9
|
StructuredError,
|
|
10
10
|
} from '@agentuity/core';
|
|
11
|
+
import { internal } from '../../logger/internal';
|
|
11
12
|
|
|
12
13
|
const SessionResponseError = StructuredError('SessionResponseError');
|
|
13
14
|
|
|
@@ -29,6 +30,7 @@ export class HTTPSessionEventProvider implements SessionEventProvider {
|
|
|
29
30
|
* @param event SessionStartEvent
|
|
30
31
|
*/
|
|
31
32
|
async start(event: SessionStartEvent): Promise<void> {
|
|
33
|
+
internal.info('[session-http] sending start event: %s', event.id);
|
|
32
34
|
this.logger.debug('Sending session start event: %s', event.id);
|
|
33
35
|
const resp = await this.apiClient.post(
|
|
34
36
|
'/session/2025-03-17',
|
|
@@ -37,9 +39,11 @@ export class HTTPSessionEventProvider implements SessionEventProvider {
|
|
|
37
39
|
SessionStartEventDelayedSchema
|
|
38
40
|
);
|
|
39
41
|
if (resp.success) {
|
|
42
|
+
internal.info('[session-http] start event sent successfully: %s', event.id);
|
|
40
43
|
this.logger.debug('Session start event sent successfully: %s', event.id);
|
|
41
44
|
return;
|
|
42
45
|
}
|
|
46
|
+
internal.info('[session-http] start event failed: %s - %s', event.id, resp.message);
|
|
43
47
|
throw new SessionResponseError({ message: resp.message });
|
|
44
48
|
}
|
|
45
49
|
|
|
@@ -49,6 +53,11 @@ export class HTTPSessionEventProvider implements SessionEventProvider {
|
|
|
49
53
|
* @param event SessionCompleteEvent
|
|
50
54
|
*/
|
|
51
55
|
async complete(event: SessionCompleteEvent): Promise<void> {
|
|
56
|
+
internal.info(
|
|
57
|
+
'[session-http] sending complete event: %s, userData: %s',
|
|
58
|
+
event.id,
|
|
59
|
+
event.userData ? `${event.userData.length} bytes` : 'none'
|
|
60
|
+
);
|
|
52
61
|
this.logger.debug('Sending session complete event: %s', event.id);
|
|
53
62
|
const resp = await this.apiClient.put(
|
|
54
63
|
'/session/2025-03-17',
|
|
@@ -57,9 +66,11 @@ export class HTTPSessionEventProvider implements SessionEventProvider {
|
|
|
57
66
|
SessionCompleteEventDelayedSchema
|
|
58
67
|
);
|
|
59
68
|
if (resp.success) {
|
|
69
|
+
internal.info('[session-http] complete event sent successfully: %s', event.id);
|
|
60
70
|
this.logger.debug('Session complete event sent successfully: %s', event.id);
|
|
61
71
|
return;
|
|
62
72
|
}
|
|
73
|
+
internal.info('[session-http] complete event failed: %s - %s', event.id, resp.message);
|
|
63
74
|
throw new SessionResponseError({ message: resp.message });
|
|
64
75
|
}
|
|
65
76
|
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type SessionStartEvent,
|
|
4
4
|
type SessionCompleteEvent,
|
|
5
5
|
} from '@agentuity/core';
|
|
6
|
+
import { internal } from '../../logger/internal';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* An implementation of the SessionEventProvider which is no-op
|
|
@@ -13,8 +14,8 @@ export class LocalSessionEventProvider implements SessionEventProvider {
|
|
|
13
14
|
*
|
|
14
15
|
* @param event SessionStartEvent
|
|
15
16
|
*/
|
|
16
|
-
async start(
|
|
17
|
-
|
|
17
|
+
async start(event: SessionStartEvent): Promise<void> {
|
|
18
|
+
internal.info('[session-local] start event (no-op): %s', event.id);
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -22,7 +23,11 @@ export class LocalSessionEventProvider implements SessionEventProvider {
|
|
|
22
23
|
*
|
|
23
24
|
* @param event SessionCompleteEvent
|
|
24
25
|
*/
|
|
25
|
-
async complete(
|
|
26
|
-
|
|
26
|
+
async complete(event: SessionCompleteEvent): Promise<void> {
|
|
27
|
+
internal.info(
|
|
28
|
+
'[session-local] complete event (no-op): %s, userData: %s',
|
|
29
|
+
event.id,
|
|
30
|
+
event.userData ? `${event.userData.length} bytes` : 'none'
|
|
31
|
+
);
|
|
27
32
|
}
|
|
28
33
|
}
|
package/src/session.ts
CHANGED
|
@@ -72,6 +72,19 @@ export interface Thread {
|
|
|
72
72
|
*/
|
|
73
73
|
state: Map<string, unknown>;
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Unencrypted metadata for filtering and querying threads.
|
|
77
|
+
* Unlike state, metadata is stored as-is in the database with GIN indexes
|
|
78
|
+
* for efficient filtering. Initialized to empty object, only persisted if non-empty.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```typescript
|
|
82
|
+
* ctx.thread.metadata.userId = 'user123';
|
|
83
|
+
* ctx.thread.metadata.department = 'sales';
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
metadata: Record<string, unknown>;
|
|
87
|
+
|
|
75
88
|
/**
|
|
76
89
|
* Register an event listener for when the thread is destroyed.
|
|
77
90
|
* Thread is destroyed when it expires or is manually destroyed.
|
|
@@ -182,6 +195,19 @@ export interface Session {
|
|
|
182
195
|
*/
|
|
183
196
|
state: Map<string, unknown>;
|
|
184
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Unencrypted metadata for filtering and querying sessions.
|
|
200
|
+
* Unlike state, metadata is stored as-is in the database with GIN indexes
|
|
201
|
+
* for efficient filtering. Initialized to empty object, only persisted if non-empty.
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* ```typescript
|
|
205
|
+
* ctx.session.metadata.userId = 'user123';
|
|
206
|
+
* ctx.session.metadata.requestType = 'chat';
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
metadata: Record<string, unknown>;
|
|
210
|
+
|
|
185
211
|
/**
|
|
186
212
|
* Register an event listener for when the session completes.
|
|
187
213
|
* Fired after the agent handler returns and response is sent.
|
|
@@ -653,13 +679,20 @@ export class DefaultThread implements Thread {
|
|
|
653
679
|
#initialStateJson: string | undefined;
|
|
654
680
|
readonly id: string;
|
|
655
681
|
readonly state: Map<string, unknown>;
|
|
682
|
+
metadata: Record<string, unknown>;
|
|
656
683
|
private provider: ThreadProvider;
|
|
657
684
|
|
|
658
|
-
constructor(
|
|
685
|
+
constructor(
|
|
686
|
+
provider: ThreadProvider,
|
|
687
|
+
id: string,
|
|
688
|
+
initialStateJson?: string,
|
|
689
|
+
metadata?: Record<string, unknown>
|
|
690
|
+
) {
|
|
659
691
|
this.provider = provider;
|
|
660
692
|
this.id = id;
|
|
661
693
|
this.state = new Map();
|
|
662
694
|
this.#initialStateJson = initialStateJson;
|
|
695
|
+
this.metadata = metadata || {};
|
|
663
696
|
}
|
|
664
697
|
|
|
665
698
|
addEventListener(eventName: ThreadEventName, callback: ThreadEventCallback<any>): void {
|
|
@@ -707,10 +740,10 @@ export class DefaultThread implements Thread {
|
|
|
707
740
|
}
|
|
708
741
|
|
|
709
742
|
/**
|
|
710
|
-
* Check if thread has any data
|
|
743
|
+
* Check if thread has any data (state or metadata)
|
|
711
744
|
*/
|
|
712
745
|
empty(): boolean {
|
|
713
|
-
return this.state.size === 0;
|
|
746
|
+
return this.state.size === 0 && Object.keys(this.metadata).length === 0;
|
|
714
747
|
}
|
|
715
748
|
|
|
716
749
|
/**
|
|
@@ -718,7 +751,24 @@ export class DefaultThread implements Thread {
|
|
|
718
751
|
* @internal
|
|
719
752
|
*/
|
|
720
753
|
getSerializedState(): string {
|
|
721
|
-
|
|
754
|
+
const hasState = this.state.size > 0;
|
|
755
|
+
const hasMetadata = Object.keys(this.metadata).length > 0;
|
|
756
|
+
|
|
757
|
+
if (!hasState && !hasMetadata) {
|
|
758
|
+
return '';
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const data: { state?: Record<string, unknown>; metadata?: Record<string, unknown> } = {};
|
|
762
|
+
|
|
763
|
+
if (hasState) {
|
|
764
|
+
data.state = Object.fromEntries(this.state);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (hasMetadata) {
|
|
768
|
+
data.metadata = this.metadata;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
return JSON.stringify(data);
|
|
722
772
|
}
|
|
723
773
|
}
|
|
724
774
|
|
|
@@ -726,11 +776,13 @@ export class DefaultSession implements Session {
|
|
|
726
776
|
readonly id: string;
|
|
727
777
|
readonly thread: Thread;
|
|
728
778
|
readonly state: Map<string, unknown>;
|
|
779
|
+
metadata: Record<string, unknown>;
|
|
729
780
|
|
|
730
|
-
constructor(thread: Thread, id: string) {
|
|
781
|
+
constructor(thread: Thread, id: string, metadata?: Record<string, unknown>) {
|
|
731
782
|
this.id = id;
|
|
732
783
|
this.thread = thread;
|
|
733
784
|
this.state = new Map();
|
|
785
|
+
this.metadata = metadata || {};
|
|
734
786
|
}
|
|
735
787
|
|
|
736
788
|
addEventListener(eventName: SessionEventName, callback: SessionEventCallback<any>): void {
|
|
@@ -1032,7 +1084,11 @@ export class ThreadWebSocketClient {
|
|
|
1032
1084
|
});
|
|
1033
1085
|
}
|
|
1034
1086
|
|
|
1035
|
-
async save(
|
|
1087
|
+
async save(
|
|
1088
|
+
threadId: string,
|
|
1089
|
+
userData: string,
|
|
1090
|
+
threadMetadata?: Record<string, unknown>
|
|
1091
|
+
): Promise<void> {
|
|
1036
1092
|
// Wait for connection/reconnection if in progress
|
|
1037
1093
|
if (this.wsConnecting) {
|
|
1038
1094
|
await this.wsConnecting;
|
|
@@ -1058,10 +1114,20 @@ export class ThreadWebSocketClient {
|
|
|
1058
1114
|
reject,
|
|
1059
1115
|
});
|
|
1060
1116
|
|
|
1117
|
+
const data: { thread_id: string; user_data: string; metadata?: Record<string, unknown> } =
|
|
1118
|
+
{
|
|
1119
|
+
thread_id: threadId,
|
|
1120
|
+
user_data: userData,
|
|
1121
|
+
};
|
|
1122
|
+
|
|
1123
|
+
if (threadMetadata && Object.keys(threadMetadata).length > 0) {
|
|
1124
|
+
data.metadata = threadMetadata;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1061
1127
|
const message = {
|
|
1062
1128
|
id: requestId,
|
|
1063
1129
|
action: 'save',
|
|
1064
|
-
data
|
|
1130
|
+
data,
|
|
1065
1131
|
};
|
|
1066
1132
|
|
|
1067
1133
|
this.ws!.send(JSON.stringify(message));
|
|
@@ -1174,26 +1240,59 @@ export class DefaultThreadProvider implements ThreadProvider {
|
|
|
1174
1240
|
async restore(ctx: Context<Env>): Promise<Thread> {
|
|
1175
1241
|
const threadId = await this.threadIDProvider!.getThreadId(this.appState!, ctx);
|
|
1176
1242
|
validateThreadIdOrThrow(threadId);
|
|
1243
|
+
internal.info('[thread] restoring thread %s', threadId);
|
|
1177
1244
|
|
|
1178
1245
|
// Wait for WebSocket connection if still connecting
|
|
1179
1246
|
if (this.wsConnecting) {
|
|
1247
|
+
internal.info('[thread] waiting for WebSocket connection');
|
|
1180
1248
|
await this.wsConnecting;
|
|
1181
1249
|
}
|
|
1182
1250
|
|
|
1183
|
-
// Restore thread state from WebSocket if available
|
|
1251
|
+
// Restore thread state and metadata from WebSocket if available
|
|
1184
1252
|
let initialStateJson: string | undefined;
|
|
1253
|
+
let restoredMetadata: Record<string, unknown> | undefined;
|
|
1185
1254
|
if (this.wsClient) {
|
|
1186
1255
|
try {
|
|
1256
|
+
internal.info('[thread] restoring state from WebSocket');
|
|
1187
1257
|
const restoredData = await this.wsClient.restore(threadId);
|
|
1188
1258
|
if (restoredData) {
|
|
1189
1259
|
initialStateJson = restoredData;
|
|
1260
|
+
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
|
+
}
|
|
1284
|
+
} else {
|
|
1285
|
+
internal.info('[thread] no existing state found');
|
|
1190
1286
|
}
|
|
1191
|
-
} catch {
|
|
1287
|
+
} catch (err) {
|
|
1288
|
+
internal.info('[thread] WebSocket restore failed: %s', err);
|
|
1192
1289
|
// Continue with empty state rather than failing
|
|
1193
1290
|
}
|
|
1291
|
+
} else {
|
|
1292
|
+
internal.info('[thread] no WebSocket client available');
|
|
1194
1293
|
}
|
|
1195
1294
|
|
|
1196
|
-
const thread = new DefaultThread(this, threadId, initialStateJson);
|
|
1295
|
+
const thread = new DefaultThread(this, threadId, initialStateJson, restoredMetadata);
|
|
1197
1296
|
|
|
1198
1297
|
// Populate thread state from restored data
|
|
1199
1298
|
if (initialStateJson) {
|
|
@@ -1202,7 +1301,9 @@ export class DefaultThreadProvider implements ThreadProvider {
|
|
|
1202
1301
|
for (const [key, value] of Object.entries(data)) {
|
|
1203
1302
|
thread.state.set(key, value);
|
|
1204
1303
|
}
|
|
1205
|
-
|
|
1304
|
+
internal.info('[thread] populated state with %d keys', thread.state.size);
|
|
1305
|
+
} catch (err) {
|
|
1306
|
+
internal.info('[thread] failed to parse state JSON: %s', err);
|
|
1206
1307
|
// Continue with empty state if parsing fails
|
|
1207
1308
|
}
|
|
1208
1309
|
}
|
|
@@ -1213,8 +1314,16 @@ export class DefaultThreadProvider implements ThreadProvider {
|
|
|
1213
1314
|
|
|
1214
1315
|
async save(thread: Thread): Promise<void> {
|
|
1215
1316
|
if (thread instanceof DefaultThread) {
|
|
1317
|
+
internal.info(
|
|
1318
|
+
'[thread] DefaultThreadProvider.save() - thread %s, isDirty: %s, hasWsClient: %s',
|
|
1319
|
+
thread.id,
|
|
1320
|
+
thread.isDirty(),
|
|
1321
|
+
!!this.wsClient
|
|
1322
|
+
);
|
|
1323
|
+
|
|
1216
1324
|
// Wait for WebSocket connection if still connecting
|
|
1217
1325
|
if (this.wsConnecting) {
|
|
1326
|
+
internal.info('[thread] waiting for WebSocket connection');
|
|
1218
1327
|
await this.wsConnecting;
|
|
1219
1328
|
}
|
|
1220
1329
|
|
|
@@ -1222,10 +1331,20 @@ export class DefaultThreadProvider implements ThreadProvider {
|
|
|
1222
1331
|
if (this.wsClient && thread.isDirty()) {
|
|
1223
1332
|
try {
|
|
1224
1333
|
const serialized = thread.getSerializedState();
|
|
1225
|
-
|
|
1226
|
-
|
|
1334
|
+
internal.info(
|
|
1335
|
+
'[thread] saving to WebSocket, serialized length: %d',
|
|
1336
|
+
serialized.length
|
|
1337
|
+
);
|
|
1338
|
+
const metadata =
|
|
1339
|
+
Object.keys(thread.metadata).length > 0 ? thread.metadata : undefined;
|
|
1340
|
+
await this.wsClient.save(thread.id, serialized, metadata);
|
|
1341
|
+
internal.info('[thread] WebSocket save completed');
|
|
1342
|
+
} catch (err) {
|
|
1343
|
+
internal.info('[thread] WebSocket save failed: %s', err);
|
|
1227
1344
|
// Don't throw - allow request to complete even if save fails
|
|
1228
1345
|
}
|
|
1346
|
+
} else {
|
|
1347
|
+
internal.info('[thread] skipping save - no wsClient or thread not dirty');
|
|
1229
1348
|
}
|
|
1230
1349
|
}
|
|
1231
1350
|
}
|
|
@@ -1269,20 +1388,30 @@ export class DefaultSessionProvider implements SessionProvider {
|
|
|
1269
1388
|
}
|
|
1270
1389
|
|
|
1271
1390
|
async restore(thread: Thread, sessionId: string): Promise<Session> {
|
|
1391
|
+
internal.info('[session] restoring session %s for thread %s', sessionId, thread.id);
|
|
1272
1392
|
let session = this.sessions.get(sessionId);
|
|
1273
1393
|
if (!session) {
|
|
1274
1394
|
session = new DefaultSession(thread, sessionId);
|
|
1275
1395
|
this.sessions.set(sessionId, session);
|
|
1396
|
+
internal.info('[session] created new session, firing session.started');
|
|
1276
1397
|
await fireEvent('session.started', session);
|
|
1398
|
+
} else {
|
|
1399
|
+
internal.info('[session] found existing session');
|
|
1277
1400
|
}
|
|
1278
1401
|
return session;
|
|
1279
1402
|
}
|
|
1280
1403
|
|
|
1281
1404
|
async save(session: Session): Promise<void> {
|
|
1282
1405
|
if (session instanceof DefaultSession) {
|
|
1406
|
+
internal.info(
|
|
1407
|
+
'[session] DefaultSessionProvider.save() - firing completed event for session %s',
|
|
1408
|
+
session.id
|
|
1409
|
+
);
|
|
1283
1410
|
try {
|
|
1284
1411
|
await session.fireEvent('completed');
|
|
1412
|
+
internal.info('[session] session.fireEvent completed, firing app event');
|
|
1285
1413
|
await fireEvent('session.completed', session);
|
|
1414
|
+
internal.info('[session] session.completed app event fired');
|
|
1286
1415
|
} finally {
|
|
1287
1416
|
this.sessions.delete(session.id);
|
|
1288
1417
|
sessionEventListeners.delete(session);
|