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