@astralform/js 0.2.3 → 1.1.0
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/README.md +160 -73
- package/dist/index.cjs +972 -1022
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +738 -564
- package/dist/index.d.ts +738 -564
- package/dist/index.js +965 -1019
- package/dist/index.js.map +1 -1
- package/package.json +17 -10
package/dist/index.js
CHANGED
|
@@ -13,9 +13,10 @@ var AuthenticationError = class extends AstralformError {
|
|
|
13
13
|
}
|
|
14
14
|
};
|
|
15
15
|
var RateLimitError = class extends AstralformError {
|
|
16
|
-
constructor(message = "Rate limit exceeded") {
|
|
16
|
+
constructor(message = "Rate limit exceeded", details = {}) {
|
|
17
17
|
super(message, "rate_limit_error");
|
|
18
18
|
this.name = "RateLimitError";
|
|
19
|
+
Object.assign(this, details);
|
|
19
20
|
}
|
|
20
21
|
};
|
|
21
22
|
var LLMNotConfiguredError = class extends AstralformError {
|
|
@@ -43,6 +44,143 @@ var StreamAbortedError = class extends AstralformError {
|
|
|
43
44
|
}
|
|
44
45
|
};
|
|
45
46
|
|
|
47
|
+
// src/utils.ts
|
|
48
|
+
function sanitizeErrorText(text) {
|
|
49
|
+
return text.slice(0, 500).replace(/Bearer\s+\S+/gi, "Bearer [REDACTED]");
|
|
50
|
+
}
|
|
51
|
+
function generateId() {
|
|
52
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
53
|
+
return crypto.randomUUID();
|
|
54
|
+
}
|
|
55
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
56
|
+
const r = Math.random() * 16 | 0;
|
|
57
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
58
|
+
return v.toString(16);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/rate-limit.ts
|
|
63
|
+
var DEFAULT_MESSAGE = "Rate limit exceeded";
|
|
64
|
+
function parseNumber(value) {
|
|
65
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
if (typeof value === "string") {
|
|
69
|
+
const trimmed = value.trim();
|
|
70
|
+
if (!trimmed) {
|
|
71
|
+
return void 0;
|
|
72
|
+
}
|
|
73
|
+
const parsed = Number(trimmed);
|
|
74
|
+
if (Number.isFinite(parsed)) {
|
|
75
|
+
return parsed;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return void 0;
|
|
79
|
+
}
|
|
80
|
+
function parseString(value) {
|
|
81
|
+
if (typeof value !== "string") {
|
|
82
|
+
return void 0;
|
|
83
|
+
}
|
|
84
|
+
const trimmed = value.trim();
|
|
85
|
+
return trimmed ? trimmed : void 0;
|
|
86
|
+
}
|
|
87
|
+
function parseJsonObject(rawText) {
|
|
88
|
+
if (!rawText) {
|
|
89
|
+
return void 0;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(rawText);
|
|
93
|
+
if (parsed && typeof parsed === "object") {
|
|
94
|
+
return parsed;
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
}
|
|
98
|
+
return void 0;
|
|
99
|
+
}
|
|
100
|
+
function parseRetryAfterHeader(value) {
|
|
101
|
+
if (!value) {
|
|
102
|
+
return void 0;
|
|
103
|
+
}
|
|
104
|
+
const numeric = parseNumber(value);
|
|
105
|
+
if (numeric !== void 0) {
|
|
106
|
+
return Math.max(0, Math.ceil(numeric));
|
|
107
|
+
}
|
|
108
|
+
const asDate = Date.parse(value);
|
|
109
|
+
if (Number.isFinite(asDate)) {
|
|
110
|
+
const diffMs = asDate - Date.now();
|
|
111
|
+
return Math.max(0, Math.ceil(diffMs / 1e3));
|
|
112
|
+
}
|
|
113
|
+
return void 0;
|
|
114
|
+
}
|
|
115
|
+
function parseResetTimestamp(value) {
|
|
116
|
+
const numeric = parseNumber(value);
|
|
117
|
+
if (numeric !== void 0) {
|
|
118
|
+
if (numeric > 1e12) {
|
|
119
|
+
return Math.floor(numeric);
|
|
120
|
+
}
|
|
121
|
+
return Math.floor(numeric * 1e3);
|
|
122
|
+
}
|
|
123
|
+
const asString = parseString(value);
|
|
124
|
+
if (!asString) {
|
|
125
|
+
return void 0;
|
|
126
|
+
}
|
|
127
|
+
const asDate = Date.parse(asString);
|
|
128
|
+
if (Number.isFinite(asDate)) {
|
|
129
|
+
return asDate;
|
|
130
|
+
}
|
|
131
|
+
return void 0;
|
|
132
|
+
}
|
|
133
|
+
function pickFirst(payload, keys, parser) {
|
|
134
|
+
for (const key of keys) {
|
|
135
|
+
const parsed = parser(payload[key]);
|
|
136
|
+
if (parsed !== void 0) {
|
|
137
|
+
return parsed;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return void 0;
|
|
141
|
+
}
|
|
142
|
+
function buildRateLimitDetails(payload, headers) {
|
|
143
|
+
const headerRetryAfter = headers ? parseRetryAfterHeader(headers.get("retry-after")) : void 0;
|
|
144
|
+
const bodyRetryAfter = pickFirst(
|
|
145
|
+
payload,
|
|
146
|
+
["retry_after", "retryAfter", "retry_after_sec", "retryAfterSec"],
|
|
147
|
+
parseNumber
|
|
148
|
+
);
|
|
149
|
+
const retryAfterSec = bodyRetryAfter ?? headerRetryAfter;
|
|
150
|
+
const headerReset = headers ? parseResetTimestamp(
|
|
151
|
+
headers.get("x-ratelimit-reset") ?? headers.get("x-ratelimit-reset-at")
|
|
152
|
+
) : void 0;
|
|
153
|
+
const bodyReset = parseResetTimestamp(
|
|
154
|
+
payload.reset_at ?? payload.resetAt ?? payload.reset
|
|
155
|
+
);
|
|
156
|
+
const resetAt = bodyReset ?? headerReset ?? (retryAfterSec !== void 0 ? Date.now() + retryAfterSec * 1e3 : void 0);
|
|
157
|
+
const limit = pickFirst(payload, ["limit", "rate_limit", "max"], parseNumber) ?? (headers ? parseNumber(headers.get("x-ratelimit-limit")) : void 0);
|
|
158
|
+
const remaining = pickFirst(payload, ["remaining", "rate_limit_remaining"], parseNumber) ?? (headers ? parseNumber(headers.get("x-ratelimit-remaining")) : void 0);
|
|
159
|
+
const scope = pickFirst(payload, ["scope", "limit_scope"], parseString) ?? (headers ? parseString(headers.get("x-ratelimit-scope")) : void 0);
|
|
160
|
+
const policyId = pickFirst(payload, ["policy_id", "policyId", "policy"], parseString) ?? (headers ? parseString(
|
|
161
|
+
headers.get("x-ratelimit-policy") ?? headers.get("x-ratelimit-policy-id")
|
|
162
|
+
) : void 0);
|
|
163
|
+
const requestId = pickFirst(payload, ["request_id", "requestId"], parseString) ?? (headers ? parseString(
|
|
164
|
+
headers.get("x-request-id") ?? headers.get("x-correlation-id")
|
|
165
|
+
) : void 0);
|
|
166
|
+
return {
|
|
167
|
+
retryAfterSec,
|
|
168
|
+
resetAt,
|
|
169
|
+
scope,
|
|
170
|
+
policyId,
|
|
171
|
+
limit,
|
|
172
|
+
remaining,
|
|
173
|
+
requestId
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function createRateLimitErrorFromHttp(response, rawText) {
|
|
177
|
+
const payload = parseJsonObject(rawText) ?? {};
|
|
178
|
+
const details = buildRateLimitDetails(payload, response.headers);
|
|
179
|
+
const sanitizedText = sanitizeErrorText(rawText);
|
|
180
|
+
const message = pickFirst(payload, ["message", "error_description"], parseString) ?? (sanitizedText || DEFAULT_MESSAGE);
|
|
181
|
+
return new RateLimitError(message, details);
|
|
182
|
+
}
|
|
183
|
+
|
|
46
184
|
// src/streaming.ts
|
|
47
185
|
async function* streamJobSSE(options) {
|
|
48
186
|
const { url, headers, signal, fetchFn } = options;
|
|
@@ -63,12 +201,12 @@ async function* streamJobSSE(options) {
|
|
|
63
201
|
}
|
|
64
202
|
if (!response.ok) {
|
|
65
203
|
const rawText = await response.text().catch(() => "");
|
|
66
|
-
const text = rawText ? rawText
|
|
204
|
+
const text = rawText ? sanitizeErrorText(rawText) : "";
|
|
67
205
|
switch (response.status) {
|
|
68
206
|
case 401:
|
|
69
207
|
throw new AuthenticationError();
|
|
70
208
|
case 429:
|
|
71
|
-
throw
|
|
209
|
+
throw createRateLimitErrorFromHttp(response, rawText);
|
|
72
210
|
default:
|
|
73
211
|
throw new ServerError(text || `HTTP ${response.status}`);
|
|
74
212
|
}
|
|
@@ -131,20 +269,126 @@ function validateBaseURL(url) {
|
|
|
131
269
|
throw new Error(`Invalid baseURL: "${cleaned}" is not a valid URL`);
|
|
132
270
|
}
|
|
133
271
|
}
|
|
272
|
+
function isApiKeyConfig(config) {
|
|
273
|
+
return "apiKey" in config;
|
|
274
|
+
}
|
|
134
275
|
var AstralformClient = class {
|
|
135
276
|
constructor(config) {
|
|
136
|
-
if (
|
|
137
|
-
|
|
277
|
+
if (isApiKeyConfig(config)) {
|
|
278
|
+
if (!config.apiKey || typeof config.apiKey !== "string") {
|
|
279
|
+
throw new Error("apiKey is required and must be a non-empty string");
|
|
280
|
+
}
|
|
281
|
+
if (!config.userId || typeof config.userId !== "string") {
|
|
282
|
+
throw new Error("userId is required in API-key mode");
|
|
283
|
+
}
|
|
284
|
+
this.auth = {
|
|
285
|
+
kind: "api_key",
|
|
286
|
+
apiKey: config.apiKey,
|
|
287
|
+
userId: config.userId
|
|
288
|
+
};
|
|
289
|
+
} else {
|
|
290
|
+
if (!config.accessToken || typeof config.accessToken !== "string") {
|
|
291
|
+
throw new Error(
|
|
292
|
+
"accessToken is required and must be a non-empty string in user-token mode"
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
const projectId = typeof config.projectId === "string" && config.projectId.length > 0 ? config.projectId : null;
|
|
296
|
+
this.auth = {
|
|
297
|
+
kind: "user_token",
|
|
298
|
+
accessToken: config.accessToken,
|
|
299
|
+
projectId,
|
|
300
|
+
endUserId: typeof config.endUserId === "string" && config.endUserId.length > 0 ? config.endUserId : null
|
|
301
|
+
};
|
|
138
302
|
}
|
|
139
|
-
this.apiKey = config.apiKey;
|
|
140
303
|
this.baseURL = validateBaseURL(config.baseURL ?? DEFAULT_BASE_URL);
|
|
141
|
-
this.userId = config.userId;
|
|
142
304
|
this.fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
143
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* Replace the current OIDC access token without reconstructing the client.
|
|
308
|
+
* Use after refreshing via the host's token manager (e.g., Supabase JS SDK).
|
|
309
|
+
* Throws if the client was created in API-key mode.
|
|
310
|
+
*/
|
|
311
|
+
updateAccessToken(accessToken) {
|
|
312
|
+
if (this.auth.kind !== "user_token") {
|
|
313
|
+
throw new Error("updateAccessToken is only valid in user-token mode");
|
|
314
|
+
}
|
|
315
|
+
if (!accessToken || typeof accessToken !== "string") {
|
|
316
|
+
throw new Error("accessToken must be a non-empty string");
|
|
317
|
+
}
|
|
318
|
+
this.auth = { ...this.auth, accessToken };
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Swap the active project for a user-token client. The backend verifies the
|
|
322
|
+
* current developer has access to the new project; a 403 comes back if not.
|
|
323
|
+
*/
|
|
324
|
+
updateProjectId(projectId) {
|
|
325
|
+
if (this.auth.kind !== "user_token") {
|
|
326
|
+
throw new Error("updateProjectId is only valid in user-token mode");
|
|
327
|
+
}
|
|
328
|
+
if (!projectId || typeof projectId !== "string") {
|
|
329
|
+
throw new Error("projectId must be a non-empty string");
|
|
330
|
+
}
|
|
331
|
+
this.auth = { ...this.auth, projectId };
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Set (or clear) the end-user override for user-token mode.
|
|
335
|
+
*
|
|
336
|
+
* Pass `null` or an empty string to clear — subsequent requests go
|
|
337
|
+
* back to scoping against the developer's own identity. Throws if
|
|
338
|
+
* called in API-key mode, where end-user context already travels via
|
|
339
|
+
* the constructor's `userId` field.
|
|
340
|
+
*/
|
|
341
|
+
updateEndUserId(endUserId) {
|
|
342
|
+
if (this.auth.kind !== "user_token") {
|
|
343
|
+
throw new Error("updateEndUserId is only valid in user-token mode");
|
|
344
|
+
}
|
|
345
|
+
const normalized = typeof endUserId === "string" && endUserId.length > 0 ? endUserId : null;
|
|
346
|
+
this.auth = { ...this.auth, endUserId: normalized };
|
|
347
|
+
}
|
|
348
|
+
/** Current end-user override in user-token mode, or `null` if unset. */
|
|
349
|
+
get endUserId() {
|
|
350
|
+
return this.auth.kind === "user_token" ? this.auth.endUserId : null;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Active project for user-token mode, or `null` if pre-pick (client
|
|
354
|
+
* was constructed without one). For API-key mode the project is baked
|
|
355
|
+
* into the key, so this getter returns `null` there too — use
|
|
356
|
+
* `authMode` to disambiguate.
|
|
357
|
+
*/
|
|
358
|
+
get projectId() {
|
|
359
|
+
return this.auth.kind === "user_token" ? this.auth.projectId : null;
|
|
360
|
+
}
|
|
361
|
+
/** Which auth mode this client was constructed with. */
|
|
362
|
+
get authMode() {
|
|
363
|
+
return this.auth.kind;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Authorization + identity headers for the current auth mode, without
|
|
367
|
+
* `Content-Type`. Suitable for JSON requests (paired with the JSON header
|
|
368
|
+
* in the `headers` getter) and for multipart uploads where the browser
|
|
369
|
+
* must set its own `Content-Type` boundary.
|
|
370
|
+
*/
|
|
371
|
+
get authHeaders() {
|
|
372
|
+
if (this.auth.kind === "api_key") {
|
|
373
|
+
return {
|
|
374
|
+
Authorization: `Bearer ${this.auth.apiKey}`,
|
|
375
|
+
"X-End-User-ID": this.auth.userId
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
const headers = {
|
|
379
|
+
Authorization: `Bearer ${this.auth.accessToken}`
|
|
380
|
+
};
|
|
381
|
+
if (this.auth.projectId) {
|
|
382
|
+
headers["X-Project-ID"] = this.auth.projectId;
|
|
383
|
+
}
|
|
384
|
+
if (this.auth.endUserId) {
|
|
385
|
+
headers["X-End-User-ID"] = this.auth.endUserId;
|
|
386
|
+
}
|
|
387
|
+
return headers;
|
|
388
|
+
}
|
|
144
389
|
get headers() {
|
|
145
390
|
return {
|
|
146
|
-
|
|
147
|
-
"X-End-User-ID": this.userId,
|
|
391
|
+
...this.authHeaders,
|
|
148
392
|
"Content-Type": "application/json"
|
|
149
393
|
};
|
|
150
394
|
}
|
|
@@ -179,9 +423,9 @@ var AstralformClient = class {
|
|
|
179
423
|
case 401:
|
|
180
424
|
throw new AuthenticationError();
|
|
181
425
|
case 429:
|
|
182
|
-
throw
|
|
426
|
+
throw createRateLimitErrorFromHttp(response, text);
|
|
183
427
|
default: {
|
|
184
|
-
const safeText = text ? text
|
|
428
|
+
const safeText = text ? sanitizeErrorText(text) : "";
|
|
185
429
|
throw new ServerError(safeText || `HTTP ${response.status}`);
|
|
186
430
|
}
|
|
187
431
|
}
|
|
@@ -192,12 +436,18 @@ var AstralformClient = class {
|
|
|
192
436
|
}
|
|
193
437
|
async getProjectStatus() {
|
|
194
438
|
const raw = await this.get("/v1/project/status");
|
|
439
|
+
const ui = raw.ui_components ?? {};
|
|
195
440
|
return {
|
|
196
441
|
isReady: raw.is_ready,
|
|
197
442
|
llmConfigured: raw.llm_configured,
|
|
198
443
|
llmProvider: raw.llm_provider,
|
|
199
444
|
llmModel: raw.llm_model,
|
|
200
|
-
message: raw.message
|
|
445
|
+
message: raw.message,
|
|
446
|
+
uiComponents: {
|
|
447
|
+
enabled: Boolean(ui.enabled),
|
|
448
|
+
protocol: ui.protocol ?? null,
|
|
449
|
+
mimeType: ui.mime_type ?? null
|
|
450
|
+
}
|
|
201
451
|
};
|
|
202
452
|
}
|
|
203
453
|
async getConversations(limit = 50, offset = 0) {
|
|
@@ -255,6 +505,52 @@ var AstralformClient = class {
|
|
|
255
505
|
async submitToolResult(request) {
|
|
256
506
|
await this.post("/v1/tool-result", request);
|
|
257
507
|
}
|
|
508
|
+
async submitToolApproval(request) {
|
|
509
|
+
await this.post("/v1/tool-approval", request);
|
|
510
|
+
}
|
|
511
|
+
// --- End-user tool-permission self-service ---
|
|
512
|
+
/**
|
|
513
|
+
* List the current end user's own remembered tool-permission grants.
|
|
514
|
+
* Only `conversation`/`always` grants exist (`once` is never persisted).
|
|
515
|
+
* Paginated via `limit` (default 100, max 200) / `offset`; `total` lets you
|
|
516
|
+
* page through all of them.
|
|
517
|
+
*/
|
|
518
|
+
async getMyToolPermissions(options) {
|
|
519
|
+
const params = new URLSearchParams();
|
|
520
|
+
if (options?.limit != null) {
|
|
521
|
+
const safeLimit = Math.max(
|
|
522
|
+
1,
|
|
523
|
+
Math.min(200, Math.floor(Number(options.limit)))
|
|
524
|
+
);
|
|
525
|
+
params.set("limit", String(safeLimit));
|
|
526
|
+
}
|
|
527
|
+
if (options?.offset != null) {
|
|
528
|
+
const safeOffset = Math.max(0, Math.floor(Number(options.offset)));
|
|
529
|
+
params.set("offset", String(safeOffset));
|
|
530
|
+
}
|
|
531
|
+
const qs = params.toString();
|
|
532
|
+
const raw = await this.get(`/v1/me/tool-permissions${qs ? `?${qs}` : ""}`);
|
|
533
|
+
return {
|
|
534
|
+
grants: raw.grants.map((g) => ({
|
|
535
|
+
id: g.id,
|
|
536
|
+
toolName: g.tool_name,
|
|
537
|
+
decision: g.decision,
|
|
538
|
+
scope: g.scope,
|
|
539
|
+
conversationId: g.conversation_id,
|
|
540
|
+
createdAt: g.created_at
|
|
541
|
+
})),
|
|
542
|
+
total: raw.total,
|
|
543
|
+
limit: raw.limit,
|
|
544
|
+
offset: raw.offset
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Revoke one of the current end user's remembered grants by id. The agent
|
|
549
|
+
* will ask again the next time that tool is used.
|
|
550
|
+
*/
|
|
551
|
+
async revokeToolPermission(id) {
|
|
552
|
+
await this.del(`/v1/me/tool-permissions/${encodeURIComponent(id)}`);
|
|
553
|
+
}
|
|
258
554
|
// --- Conversation Assets ---
|
|
259
555
|
mapAsset(raw) {
|
|
260
556
|
return {
|
|
@@ -276,10 +572,7 @@ var AstralformClient = class {
|
|
|
276
572
|
`${this.baseURL}/v1/conversations/${encodeURIComponent(conversationId)}/uploads`,
|
|
277
573
|
{
|
|
278
574
|
method: "POST",
|
|
279
|
-
headers:
|
|
280
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
281
|
-
"X-End-User-ID": this.userId
|
|
282
|
-
},
|
|
575
|
+
headers: this.authHeaders,
|
|
283
576
|
body: formData
|
|
284
577
|
}
|
|
285
578
|
).catch((err) => {
|
|
@@ -303,6 +596,31 @@ var AstralformClient = class {
|
|
|
303
596
|
);
|
|
304
597
|
return raw.map((r) => this.mapAsset(r));
|
|
305
598
|
}
|
|
599
|
+
// --- Account-scoped discovery (user-token mode) ---
|
|
600
|
+
//
|
|
601
|
+
// Lets a signed-in user pick which team/project they want to act on.
|
|
602
|
+
// Backend gates these on OIDC user context (no X-Project-ID required) —
|
|
603
|
+
// sending them in API-key mode yields 401.
|
|
604
|
+
async listTeams() {
|
|
605
|
+
const raw = await this.get("/v1/teams");
|
|
606
|
+
return raw.map((t) => ({
|
|
607
|
+
id: t.id,
|
|
608
|
+
name: t.name,
|
|
609
|
+
slug: t.slug,
|
|
610
|
+
isDefault: t.is_default,
|
|
611
|
+
role: t.role
|
|
612
|
+
}));
|
|
613
|
+
}
|
|
614
|
+
async listProjects(teamId) {
|
|
615
|
+
const raw = await this.get(`/v1/teams/${encodeURIComponent(teamId)}/projects`);
|
|
616
|
+
return raw.map((p) => ({
|
|
617
|
+
id: p.id,
|
|
618
|
+
name: p.name,
|
|
619
|
+
teamId: p.team_id,
|
|
620
|
+
createdAt: p.created_at,
|
|
621
|
+
updatedAt: p.updated_at
|
|
622
|
+
}));
|
|
623
|
+
}
|
|
306
624
|
// --- Jobs API ---
|
|
307
625
|
async createJob(request) {
|
|
308
626
|
return this.post("/v1/jobs", request);
|
|
@@ -319,90 +637,50 @@ var AstralformClient = class {
|
|
|
319
637
|
async cancelJob(jobId) {
|
|
320
638
|
await this.post(`/v1/jobs/${encodeURIComponent(jobId)}/cancel`, {});
|
|
321
639
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
this.thinkingStartMs = null;
|
|
335
|
-
this.activeEditorId = null;
|
|
336
|
-
this.activeTodoId = null;
|
|
337
|
-
}
|
|
338
|
-
// ── Registration ──────────────────────────────────────────────
|
|
339
|
-
on(eventType, handler) {
|
|
340
|
-
this._handlers.set(eventType, handler);
|
|
341
|
-
}
|
|
342
|
-
registerHandlers(handlers) {
|
|
343
|
-
for (const [type, handler] of Object.entries(handlers)) {
|
|
344
|
-
this._handlers.set(type, handler);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
// ── Event processing ──────────────────────────────────────────
|
|
348
|
-
processEvent(event) {
|
|
349
|
-
const handler = this._handlers.get(event.type);
|
|
350
|
-
if (handler === null) return;
|
|
351
|
-
if (!handler) return;
|
|
352
|
-
handler(event, this);
|
|
353
|
-
}
|
|
354
|
-
// ── State ─────────────────────────────────────────────────────
|
|
355
|
-
getBlocks() {
|
|
356
|
-
return [...this._blocks];
|
|
357
|
-
}
|
|
358
|
-
reset() {
|
|
359
|
-
this._blocks = [];
|
|
360
|
-
this.activeTextId = null;
|
|
361
|
-
this.activeThinkingId = null;
|
|
362
|
-
this.thinkingStartMs = null;
|
|
363
|
-
this.activeEditorId = null;
|
|
364
|
-
this.activeTodoId = null;
|
|
365
|
-
}
|
|
366
|
-
setOnChange(fn) {
|
|
367
|
-
this._onChange = fn;
|
|
368
|
-
}
|
|
369
|
-
// ── Block manipulation (used by handlers) ─────────────────────
|
|
370
|
-
addBlock(block) {
|
|
371
|
-
this._blocks = [...this._blocks, block];
|
|
372
|
-
this._notify();
|
|
373
|
-
}
|
|
374
|
-
updateBlock(id, updater) {
|
|
375
|
-
let changed = false;
|
|
376
|
-
this._blocks = this._blocks.map((b) => {
|
|
377
|
-
if (b.id !== id) return b;
|
|
378
|
-
changed = true;
|
|
379
|
-
return updater(b);
|
|
380
|
-
});
|
|
381
|
-
if (changed) this._notify();
|
|
382
|
-
}
|
|
383
|
-
/** Update any block by id with a partial update (type-loose for handlers). */
|
|
384
|
-
patchBlock(id, patch) {
|
|
385
|
-
let changed = false;
|
|
386
|
-
this._blocks = this._blocks.map((b) => {
|
|
387
|
-
if (b.id !== id) return b;
|
|
388
|
-
changed = true;
|
|
389
|
-
return { ...b, ...patch };
|
|
390
|
-
});
|
|
391
|
-
if (changed) this._notify();
|
|
640
|
+
async getJob(jobId) {
|
|
641
|
+
const raw = await this.get(`/v1/jobs/${encodeURIComponent(jobId)}`);
|
|
642
|
+
return {
|
|
643
|
+
jobId: raw.job_id,
|
|
644
|
+
status: raw.status,
|
|
645
|
+
createdAt: raw.created_at ?? null,
|
|
646
|
+
startedAt: raw.started_at ?? null,
|
|
647
|
+
completedAt: raw.completed_at ?? null,
|
|
648
|
+
errorMessage: raw.error_message ?? null,
|
|
649
|
+
inputTokens: raw.input_tokens ?? 0,
|
|
650
|
+
outputTokens: raw.output_tokens ?? 0
|
|
651
|
+
};
|
|
392
652
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
653
|
+
async submitFeedback(jobId, request) {
|
|
654
|
+
const body = {
|
|
655
|
+
rating: request.rating
|
|
656
|
+
};
|
|
657
|
+
if (request.comment != null) body.comment = request.comment;
|
|
658
|
+
const raw = await this.post(`/v1/jobs/${encodeURIComponent(jobId)}/feedback`, body);
|
|
659
|
+
return {
|
|
660
|
+
id: raw.id,
|
|
661
|
+
jobId: raw.job_id,
|
|
662
|
+
rating: raw.rating,
|
|
663
|
+
comment: raw.comment,
|
|
664
|
+
createdAt: raw.created_at
|
|
665
|
+
};
|
|
399
666
|
}
|
|
400
|
-
|
|
401
|
-
|
|
667
|
+
async getActiveJob(conversationId) {
|
|
668
|
+
const raw = await this.get(`/v1/conversations/${encodeURIComponent(conversationId)}/active-job`);
|
|
669
|
+
return {
|
|
670
|
+
jobId: raw.job_id ?? null,
|
|
671
|
+
status: raw.status
|
|
672
|
+
};
|
|
402
673
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
674
|
+
async listJobs(conversationId) {
|
|
675
|
+
const raw = await this.get(`/v1/conversations/${encodeURIComponent(conversationId)}/jobs`);
|
|
676
|
+
return raw.map((j) => ({
|
|
677
|
+
jobId: j.job_id,
|
|
678
|
+
status: j.status,
|
|
679
|
+
replacesJobId: j.replaces_job_id ?? null,
|
|
680
|
+
responseContent: j.response_content ?? null,
|
|
681
|
+
metrics: j.metrics ?? null,
|
|
682
|
+
createdAt: j.created_at ?? null
|
|
683
|
+
}));
|
|
406
684
|
}
|
|
407
685
|
};
|
|
408
686
|
|
|
@@ -556,40 +834,348 @@ var ToolRegistry = class {
|
|
|
556
834
|
}
|
|
557
835
|
};
|
|
558
836
|
|
|
559
|
-
// src/
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
837
|
+
// src/protocol-registry.ts
|
|
838
|
+
var ProtocolRegistry = class {
|
|
839
|
+
constructor() {
|
|
840
|
+
this.adapters = /* @__PURE__ */ new Map();
|
|
841
|
+
}
|
|
842
|
+
/** Register or replace the adapter for a MIME type. */
|
|
843
|
+
register(adapter) {
|
|
844
|
+
this.adapters.set(adapter.mimeType, adapter);
|
|
845
|
+
}
|
|
846
|
+
/** Remove the adapter for a MIME type. No-op if not registered. */
|
|
847
|
+
unregister(mimeType) {
|
|
848
|
+
this.adapters.delete(mimeType);
|
|
849
|
+
}
|
|
850
|
+
/** Returns the adapter for a MIME type, or ``null`` if none is registered. */
|
|
851
|
+
get(mimeType) {
|
|
852
|
+
return this.adapters.get(mimeType) ?? null;
|
|
853
|
+
}
|
|
854
|
+
has(mimeType) {
|
|
855
|
+
return this.adapters.has(mimeType);
|
|
856
|
+
}
|
|
857
|
+
/** Drop every adapter. Called when a session disconnects. */
|
|
858
|
+
clear() {
|
|
859
|
+
this.adapters.clear();
|
|
860
|
+
}
|
|
861
|
+
listMimeTypes() {
|
|
862
|
+
return Array.from(this.adapters.keys());
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
|
|
866
|
+
// src/translate.ts
|
|
867
|
+
function translateDelta(wire) {
|
|
868
|
+
switch (wire.channel) {
|
|
869
|
+
case "text":
|
|
870
|
+
return { channel: "text", text: wire.text };
|
|
871
|
+
case "thinking":
|
|
872
|
+
return { channel: "thinking", text: wire.text };
|
|
873
|
+
case "signature":
|
|
874
|
+
return { channel: "signature", signature: wire.signature };
|
|
875
|
+
case "input":
|
|
876
|
+
return { channel: "input", partialJson: wire.partial_json };
|
|
877
|
+
case "input_arg":
|
|
878
|
+
return {
|
|
879
|
+
channel: "inputArg",
|
|
880
|
+
argName: wire.arg_name,
|
|
881
|
+
text: wire.text
|
|
882
|
+
};
|
|
883
|
+
case "output":
|
|
884
|
+
return { channel: "output", stream: wire.stream, chunk: wire.chunk };
|
|
885
|
+
case "status":
|
|
886
|
+
return {
|
|
887
|
+
channel: "status",
|
|
888
|
+
status: wire.status,
|
|
889
|
+
note: wire.note
|
|
890
|
+
};
|
|
891
|
+
default:
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
function translateAgentIdentity(raw) {
|
|
896
|
+
return {
|
|
897
|
+
name: raw.name ?? "",
|
|
898
|
+
displayName: raw.display_name ?? null,
|
|
899
|
+
avatarUrl: raw.avatar_url ?? null,
|
|
900
|
+
description: raw.description ?? null
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
function translateCustomEvent(name, data) {
|
|
904
|
+
switch (name) {
|
|
905
|
+
case "user_message":
|
|
906
|
+
return {
|
|
907
|
+
type: "user_message",
|
|
908
|
+
content: data.content ?? "",
|
|
909
|
+
createdAt: data.created_at
|
|
910
|
+
};
|
|
911
|
+
case "title_generated":
|
|
912
|
+
return {
|
|
913
|
+
type: "title_generated",
|
|
914
|
+
title: data.title ?? ""
|
|
915
|
+
};
|
|
916
|
+
case "todo_update":
|
|
917
|
+
return {
|
|
918
|
+
type: "todo_update",
|
|
919
|
+
todos: data.todos ?? []
|
|
920
|
+
};
|
|
921
|
+
case "context_update":
|
|
922
|
+
return {
|
|
923
|
+
type: "context_update",
|
|
924
|
+
context: data.context ?? {},
|
|
925
|
+
phase: data.phase ?? null,
|
|
926
|
+
updatedAt: data.updated_at ?? null
|
|
927
|
+
};
|
|
928
|
+
case "subagent_start":
|
|
929
|
+
return {
|
|
930
|
+
type: "subagent_start",
|
|
931
|
+
agent: translateAgentIdentity(
|
|
932
|
+
data.agent ?? {}
|
|
933
|
+
),
|
|
934
|
+
taskCallId: data.task_call_id ?? null
|
|
935
|
+
};
|
|
936
|
+
case "subagent_stop":
|
|
937
|
+
return {
|
|
938
|
+
type: "subagent_stop",
|
|
939
|
+
agent: translateAgentIdentity(
|
|
940
|
+
data.agent ?? {}
|
|
941
|
+
),
|
|
942
|
+
taskCallId: data.task_call_id ?? null
|
|
943
|
+
};
|
|
944
|
+
case "context_warning":
|
|
945
|
+
return {
|
|
946
|
+
type: "context_warning",
|
|
947
|
+
severity: data.severity ?? "warning",
|
|
948
|
+
utilizationPct: data.utilization_pct ?? 0,
|
|
949
|
+
remainingTokens: data.remaining_tokens ?? 0,
|
|
950
|
+
windowTokens: data.window_tokens ?? 0,
|
|
951
|
+
inputTokens: data.input_tokens ?? 0,
|
|
952
|
+
message: data.message ?? ""
|
|
953
|
+
};
|
|
954
|
+
case "memory_recall":
|
|
955
|
+
return {
|
|
956
|
+
type: "memory_recall",
|
|
957
|
+
memories: data.memories ?? []
|
|
958
|
+
};
|
|
959
|
+
case "memory_update":
|
|
960
|
+
return {
|
|
961
|
+
type: "memory_update",
|
|
962
|
+
action: data.action ?? "",
|
|
963
|
+
memoryId: data.memory_id ?? null,
|
|
964
|
+
key: data.key ?? null,
|
|
965
|
+
namespace: data.namespace ?? null
|
|
966
|
+
};
|
|
967
|
+
case "desktop_stream":
|
|
968
|
+
return {
|
|
969
|
+
type: "desktop_stream",
|
|
970
|
+
url: data.url ?? "",
|
|
971
|
+
sandboxId: data.sandbox_id ?? null
|
|
972
|
+
};
|
|
973
|
+
case "attachment_staged":
|
|
974
|
+
return {
|
|
975
|
+
type: "attachment_staged",
|
|
976
|
+
attachmentId: data.attachment_id ?? "",
|
|
977
|
+
filename: data.filename ?? "",
|
|
978
|
+
contentType: data.content_type ?? null,
|
|
979
|
+
sizeBytes: data.size_bytes ?? null
|
|
980
|
+
};
|
|
981
|
+
case "workspace_ready":
|
|
982
|
+
return {
|
|
983
|
+
type: "workspace_ready",
|
|
984
|
+
sandboxId: data.sandbox_id ?? "",
|
|
985
|
+
workspacePath: data.workspace_path ?? null
|
|
986
|
+
};
|
|
987
|
+
case "asset_created":
|
|
988
|
+
return {
|
|
989
|
+
type: "asset_created",
|
|
990
|
+
assetId: data.asset_id ?? "",
|
|
991
|
+
filename: data.filename ?? "",
|
|
992
|
+
url: data.url ?? null,
|
|
993
|
+
contentType: data.content_type ?? null
|
|
994
|
+
};
|
|
995
|
+
case "tool_approval_requested":
|
|
996
|
+
return {
|
|
997
|
+
type: "tool_approval_requested",
|
|
998
|
+
toolName: data.tool_name ?? "",
|
|
999
|
+
callId: data.call_id ?? "",
|
|
1000
|
+
arguments: data.arguments ?? {},
|
|
1001
|
+
riskLevel: data.risk_level ?? null,
|
|
1002
|
+
reason: data.reason ?? null
|
|
1003
|
+
};
|
|
1004
|
+
case "tool_approval_granted":
|
|
1005
|
+
return {
|
|
1006
|
+
type: "tool_approval_granted",
|
|
1007
|
+
toolName: data.tool_name ?? "",
|
|
1008
|
+
callId: data.call_id ?? ""
|
|
1009
|
+
};
|
|
1010
|
+
case "tool_permission_denied":
|
|
1011
|
+
return {
|
|
1012
|
+
type: "tool_permission_denied",
|
|
1013
|
+
toolName: data.tool_name ?? "",
|
|
1014
|
+
callId: data.call_id ?? "",
|
|
1015
|
+
reason: data.reason ?? null,
|
|
1016
|
+
deniedBy: data.denied_by ?? null
|
|
1017
|
+
};
|
|
1018
|
+
case "tool_harness_warning":
|
|
1019
|
+
return {
|
|
1020
|
+
type: "tool_harness_warning",
|
|
1021
|
+
toolName: data.tool_name ?? "",
|
|
1022
|
+
callId: data.call_id ?? "",
|
|
1023
|
+
message: data.message ?? null,
|
|
1024
|
+
details: data.details ?? null
|
|
1025
|
+
};
|
|
1026
|
+
case "user_unavailable":
|
|
1027
|
+
return {
|
|
1028
|
+
type: "user_unavailable",
|
|
1029
|
+
consecutiveTimeouts: data.consecutive_timeouts ?? 0,
|
|
1030
|
+
toolName: data.tool_name ?? null
|
|
1031
|
+
};
|
|
1032
|
+
case "prompt_suggestion":
|
|
1033
|
+
return {
|
|
1034
|
+
type: "prompt_suggestion",
|
|
1035
|
+
suggestions: data.suggestions ?? []
|
|
1036
|
+
};
|
|
1037
|
+
case "state_changed":
|
|
1038
|
+
return {
|
|
1039
|
+
type: "state_changed",
|
|
1040
|
+
state: data.state ?? ""
|
|
1041
|
+
};
|
|
1042
|
+
default:
|
|
1043
|
+
return { type: "custom", name, data };
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
function legacyCustomEventData(wire) {
|
|
1047
|
+
return wire;
|
|
1048
|
+
}
|
|
1049
|
+
function translateWireEvent(wire) {
|
|
1050
|
+
if (wire.type === "prompt_suggestion") {
|
|
1051
|
+
return translateCustomEvent(
|
|
1052
|
+
"prompt_suggestion",
|
|
1053
|
+
legacyCustomEventData(wire)
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
switch (wire.type) {
|
|
1057
|
+
case "message_start":
|
|
1058
|
+
return {
|
|
1059
|
+
type: "message_start",
|
|
1060
|
+
turnId: wire.turn_id,
|
|
1061
|
+
model: wire.model,
|
|
1062
|
+
agentName: wire.agent_name,
|
|
1063
|
+
agentDisplayName: wire.agent_display_name,
|
|
1064
|
+
agentAvatarUrl: wire.agent_avatar_url
|
|
1065
|
+
};
|
|
1066
|
+
case "block_start":
|
|
1067
|
+
return {
|
|
1068
|
+
type: "block_start",
|
|
1069
|
+
turnId: wire.turn_id,
|
|
1070
|
+
path: wire.path,
|
|
1071
|
+
parentPath: wire.parent_path ?? null,
|
|
1072
|
+
kind: wire.kind,
|
|
1073
|
+
metadata: wire.metadata
|
|
1074
|
+
};
|
|
1075
|
+
case "block_delta": {
|
|
1076
|
+
const delta = translateDelta(wire.delta);
|
|
1077
|
+
if (!delta) return null;
|
|
1078
|
+
return {
|
|
1079
|
+
type: "block_delta",
|
|
1080
|
+
turnId: wire.turn_id,
|
|
1081
|
+
path: wire.path,
|
|
1082
|
+
delta
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
case "block_stop":
|
|
1086
|
+
return {
|
|
1087
|
+
type: "block_stop",
|
|
1088
|
+
turnId: wire.turn_id,
|
|
1089
|
+
path: wire.path,
|
|
1090
|
+
status: wire.status,
|
|
1091
|
+
final: wire.final
|
|
1092
|
+
};
|
|
1093
|
+
case "message_stop":
|
|
1094
|
+
return {
|
|
1095
|
+
type: "message_stop",
|
|
1096
|
+
turnId: wire.turn_id,
|
|
1097
|
+
jobId: wire.job_id,
|
|
1098
|
+
stopReason: wire.stop_reason,
|
|
1099
|
+
usage: {
|
|
1100
|
+
inputTokens: wire.usage.input_tokens ?? 0,
|
|
1101
|
+
outputTokens: wire.usage.output_tokens ?? 0,
|
|
1102
|
+
cachedTokens: wire.usage.cached_tokens ?? 0
|
|
1103
|
+
},
|
|
1104
|
+
ttfbMs: wire.ttfb_ms,
|
|
1105
|
+
totalMs: wire.total_ms,
|
|
1106
|
+
stallCount: wire.stall_count
|
|
1107
|
+
};
|
|
1108
|
+
case "stall":
|
|
1109
|
+
return {
|
|
1110
|
+
type: "stall",
|
|
1111
|
+
sinceLastEventMs: wire.since_last_event_ms,
|
|
1112
|
+
stallCount: wire.stall_count
|
|
1113
|
+
};
|
|
1114
|
+
case "retry":
|
|
1115
|
+
return {
|
|
1116
|
+
type: "retry",
|
|
1117
|
+
attempt: wire.attempt,
|
|
1118
|
+
reason: wire.reason,
|
|
1119
|
+
backoffMs: wire.backoff_ms,
|
|
1120
|
+
strategy: wire.strategy ?? null,
|
|
1121
|
+
maxAttempts: wire.max_attempts ?? null,
|
|
1122
|
+
contextRecovery: wire.context_recovery ?? null
|
|
1123
|
+
};
|
|
1124
|
+
case "error":
|
|
1125
|
+
return {
|
|
1126
|
+
type: "error",
|
|
1127
|
+
code: wire.code,
|
|
1128
|
+
message: wire.message,
|
|
1129
|
+
blockPath: wire.block_path ?? null
|
|
1130
|
+
};
|
|
1131
|
+
case "keepalive":
|
|
1132
|
+
return {
|
|
1133
|
+
type: "keepalive",
|
|
1134
|
+
sinceLastEventMs: wire.since_last_event_ms
|
|
1135
|
+
};
|
|
1136
|
+
case "custom":
|
|
1137
|
+
return translateCustomEvent(wire.name, wire.data);
|
|
1138
|
+
default: {
|
|
1139
|
+
const _exhaustive = wire;
|
|
1140
|
+
void _exhaustive;
|
|
1141
|
+
return null;
|
|
1142
|
+
}
|
|
563
1143
|
}
|
|
564
|
-
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
565
|
-
const r = Math.random() * 16 | 0;
|
|
566
|
-
const v = c === "x" ? r : r & 3 | 8;
|
|
567
|
-
return v.toString(16);
|
|
568
|
-
});
|
|
569
1144
|
}
|
|
570
1145
|
|
|
571
1146
|
// src/session.ts
|
|
1147
|
+
function pathEquals(a, b) {
|
|
1148
|
+
if (a.length !== b.length) return false;
|
|
1149
|
+
for (let i = 0; i < a.length; i++) {
|
|
1150
|
+
if (a[i] !== b[i]) return false;
|
|
1151
|
+
}
|
|
1152
|
+
return true;
|
|
1153
|
+
}
|
|
572
1154
|
var ChatSession = class {
|
|
573
|
-
constructor(config, storage
|
|
1155
|
+
constructor(config, storage) {
|
|
1156
|
+
/**
|
|
1157
|
+
* Pluggable UI protocol adapters. Consumers register a framework-
|
|
1158
|
+
* specific adapter (e.g. React) for each MIME type they can render,
|
|
1159
|
+
* typically gated on ``session.projectStatus.uiComponents.protocol``.
|
|
1160
|
+
* ``ToolBlock``-style consumers look up the adapter for an incoming
|
|
1161
|
+
* embedded resource and hand off rendering.
|
|
1162
|
+
*/
|
|
1163
|
+
this.protocols = new ProtocolRegistry();
|
|
574
1164
|
// State
|
|
575
1165
|
this.conversationId = null;
|
|
576
1166
|
this.conversations = [];
|
|
577
1167
|
this.messages = [];
|
|
578
|
-
this.streamingContent = "";
|
|
579
1168
|
this.isStreaming = false;
|
|
580
|
-
this.executingTool = null;
|
|
581
1169
|
this.projectStatus = null;
|
|
582
1170
|
this.agents = [];
|
|
583
1171
|
this.skills = [];
|
|
584
1172
|
this.enabledClientTools = /* @__PURE__ */ new Set();
|
|
585
1173
|
this.modelDisplayName = null;
|
|
586
|
-
//
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
this.
|
|
590
|
-
this.
|
|
591
|
-
this.todos = [];
|
|
592
|
-
this.activeTools = /* @__PURE__ */ new Map();
|
|
1174
|
+
// Minimal in-session accumulation for the assistant message record.
|
|
1175
|
+
// Only top-level ``text`` blocks contribute; subagent / tool output
|
|
1176
|
+
// is tracked by the consumer's own block store.
|
|
1177
|
+
this.accumulatedText = "";
|
|
1178
|
+
this.currentTextPath = null;
|
|
593
1179
|
this.handlers = /* @__PURE__ */ new Set();
|
|
594
1180
|
this.abortController = null;
|
|
595
1181
|
/** Last received sequence number for resumable reconnection */
|
|
@@ -599,13 +1185,6 @@ var ChatSession = class {
|
|
|
599
1185
|
this.client = new AstralformClient(config);
|
|
600
1186
|
this.toolRegistry = new ToolRegistry();
|
|
601
1187
|
this.storage = storage ?? new InMemoryStorage();
|
|
602
|
-
this.blockBuilder = blockBuilder ?? new BlockBuilder();
|
|
603
|
-
this.blockBuilder.setOnChange(() => {
|
|
604
|
-
this.emit({
|
|
605
|
-
type: "blocks_changed",
|
|
606
|
-
blocks: this.blockBuilder.getBlocks()
|
|
607
|
-
});
|
|
608
|
-
});
|
|
609
1188
|
}
|
|
610
1189
|
on(handler) {
|
|
611
1190
|
this.handlers.add(handler);
|
|
@@ -614,9 +1193,6 @@ var ChatSession = class {
|
|
|
614
1193
|
};
|
|
615
1194
|
}
|
|
616
1195
|
emit(event) {
|
|
617
|
-
if (event.type !== "blocks_changed" && event.type !== "connected") {
|
|
618
|
-
this.blockBuilder.processEvent(event);
|
|
619
|
-
}
|
|
620
1196
|
for (const handler of this.handlers) {
|
|
621
1197
|
try {
|
|
622
1198
|
handler(event);
|
|
@@ -669,7 +1245,8 @@ var ChatSession = class {
|
|
|
669
1245
|
),
|
|
670
1246
|
upload_ids: options?.uploadIds,
|
|
671
1247
|
agent_name: options?.agentName,
|
|
672
|
-
enable_search: options?.enableSearch
|
|
1248
|
+
enable_search: options?.enableSearch,
|
|
1249
|
+
plan_mode: options?.planMode
|
|
673
1250
|
};
|
|
674
1251
|
await this.processStream(request);
|
|
675
1252
|
}
|
|
@@ -686,13 +1263,8 @@ var ChatSession = class {
|
|
|
686
1263
|
await this.processStream(request);
|
|
687
1264
|
}
|
|
688
1265
|
resetStreamingState() {
|
|
689
|
-
this.
|
|
690
|
-
this.
|
|
691
|
-
this.isThinking = false;
|
|
692
|
-
this.activeSubagents.clear();
|
|
693
|
-
this.capsuleOutputs = [];
|
|
694
|
-
this.todos = [];
|
|
695
|
-
this.activeTools.clear();
|
|
1266
|
+
this.accumulatedText = "";
|
|
1267
|
+
this.currentTextPath = null;
|
|
696
1268
|
}
|
|
697
1269
|
async processStream(request) {
|
|
698
1270
|
this.isStreaming = true;
|
|
@@ -704,22 +1276,42 @@ var ChatSession = class {
|
|
|
704
1276
|
if (!(err instanceof DOMException && err.name === "AbortError")) {
|
|
705
1277
|
this.emit({
|
|
706
1278
|
type: "error",
|
|
707
|
-
|
|
1279
|
+
code: "connection_error",
|
|
1280
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1281
|
+
blockPath: null
|
|
708
1282
|
});
|
|
709
1283
|
}
|
|
710
1284
|
} finally {
|
|
711
1285
|
this.isStreaming = false;
|
|
712
|
-
this.executingTool = null;
|
|
713
1286
|
this.abortController = null;
|
|
714
1287
|
}
|
|
715
1288
|
}
|
|
716
1289
|
async consumeJobStream(request) {
|
|
717
1290
|
const job = await this.client.createJob(request);
|
|
718
1291
|
this.currentJobId = job.job_id;
|
|
719
|
-
|
|
1292
|
+
const conversationId = job.conversation_id;
|
|
720
1293
|
if (!this.conversationId) {
|
|
721
1294
|
this.conversationId = conversationId;
|
|
722
1295
|
}
|
|
1296
|
+
if (!this.conversations.some((c) => c.id === conversationId)) {
|
|
1297
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1298
|
+
const conv = {
|
|
1299
|
+
id: conversationId,
|
|
1300
|
+
title: "",
|
|
1301
|
+
messageCount: 0,
|
|
1302
|
+
createdAt: now,
|
|
1303
|
+
updatedAt: now
|
|
1304
|
+
};
|
|
1305
|
+
this.conversations.unshift(conv);
|
|
1306
|
+
await this.storage.createConversation(conversationId, "").catch(() => {
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
const lastMsg = this.messages[this.messages.length - 1];
|
|
1310
|
+
if (lastMsg?.role === "user" && !lastMsg.conversationId) {
|
|
1311
|
+
lastMsg.conversationId = conversationId;
|
|
1312
|
+
await this.storage.addMessage(lastMsg, conversationId).catch(() => {
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
723
1315
|
const messageId = job.message_id;
|
|
724
1316
|
this.lastSeq = -1;
|
|
725
1317
|
const stream = this.client.streamJobEvents(
|
|
@@ -736,7 +1328,8 @@ var ChatSession = class {
|
|
|
736
1328
|
);
|
|
737
1329
|
}
|
|
738
1330
|
/**
|
|
739
|
-
* Shared event consumption loop
|
|
1331
|
+
* Shared event consumption loop. Parses each wire event, updates
|
|
1332
|
+
* minimal session state, and emits typed ChatEvents to consumers.
|
|
740
1333
|
*/
|
|
741
1334
|
async consumeEventStream(stream, conversationId, messageId, executeClientTools) {
|
|
742
1335
|
for await (const raw of stream) {
|
|
@@ -756,419 +1349,112 @@ var ChatSession = class {
|
|
|
756
1349
|
} catch {
|
|
757
1350
|
continue;
|
|
758
1351
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
displayName: parsed.display_name,
|
|
788
|
-
description: parsed.description,
|
|
789
|
-
arguments: parsed.arguments,
|
|
790
|
-
isClientTool: parsed.is_client_tool,
|
|
791
|
-
toolCategory: parsed.tool_category,
|
|
792
|
-
iconUrl: parsed.icon_url
|
|
793
|
-
}
|
|
794
|
-
]);
|
|
795
|
-
await this.client.submitToolResult({
|
|
796
|
-
conversation_id: conversationId,
|
|
797
|
-
message_id: messageId,
|
|
798
|
-
tool_results: results
|
|
799
|
-
});
|
|
800
|
-
}
|
|
801
|
-
break;
|
|
802
|
-
}
|
|
803
|
-
case "subagent_content_delta": {
|
|
804
|
-
const subagent = this.activeSubagents.get(parsed.tool_call_id);
|
|
805
|
-
if (subagent) {
|
|
806
|
-
subagent.content += parsed.delta.text;
|
|
807
|
-
}
|
|
808
|
-
this.emit({
|
|
809
|
-
type: "subagent_chunk",
|
|
810
|
-
agentName: parsed.agent_name,
|
|
811
|
-
toolCallId: parsed.tool_call_id,
|
|
812
|
-
text: parsed.delta.text
|
|
813
|
-
});
|
|
814
|
-
break;
|
|
815
|
-
}
|
|
816
|
-
case "thinking_delta":
|
|
817
|
-
this.thinkingContent += parsed.delta.text;
|
|
818
|
-
this.isThinking = true;
|
|
819
|
-
this.emit({ type: "thinking_delta", text: parsed.delta.text });
|
|
820
|
-
break;
|
|
821
|
-
case "thinking_complete":
|
|
822
|
-
this.isThinking = false;
|
|
823
|
-
this.emit({ type: "thinking_complete" });
|
|
824
|
-
break;
|
|
825
|
-
case "retry":
|
|
826
|
-
this.emit({
|
|
827
|
-
type: "retry",
|
|
828
|
-
attempt: parsed.attempt,
|
|
829
|
-
maxAttempts: parsed.max_attempts,
|
|
830
|
-
delaySeconds: parsed.delay_seconds
|
|
831
|
-
});
|
|
832
|
-
break;
|
|
833
|
-
case "message_stop":
|
|
834
|
-
await this.completeStream(
|
|
835
|
-
conversationId,
|
|
836
|
-
messageId,
|
|
837
|
-
parsed.title,
|
|
838
|
-
parsed.metrics,
|
|
839
|
-
parsed.job_id
|
|
840
|
-
);
|
|
841
|
-
this.isStreaming = false;
|
|
842
|
-
this.currentJobId = null;
|
|
843
|
-
break;
|
|
844
|
-
case "error":
|
|
845
|
-
this.emit({
|
|
846
|
-
type: "error",
|
|
847
|
-
error: new AstralformError(parsed.message, parsed.code)
|
|
848
|
-
});
|
|
849
|
-
break;
|
|
850
|
-
default:
|
|
851
|
-
this.applyEvent(parsed);
|
|
852
|
-
}
|
|
1352
|
+
await this.dispatchWireEvent(
|
|
1353
|
+
parsed,
|
|
1354
|
+
conversationId,
|
|
1355
|
+
messageId,
|
|
1356
|
+
executeClientTools
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
async dispatchWireEvent(wire, conversationId, messageId, executeClientTools) {
|
|
1361
|
+
this.applyWireSideEffects(wire, conversationId, messageId);
|
|
1362
|
+
const event = translateWireEvent(wire);
|
|
1363
|
+
if (event) {
|
|
1364
|
+
this.emit(event);
|
|
1365
|
+
}
|
|
1366
|
+
if (executeClientTools && wire.type === "block_stop" && wire.status === "awaiting_client_result" && wire.final?.call_id) {
|
|
1367
|
+
const f = wire.final;
|
|
1368
|
+
const request = {
|
|
1369
|
+
callId: f.call_id ?? "",
|
|
1370
|
+
toolName: f.tool_name ?? "",
|
|
1371
|
+
arguments: f.input ?? {},
|
|
1372
|
+
isClientTool: true
|
|
1373
|
+
};
|
|
1374
|
+
const results = await this.executeClientTools([request]);
|
|
1375
|
+
await this.client.submitToolResult({
|
|
1376
|
+
conversation_id: conversationId,
|
|
1377
|
+
message_id: messageId,
|
|
1378
|
+
tool_results: results
|
|
1379
|
+
});
|
|
853
1380
|
}
|
|
854
1381
|
}
|
|
855
1382
|
/**
|
|
856
|
-
*
|
|
857
|
-
*
|
|
1383
|
+
* State mutations driven by wire events. Kept separate from translation so
|
|
1384
|
+
* the pure wire → ChatEvent mapping can live in translate.ts and be reused
|
|
1385
|
+
* by the replay path.
|
|
1386
|
+
*
|
|
1387
|
+
* ``messageId`` is the server-assigned assistant message id for the current
|
|
1388
|
+
* turn; empty in the reconnect and conversation-switch replay paths where
|
|
1389
|
+
* messages have already been loaded from REST and shouldn't be re-pushed.
|
|
858
1390
|
*/
|
|
859
|
-
|
|
860
|
-
switch (
|
|
861
|
-
case "user_message":
|
|
862
|
-
this.emit({
|
|
863
|
-
type: "user_message",
|
|
864
|
-
content: event.content,
|
|
865
|
-
createdAt: event.created_at
|
|
866
|
-
});
|
|
867
|
-
break;
|
|
868
|
-
case "title_generated": {
|
|
869
|
-
if (this.conversationId && event.title) {
|
|
870
|
-
const conv = this.conversations.find(
|
|
871
|
-
(c) => c.id === this.conversationId
|
|
872
|
-
);
|
|
873
|
-
if (conv) {
|
|
874
|
-
conv.title = event.title;
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
this.emit({ type: "title_generated", title: event.title });
|
|
878
|
-
break;
|
|
879
|
-
}
|
|
1391
|
+
applyWireSideEffects(wire, conversationId, messageId) {
|
|
1392
|
+
switch (wire.type) {
|
|
880
1393
|
case "message_start":
|
|
881
|
-
|
|
882
|
-
|
|
1394
|
+
this.resetStreamingState();
|
|
1395
|
+
if (wire.model) {
|
|
1396
|
+
this.modelDisplayName = wire.model;
|
|
883
1397
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1398
|
+
return;
|
|
1399
|
+
case "block_start":
|
|
1400
|
+
if (wire.kind === "text" && (!wire.parent_path || wire.parent_path.length === 0)) {
|
|
1401
|
+
this.currentTextPath = wire.path;
|
|
887
1402
|
}
|
|
888
|
-
|
|
889
|
-
case "
|
|
890
|
-
this.
|
|
891
|
-
|
|
892
|
-
break;
|
|
893
|
-
case "thinking_delta":
|
|
894
|
-
this.thinkingContent += event.delta.text;
|
|
895
|
-
this.isThinking = true;
|
|
896
|
-
this.emit({ type: "thinking_delta", text: event.delta.text });
|
|
897
|
-
break;
|
|
898
|
-
case "thinking_complete":
|
|
899
|
-
this.isThinking = false;
|
|
900
|
-
this.emit({ type: "thinking_complete" });
|
|
901
|
-
break;
|
|
902
|
-
case "tool_executing":
|
|
903
|
-
this.emit({
|
|
904
|
-
type: "tool_executing",
|
|
905
|
-
name: event.tool,
|
|
906
|
-
call_id: event.call_id
|
|
907
|
-
});
|
|
908
|
-
break;
|
|
909
|
-
case "tool_progress":
|
|
910
|
-
this.emit({
|
|
911
|
-
type: "tool_progress",
|
|
912
|
-
callId: event.call_id,
|
|
913
|
-
tool: event.tool,
|
|
914
|
-
index: event.index,
|
|
915
|
-
total: event.total,
|
|
916
|
-
item: event.item
|
|
917
|
-
});
|
|
918
|
-
break;
|
|
919
|
-
case "message_stop":
|
|
920
|
-
this.emit({
|
|
921
|
-
type: "complete",
|
|
922
|
-
content: this.streamingContent,
|
|
923
|
-
conversationId: this.conversationId ?? "",
|
|
924
|
-
messageId: event.job_id ?? "",
|
|
925
|
-
title: event.title,
|
|
926
|
-
metrics: event.metrics,
|
|
927
|
-
job_id: event.job_id
|
|
928
|
-
});
|
|
929
|
-
break;
|
|
930
|
-
case "tool_use_start": {
|
|
931
|
-
const request = {
|
|
932
|
-
callId: event.call_id,
|
|
933
|
-
toolName: event.tool,
|
|
934
|
-
displayName: event.display_name,
|
|
935
|
-
description: event.description,
|
|
936
|
-
arguments: event.arguments,
|
|
937
|
-
isClientTool: event.is_client_tool,
|
|
938
|
-
toolCategory: event.tool_category,
|
|
939
|
-
iconUrl: event.icon_url
|
|
940
|
-
};
|
|
941
|
-
this.activeTools.set(event.call_id, {
|
|
942
|
-
...request,
|
|
943
|
-
status: event.is_client_tool ? "calling" : "executing"
|
|
944
|
-
});
|
|
945
|
-
this.emit({ type: "tool_call", request });
|
|
946
|
-
break;
|
|
947
|
-
}
|
|
948
|
-
case "tool_use_end": {
|
|
949
|
-
const toolState = this.activeTools.get(event.call_id);
|
|
950
|
-
if (toolState) {
|
|
951
|
-
toolState.status = "completed";
|
|
1403
|
+
return;
|
|
1404
|
+
case "block_delta":
|
|
1405
|
+
if (wire.delta.channel === "text" && this.currentTextPath !== null && pathEquals(this.currentTextPath, wire.path)) {
|
|
1406
|
+
this.accumulatedText += wire.delta.text;
|
|
952
1407
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
result: event.result,
|
|
958
|
-
sources: event.sources,
|
|
959
|
-
durationMs: event.duration_ms
|
|
960
|
-
});
|
|
961
|
-
break;
|
|
962
|
-
}
|
|
963
|
-
case "agent_start":
|
|
964
|
-
this.emit({
|
|
965
|
-
type: "agent_start",
|
|
966
|
-
agentName: event.agent_name,
|
|
967
|
-
agentDisplayName: event.agent_display_name,
|
|
968
|
-
avatarUrl: event.avatar_url
|
|
969
|
-
});
|
|
970
|
-
break;
|
|
971
|
-
case "agent_end":
|
|
972
|
-
this.emit({ type: "agent_end", agentName: event.agent_name });
|
|
973
|
-
break;
|
|
974
|
-
case "subagent_start":
|
|
975
|
-
this.activeSubagents.set(event.tool_call_id, {
|
|
976
|
-
agentName: event.agent_name,
|
|
977
|
-
displayName: event.display_name,
|
|
978
|
-
avatarUrl: event.avatar_url,
|
|
979
|
-
description: event.description,
|
|
980
|
-
content: "",
|
|
981
|
-
isActive: true
|
|
982
|
-
});
|
|
983
|
-
this.emit({
|
|
984
|
-
type: "subagent_start",
|
|
985
|
-
agentName: event.agent_name,
|
|
986
|
-
displayName: event.display_name,
|
|
987
|
-
toolCallId: event.tool_call_id,
|
|
988
|
-
avatarUrl: event.avatar_url,
|
|
989
|
-
description: event.description
|
|
990
|
-
});
|
|
991
|
-
break;
|
|
992
|
-
case "subagent_update": {
|
|
993
|
-
const sub = this.activeSubagents.get(event.tool_call_id);
|
|
994
|
-
if (sub) {
|
|
995
|
-
sub.agentName = event.agent_name;
|
|
996
|
-
sub.displayName = event.display_name;
|
|
1408
|
+
return;
|
|
1409
|
+
case "block_stop":
|
|
1410
|
+
if (this.currentTextPath !== null && pathEquals(this.currentTextPath, wire.path)) {
|
|
1411
|
+
this.currentTextPath = null;
|
|
997
1412
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1413
|
+
return;
|
|
1414
|
+
case "message_stop":
|
|
1415
|
+
if (messageId) {
|
|
1416
|
+
const assistantMessage = {
|
|
1417
|
+
id: messageId,
|
|
1418
|
+
conversationId,
|
|
1419
|
+
role: "assistant",
|
|
1420
|
+
content: this.accumulatedText,
|
|
1421
|
+
status: "complete",
|
|
1422
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1423
|
+
};
|
|
1424
|
+
this.messages.push(assistantMessage);
|
|
1425
|
+
this.storage.addMessage(assistantMessage, conversationId).catch(() => {
|
|
1426
|
+
});
|
|
1010
1427
|
}
|
|
1011
|
-
this.
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
agentName: event.agent_name,
|
|
1032
|
-
command: event.command,
|
|
1033
|
-
output: event.output,
|
|
1034
|
-
durationMs: event.duration_ms,
|
|
1035
|
-
callId: event.call_id
|
|
1036
|
-
};
|
|
1037
|
-
this.capsuleOutputs.push(capsule);
|
|
1038
|
-
this.emit({ type: "capsule_output", ...capsule });
|
|
1039
|
-
break;
|
|
1040
|
-
}
|
|
1041
|
-
case "capsule_output_chunk":
|
|
1042
|
-
this.emit({
|
|
1043
|
-
type: "capsule_output_chunk",
|
|
1044
|
-
callId: event.call_id,
|
|
1045
|
-
stream: event.stream,
|
|
1046
|
-
chunk: event.chunk
|
|
1047
|
-
});
|
|
1048
|
-
break;
|
|
1049
|
-
case "todo_update":
|
|
1050
|
-
this.todos = event.todos;
|
|
1051
|
-
this.emit({ type: "todo_update", todos: event.todos });
|
|
1052
|
-
break;
|
|
1053
|
-
case "context_update":
|
|
1054
|
-
this.emit({
|
|
1055
|
-
type: "context_update",
|
|
1056
|
-
context: event.context,
|
|
1057
|
-
phase: event.phase,
|
|
1058
|
-
updatedAt: event.updated_at
|
|
1059
|
-
});
|
|
1060
|
-
break;
|
|
1061
|
-
case "desktop_stream":
|
|
1062
|
-
this.emit({
|
|
1063
|
-
type: "desktop_stream",
|
|
1064
|
-
url: event.url,
|
|
1065
|
-
authKey: event.auth_key,
|
|
1066
|
-
sandboxId: event.sandbox_id
|
|
1067
|
-
});
|
|
1068
|
-
break;
|
|
1069
|
-
case "attachment_staged":
|
|
1070
|
-
this.emit({
|
|
1071
|
-
type: "attachment_staged",
|
|
1072
|
-
files: (event.files || []).map((f) => ({
|
|
1073
|
-
name: f.name,
|
|
1074
|
-
path: f.path,
|
|
1075
|
-
mediaType: f.media_type,
|
|
1076
|
-
sizeBytes: f.size_bytes
|
|
1077
|
-
}))
|
|
1078
|
-
});
|
|
1079
|
-
break;
|
|
1080
|
-
case "workspace_ready":
|
|
1081
|
-
this.emit({
|
|
1082
|
-
type: "workspace_ready",
|
|
1083
|
-
conversationId: event.conversation_id,
|
|
1084
|
-
sandboxId: event.sandbox_id
|
|
1085
|
-
});
|
|
1086
|
-
break;
|
|
1087
|
-
case "asset_created":
|
|
1088
|
-
this.emit({
|
|
1089
|
-
type: "asset_created",
|
|
1090
|
-
assetId: event.asset_id,
|
|
1091
|
-
name: event.name,
|
|
1092
|
-
url: event.url,
|
|
1093
|
-
mediaType: event.media_type,
|
|
1094
|
-
sizeBytes: event.size_bytes
|
|
1095
|
-
});
|
|
1096
|
-
break;
|
|
1097
|
-
case "editor_content_start":
|
|
1098
|
-
this.emit({
|
|
1099
|
-
type: "editor_content_start",
|
|
1100
|
-
callId: event.call_id,
|
|
1101
|
-
path: event.path,
|
|
1102
|
-
language: event.language
|
|
1103
|
-
});
|
|
1104
|
-
break;
|
|
1105
|
-
case "editor_content_delta":
|
|
1106
|
-
this.emit({
|
|
1107
|
-
type: "editor_content_delta",
|
|
1108
|
-
callId: event.call_id,
|
|
1109
|
-
path: event.path,
|
|
1110
|
-
delta: event.delta
|
|
1111
|
-
});
|
|
1112
|
-
break;
|
|
1113
|
-
case "editor_content_end":
|
|
1114
|
-
this.emit({
|
|
1115
|
-
type: "editor_content_end",
|
|
1116
|
-
callId: event.call_id
|
|
1117
|
-
});
|
|
1118
|
-
break;
|
|
1428
|
+
this.isStreaming = false;
|
|
1429
|
+
this.currentJobId = null;
|
|
1430
|
+
return;
|
|
1431
|
+
case "custom":
|
|
1432
|
+
if (wire.name === "title_generated") {
|
|
1433
|
+
const title = wire.data.title ?? "";
|
|
1434
|
+
if (this.conversationId && title) {
|
|
1435
|
+
const conv = this.conversations.find(
|
|
1436
|
+
(c) => c.id === this.conversationId
|
|
1437
|
+
);
|
|
1438
|
+
if (conv) {
|
|
1439
|
+
conv.title = title;
|
|
1440
|
+
}
|
|
1441
|
+
this.storage.updateConversationTitle(this.conversationId, title).catch(() => {
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
return;
|
|
1446
|
+
default:
|
|
1447
|
+
return;
|
|
1119
1448
|
}
|
|
1120
1449
|
}
|
|
1121
1450
|
async executeClientTools(toolCalls) {
|
|
1122
1451
|
const results = [];
|
|
1123
1452
|
for (const call of toolCalls) {
|
|
1124
|
-
this.executingTool = call.toolName;
|
|
1125
|
-
const toolState = this.activeTools.get(call.callId);
|
|
1126
|
-
if (toolState) {
|
|
1127
|
-
toolState.status = "executing";
|
|
1128
|
-
}
|
|
1129
|
-
this.emit({ type: "tool_executing", name: call.toolName });
|
|
1130
1453
|
const result = await this.toolRegistry.executeTool(call);
|
|
1131
1454
|
results.push(result);
|
|
1132
|
-
if (toolState) {
|
|
1133
|
-
toolState.status = "completed";
|
|
1134
|
-
}
|
|
1135
|
-
this.emit({
|
|
1136
|
-
type: "tool_completed",
|
|
1137
|
-
name: call.toolName,
|
|
1138
|
-
result: result.result
|
|
1139
|
-
});
|
|
1140
1455
|
}
|
|
1141
|
-
this.executingTool = null;
|
|
1142
1456
|
return results;
|
|
1143
1457
|
}
|
|
1144
|
-
async completeStream(conversationId, messageId, title, metrics, jobId) {
|
|
1145
|
-
const assistantMessage = {
|
|
1146
|
-
id: messageId || generateId(),
|
|
1147
|
-
conversationId,
|
|
1148
|
-
role: "assistant",
|
|
1149
|
-
content: this.streamingContent,
|
|
1150
|
-
status: "complete",
|
|
1151
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1152
|
-
};
|
|
1153
|
-
this.messages.push(assistantMessage);
|
|
1154
|
-
await this.storage.addMessage(assistantMessage, conversationId);
|
|
1155
|
-
if (title && conversationId) {
|
|
1156
|
-
await this.storage.updateConversationTitle(conversationId, title);
|
|
1157
|
-
const conv = this.conversations.find((c) => c.id === conversationId);
|
|
1158
|
-
if (conv) {
|
|
1159
|
-
conv.title = title;
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
this.emit({
|
|
1163
|
-
type: "complete",
|
|
1164
|
-
content: this.streamingContent,
|
|
1165
|
-
conversationId,
|
|
1166
|
-
messageId: assistantMessage.id,
|
|
1167
|
-
title,
|
|
1168
|
-
metrics,
|
|
1169
|
-
job_id: jobId
|
|
1170
|
-
});
|
|
1171
|
-
}
|
|
1172
1458
|
/**
|
|
1173
1459
|
* Load conversation context (messages) without replaying events.
|
|
1174
1460
|
* Used before reconnectToJob — SSE replay handles event replay.
|
|
@@ -1176,13 +1462,11 @@ var ChatSession = class {
|
|
|
1176
1462
|
async loadConversation(id) {
|
|
1177
1463
|
this.conversationId = id;
|
|
1178
1464
|
this.resetStreamingState();
|
|
1179
|
-
this.blockBuilder.reset();
|
|
1180
1465
|
this.messages = await this.client.getMessages(id).catch(() => this.storage.fetchMessages(id));
|
|
1181
1466
|
}
|
|
1182
1467
|
/**
|
|
1183
1468
|
* Reconnect to a running job's SSE stream (e.g. after page reload).
|
|
1184
1469
|
* Replays all events from the beginning and continues live.
|
|
1185
|
-
* Does NOT reset BlockBuilder — caller controls reset.
|
|
1186
1470
|
*/
|
|
1187
1471
|
async reconnectToJob(jobId) {
|
|
1188
1472
|
if (this.isStreaming) return;
|
|
@@ -1207,23 +1491,21 @@ var ChatSession = class {
|
|
|
1207
1491
|
} catch (err) {
|
|
1208
1492
|
this.emit({
|
|
1209
1493
|
type: "error",
|
|
1210
|
-
|
|
1494
|
+
code: "connection_error",
|
|
1495
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1496
|
+
blockPath: null
|
|
1211
1497
|
});
|
|
1212
1498
|
} finally {
|
|
1213
1499
|
this.isStreaming = false;
|
|
1214
|
-
this.executingTool = null;
|
|
1215
1500
|
this.abortController = null;
|
|
1216
1501
|
}
|
|
1217
1502
|
}
|
|
1218
|
-
/** Detach from the SSE stream without cancelling the job.
|
|
1219
|
-
* The backend job keeps running — caller can reconnect later. */
|
|
1503
|
+
/** Detach from the SSE stream without cancelling the job. */
|
|
1220
1504
|
detach() {
|
|
1221
1505
|
this.abortController?.abort();
|
|
1222
1506
|
this.abortController = null;
|
|
1223
1507
|
this.isStreaming = false;
|
|
1224
|
-
this.
|
|
1225
|
-
this.executingTool = null;
|
|
1226
|
-
this.blockBuilder.reset();
|
|
1508
|
+
this.resetStreamingState();
|
|
1227
1509
|
this.emit({ type: "disconnected" });
|
|
1228
1510
|
}
|
|
1229
1511
|
/** Stop the job and disconnect (explicit user action). */
|
|
@@ -1234,6 +1516,7 @@ var ChatSession = class {
|
|
|
1234
1516
|
}
|
|
1235
1517
|
this.detach();
|
|
1236
1518
|
this.currentJobId = null;
|
|
1519
|
+
this.protocols.clear();
|
|
1237
1520
|
}
|
|
1238
1521
|
async createNewConversation() {
|
|
1239
1522
|
const id = generateId();
|
|
@@ -1244,21 +1527,39 @@ var ChatSession = class {
|
|
|
1244
1527
|
this.conversations.unshift(conversation);
|
|
1245
1528
|
this.conversationId = id;
|
|
1246
1529
|
this.messages = [];
|
|
1247
|
-
this.streamingContent = "";
|
|
1248
1530
|
return id;
|
|
1249
1531
|
}
|
|
1250
|
-
async switchConversation(id, jobId) {
|
|
1532
|
+
async switchConversation(id, jobId, userMessageContent) {
|
|
1251
1533
|
this.conversationId = id;
|
|
1252
1534
|
this.resetStreamingState();
|
|
1253
|
-
this.blockBuilder.reset();
|
|
1254
1535
|
const [messagesResult, eventsResult] = await Promise.allSettled([
|
|
1255
1536
|
this.client.getMessages(id).catch(() => this.storage.fetchMessages(id)),
|
|
1256
1537
|
this.client.getConversationEvents(id, jobId)
|
|
1257
1538
|
]);
|
|
1258
1539
|
this.messages = messagesResult.status === "fulfilled" ? messagesResult.value : [];
|
|
1259
1540
|
if (eventsResult.status === "fulfilled") {
|
|
1541
|
+
let userMessageEmitted = !userMessageContent;
|
|
1260
1542
|
for (const ev of eventsResult.value) {
|
|
1261
|
-
|
|
1543
|
+
const type = ev.data.type || ev.event;
|
|
1544
|
+
if (!type || type === "done") continue;
|
|
1545
|
+
if (!userMessageEmitted && type === "message_start") {
|
|
1546
|
+
this.emit({
|
|
1547
|
+
type: "user_message",
|
|
1548
|
+
content: userMessageContent
|
|
1549
|
+
});
|
|
1550
|
+
userMessageEmitted = true;
|
|
1551
|
+
}
|
|
1552
|
+
const wire = { ...ev.data, type };
|
|
1553
|
+
try {
|
|
1554
|
+
await this.dispatchWireEvent(
|
|
1555
|
+
wire,
|
|
1556
|
+
id,
|
|
1557
|
+
"",
|
|
1558
|
+
false
|
|
1559
|
+
// don't execute client tools on replay
|
|
1560
|
+
);
|
|
1561
|
+
} catch {
|
|
1562
|
+
}
|
|
1262
1563
|
}
|
|
1263
1564
|
}
|
|
1264
1565
|
}
|
|
@@ -1284,456 +1585,46 @@ var ChatSession = class {
|
|
|
1284
1585
|
}
|
|
1285
1586
|
};
|
|
1286
1587
|
|
|
1287
|
-
// src/standard-handlers.ts
|
|
1288
|
-
function finalizeText(builder) {
|
|
1289
|
-
if (builder.activeTextId) {
|
|
1290
|
-
builder.patchBlock(builder.activeTextId, {
|
|
1291
|
-
isStreaming: false
|
|
1292
|
-
});
|
|
1293
|
-
builder.activeTextId = null;
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
function finalizeThinking(builder) {
|
|
1297
|
-
if (builder.activeThinkingId) {
|
|
1298
|
-
builder.patchBlock(builder.activeThinkingId, {
|
|
1299
|
-
isActive: false
|
|
1300
|
-
});
|
|
1301
|
-
builder.activeThinkingId = null;
|
|
1302
|
-
builder.thinkingStartMs = null;
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
function finalizeEditor(builder) {
|
|
1306
|
-
if (builder.activeEditorId) {
|
|
1307
|
-
builder.patchBlock(builder.activeEditorId, {
|
|
1308
|
-
isStreaming: false
|
|
1309
|
-
});
|
|
1310
|
-
builder.activeEditorId = null;
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
var handleUserMessage = (event, builder) => {
|
|
1314
|
-
const e = event;
|
|
1315
|
-
const existing = builder.findBlock((b) => b.type === "user");
|
|
1316
|
-
if (existing) {
|
|
1317
|
-
if (e.createdAt) {
|
|
1318
|
-
builder.patchBlock(existing.id, {
|
|
1319
|
-
createdAt: e.createdAt
|
|
1320
|
-
});
|
|
1321
|
-
}
|
|
1322
|
-
return;
|
|
1323
|
-
}
|
|
1324
|
-
builder.addBlock({
|
|
1325
|
-
type: "user",
|
|
1326
|
-
id: builder.nextId(),
|
|
1327
|
-
content: e.content,
|
|
1328
|
-
createdAt: e.createdAt
|
|
1329
|
-
});
|
|
1330
|
-
};
|
|
1331
|
-
var handleChunk = (event, builder) => {
|
|
1332
|
-
const e = event;
|
|
1333
|
-
if (!builder.activeTextId) {
|
|
1334
|
-
const id = builder.nextId();
|
|
1335
|
-
builder.activeTextId = id;
|
|
1336
|
-
builder.addBlock({
|
|
1337
|
-
type: "text",
|
|
1338
|
-
id,
|
|
1339
|
-
content: e.text,
|
|
1340
|
-
isStreaming: true
|
|
1341
|
-
});
|
|
1342
|
-
} else {
|
|
1343
|
-
const id = builder.activeTextId;
|
|
1344
|
-
const existing = builder.findBlock((b) => b.id === id);
|
|
1345
|
-
if (existing && existing.type === "text") {
|
|
1346
|
-
builder.patchBlock(id, {
|
|
1347
|
-
content: existing.content + e.text
|
|
1348
|
-
});
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
};
|
|
1352
|
-
var handleToolCall = (event, builder) => {
|
|
1353
|
-
const e = event;
|
|
1354
|
-
finalizeText(builder);
|
|
1355
|
-
builder.addBlock({
|
|
1356
|
-
type: "tool",
|
|
1357
|
-
id: builder.nextId(),
|
|
1358
|
-
callId: e.request.callId,
|
|
1359
|
-
toolName: e.request.toolName,
|
|
1360
|
-
displayName: e.request.displayName,
|
|
1361
|
-
description: e.request.description,
|
|
1362
|
-
arguments: e.request.arguments,
|
|
1363
|
-
toolCategory: e.request.toolCategory,
|
|
1364
|
-
iconUrl: e.request.iconUrl,
|
|
1365
|
-
status: "calling"
|
|
1366
|
-
});
|
|
1367
|
-
};
|
|
1368
|
-
var handleToolExecuting = (event, builder) => {
|
|
1369
|
-
const e = event;
|
|
1370
|
-
const block = builder.findBlock(
|
|
1371
|
-
(b) => b.type === "tool" && (e.call_id ? b.callId === e.call_id : b.toolName === e.name) && b.status === "calling"
|
|
1372
|
-
);
|
|
1373
|
-
if (block) {
|
|
1374
|
-
builder.patchBlock(block.id, { status: "executing" });
|
|
1375
|
-
}
|
|
1376
|
-
};
|
|
1377
|
-
var handleToolProgress = (event, builder) => {
|
|
1378
|
-
const e = event;
|
|
1379
|
-
const block = builder.findBlock(
|
|
1380
|
-
(b) => b.type === "tool" && b.callId === e.callId
|
|
1381
|
-
);
|
|
1382
|
-
if (block && block.type === "tool") {
|
|
1383
|
-
const sources = block.sources ? [...block.sources] : [];
|
|
1384
|
-
sources.push(e.item);
|
|
1385
|
-
builder.patchBlock(block.id, {
|
|
1386
|
-
sources,
|
|
1387
|
-
status: "executing"
|
|
1388
|
-
});
|
|
1389
|
-
}
|
|
1390
|
-
};
|
|
1391
|
-
var handleToolEnd = (event, builder) => {
|
|
1392
|
-
const e = event;
|
|
1393
|
-
const callId = e.type === "tool_end" ? e.callId : void 0;
|
|
1394
|
-
const name = e.type === "tool_end" ? e.toolName : e.name;
|
|
1395
|
-
const block = builder.findBlock(
|
|
1396
|
-
(b) => b.type === "tool" && (callId ? b.callId === callId : b.toolName === name) && b.status !== "completed"
|
|
1397
|
-
);
|
|
1398
|
-
if (block) {
|
|
1399
|
-
const toolEnd = e.type === "tool_end" ? e : null;
|
|
1400
|
-
builder.patchBlock(block.id, {
|
|
1401
|
-
status: "completed",
|
|
1402
|
-
...toolEnd?.sources ? { sources: toolEnd.sources } : {},
|
|
1403
|
-
...toolEnd?.durationMs != null ? { durationMs: toolEnd.durationMs } : {},
|
|
1404
|
-
...toolEnd?.result ? { result: toolEnd.result } : {}
|
|
1405
|
-
});
|
|
1406
|
-
}
|
|
1407
|
-
};
|
|
1408
|
-
var handleAgentStart = (event, builder) => {
|
|
1409
|
-
const e = event;
|
|
1410
|
-
finalizeText(builder);
|
|
1411
|
-
builder.addBlock({
|
|
1412
|
-
type: "agent",
|
|
1413
|
-
id: builder.nextId(),
|
|
1414
|
-
agentName: e.agentName,
|
|
1415
|
-
displayName: e.agentDisplayName,
|
|
1416
|
-
avatarUrl: e.avatarUrl
|
|
1417
|
-
});
|
|
1418
|
-
};
|
|
1419
|
-
var handleThinkingDelta = (event, builder) => {
|
|
1420
|
-
const e = event;
|
|
1421
|
-
if (!builder.activeThinkingId) {
|
|
1422
|
-
const id = builder.nextId();
|
|
1423
|
-
builder.activeThinkingId = id;
|
|
1424
|
-
builder.thinkingStartMs = Date.now();
|
|
1425
|
-
builder.addBlock({
|
|
1426
|
-
type: "thinking",
|
|
1427
|
-
id,
|
|
1428
|
-
content: e.text,
|
|
1429
|
-
isActive: true
|
|
1430
|
-
});
|
|
1431
|
-
} else {
|
|
1432
|
-
const id = builder.activeThinkingId;
|
|
1433
|
-
const existing = builder.findBlock((b) => b.id === id);
|
|
1434
|
-
if (existing && existing.type === "thinking") {
|
|
1435
|
-
builder.patchBlock(id, {
|
|
1436
|
-
content: existing.content + e.text
|
|
1437
|
-
});
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
};
|
|
1441
|
-
var handleThinkingComplete = (_event, builder) => {
|
|
1442
|
-
if (builder.activeThinkingId) {
|
|
1443
|
-
const durationMs = builder.thinkingStartMs ? Math.max(0, Date.now() - builder.thinkingStartMs) : void 0;
|
|
1444
|
-
builder.patchBlock(builder.activeThinkingId, {
|
|
1445
|
-
isActive: false,
|
|
1446
|
-
durationMs
|
|
1447
|
-
});
|
|
1448
|
-
builder.activeThinkingId = null;
|
|
1449
|
-
builder.thinkingStartMs = null;
|
|
1450
|
-
}
|
|
1451
|
-
};
|
|
1452
|
-
var handleSubagentStart = (event, builder) => {
|
|
1453
|
-
const e = event;
|
|
1454
|
-
finalizeText(builder);
|
|
1455
|
-
builder.addBlock({
|
|
1456
|
-
type: "subagent",
|
|
1457
|
-
id: builder.nextId(),
|
|
1458
|
-
agentName: e.agentName,
|
|
1459
|
-
displayName: e.displayName,
|
|
1460
|
-
toolCallId: e.toolCallId,
|
|
1461
|
-
avatarUrl: e.avatarUrl,
|
|
1462
|
-
description: e.description,
|
|
1463
|
-
content: "",
|
|
1464
|
-
isActive: true
|
|
1465
|
-
});
|
|
1466
|
-
};
|
|
1467
|
-
var handleSubagentChunk = (event, builder) => {
|
|
1468
|
-
const e = event;
|
|
1469
|
-
const block = builder.findBlock(
|
|
1470
|
-
(b) => b.type === "subagent" && b.toolCallId === e.toolCallId
|
|
1471
|
-
);
|
|
1472
|
-
if (block && block.type === "subagent") {
|
|
1473
|
-
builder.patchBlock(block.id, {
|
|
1474
|
-
content: block.content + e.text
|
|
1475
|
-
});
|
|
1476
|
-
}
|
|
1477
|
-
};
|
|
1478
|
-
var handleSubagentUpdate = (event, builder) => {
|
|
1479
|
-
const e = event;
|
|
1480
|
-
const block = builder.findBlock(
|
|
1481
|
-
(b) => b.type === "subagent" && b.toolCallId === e.toolCallId
|
|
1482
|
-
);
|
|
1483
|
-
if (block) {
|
|
1484
|
-
builder.patchBlock(block.id, {
|
|
1485
|
-
displayName: e.displayName
|
|
1486
|
-
});
|
|
1487
|
-
}
|
|
1488
|
-
};
|
|
1489
|
-
var handleSubagentEnd = (event, builder) => {
|
|
1490
|
-
const e = event;
|
|
1491
|
-
const block = builder.findBlock(
|
|
1492
|
-
(b) => b.type === "subagent" && b.toolCallId === e.toolCallId
|
|
1493
|
-
);
|
|
1494
|
-
if (block) {
|
|
1495
|
-
builder.patchBlock(block.id, {
|
|
1496
|
-
isActive: false
|
|
1497
|
-
});
|
|
1498
|
-
}
|
|
1499
|
-
};
|
|
1500
|
-
var handleComplete = (_event, builder) => {
|
|
1501
|
-
finalizeText(builder);
|
|
1502
|
-
finalizeThinking(builder);
|
|
1503
|
-
finalizeEditor(builder);
|
|
1504
|
-
for (const b of builder.getBlocks()) {
|
|
1505
|
-
if (b.type === "tool" && b.status !== "completed") {
|
|
1506
|
-
builder.patchBlock(b.id, { status: "completed" });
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
};
|
|
1510
|
-
var handleError = (event, builder) => {
|
|
1511
|
-
const e = event;
|
|
1512
|
-
finalizeText(builder);
|
|
1513
|
-
finalizeThinking(builder);
|
|
1514
|
-
builder.addBlock({
|
|
1515
|
-
type: "error",
|
|
1516
|
-
id: builder.nextId(),
|
|
1517
|
-
message: e.error.message
|
|
1518
|
-
});
|
|
1519
|
-
};
|
|
1520
|
-
var handleDisconnected = (_event, builder) => {
|
|
1521
|
-
finalizeText(builder);
|
|
1522
|
-
finalizeThinking(builder);
|
|
1523
|
-
finalizeEditor(builder);
|
|
1524
|
-
};
|
|
1525
|
-
var handleCapsuleOutputChunk = (event, builder) => {
|
|
1526
|
-
const e = event;
|
|
1527
|
-
const block = builder.findBlock(
|
|
1528
|
-
(b) => b.type === "capsule" && b.callId === e.callId
|
|
1529
|
-
);
|
|
1530
|
-
if (block && block.type === "capsule") {
|
|
1531
|
-
builder.patchBlock(block.id, {
|
|
1532
|
-
output: block.output + e.chunk
|
|
1533
|
-
});
|
|
1534
|
-
} else {
|
|
1535
|
-
builder.addBlock({
|
|
1536
|
-
type: "capsule",
|
|
1537
|
-
id: builder.nextId(),
|
|
1538
|
-
callId: e.callId,
|
|
1539
|
-
toolName: "",
|
|
1540
|
-
output: e.chunk,
|
|
1541
|
-
isActive: true
|
|
1542
|
-
});
|
|
1543
|
-
}
|
|
1544
|
-
};
|
|
1545
|
-
var handleCapsuleOutput = (event, builder) => {
|
|
1546
|
-
const e = event;
|
|
1547
|
-
const block = builder.findBlock(
|
|
1548
|
-
(b) => b.type === "capsule" && b.callId === (e.callId ?? "")
|
|
1549
|
-
);
|
|
1550
|
-
if (block) {
|
|
1551
|
-
builder.patchBlock(block.id, {
|
|
1552
|
-
output: e.output,
|
|
1553
|
-
command: e.command,
|
|
1554
|
-
toolName: e.toolName,
|
|
1555
|
-
durationMs: e.durationMs,
|
|
1556
|
-
isActive: false
|
|
1557
|
-
});
|
|
1558
|
-
} else {
|
|
1559
|
-
builder.addBlock({
|
|
1560
|
-
type: "capsule",
|
|
1561
|
-
id: builder.nextId(),
|
|
1562
|
-
callId: e.callId ?? "",
|
|
1563
|
-
toolName: e.toolName,
|
|
1564
|
-
command: e.command,
|
|
1565
|
-
output: e.output,
|
|
1566
|
-
durationMs: e.durationMs,
|
|
1567
|
-
isActive: false
|
|
1568
|
-
});
|
|
1569
|
-
}
|
|
1570
|
-
};
|
|
1571
|
-
var handleAssetCreated = (event, builder) => {
|
|
1572
|
-
const e = event;
|
|
1573
|
-
builder.addBlock({
|
|
1574
|
-
type: "asset",
|
|
1575
|
-
id: builder.nextId(),
|
|
1576
|
-
assetId: e.assetId,
|
|
1577
|
-
name: e.name,
|
|
1578
|
-
url: e.url,
|
|
1579
|
-
mediaType: e.mediaType,
|
|
1580
|
-
sizeBytes: e.sizeBytes
|
|
1581
|
-
});
|
|
1582
|
-
};
|
|
1583
|
-
var handleTodoUpdate = (event, builder) => {
|
|
1584
|
-
const e = event;
|
|
1585
|
-
if (builder.activeTodoId) {
|
|
1586
|
-
builder.patchBlock(builder.activeTodoId, {
|
|
1587
|
-
todos: e.todos
|
|
1588
|
-
});
|
|
1589
|
-
} else {
|
|
1590
|
-
const id = builder.nextId();
|
|
1591
|
-
builder.activeTodoId = id;
|
|
1592
|
-
builder.addBlock({
|
|
1593
|
-
type: "todo",
|
|
1594
|
-
id,
|
|
1595
|
-
todos: e.todos
|
|
1596
|
-
});
|
|
1597
|
-
}
|
|
1598
|
-
};
|
|
1599
|
-
var handleEditorContentStart = (event, builder) => {
|
|
1600
|
-
const e = event;
|
|
1601
|
-
const id = builder.nextId();
|
|
1602
|
-
builder.activeEditorId = id;
|
|
1603
|
-
builder.addBlock({
|
|
1604
|
-
type: "editor",
|
|
1605
|
-
id,
|
|
1606
|
-
callId: e.callId,
|
|
1607
|
-
path: e.path,
|
|
1608
|
-
language: e.language,
|
|
1609
|
-
content: "",
|
|
1610
|
-
isStreaming: true
|
|
1611
|
-
});
|
|
1612
|
-
};
|
|
1613
|
-
var handleEditorContentDelta = (event, builder) => {
|
|
1614
|
-
const e = event;
|
|
1615
|
-
const block = builder.findBlock(
|
|
1616
|
-
(b) => b.type === "editor" && b.callId === e.callId
|
|
1617
|
-
);
|
|
1618
|
-
if (block && block.type === "editor") {
|
|
1619
|
-
builder.patchBlock(block.id, {
|
|
1620
|
-
content: block.content + e.delta
|
|
1621
|
-
});
|
|
1622
|
-
}
|
|
1623
|
-
};
|
|
1624
|
-
var handleEditorContentEnd = (event, builder) => {
|
|
1625
|
-
const e = event;
|
|
1626
|
-
const block = builder.findBlock(
|
|
1627
|
-
(b) => b.type === "editor" && b.callId === e.callId
|
|
1628
|
-
);
|
|
1629
|
-
if (block) {
|
|
1630
|
-
builder.patchBlock(block.id, {
|
|
1631
|
-
isStreaming: false
|
|
1632
|
-
});
|
|
1633
|
-
}
|
|
1634
|
-
builder.activeEditorId = null;
|
|
1635
|
-
};
|
|
1636
|
-
var handleDesktopStream = (event, builder) => {
|
|
1637
|
-
const e = event;
|
|
1638
|
-
if (!e.url) return;
|
|
1639
|
-
const existing = builder.findBlock((b) => b.type === "desktop_stream");
|
|
1640
|
-
if (existing) {
|
|
1641
|
-
builder.patchBlock(existing.id, {
|
|
1642
|
-
url: e.url,
|
|
1643
|
-
authKey: e.authKey,
|
|
1644
|
-
sandboxId: e.sandboxId
|
|
1645
|
-
});
|
|
1646
|
-
} else {
|
|
1647
|
-
builder.addBlock({
|
|
1648
|
-
type: "desktop_stream",
|
|
1649
|
-
id: builder.nextId(),
|
|
1650
|
-
url: e.url,
|
|
1651
|
-
authKey: e.authKey,
|
|
1652
|
-
sandboxId: e.sandboxId
|
|
1653
|
-
});
|
|
1654
|
-
}
|
|
1655
|
-
};
|
|
1656
|
-
var handleAttachmentStaged = (event, builder) => {
|
|
1657
|
-
const e = event;
|
|
1658
|
-
if (!e.files || e.files.length === 0) return;
|
|
1659
|
-
builder.addBlock({
|
|
1660
|
-
type: "attachment",
|
|
1661
|
-
id: builder.nextId(),
|
|
1662
|
-
files: e.files
|
|
1663
|
-
});
|
|
1664
|
-
};
|
|
1665
|
-
var noop = () => {
|
|
1666
|
-
};
|
|
1667
|
-
var standardHandlers = {
|
|
1668
|
-
user_message: handleUserMessage,
|
|
1669
|
-
chunk: handleChunk,
|
|
1670
|
-
tool_call: handleToolCall,
|
|
1671
|
-
tool_executing: handleToolExecuting,
|
|
1672
|
-
tool_progress: handleToolProgress,
|
|
1673
|
-
tool_completed: handleToolEnd,
|
|
1674
|
-
tool_end: handleToolEnd,
|
|
1675
|
-
agent_start: handleAgentStart,
|
|
1676
|
-
agent_end: noop,
|
|
1677
|
-
thinking_delta: handleThinkingDelta,
|
|
1678
|
-
thinking_complete: handleThinkingComplete,
|
|
1679
|
-
subagent_start: handleSubagentStart,
|
|
1680
|
-
subagent_chunk: handleSubagentChunk,
|
|
1681
|
-
subagent_update: handleSubagentUpdate,
|
|
1682
|
-
subagent_end: handleSubagentEnd,
|
|
1683
|
-
subagent_tool_use: noop,
|
|
1684
|
-
capsule_output: handleCapsuleOutput,
|
|
1685
|
-
capsule_output_chunk: handleCapsuleOutputChunk,
|
|
1686
|
-
asset_created: handleAssetCreated,
|
|
1687
|
-
todo_update: handleTodoUpdate,
|
|
1688
|
-
editor_content_start: handleEditorContentStart,
|
|
1689
|
-
editor_content_delta: handleEditorContentDelta,
|
|
1690
|
-
editor_content_end: handleEditorContentEnd,
|
|
1691
|
-
desktop_stream: handleDesktopStream,
|
|
1692
|
-
attachment_staged: handleAttachmentStaged,
|
|
1693
|
-
workspace_ready: noop,
|
|
1694
|
-
retry: noop,
|
|
1695
|
-
complete: handleComplete,
|
|
1696
|
-
error: handleError,
|
|
1697
|
-
disconnected: handleDisconnected
|
|
1698
|
-
};
|
|
1699
|
-
|
|
1700
1588
|
// src/types.ts
|
|
1701
1589
|
var ChatEventType = {
|
|
1590
|
+
// Connection lifecycle (SDK-local, not wire)
|
|
1702
1591
|
Connected: "connected",
|
|
1703
|
-
|
|
1592
|
+
Disconnected: "disconnected",
|
|
1593
|
+
// Turn lifecycle
|
|
1594
|
+
MessageStart: "message_start",
|
|
1595
|
+
MessageStop: "message_stop",
|
|
1596
|
+
// Block lifecycle
|
|
1597
|
+
BlockStart: "block_start",
|
|
1598
|
+
BlockDelta: "block_delta",
|
|
1599
|
+
BlockStop: "block_stop",
|
|
1600
|
+
// Reliability
|
|
1601
|
+
Stall: "stall",
|
|
1602
|
+
Retry: "retry",
|
|
1603
|
+
Error: "error",
|
|
1604
|
+
Keepalive: "keepalive",
|
|
1605
|
+
// Conversation-level (typed custom events)
|
|
1704
1606
|
UserMessage: "user_message",
|
|
1705
1607
|
TitleGenerated: "title_generated",
|
|
1706
|
-
ModelInfo: "model_info",
|
|
1707
|
-
Chunk: "chunk",
|
|
1708
|
-
ToolCall: "tool_call",
|
|
1709
|
-
ToolExecuting: "tool_executing",
|
|
1710
|
-
ToolProgress: "tool_progress",
|
|
1711
|
-
ToolCompleted: "tool_completed",
|
|
1712
|
-
ToolEnd: "tool_end",
|
|
1713
|
-
AgentStart: "agent_start",
|
|
1714
|
-
AgentEnd: "agent_end",
|
|
1715
|
-
ThinkingDelta: "thinking_delta",
|
|
1716
|
-
ThinkingComplete: "thinking_complete",
|
|
1717
|
-
SubagentStart: "subagent_start",
|
|
1718
|
-
SubagentChunk: "subagent_chunk",
|
|
1719
|
-
SubagentUpdate: "subagent_update",
|
|
1720
|
-
SubagentEnd: "subagent_end",
|
|
1721
|
-
SubagentToolUse: "subagent_tool_use",
|
|
1722
|
-
CapsuleOutput: "capsule_output",
|
|
1723
|
-
CapsuleOutputChunk: "capsule_output_chunk",
|
|
1724
|
-
AssetCreated: "asset_created",
|
|
1725
1608
|
TodoUpdate: "todo_update",
|
|
1726
|
-
EditorContentStart: "editor_content_start",
|
|
1727
|
-
EditorContentDelta: "editor_content_delta",
|
|
1728
|
-
EditorContentEnd: "editor_content_end",
|
|
1729
|
-
Complete: "complete",
|
|
1730
|
-
Error: "error",
|
|
1731
|
-
Disconnected: "disconnected",
|
|
1732
|
-
Retry: "retry",
|
|
1733
1609
|
ContextUpdate: "context_update",
|
|
1610
|
+
SubagentStart: "subagent_start",
|
|
1611
|
+
SubagentStop: "subagent_stop",
|
|
1612
|
+
ContextWarning: "context_warning",
|
|
1613
|
+
MemoryRecall: "memory_recall",
|
|
1614
|
+
MemoryUpdate: "memory_update",
|
|
1734
1615
|
DesktopStream: "desktop_stream",
|
|
1735
1616
|
AttachmentStaged: "attachment_staged",
|
|
1736
|
-
WorkspaceReady: "workspace_ready"
|
|
1617
|
+
WorkspaceReady: "workspace_ready",
|
|
1618
|
+
AssetCreated: "asset_created",
|
|
1619
|
+
ToolApprovalRequested: "tool_approval_requested",
|
|
1620
|
+
ToolApprovalGranted: "tool_approval_granted",
|
|
1621
|
+
ToolPermissionDenied: "tool_permission_denied",
|
|
1622
|
+
ToolHarnessWarning: "tool_harness_warning",
|
|
1623
|
+
UserUnavailable: "user_unavailable",
|
|
1624
|
+
PromptSuggestion: "prompt_suggestion",
|
|
1625
|
+
StateChanged: "state_changed",
|
|
1626
|
+
// Generic fallthrough for unknown custom events
|
|
1627
|
+
Custom: "custom"
|
|
1737
1628
|
};
|
|
1738
1629
|
|
|
1739
1630
|
// src/stream-manager.ts
|
|
@@ -1788,22 +1679,12 @@ var StreamManager = class {
|
|
|
1788
1679
|
}
|
|
1789
1680
|
onSessionEvent(event) {
|
|
1790
1681
|
const convId = this.session.conversationId;
|
|
1791
|
-
if (event.type === ChatEventType.BlocksChanged) {
|
|
1792
|
-
if (this._state === "streaming" && convId) {
|
|
1793
|
-
this.emit({
|
|
1794
|
-
type: "blocksChanged",
|
|
1795
|
-
conversationId: convId,
|
|
1796
|
-
blocks: event.blocks
|
|
1797
|
-
});
|
|
1798
|
-
}
|
|
1799
|
-
return;
|
|
1800
|
-
}
|
|
1801
1682
|
this.emit({
|
|
1802
1683
|
type: "event",
|
|
1803
1684
|
conversationId: convId,
|
|
1804
1685
|
event
|
|
1805
1686
|
});
|
|
1806
|
-
if (event.type === ChatEventType.
|
|
1687
|
+
if (event.type === ChatEventType.MessageStop) {
|
|
1807
1688
|
if (this._state === "streaming") {
|
|
1808
1689
|
this.setState("idle");
|
|
1809
1690
|
}
|
|
@@ -1816,13 +1697,13 @@ var StreamManager = class {
|
|
|
1816
1697
|
const id = await this.session.createNewConversation();
|
|
1817
1698
|
this.setActiveConversation(id);
|
|
1818
1699
|
}
|
|
1819
|
-
this.prepareUserBlock(content);
|
|
1820
1700
|
this.setState("streaming");
|
|
1821
1701
|
try {
|
|
1822
1702
|
await this.session.send(content, {
|
|
1823
1703
|
enableSearch: options?.enableSearch,
|
|
1824
1704
|
agentName: options?.agentName,
|
|
1825
|
-
uploadIds: options?.uploadIds
|
|
1705
|
+
uploadIds: options?.uploadIds,
|
|
1706
|
+
planMode: options?.planMode
|
|
1826
1707
|
});
|
|
1827
1708
|
} catch {
|
|
1828
1709
|
}
|
|
@@ -1836,7 +1717,6 @@ var StreamManager = class {
|
|
|
1836
1717
|
);
|
|
1837
1718
|
const lastUserMsg = userMsgs[userMsgs.length - 1];
|
|
1838
1719
|
if (!lastUserMsg) return;
|
|
1839
|
-
this.prepareUserBlock(lastUserMsg.content);
|
|
1840
1720
|
this.setState("streaming");
|
|
1841
1721
|
try {
|
|
1842
1722
|
await this.session.resendFromCheckpoint(
|
|
@@ -1900,14 +1780,6 @@ var StreamManager = class {
|
|
|
1900
1780
|
this.handlers = [];
|
|
1901
1781
|
}
|
|
1902
1782
|
// ── Internal: helpers ──────────────────────────────────────────
|
|
1903
|
-
prepareUserBlock(content) {
|
|
1904
|
-
this.session.blockBuilder.reset();
|
|
1905
|
-
this.session.blockBuilder.addBlock({
|
|
1906
|
-
type: "user",
|
|
1907
|
-
id: this.session.blockBuilder.nextId(),
|
|
1908
|
-
content
|
|
1909
|
-
});
|
|
1910
|
-
}
|
|
1911
1783
|
finalizeStream() {
|
|
1912
1784
|
if (this._state === "streaming") {
|
|
1913
1785
|
this.setState("idle");
|
|
@@ -1918,8 +1790,8 @@ var StreamManager = class {
|
|
|
1918
1790
|
this.setState("restoring");
|
|
1919
1791
|
let activeJobId = null;
|
|
1920
1792
|
try {
|
|
1921
|
-
const res = await this.session.client.
|
|
1922
|
-
activeJobId = res.
|
|
1793
|
+
const res = await this.session.client.getActiveJob(conversationId);
|
|
1794
|
+
activeJobId = res.jobId;
|
|
1923
1795
|
} catch {
|
|
1924
1796
|
}
|
|
1925
1797
|
if (activeJobId) {
|
|
@@ -1939,15 +1811,19 @@ var StreamManager = class {
|
|
|
1939
1811
|
const completedJobs = jobs.filter(
|
|
1940
1812
|
(j) => j.status === "completed"
|
|
1941
1813
|
);
|
|
1942
|
-
|
|
1943
|
-
|
|
1814
|
+
const userMessages = this.session.messages.filter(
|
|
1815
|
+
(m) => m.role === "user"
|
|
1816
|
+
);
|
|
1817
|
+
for (let i = 0; i < completedJobs.length; i++) {
|
|
1818
|
+
const job = completedJobs[i];
|
|
1819
|
+
const userContent = userMessages[i]?.content;
|
|
1820
|
+
await this.session.switchConversation(
|
|
1821
|
+
conversationId,
|
|
1822
|
+
job.job_id,
|
|
1823
|
+
userContent
|
|
1824
|
+
);
|
|
1944
1825
|
}
|
|
1945
1826
|
if (completedJobs.length > 0) {
|
|
1946
|
-
this.emit({
|
|
1947
|
-
type: "blocksChanged",
|
|
1948
|
-
conversationId,
|
|
1949
|
-
blocks: this.session.blockBuilder.getBlocks()
|
|
1950
|
-
});
|
|
1951
1827
|
this.emit({
|
|
1952
1828
|
type: "versionsReady",
|
|
1953
1829
|
conversationId,
|
|
@@ -1965,23 +1841,93 @@ var StreamManager = class {
|
|
|
1965
1841
|
this.emit({ type: "conversationChanged", conversationId: id });
|
|
1966
1842
|
}
|
|
1967
1843
|
};
|
|
1844
|
+
|
|
1845
|
+
// src/replay.ts
|
|
1846
|
+
function toWireEvent(raw) {
|
|
1847
|
+
const type = raw.data.type || raw.event;
|
|
1848
|
+
if (!type || type === "done") return null;
|
|
1849
|
+
return { ...raw.data, type };
|
|
1850
|
+
}
|
|
1851
|
+
function mapSseToChat(raw) {
|
|
1852
|
+
const wire = toWireEvent(raw);
|
|
1853
|
+
if (!wire) return [];
|
|
1854
|
+
const event = translateWireEvent(wire);
|
|
1855
|
+
return event ? [event] : [];
|
|
1856
|
+
}
|
|
1857
|
+
function replayEvents(sseEvents, userMessages, handleEvent, addBlock) {
|
|
1858
|
+
const userMsgs = userMessages.filter((m) => m.role === "user");
|
|
1859
|
+
let userIdx = 0;
|
|
1860
|
+
let expectingUserMessage = true;
|
|
1861
|
+
for (const raw of sseEvents) {
|
|
1862
|
+
const type = raw.data.type || raw.event;
|
|
1863
|
+
if (type === "message_stop") {
|
|
1864
|
+
expectingUserMessage = true;
|
|
1865
|
+
}
|
|
1866
|
+
if (type === "message_start" && expectingUserMessage && userIdx < userMsgs.length) {
|
|
1867
|
+
const userMsg = userMsgs[userIdx];
|
|
1868
|
+
addBlock({
|
|
1869
|
+
type: "user",
|
|
1870
|
+
id: `replay_user_${userIdx}`,
|
|
1871
|
+
content: userMsg.content
|
|
1872
|
+
});
|
|
1873
|
+
userIdx++;
|
|
1874
|
+
expectingUserMessage = false;
|
|
1875
|
+
}
|
|
1876
|
+
for (const ce of mapSseToChat(raw)) {
|
|
1877
|
+
handleEvent(ce);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
// src/embedded-resource.ts
|
|
1883
|
+
function isEmbeddedResource(value) {
|
|
1884
|
+
return typeof value === "object" && value !== null && value._embedded_resource === true;
|
|
1885
|
+
}
|
|
1886
|
+
function parseEmbeddedResource(value) {
|
|
1887
|
+
let candidate = value;
|
|
1888
|
+
if (typeof candidate === "string") {
|
|
1889
|
+
const trimmed = candidate.trim();
|
|
1890
|
+
if (!trimmed.startsWith("{")) return null;
|
|
1891
|
+
try {
|
|
1892
|
+
candidate = JSON.parse(trimmed);
|
|
1893
|
+
} catch {
|
|
1894
|
+
return null;
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
if (!isEmbeddedResource(candidate)) return null;
|
|
1898
|
+
const mimeType = candidate.mime_type;
|
|
1899
|
+
const uri = candidate.uri;
|
|
1900
|
+
const payload = candidate.payload;
|
|
1901
|
+
if (typeof mimeType !== "string" || !mimeType) return null;
|
|
1902
|
+
if (typeof uri !== "string" || !uri) return null;
|
|
1903
|
+
if (!payload || typeof payload !== "object") return null;
|
|
1904
|
+
return {
|
|
1905
|
+
mimeType,
|
|
1906
|
+
uri,
|
|
1907
|
+
payload
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1968
1910
|
export {
|
|
1969
1911
|
AstralformClient,
|
|
1970
1912
|
AstralformError,
|
|
1971
1913
|
AuthenticationError,
|
|
1972
|
-
BlockBuilder,
|
|
1973
1914
|
ChatEventType,
|
|
1974
1915
|
ChatSession,
|
|
1975
1916
|
ConnectionError,
|
|
1976
1917
|
InMemoryStorage,
|
|
1977
1918
|
LLMNotConfiguredError,
|
|
1919
|
+
ProtocolRegistry,
|
|
1978
1920
|
RateLimitError,
|
|
1979
1921
|
ServerError,
|
|
1980
1922
|
StreamAbortedError,
|
|
1981
1923
|
StreamManager,
|
|
1982
1924
|
ToolRegistry,
|
|
1983
1925
|
generateId,
|
|
1984
|
-
|
|
1985
|
-
|
|
1926
|
+
isEmbeddedResource,
|
|
1927
|
+
mapSseToChat,
|
|
1928
|
+
parseEmbeddedResource,
|
|
1929
|
+
replayEvents,
|
|
1930
|
+
streamJobSSE,
|
|
1931
|
+
translateDelta
|
|
1986
1932
|
};
|
|
1987
1933
|
//# sourceMappingURL=index.js.map
|