@astralform/js 0.2.2 → 1.0.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 +931 -954
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +707 -500
- package/dist/index.d.ts +707 -500
- package/dist/index.js +924 -951
- package/dist/index.js.map +1 -1
- package/package.json +7 -1
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,9 @@ 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
|
+
}
|
|
258
511
|
// --- Conversation Assets ---
|
|
259
512
|
mapAsset(raw) {
|
|
260
513
|
return {
|
|
@@ -276,10 +529,7 @@ var AstralformClient = class {
|
|
|
276
529
|
`${this.baseURL}/v1/conversations/${encodeURIComponent(conversationId)}/uploads`,
|
|
277
530
|
{
|
|
278
531
|
method: "POST",
|
|
279
|
-
headers:
|
|
280
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
281
|
-
"X-End-User-ID": this.userId
|
|
282
|
-
},
|
|
532
|
+
headers: this.authHeaders,
|
|
283
533
|
body: formData
|
|
284
534
|
}
|
|
285
535
|
).catch((err) => {
|
|
@@ -303,6 +553,31 @@ var AstralformClient = class {
|
|
|
303
553
|
);
|
|
304
554
|
return raw.map((r) => this.mapAsset(r));
|
|
305
555
|
}
|
|
556
|
+
// --- Account-scoped discovery (user-token mode) ---
|
|
557
|
+
//
|
|
558
|
+
// Lets a signed-in user pick which team/project they want to act on.
|
|
559
|
+
// Backend gates these on OIDC user context (no X-Project-ID required) —
|
|
560
|
+
// sending them in API-key mode yields 401.
|
|
561
|
+
async listTeams() {
|
|
562
|
+
const raw = await this.get("/v1/teams");
|
|
563
|
+
return raw.map((t) => ({
|
|
564
|
+
id: t.id,
|
|
565
|
+
name: t.name,
|
|
566
|
+
slug: t.slug,
|
|
567
|
+
isDefault: t.is_default,
|
|
568
|
+
role: t.role
|
|
569
|
+
}));
|
|
570
|
+
}
|
|
571
|
+
async listProjects(teamId) {
|
|
572
|
+
const raw = await this.get(`/v1/teams/${encodeURIComponent(teamId)}/projects`);
|
|
573
|
+
return raw.map((p) => ({
|
|
574
|
+
id: p.id,
|
|
575
|
+
name: p.name,
|
|
576
|
+
teamId: p.team_id,
|
|
577
|
+
createdAt: p.created_at,
|
|
578
|
+
updatedAt: p.updated_at
|
|
579
|
+
}));
|
|
580
|
+
}
|
|
306
581
|
// --- Jobs API ---
|
|
307
582
|
async createJob(request) {
|
|
308
583
|
return this.post("/v1/jobs", request);
|
|
@@ -319,90 +594,50 @@ var AstralformClient = class {
|
|
|
319
594
|
async cancelJob(jobId) {
|
|
320
595
|
await this.post(`/v1/jobs/${encodeURIComponent(jobId)}/cancel`, {});
|
|
321
596
|
}
|
|
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();
|
|
597
|
+
async getJob(jobId) {
|
|
598
|
+
const raw = await this.get(`/v1/jobs/${encodeURIComponent(jobId)}`);
|
|
599
|
+
return {
|
|
600
|
+
jobId: raw.job_id,
|
|
601
|
+
status: raw.status,
|
|
602
|
+
createdAt: raw.created_at ?? null,
|
|
603
|
+
startedAt: raw.started_at ?? null,
|
|
604
|
+
completedAt: raw.completed_at ?? null,
|
|
605
|
+
errorMessage: raw.error_message ?? null,
|
|
606
|
+
inputTokens: raw.input_tokens ?? 0,
|
|
607
|
+
outputTokens: raw.output_tokens ?? 0
|
|
608
|
+
};
|
|
392
609
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
610
|
+
async submitFeedback(jobId, request) {
|
|
611
|
+
const body = {
|
|
612
|
+
rating: request.rating
|
|
613
|
+
};
|
|
614
|
+
if (request.comment != null) body.comment = request.comment;
|
|
615
|
+
const raw = await this.post(`/v1/jobs/${encodeURIComponent(jobId)}/feedback`, body);
|
|
616
|
+
return {
|
|
617
|
+
id: raw.id,
|
|
618
|
+
jobId: raw.job_id,
|
|
619
|
+
rating: raw.rating,
|
|
620
|
+
comment: raw.comment,
|
|
621
|
+
createdAt: raw.created_at
|
|
622
|
+
};
|
|
399
623
|
}
|
|
400
|
-
|
|
401
|
-
|
|
624
|
+
async getActiveJob(conversationId) {
|
|
625
|
+
const raw = await this.get(`/v1/conversations/${encodeURIComponent(conversationId)}/active-job`);
|
|
626
|
+
return {
|
|
627
|
+
jobId: raw.job_id ?? null,
|
|
628
|
+
status: raw.status
|
|
629
|
+
};
|
|
402
630
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
631
|
+
async listJobs(conversationId) {
|
|
632
|
+
const raw = await this.get(`/v1/conversations/${encodeURIComponent(conversationId)}/jobs`);
|
|
633
|
+
return raw.map((j) => ({
|
|
634
|
+
jobId: j.job_id,
|
|
635
|
+
status: j.status,
|
|
636
|
+
replacesJobId: j.replaces_job_id ?? null,
|
|
637
|
+
responseContent: j.response_content ?? null,
|
|
638
|
+
metrics: j.metrics ?? null,
|
|
639
|
+
createdAt: j.created_at ?? null
|
|
640
|
+
}));
|
|
406
641
|
}
|
|
407
642
|
};
|
|
408
643
|
|
|
@@ -556,40 +791,348 @@ var ToolRegistry = class {
|
|
|
556
791
|
}
|
|
557
792
|
};
|
|
558
793
|
|
|
559
|
-
// src/
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
794
|
+
// src/protocol-registry.ts
|
|
795
|
+
var ProtocolRegistry = class {
|
|
796
|
+
constructor() {
|
|
797
|
+
this.adapters = /* @__PURE__ */ new Map();
|
|
798
|
+
}
|
|
799
|
+
/** Register or replace the adapter for a MIME type. */
|
|
800
|
+
register(adapter) {
|
|
801
|
+
this.adapters.set(adapter.mimeType, adapter);
|
|
802
|
+
}
|
|
803
|
+
/** Remove the adapter for a MIME type. No-op if not registered. */
|
|
804
|
+
unregister(mimeType) {
|
|
805
|
+
this.adapters.delete(mimeType);
|
|
806
|
+
}
|
|
807
|
+
/** Returns the adapter for a MIME type, or ``null`` if none is registered. */
|
|
808
|
+
get(mimeType) {
|
|
809
|
+
return this.adapters.get(mimeType) ?? null;
|
|
810
|
+
}
|
|
811
|
+
has(mimeType) {
|
|
812
|
+
return this.adapters.has(mimeType);
|
|
813
|
+
}
|
|
814
|
+
/** Drop every adapter. Called when a session disconnects. */
|
|
815
|
+
clear() {
|
|
816
|
+
this.adapters.clear();
|
|
817
|
+
}
|
|
818
|
+
listMimeTypes() {
|
|
819
|
+
return Array.from(this.adapters.keys());
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
// src/translate.ts
|
|
824
|
+
function translateDelta(wire) {
|
|
825
|
+
switch (wire.channel) {
|
|
826
|
+
case "text":
|
|
827
|
+
return { channel: "text", text: wire.text };
|
|
828
|
+
case "thinking":
|
|
829
|
+
return { channel: "thinking", text: wire.text };
|
|
830
|
+
case "signature":
|
|
831
|
+
return { channel: "signature", signature: wire.signature };
|
|
832
|
+
case "input":
|
|
833
|
+
return { channel: "input", partialJson: wire.partial_json };
|
|
834
|
+
case "input_arg":
|
|
835
|
+
return {
|
|
836
|
+
channel: "inputArg",
|
|
837
|
+
argName: wire.arg_name,
|
|
838
|
+
text: wire.text
|
|
839
|
+
};
|
|
840
|
+
case "output":
|
|
841
|
+
return { channel: "output", stream: wire.stream, chunk: wire.chunk };
|
|
842
|
+
case "status":
|
|
843
|
+
return {
|
|
844
|
+
channel: "status",
|
|
845
|
+
status: wire.status,
|
|
846
|
+
note: wire.note
|
|
847
|
+
};
|
|
848
|
+
default:
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
function translateAgentIdentity(raw) {
|
|
853
|
+
return {
|
|
854
|
+
name: raw.name ?? "",
|
|
855
|
+
displayName: raw.display_name ?? null,
|
|
856
|
+
avatarUrl: raw.avatar_url ?? null,
|
|
857
|
+
description: raw.description ?? null
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
function translateCustomEvent(name, data) {
|
|
861
|
+
switch (name) {
|
|
862
|
+
case "user_message":
|
|
863
|
+
return {
|
|
864
|
+
type: "user_message",
|
|
865
|
+
content: data.content ?? "",
|
|
866
|
+
createdAt: data.created_at
|
|
867
|
+
};
|
|
868
|
+
case "title_generated":
|
|
869
|
+
return {
|
|
870
|
+
type: "title_generated",
|
|
871
|
+
title: data.title ?? ""
|
|
872
|
+
};
|
|
873
|
+
case "todo_update":
|
|
874
|
+
return {
|
|
875
|
+
type: "todo_update",
|
|
876
|
+
todos: data.todos ?? []
|
|
877
|
+
};
|
|
878
|
+
case "context_update":
|
|
879
|
+
return {
|
|
880
|
+
type: "context_update",
|
|
881
|
+
context: data.context ?? {},
|
|
882
|
+
phase: data.phase ?? null,
|
|
883
|
+
updatedAt: data.updated_at ?? null
|
|
884
|
+
};
|
|
885
|
+
case "subagent_start":
|
|
886
|
+
return {
|
|
887
|
+
type: "subagent_start",
|
|
888
|
+
agent: translateAgentIdentity(
|
|
889
|
+
data.agent ?? {}
|
|
890
|
+
),
|
|
891
|
+
taskCallId: data.task_call_id ?? null
|
|
892
|
+
};
|
|
893
|
+
case "subagent_stop":
|
|
894
|
+
return {
|
|
895
|
+
type: "subagent_stop",
|
|
896
|
+
agent: translateAgentIdentity(
|
|
897
|
+
data.agent ?? {}
|
|
898
|
+
),
|
|
899
|
+
taskCallId: data.task_call_id ?? null
|
|
900
|
+
};
|
|
901
|
+
case "context_warning":
|
|
902
|
+
return {
|
|
903
|
+
type: "context_warning",
|
|
904
|
+
severity: data.severity ?? "warning",
|
|
905
|
+
utilizationPct: data.utilization_pct ?? 0,
|
|
906
|
+
remainingTokens: data.remaining_tokens ?? 0,
|
|
907
|
+
windowTokens: data.window_tokens ?? 0,
|
|
908
|
+
inputTokens: data.input_tokens ?? 0,
|
|
909
|
+
message: data.message ?? ""
|
|
910
|
+
};
|
|
911
|
+
case "memory_recall":
|
|
912
|
+
return {
|
|
913
|
+
type: "memory_recall",
|
|
914
|
+
memories: data.memories ?? []
|
|
915
|
+
};
|
|
916
|
+
case "memory_update":
|
|
917
|
+
return {
|
|
918
|
+
type: "memory_update",
|
|
919
|
+
action: data.action ?? "",
|
|
920
|
+
memoryId: data.memory_id ?? null,
|
|
921
|
+
key: data.key ?? null,
|
|
922
|
+
namespace: data.namespace ?? null
|
|
923
|
+
};
|
|
924
|
+
case "desktop_stream":
|
|
925
|
+
return {
|
|
926
|
+
type: "desktop_stream",
|
|
927
|
+
url: data.url ?? "",
|
|
928
|
+
sandboxId: data.sandbox_id ?? null
|
|
929
|
+
};
|
|
930
|
+
case "attachment_staged":
|
|
931
|
+
return {
|
|
932
|
+
type: "attachment_staged",
|
|
933
|
+
attachmentId: data.attachment_id ?? "",
|
|
934
|
+
filename: data.filename ?? "",
|
|
935
|
+
contentType: data.content_type ?? null,
|
|
936
|
+
sizeBytes: data.size_bytes ?? null
|
|
937
|
+
};
|
|
938
|
+
case "workspace_ready":
|
|
939
|
+
return {
|
|
940
|
+
type: "workspace_ready",
|
|
941
|
+
sandboxId: data.sandbox_id ?? "",
|
|
942
|
+
workspacePath: data.workspace_path ?? null
|
|
943
|
+
};
|
|
944
|
+
case "asset_created":
|
|
945
|
+
return {
|
|
946
|
+
type: "asset_created",
|
|
947
|
+
assetId: data.asset_id ?? "",
|
|
948
|
+
filename: data.filename ?? "",
|
|
949
|
+
url: data.url ?? null,
|
|
950
|
+
contentType: data.content_type ?? null
|
|
951
|
+
};
|
|
952
|
+
case "tool_approval_requested":
|
|
953
|
+
return {
|
|
954
|
+
type: "tool_approval_requested",
|
|
955
|
+
toolName: data.tool_name ?? "",
|
|
956
|
+
callId: data.call_id ?? "",
|
|
957
|
+
arguments: data.arguments ?? {},
|
|
958
|
+
riskLevel: data.risk_level ?? null,
|
|
959
|
+
reason: data.reason ?? null
|
|
960
|
+
};
|
|
961
|
+
case "tool_approval_granted":
|
|
962
|
+
return {
|
|
963
|
+
type: "tool_approval_granted",
|
|
964
|
+
toolName: data.tool_name ?? "",
|
|
965
|
+
callId: data.call_id ?? ""
|
|
966
|
+
};
|
|
967
|
+
case "tool_permission_denied":
|
|
968
|
+
return {
|
|
969
|
+
type: "tool_permission_denied",
|
|
970
|
+
toolName: data.tool_name ?? "",
|
|
971
|
+
callId: data.call_id ?? "",
|
|
972
|
+
reason: data.reason ?? null,
|
|
973
|
+
deniedBy: data.denied_by ?? null
|
|
974
|
+
};
|
|
975
|
+
case "tool_harness_warning":
|
|
976
|
+
return {
|
|
977
|
+
type: "tool_harness_warning",
|
|
978
|
+
toolName: data.tool_name ?? "",
|
|
979
|
+
callId: data.call_id ?? "",
|
|
980
|
+
message: data.message ?? null,
|
|
981
|
+
details: data.details ?? null
|
|
982
|
+
};
|
|
983
|
+
case "user_unavailable":
|
|
984
|
+
return {
|
|
985
|
+
type: "user_unavailable",
|
|
986
|
+
consecutiveTimeouts: data.consecutive_timeouts ?? 0,
|
|
987
|
+
toolName: data.tool_name ?? null
|
|
988
|
+
};
|
|
989
|
+
case "prompt_suggestion":
|
|
990
|
+
return {
|
|
991
|
+
type: "prompt_suggestion",
|
|
992
|
+
suggestions: data.suggestions ?? []
|
|
993
|
+
};
|
|
994
|
+
case "state_changed":
|
|
995
|
+
return {
|
|
996
|
+
type: "state_changed",
|
|
997
|
+
state: data.state ?? ""
|
|
998
|
+
};
|
|
999
|
+
default:
|
|
1000
|
+
return { type: "custom", name, data };
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
function legacyCustomEventData(wire) {
|
|
1004
|
+
return wire;
|
|
1005
|
+
}
|
|
1006
|
+
function translateWireEvent(wire) {
|
|
1007
|
+
if (wire.type === "prompt_suggestion") {
|
|
1008
|
+
return translateCustomEvent(
|
|
1009
|
+
"prompt_suggestion",
|
|
1010
|
+
legacyCustomEventData(wire)
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
switch (wire.type) {
|
|
1014
|
+
case "message_start":
|
|
1015
|
+
return {
|
|
1016
|
+
type: "message_start",
|
|
1017
|
+
turnId: wire.turn_id,
|
|
1018
|
+
model: wire.model,
|
|
1019
|
+
agentName: wire.agent_name,
|
|
1020
|
+
agentDisplayName: wire.agent_display_name,
|
|
1021
|
+
agentAvatarUrl: wire.agent_avatar_url
|
|
1022
|
+
};
|
|
1023
|
+
case "block_start":
|
|
1024
|
+
return {
|
|
1025
|
+
type: "block_start",
|
|
1026
|
+
turnId: wire.turn_id,
|
|
1027
|
+
path: wire.path,
|
|
1028
|
+
parentPath: wire.parent_path ?? null,
|
|
1029
|
+
kind: wire.kind,
|
|
1030
|
+
metadata: wire.metadata
|
|
1031
|
+
};
|
|
1032
|
+
case "block_delta": {
|
|
1033
|
+
const delta = translateDelta(wire.delta);
|
|
1034
|
+
if (!delta) return null;
|
|
1035
|
+
return {
|
|
1036
|
+
type: "block_delta",
|
|
1037
|
+
turnId: wire.turn_id,
|
|
1038
|
+
path: wire.path,
|
|
1039
|
+
delta
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
case "block_stop":
|
|
1043
|
+
return {
|
|
1044
|
+
type: "block_stop",
|
|
1045
|
+
turnId: wire.turn_id,
|
|
1046
|
+
path: wire.path,
|
|
1047
|
+
status: wire.status,
|
|
1048
|
+
final: wire.final
|
|
1049
|
+
};
|
|
1050
|
+
case "message_stop":
|
|
1051
|
+
return {
|
|
1052
|
+
type: "message_stop",
|
|
1053
|
+
turnId: wire.turn_id,
|
|
1054
|
+
jobId: wire.job_id,
|
|
1055
|
+
stopReason: wire.stop_reason,
|
|
1056
|
+
usage: {
|
|
1057
|
+
inputTokens: wire.usage.input_tokens ?? 0,
|
|
1058
|
+
outputTokens: wire.usage.output_tokens ?? 0,
|
|
1059
|
+
cachedTokens: wire.usage.cached_tokens ?? 0
|
|
1060
|
+
},
|
|
1061
|
+
ttfbMs: wire.ttfb_ms,
|
|
1062
|
+
totalMs: wire.total_ms,
|
|
1063
|
+
stallCount: wire.stall_count
|
|
1064
|
+
};
|
|
1065
|
+
case "stall":
|
|
1066
|
+
return {
|
|
1067
|
+
type: "stall",
|
|
1068
|
+
sinceLastEventMs: wire.since_last_event_ms,
|
|
1069
|
+
stallCount: wire.stall_count
|
|
1070
|
+
};
|
|
1071
|
+
case "retry":
|
|
1072
|
+
return {
|
|
1073
|
+
type: "retry",
|
|
1074
|
+
attempt: wire.attempt,
|
|
1075
|
+
reason: wire.reason,
|
|
1076
|
+
backoffMs: wire.backoff_ms,
|
|
1077
|
+
strategy: wire.strategy ?? null,
|
|
1078
|
+
maxAttempts: wire.max_attempts ?? null,
|
|
1079
|
+
contextRecovery: wire.context_recovery ?? null
|
|
1080
|
+
};
|
|
1081
|
+
case "error":
|
|
1082
|
+
return {
|
|
1083
|
+
type: "error",
|
|
1084
|
+
code: wire.code,
|
|
1085
|
+
message: wire.message,
|
|
1086
|
+
blockPath: wire.block_path ?? null
|
|
1087
|
+
};
|
|
1088
|
+
case "keepalive":
|
|
1089
|
+
return {
|
|
1090
|
+
type: "keepalive",
|
|
1091
|
+
sinceLastEventMs: wire.since_last_event_ms
|
|
1092
|
+
};
|
|
1093
|
+
case "custom":
|
|
1094
|
+
return translateCustomEvent(wire.name, wire.data);
|
|
1095
|
+
default: {
|
|
1096
|
+
const _exhaustive = wire;
|
|
1097
|
+
void _exhaustive;
|
|
1098
|
+
return null;
|
|
1099
|
+
}
|
|
563
1100
|
}
|
|
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
1101
|
}
|
|
570
1102
|
|
|
571
1103
|
// src/session.ts
|
|
1104
|
+
function pathEquals(a, b) {
|
|
1105
|
+
if (a.length !== b.length) return false;
|
|
1106
|
+
for (let i = 0; i < a.length; i++) {
|
|
1107
|
+
if (a[i] !== b[i]) return false;
|
|
1108
|
+
}
|
|
1109
|
+
return true;
|
|
1110
|
+
}
|
|
572
1111
|
var ChatSession = class {
|
|
573
|
-
constructor(config, storage
|
|
1112
|
+
constructor(config, storage) {
|
|
1113
|
+
/**
|
|
1114
|
+
* Pluggable UI protocol adapters. Consumers register a framework-
|
|
1115
|
+
* specific adapter (e.g. React) for each MIME type they can render,
|
|
1116
|
+
* typically gated on ``session.projectStatus.uiComponents.protocol``.
|
|
1117
|
+
* ``ToolBlock``-style consumers look up the adapter for an incoming
|
|
1118
|
+
* embedded resource and hand off rendering.
|
|
1119
|
+
*/
|
|
1120
|
+
this.protocols = new ProtocolRegistry();
|
|
574
1121
|
// State
|
|
575
1122
|
this.conversationId = null;
|
|
576
1123
|
this.conversations = [];
|
|
577
1124
|
this.messages = [];
|
|
578
|
-
this.streamingContent = "";
|
|
579
1125
|
this.isStreaming = false;
|
|
580
|
-
this.executingTool = null;
|
|
581
1126
|
this.projectStatus = null;
|
|
582
1127
|
this.agents = [];
|
|
583
1128
|
this.skills = [];
|
|
584
1129
|
this.enabledClientTools = /* @__PURE__ */ new Set();
|
|
585
1130
|
this.modelDisplayName = null;
|
|
586
|
-
//
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
this.
|
|
590
|
-
this.
|
|
591
|
-
this.todos = [];
|
|
592
|
-
this.activeTools = /* @__PURE__ */ new Map();
|
|
1131
|
+
// Minimal in-session accumulation for the assistant message record.
|
|
1132
|
+
// Only top-level ``text`` blocks contribute; subagent / tool output
|
|
1133
|
+
// is tracked by the consumer's own block store.
|
|
1134
|
+
this.accumulatedText = "";
|
|
1135
|
+
this.currentTextPath = null;
|
|
593
1136
|
this.handlers = /* @__PURE__ */ new Set();
|
|
594
1137
|
this.abortController = null;
|
|
595
1138
|
/** Last received sequence number for resumable reconnection */
|
|
@@ -599,13 +1142,6 @@ var ChatSession = class {
|
|
|
599
1142
|
this.client = new AstralformClient(config);
|
|
600
1143
|
this.toolRegistry = new ToolRegistry();
|
|
601
1144
|
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
1145
|
}
|
|
610
1146
|
on(handler) {
|
|
611
1147
|
this.handlers.add(handler);
|
|
@@ -614,9 +1150,6 @@ var ChatSession = class {
|
|
|
614
1150
|
};
|
|
615
1151
|
}
|
|
616
1152
|
emit(event) {
|
|
617
|
-
if (event.type !== "blocks_changed" && event.type !== "connected") {
|
|
618
|
-
this.blockBuilder.processEvent(event);
|
|
619
|
-
}
|
|
620
1153
|
for (const handler of this.handlers) {
|
|
621
1154
|
try {
|
|
622
1155
|
handler(event);
|
|
@@ -669,7 +1202,8 @@ var ChatSession = class {
|
|
|
669
1202
|
),
|
|
670
1203
|
upload_ids: options?.uploadIds,
|
|
671
1204
|
agent_name: options?.agentName,
|
|
672
|
-
enable_search: options?.enableSearch
|
|
1205
|
+
enable_search: options?.enableSearch,
|
|
1206
|
+
plan_mode: options?.planMode
|
|
673
1207
|
};
|
|
674
1208
|
await this.processStream(request);
|
|
675
1209
|
}
|
|
@@ -686,13 +1220,8 @@ var ChatSession = class {
|
|
|
686
1220
|
await this.processStream(request);
|
|
687
1221
|
}
|
|
688
1222
|
resetStreamingState() {
|
|
689
|
-
this.
|
|
690
|
-
this.
|
|
691
|
-
this.isThinking = false;
|
|
692
|
-
this.activeSubagents.clear();
|
|
693
|
-
this.capsuleOutputs = [];
|
|
694
|
-
this.todos = [];
|
|
695
|
-
this.activeTools.clear();
|
|
1223
|
+
this.accumulatedText = "";
|
|
1224
|
+
this.currentTextPath = null;
|
|
696
1225
|
}
|
|
697
1226
|
async processStream(request) {
|
|
698
1227
|
this.isStreaming = true;
|
|
@@ -704,22 +1233,42 @@ var ChatSession = class {
|
|
|
704
1233
|
if (!(err instanceof DOMException && err.name === "AbortError")) {
|
|
705
1234
|
this.emit({
|
|
706
1235
|
type: "error",
|
|
707
|
-
|
|
1236
|
+
code: "connection_error",
|
|
1237
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1238
|
+
blockPath: null
|
|
708
1239
|
});
|
|
709
1240
|
}
|
|
710
1241
|
} finally {
|
|
711
1242
|
this.isStreaming = false;
|
|
712
|
-
this.executingTool = null;
|
|
713
1243
|
this.abortController = null;
|
|
714
1244
|
}
|
|
715
1245
|
}
|
|
716
1246
|
async consumeJobStream(request) {
|
|
717
1247
|
const job = await this.client.createJob(request);
|
|
718
1248
|
this.currentJobId = job.job_id;
|
|
719
|
-
|
|
1249
|
+
const conversationId = job.conversation_id;
|
|
720
1250
|
if (!this.conversationId) {
|
|
721
1251
|
this.conversationId = conversationId;
|
|
722
1252
|
}
|
|
1253
|
+
if (!this.conversations.some((c) => c.id === conversationId)) {
|
|
1254
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1255
|
+
const conv = {
|
|
1256
|
+
id: conversationId,
|
|
1257
|
+
title: "",
|
|
1258
|
+
messageCount: 0,
|
|
1259
|
+
createdAt: now,
|
|
1260
|
+
updatedAt: now
|
|
1261
|
+
};
|
|
1262
|
+
this.conversations.unshift(conv);
|
|
1263
|
+
await this.storage.createConversation(conversationId, "").catch(() => {
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
const lastMsg = this.messages[this.messages.length - 1];
|
|
1267
|
+
if (lastMsg?.role === "user" && !lastMsg.conversationId) {
|
|
1268
|
+
lastMsg.conversationId = conversationId;
|
|
1269
|
+
await this.storage.addMessage(lastMsg, conversationId).catch(() => {
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
723
1272
|
const messageId = job.message_id;
|
|
724
1273
|
this.lastSeq = -1;
|
|
725
1274
|
const stream = this.client.streamJobEvents(
|
|
@@ -736,7 +1285,8 @@ var ChatSession = class {
|
|
|
736
1285
|
);
|
|
737
1286
|
}
|
|
738
1287
|
/**
|
|
739
|
-
* Shared event consumption loop
|
|
1288
|
+
* Shared event consumption loop. Parses each wire event, updates
|
|
1289
|
+
* minimal session state, and emits typed ChatEvents to consumers.
|
|
740
1290
|
*/
|
|
741
1291
|
async consumeEventStream(stream, conversationId, messageId, executeClientTools) {
|
|
742
1292
|
for await (const raw of stream) {
|
|
@@ -756,385 +1306,112 @@ var ChatSession = class {
|
|
|
756
1306
|
} catch {
|
|
757
1307
|
continue;
|
|
758
1308
|
}
|
|
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
|
-
}
|
|
1309
|
+
await this.dispatchWireEvent(
|
|
1310
|
+
parsed,
|
|
1311
|
+
conversationId,
|
|
1312
|
+
messageId,
|
|
1313
|
+
executeClientTools
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
async dispatchWireEvent(wire, conversationId, messageId, executeClientTools) {
|
|
1318
|
+
this.applyWireSideEffects(wire, conversationId, messageId);
|
|
1319
|
+
const event = translateWireEvent(wire);
|
|
1320
|
+
if (event) {
|
|
1321
|
+
this.emit(event);
|
|
1322
|
+
}
|
|
1323
|
+
if (executeClientTools && wire.type === "block_stop" && wire.status === "awaiting_client_result" && wire.final?.call_id) {
|
|
1324
|
+
const f = wire.final;
|
|
1325
|
+
const request = {
|
|
1326
|
+
callId: f.call_id ?? "",
|
|
1327
|
+
toolName: f.tool_name ?? "",
|
|
1328
|
+
arguments: f.input ?? {},
|
|
1329
|
+
isClientTool: true
|
|
1330
|
+
};
|
|
1331
|
+
const results = await this.executeClientTools([request]);
|
|
1332
|
+
await this.client.submitToolResult({
|
|
1333
|
+
conversation_id: conversationId,
|
|
1334
|
+
message_id: messageId,
|
|
1335
|
+
tool_results: results
|
|
1336
|
+
});
|
|
853
1337
|
}
|
|
854
1338
|
}
|
|
855
1339
|
/**
|
|
856
|
-
*
|
|
857
|
-
*
|
|
1340
|
+
* State mutations driven by wire events. Kept separate from translation so
|
|
1341
|
+
* the pure wire → ChatEvent mapping can live in translate.ts and be reused
|
|
1342
|
+
* by the replay path.
|
|
1343
|
+
*
|
|
1344
|
+
* ``messageId`` is the server-assigned assistant message id for the current
|
|
1345
|
+
* turn; empty in the reconnect and conversation-switch replay paths where
|
|
1346
|
+
* messages have already been loaded from REST and shouldn't be re-pushed.
|
|
858
1347
|
*/
|
|
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
|
-
}
|
|
1348
|
+
applyWireSideEffects(wire, conversationId, messageId) {
|
|
1349
|
+
switch (wire.type) {
|
|
880
1350
|
case "message_start":
|
|
881
|
-
|
|
882
|
-
|
|
1351
|
+
this.resetStreamingState();
|
|
1352
|
+
if (wire.model) {
|
|
1353
|
+
this.modelDisplayName = wire.model;
|
|
883
1354
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1355
|
+
return;
|
|
1356
|
+
case "block_start":
|
|
1357
|
+
if (wire.kind === "text" && (!wire.parent_path || wire.parent_path.length === 0)) {
|
|
1358
|
+
this.currentTextPath = wire.path;
|
|
887
1359
|
}
|
|
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";
|
|
1360
|
+
return;
|
|
1361
|
+
case "block_delta":
|
|
1362
|
+
if (wire.delta.channel === "text" && this.currentTextPath !== null && pathEquals(this.currentTextPath, wire.path)) {
|
|
1363
|
+
this.accumulatedText += wire.delta.text;
|
|
952
1364
|
}
|
|
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;
|
|
1365
|
+
return;
|
|
1366
|
+
case "block_stop":
|
|
1367
|
+
if (this.currentTextPath !== null && pathEquals(this.currentTextPath, wire.path)) {
|
|
1368
|
+
this.currentTextPath = null;
|
|
997
1369
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1370
|
+
return;
|
|
1371
|
+
case "message_stop":
|
|
1372
|
+
if (messageId) {
|
|
1373
|
+
const assistantMessage = {
|
|
1374
|
+
id: messageId,
|
|
1375
|
+
conversationId,
|
|
1376
|
+
role: "assistant",
|
|
1377
|
+
content: this.accumulatedText,
|
|
1378
|
+
status: "complete",
|
|
1379
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1380
|
+
};
|
|
1381
|
+
this.messages.push(assistantMessage);
|
|
1382
|
+
this.storage.addMessage(assistantMessage, conversationId).catch(() => {
|
|
1383
|
+
});
|
|
1010
1384
|
}
|
|
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 "asset_created":
|
|
1054
|
-
this.emit({
|
|
1055
|
-
type: "asset_created",
|
|
1056
|
-
assetId: event.asset_id,
|
|
1057
|
-
name: event.name,
|
|
1058
|
-
url: event.url,
|
|
1059
|
-
mediaType: event.media_type,
|
|
1060
|
-
sizeBytes: event.size_bytes
|
|
1061
|
-
});
|
|
1062
|
-
break;
|
|
1063
|
-
case "editor_content_start":
|
|
1064
|
-
this.emit({
|
|
1065
|
-
type: "editor_content_start",
|
|
1066
|
-
callId: event.call_id,
|
|
1067
|
-
path: event.path,
|
|
1068
|
-
language: event.language
|
|
1069
|
-
});
|
|
1070
|
-
break;
|
|
1071
|
-
case "editor_content_delta":
|
|
1072
|
-
this.emit({
|
|
1073
|
-
type: "editor_content_delta",
|
|
1074
|
-
callId: event.call_id,
|
|
1075
|
-
path: event.path,
|
|
1076
|
-
delta: event.delta
|
|
1077
|
-
});
|
|
1078
|
-
break;
|
|
1079
|
-
case "editor_content_end":
|
|
1080
|
-
this.emit({
|
|
1081
|
-
type: "editor_content_end",
|
|
1082
|
-
callId: event.call_id
|
|
1083
|
-
});
|
|
1084
|
-
break;
|
|
1385
|
+
this.isStreaming = false;
|
|
1386
|
+
this.currentJobId = null;
|
|
1387
|
+
return;
|
|
1388
|
+
case "custom":
|
|
1389
|
+
if (wire.name === "title_generated") {
|
|
1390
|
+
const title = wire.data.title ?? "";
|
|
1391
|
+
if (this.conversationId && title) {
|
|
1392
|
+
const conv = this.conversations.find(
|
|
1393
|
+
(c) => c.id === this.conversationId
|
|
1394
|
+
);
|
|
1395
|
+
if (conv) {
|
|
1396
|
+
conv.title = title;
|
|
1397
|
+
}
|
|
1398
|
+
this.storage.updateConversationTitle(this.conversationId, title).catch(() => {
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
return;
|
|
1403
|
+
default:
|
|
1404
|
+
return;
|
|
1085
1405
|
}
|
|
1086
1406
|
}
|
|
1087
1407
|
async executeClientTools(toolCalls) {
|
|
1088
1408
|
const results = [];
|
|
1089
1409
|
for (const call of toolCalls) {
|
|
1090
|
-
this.executingTool = call.toolName;
|
|
1091
|
-
const toolState = this.activeTools.get(call.callId);
|
|
1092
|
-
if (toolState) {
|
|
1093
|
-
toolState.status = "executing";
|
|
1094
|
-
}
|
|
1095
|
-
this.emit({ type: "tool_executing", name: call.toolName });
|
|
1096
1410
|
const result = await this.toolRegistry.executeTool(call);
|
|
1097
1411
|
results.push(result);
|
|
1098
|
-
if (toolState) {
|
|
1099
|
-
toolState.status = "completed";
|
|
1100
|
-
}
|
|
1101
|
-
this.emit({
|
|
1102
|
-
type: "tool_completed",
|
|
1103
|
-
name: call.toolName,
|
|
1104
|
-
result: result.result
|
|
1105
|
-
});
|
|
1106
1412
|
}
|
|
1107
|
-
this.executingTool = null;
|
|
1108
1413
|
return results;
|
|
1109
1414
|
}
|
|
1110
|
-
async completeStream(conversationId, messageId, title, metrics, jobId) {
|
|
1111
|
-
const assistantMessage = {
|
|
1112
|
-
id: messageId || generateId(),
|
|
1113
|
-
conversationId,
|
|
1114
|
-
role: "assistant",
|
|
1115
|
-
content: this.streamingContent,
|
|
1116
|
-
status: "complete",
|
|
1117
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1118
|
-
};
|
|
1119
|
-
this.messages.push(assistantMessage);
|
|
1120
|
-
await this.storage.addMessage(assistantMessage, conversationId);
|
|
1121
|
-
if (title && conversationId) {
|
|
1122
|
-
await this.storage.updateConversationTitle(conversationId, title);
|
|
1123
|
-
const conv = this.conversations.find((c) => c.id === conversationId);
|
|
1124
|
-
if (conv) {
|
|
1125
|
-
conv.title = title;
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
this.emit({
|
|
1129
|
-
type: "complete",
|
|
1130
|
-
content: this.streamingContent,
|
|
1131
|
-
conversationId,
|
|
1132
|
-
messageId: assistantMessage.id,
|
|
1133
|
-
title,
|
|
1134
|
-
metrics,
|
|
1135
|
-
job_id: jobId
|
|
1136
|
-
});
|
|
1137
|
-
}
|
|
1138
1415
|
/**
|
|
1139
1416
|
* Load conversation context (messages) without replaying events.
|
|
1140
1417
|
* Used before reconnectToJob — SSE replay handles event replay.
|
|
@@ -1142,13 +1419,11 @@ var ChatSession = class {
|
|
|
1142
1419
|
async loadConversation(id) {
|
|
1143
1420
|
this.conversationId = id;
|
|
1144
1421
|
this.resetStreamingState();
|
|
1145
|
-
this.blockBuilder.reset();
|
|
1146
1422
|
this.messages = await this.client.getMessages(id).catch(() => this.storage.fetchMessages(id));
|
|
1147
1423
|
}
|
|
1148
1424
|
/**
|
|
1149
1425
|
* Reconnect to a running job's SSE stream (e.g. after page reload).
|
|
1150
1426
|
* Replays all events from the beginning and continues live.
|
|
1151
|
-
* Does NOT reset BlockBuilder — caller controls reset.
|
|
1152
1427
|
*/
|
|
1153
1428
|
async reconnectToJob(jobId) {
|
|
1154
1429
|
if (this.isStreaming) return;
|
|
@@ -1173,23 +1448,21 @@ var ChatSession = class {
|
|
|
1173
1448
|
} catch (err) {
|
|
1174
1449
|
this.emit({
|
|
1175
1450
|
type: "error",
|
|
1176
|
-
|
|
1451
|
+
code: "connection_error",
|
|
1452
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1453
|
+
blockPath: null
|
|
1177
1454
|
});
|
|
1178
1455
|
} finally {
|
|
1179
1456
|
this.isStreaming = false;
|
|
1180
|
-
this.executingTool = null;
|
|
1181
1457
|
this.abortController = null;
|
|
1182
1458
|
}
|
|
1183
1459
|
}
|
|
1184
|
-
/** Detach from the SSE stream without cancelling the job.
|
|
1185
|
-
* The backend job keeps running — caller can reconnect later. */
|
|
1460
|
+
/** Detach from the SSE stream without cancelling the job. */
|
|
1186
1461
|
detach() {
|
|
1187
1462
|
this.abortController?.abort();
|
|
1188
1463
|
this.abortController = null;
|
|
1189
1464
|
this.isStreaming = false;
|
|
1190
|
-
this.
|
|
1191
|
-
this.executingTool = null;
|
|
1192
|
-
this.blockBuilder.reset();
|
|
1465
|
+
this.resetStreamingState();
|
|
1193
1466
|
this.emit({ type: "disconnected" });
|
|
1194
1467
|
}
|
|
1195
1468
|
/** Stop the job and disconnect (explicit user action). */
|
|
@@ -1200,6 +1473,7 @@ var ChatSession = class {
|
|
|
1200
1473
|
}
|
|
1201
1474
|
this.detach();
|
|
1202
1475
|
this.currentJobId = null;
|
|
1476
|
+
this.protocols.clear();
|
|
1203
1477
|
}
|
|
1204
1478
|
async createNewConversation() {
|
|
1205
1479
|
const id = generateId();
|
|
@@ -1210,21 +1484,39 @@ var ChatSession = class {
|
|
|
1210
1484
|
this.conversations.unshift(conversation);
|
|
1211
1485
|
this.conversationId = id;
|
|
1212
1486
|
this.messages = [];
|
|
1213
|
-
this.streamingContent = "";
|
|
1214
1487
|
return id;
|
|
1215
1488
|
}
|
|
1216
|
-
async switchConversation(id, jobId) {
|
|
1489
|
+
async switchConversation(id, jobId, userMessageContent) {
|
|
1217
1490
|
this.conversationId = id;
|
|
1218
1491
|
this.resetStreamingState();
|
|
1219
|
-
this.blockBuilder.reset();
|
|
1220
1492
|
const [messagesResult, eventsResult] = await Promise.allSettled([
|
|
1221
1493
|
this.client.getMessages(id).catch(() => this.storage.fetchMessages(id)),
|
|
1222
1494
|
this.client.getConversationEvents(id, jobId)
|
|
1223
1495
|
]);
|
|
1224
1496
|
this.messages = messagesResult.status === "fulfilled" ? messagesResult.value : [];
|
|
1225
1497
|
if (eventsResult.status === "fulfilled") {
|
|
1498
|
+
let userMessageEmitted = !userMessageContent;
|
|
1226
1499
|
for (const ev of eventsResult.value) {
|
|
1227
|
-
|
|
1500
|
+
const type = ev.data.type || ev.event;
|
|
1501
|
+
if (!type || type === "done") continue;
|
|
1502
|
+
if (!userMessageEmitted && type === "message_start") {
|
|
1503
|
+
this.emit({
|
|
1504
|
+
type: "user_message",
|
|
1505
|
+
content: userMessageContent
|
|
1506
|
+
});
|
|
1507
|
+
userMessageEmitted = true;
|
|
1508
|
+
}
|
|
1509
|
+
const wire = { ...ev.data, type };
|
|
1510
|
+
try {
|
|
1511
|
+
await this.dispatchWireEvent(
|
|
1512
|
+
wire,
|
|
1513
|
+
id,
|
|
1514
|
+
"",
|
|
1515
|
+
false
|
|
1516
|
+
// don't execute client tools on replay
|
|
1517
|
+
);
|
|
1518
|
+
} catch {
|
|
1519
|
+
}
|
|
1228
1520
|
}
|
|
1229
1521
|
}
|
|
1230
1522
|
}
|
|
@@ -1250,420 +1542,46 @@ var ChatSession = class {
|
|
|
1250
1542
|
}
|
|
1251
1543
|
};
|
|
1252
1544
|
|
|
1253
|
-
// src/standard-handlers.ts
|
|
1254
|
-
function finalizeText(builder) {
|
|
1255
|
-
if (builder.activeTextId) {
|
|
1256
|
-
builder.patchBlock(builder.activeTextId, {
|
|
1257
|
-
isStreaming: false
|
|
1258
|
-
});
|
|
1259
|
-
builder.activeTextId = null;
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
function finalizeThinking(builder) {
|
|
1263
|
-
if (builder.activeThinkingId) {
|
|
1264
|
-
builder.patchBlock(builder.activeThinkingId, {
|
|
1265
|
-
isActive: false
|
|
1266
|
-
});
|
|
1267
|
-
builder.activeThinkingId = null;
|
|
1268
|
-
builder.thinkingStartMs = null;
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
function finalizeEditor(builder) {
|
|
1272
|
-
if (builder.activeEditorId) {
|
|
1273
|
-
builder.patchBlock(builder.activeEditorId, {
|
|
1274
|
-
isStreaming: false
|
|
1275
|
-
});
|
|
1276
|
-
builder.activeEditorId = null;
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
var handleUserMessage = (event, builder) => {
|
|
1280
|
-
const e = event;
|
|
1281
|
-
const existing = builder.findBlock((b) => b.type === "user");
|
|
1282
|
-
if (existing) {
|
|
1283
|
-
if (e.createdAt) {
|
|
1284
|
-
builder.patchBlock(existing.id, {
|
|
1285
|
-
createdAt: e.createdAt
|
|
1286
|
-
});
|
|
1287
|
-
}
|
|
1288
|
-
return;
|
|
1289
|
-
}
|
|
1290
|
-
builder.addBlock({
|
|
1291
|
-
type: "user",
|
|
1292
|
-
id: builder.nextId(),
|
|
1293
|
-
content: e.content,
|
|
1294
|
-
createdAt: e.createdAt
|
|
1295
|
-
});
|
|
1296
|
-
};
|
|
1297
|
-
var handleChunk = (event, builder) => {
|
|
1298
|
-
const e = event;
|
|
1299
|
-
if (!builder.activeTextId) {
|
|
1300
|
-
const id = builder.nextId();
|
|
1301
|
-
builder.activeTextId = id;
|
|
1302
|
-
builder.addBlock({
|
|
1303
|
-
type: "text",
|
|
1304
|
-
id,
|
|
1305
|
-
content: e.text,
|
|
1306
|
-
isStreaming: true
|
|
1307
|
-
});
|
|
1308
|
-
} else {
|
|
1309
|
-
const id = builder.activeTextId;
|
|
1310
|
-
const existing = builder.findBlock((b) => b.id === id);
|
|
1311
|
-
if (existing && existing.type === "text") {
|
|
1312
|
-
builder.patchBlock(id, {
|
|
1313
|
-
content: existing.content + e.text
|
|
1314
|
-
});
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
};
|
|
1318
|
-
var handleToolCall = (event, builder) => {
|
|
1319
|
-
const e = event;
|
|
1320
|
-
finalizeText(builder);
|
|
1321
|
-
builder.addBlock({
|
|
1322
|
-
type: "tool",
|
|
1323
|
-
id: builder.nextId(),
|
|
1324
|
-
callId: e.request.callId,
|
|
1325
|
-
toolName: e.request.toolName,
|
|
1326
|
-
displayName: e.request.displayName,
|
|
1327
|
-
description: e.request.description,
|
|
1328
|
-
arguments: e.request.arguments,
|
|
1329
|
-
toolCategory: e.request.toolCategory,
|
|
1330
|
-
iconUrl: e.request.iconUrl,
|
|
1331
|
-
status: "calling"
|
|
1332
|
-
});
|
|
1333
|
-
};
|
|
1334
|
-
var handleToolExecuting = (event, builder) => {
|
|
1335
|
-
const e = event;
|
|
1336
|
-
const block = builder.findBlock(
|
|
1337
|
-
(b) => b.type === "tool" && (e.call_id ? b.callId === e.call_id : b.toolName === e.name) && b.status === "calling"
|
|
1338
|
-
);
|
|
1339
|
-
if (block) {
|
|
1340
|
-
builder.patchBlock(block.id, { status: "executing" });
|
|
1341
|
-
}
|
|
1342
|
-
};
|
|
1343
|
-
var handleToolProgress = (event, builder) => {
|
|
1344
|
-
const e = event;
|
|
1345
|
-
const block = builder.findBlock(
|
|
1346
|
-
(b) => b.type === "tool" && b.callId === e.callId
|
|
1347
|
-
);
|
|
1348
|
-
if (block && block.type === "tool") {
|
|
1349
|
-
const sources = block.sources ? [...block.sources] : [];
|
|
1350
|
-
sources.push(e.item);
|
|
1351
|
-
builder.patchBlock(block.id, {
|
|
1352
|
-
sources,
|
|
1353
|
-
status: "executing"
|
|
1354
|
-
});
|
|
1355
|
-
}
|
|
1356
|
-
};
|
|
1357
|
-
var handleToolEnd = (event, builder) => {
|
|
1358
|
-
const e = event;
|
|
1359
|
-
const callId = e.type === "tool_end" ? e.callId : void 0;
|
|
1360
|
-
const name = e.type === "tool_end" ? e.toolName : e.name;
|
|
1361
|
-
const block = builder.findBlock(
|
|
1362
|
-
(b) => b.type === "tool" && (callId ? b.callId === callId : b.toolName === name) && b.status !== "completed"
|
|
1363
|
-
);
|
|
1364
|
-
if (block) {
|
|
1365
|
-
const toolEnd = e.type === "tool_end" ? e : null;
|
|
1366
|
-
builder.patchBlock(block.id, {
|
|
1367
|
-
status: "completed",
|
|
1368
|
-
...toolEnd?.sources ? { sources: toolEnd.sources } : {},
|
|
1369
|
-
...toolEnd?.durationMs != null ? { durationMs: toolEnd.durationMs } : {},
|
|
1370
|
-
...toolEnd?.result ? { result: toolEnd.result } : {}
|
|
1371
|
-
});
|
|
1372
|
-
}
|
|
1373
|
-
};
|
|
1374
|
-
var handleAgentStart = (event, builder) => {
|
|
1375
|
-
const e = event;
|
|
1376
|
-
finalizeText(builder);
|
|
1377
|
-
builder.addBlock({
|
|
1378
|
-
type: "agent",
|
|
1379
|
-
id: builder.nextId(),
|
|
1380
|
-
agentName: e.agentName,
|
|
1381
|
-
displayName: e.agentDisplayName,
|
|
1382
|
-
avatarUrl: e.avatarUrl
|
|
1383
|
-
});
|
|
1384
|
-
};
|
|
1385
|
-
var handleThinkingDelta = (event, builder) => {
|
|
1386
|
-
const e = event;
|
|
1387
|
-
if (!builder.activeThinkingId) {
|
|
1388
|
-
const id = builder.nextId();
|
|
1389
|
-
builder.activeThinkingId = id;
|
|
1390
|
-
builder.thinkingStartMs = Date.now();
|
|
1391
|
-
builder.addBlock({
|
|
1392
|
-
type: "thinking",
|
|
1393
|
-
id,
|
|
1394
|
-
content: e.text,
|
|
1395
|
-
isActive: true
|
|
1396
|
-
});
|
|
1397
|
-
} else {
|
|
1398
|
-
const id = builder.activeThinkingId;
|
|
1399
|
-
const existing = builder.findBlock((b) => b.id === id);
|
|
1400
|
-
if (existing && existing.type === "thinking") {
|
|
1401
|
-
builder.patchBlock(id, {
|
|
1402
|
-
content: existing.content + e.text
|
|
1403
|
-
});
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
};
|
|
1407
|
-
var handleThinkingComplete = (_event, builder) => {
|
|
1408
|
-
if (builder.activeThinkingId) {
|
|
1409
|
-
const durationMs = builder.thinkingStartMs ? Math.max(0, Date.now() - builder.thinkingStartMs) : void 0;
|
|
1410
|
-
builder.patchBlock(builder.activeThinkingId, {
|
|
1411
|
-
isActive: false,
|
|
1412
|
-
durationMs
|
|
1413
|
-
});
|
|
1414
|
-
builder.activeThinkingId = null;
|
|
1415
|
-
builder.thinkingStartMs = null;
|
|
1416
|
-
}
|
|
1417
|
-
};
|
|
1418
|
-
var handleSubagentStart = (event, builder) => {
|
|
1419
|
-
const e = event;
|
|
1420
|
-
finalizeText(builder);
|
|
1421
|
-
builder.addBlock({
|
|
1422
|
-
type: "subagent",
|
|
1423
|
-
id: builder.nextId(),
|
|
1424
|
-
agentName: e.agentName,
|
|
1425
|
-
displayName: e.displayName,
|
|
1426
|
-
toolCallId: e.toolCallId,
|
|
1427
|
-
avatarUrl: e.avatarUrl,
|
|
1428
|
-
description: e.description,
|
|
1429
|
-
content: "",
|
|
1430
|
-
isActive: true
|
|
1431
|
-
});
|
|
1432
|
-
};
|
|
1433
|
-
var handleSubagentChunk = (event, builder) => {
|
|
1434
|
-
const e = event;
|
|
1435
|
-
const block = builder.findBlock(
|
|
1436
|
-
(b) => b.type === "subagent" && b.toolCallId === e.toolCallId
|
|
1437
|
-
);
|
|
1438
|
-
if (block && block.type === "subagent") {
|
|
1439
|
-
builder.patchBlock(block.id, {
|
|
1440
|
-
content: block.content + e.text
|
|
1441
|
-
});
|
|
1442
|
-
}
|
|
1443
|
-
};
|
|
1444
|
-
var handleSubagentUpdate = (event, builder) => {
|
|
1445
|
-
const e = event;
|
|
1446
|
-
const block = builder.findBlock(
|
|
1447
|
-
(b) => b.type === "subagent" && b.toolCallId === e.toolCallId
|
|
1448
|
-
);
|
|
1449
|
-
if (block) {
|
|
1450
|
-
builder.patchBlock(block.id, {
|
|
1451
|
-
displayName: e.displayName
|
|
1452
|
-
});
|
|
1453
|
-
}
|
|
1454
|
-
};
|
|
1455
|
-
var handleSubagentEnd = (event, builder) => {
|
|
1456
|
-
const e = event;
|
|
1457
|
-
const block = builder.findBlock(
|
|
1458
|
-
(b) => b.type === "subagent" && b.toolCallId === e.toolCallId
|
|
1459
|
-
);
|
|
1460
|
-
if (block) {
|
|
1461
|
-
builder.patchBlock(block.id, {
|
|
1462
|
-
isActive: false
|
|
1463
|
-
});
|
|
1464
|
-
}
|
|
1465
|
-
};
|
|
1466
|
-
var handleComplete = (_event, builder) => {
|
|
1467
|
-
finalizeText(builder);
|
|
1468
|
-
finalizeThinking(builder);
|
|
1469
|
-
finalizeEditor(builder);
|
|
1470
|
-
for (const b of builder.getBlocks()) {
|
|
1471
|
-
if (b.type === "tool" && b.status !== "completed") {
|
|
1472
|
-
builder.patchBlock(b.id, { status: "completed" });
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
};
|
|
1476
|
-
var handleError = (event, builder) => {
|
|
1477
|
-
const e = event;
|
|
1478
|
-
finalizeText(builder);
|
|
1479
|
-
finalizeThinking(builder);
|
|
1480
|
-
builder.addBlock({
|
|
1481
|
-
type: "error",
|
|
1482
|
-
id: builder.nextId(),
|
|
1483
|
-
message: e.error.message
|
|
1484
|
-
});
|
|
1485
|
-
};
|
|
1486
|
-
var handleDisconnected = (_event, builder) => {
|
|
1487
|
-
finalizeText(builder);
|
|
1488
|
-
finalizeThinking(builder);
|
|
1489
|
-
finalizeEditor(builder);
|
|
1490
|
-
};
|
|
1491
|
-
var handleCapsuleOutputChunk = (event, builder) => {
|
|
1492
|
-
const e = event;
|
|
1493
|
-
const block = builder.findBlock(
|
|
1494
|
-
(b) => b.type === "capsule" && b.callId === e.callId
|
|
1495
|
-
);
|
|
1496
|
-
if (block && block.type === "capsule") {
|
|
1497
|
-
builder.patchBlock(block.id, {
|
|
1498
|
-
output: block.output + e.chunk
|
|
1499
|
-
});
|
|
1500
|
-
} else {
|
|
1501
|
-
builder.addBlock({
|
|
1502
|
-
type: "capsule",
|
|
1503
|
-
id: builder.nextId(),
|
|
1504
|
-
callId: e.callId,
|
|
1505
|
-
toolName: "",
|
|
1506
|
-
output: e.chunk,
|
|
1507
|
-
isActive: true
|
|
1508
|
-
});
|
|
1509
|
-
}
|
|
1510
|
-
};
|
|
1511
|
-
var handleCapsuleOutput = (event, builder) => {
|
|
1512
|
-
const e = event;
|
|
1513
|
-
const block = builder.findBlock(
|
|
1514
|
-
(b) => b.type === "capsule" && b.callId === (e.callId ?? "")
|
|
1515
|
-
);
|
|
1516
|
-
if (block) {
|
|
1517
|
-
builder.patchBlock(block.id, {
|
|
1518
|
-
output: e.output,
|
|
1519
|
-
command: e.command,
|
|
1520
|
-
toolName: e.toolName,
|
|
1521
|
-
durationMs: e.durationMs,
|
|
1522
|
-
isActive: false
|
|
1523
|
-
});
|
|
1524
|
-
} else {
|
|
1525
|
-
builder.addBlock({
|
|
1526
|
-
type: "capsule",
|
|
1527
|
-
id: builder.nextId(),
|
|
1528
|
-
callId: e.callId ?? "",
|
|
1529
|
-
toolName: e.toolName,
|
|
1530
|
-
command: e.command,
|
|
1531
|
-
output: e.output,
|
|
1532
|
-
durationMs: e.durationMs,
|
|
1533
|
-
isActive: false
|
|
1534
|
-
});
|
|
1535
|
-
}
|
|
1536
|
-
};
|
|
1537
|
-
var handleAssetCreated = (event, builder) => {
|
|
1538
|
-
const e = event;
|
|
1539
|
-
builder.addBlock({
|
|
1540
|
-
type: "asset",
|
|
1541
|
-
id: builder.nextId(),
|
|
1542
|
-
assetId: e.assetId,
|
|
1543
|
-
name: e.name,
|
|
1544
|
-
url: e.url,
|
|
1545
|
-
mediaType: e.mediaType,
|
|
1546
|
-
sizeBytes: e.sizeBytes
|
|
1547
|
-
});
|
|
1548
|
-
};
|
|
1549
|
-
var handleTodoUpdate = (event, builder) => {
|
|
1550
|
-
const e = event;
|
|
1551
|
-
if (builder.activeTodoId) {
|
|
1552
|
-
builder.patchBlock(builder.activeTodoId, {
|
|
1553
|
-
todos: e.todos
|
|
1554
|
-
});
|
|
1555
|
-
} else {
|
|
1556
|
-
const id = builder.nextId();
|
|
1557
|
-
builder.activeTodoId = id;
|
|
1558
|
-
builder.addBlock({
|
|
1559
|
-
type: "todo",
|
|
1560
|
-
id,
|
|
1561
|
-
todos: e.todos
|
|
1562
|
-
});
|
|
1563
|
-
}
|
|
1564
|
-
};
|
|
1565
|
-
var handleEditorContentStart = (event, builder) => {
|
|
1566
|
-
const e = event;
|
|
1567
|
-
const id = builder.nextId();
|
|
1568
|
-
builder.activeEditorId = id;
|
|
1569
|
-
builder.addBlock({
|
|
1570
|
-
type: "editor",
|
|
1571
|
-
id,
|
|
1572
|
-
callId: e.callId,
|
|
1573
|
-
path: e.path,
|
|
1574
|
-
language: e.language,
|
|
1575
|
-
content: "",
|
|
1576
|
-
isStreaming: true
|
|
1577
|
-
});
|
|
1578
|
-
};
|
|
1579
|
-
var handleEditorContentDelta = (event, builder) => {
|
|
1580
|
-
const e = event;
|
|
1581
|
-
const block = builder.findBlock(
|
|
1582
|
-
(b) => b.type === "editor" && b.callId === e.callId
|
|
1583
|
-
);
|
|
1584
|
-
if (block && block.type === "editor") {
|
|
1585
|
-
builder.patchBlock(block.id, {
|
|
1586
|
-
content: block.content + e.delta
|
|
1587
|
-
});
|
|
1588
|
-
}
|
|
1589
|
-
};
|
|
1590
|
-
var handleEditorContentEnd = (event, builder) => {
|
|
1591
|
-
const e = event;
|
|
1592
|
-
const block = builder.findBlock(
|
|
1593
|
-
(b) => b.type === "editor" && b.callId === e.callId
|
|
1594
|
-
);
|
|
1595
|
-
if (block) {
|
|
1596
|
-
builder.patchBlock(block.id, {
|
|
1597
|
-
isStreaming: false
|
|
1598
|
-
});
|
|
1599
|
-
}
|
|
1600
|
-
builder.activeEditorId = null;
|
|
1601
|
-
};
|
|
1602
|
-
var noop = () => {
|
|
1603
|
-
};
|
|
1604
|
-
var standardHandlers = {
|
|
1605
|
-
user_message: handleUserMessage,
|
|
1606
|
-
chunk: handleChunk,
|
|
1607
|
-
tool_call: handleToolCall,
|
|
1608
|
-
tool_executing: handleToolExecuting,
|
|
1609
|
-
tool_progress: handleToolProgress,
|
|
1610
|
-
tool_completed: handleToolEnd,
|
|
1611
|
-
tool_end: handleToolEnd,
|
|
1612
|
-
agent_start: handleAgentStart,
|
|
1613
|
-
agent_end: noop,
|
|
1614
|
-
thinking_delta: handleThinkingDelta,
|
|
1615
|
-
thinking_complete: handleThinkingComplete,
|
|
1616
|
-
subagent_start: handleSubagentStart,
|
|
1617
|
-
subagent_chunk: handleSubagentChunk,
|
|
1618
|
-
subagent_update: handleSubagentUpdate,
|
|
1619
|
-
subagent_end: handleSubagentEnd,
|
|
1620
|
-
subagent_tool_use: noop,
|
|
1621
|
-
capsule_output: handleCapsuleOutput,
|
|
1622
|
-
capsule_output_chunk: handleCapsuleOutputChunk,
|
|
1623
|
-
asset_created: handleAssetCreated,
|
|
1624
|
-
todo_update: handleTodoUpdate,
|
|
1625
|
-
editor_content_start: handleEditorContentStart,
|
|
1626
|
-
editor_content_delta: handleEditorContentDelta,
|
|
1627
|
-
editor_content_end: handleEditorContentEnd,
|
|
1628
|
-
retry: noop,
|
|
1629
|
-
complete: handleComplete,
|
|
1630
|
-
error: handleError,
|
|
1631
|
-
disconnected: handleDisconnected
|
|
1632
|
-
};
|
|
1633
|
-
|
|
1634
1545
|
// src/types.ts
|
|
1635
1546
|
var ChatEventType = {
|
|
1547
|
+
// Connection lifecycle (SDK-local, not wire)
|
|
1636
1548
|
Connected: "connected",
|
|
1637
|
-
|
|
1549
|
+
Disconnected: "disconnected",
|
|
1550
|
+
// Turn lifecycle
|
|
1551
|
+
MessageStart: "message_start",
|
|
1552
|
+
MessageStop: "message_stop",
|
|
1553
|
+
// Block lifecycle
|
|
1554
|
+
BlockStart: "block_start",
|
|
1555
|
+
BlockDelta: "block_delta",
|
|
1556
|
+
BlockStop: "block_stop",
|
|
1557
|
+
// Reliability
|
|
1558
|
+
Stall: "stall",
|
|
1559
|
+
Retry: "retry",
|
|
1560
|
+
Error: "error",
|
|
1561
|
+
Keepalive: "keepalive",
|
|
1562
|
+
// Conversation-level (typed custom events)
|
|
1638
1563
|
UserMessage: "user_message",
|
|
1639
1564
|
TitleGenerated: "title_generated",
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
ToolCall: "tool_call",
|
|
1643
|
-
ToolExecuting: "tool_executing",
|
|
1644
|
-
ToolProgress: "tool_progress",
|
|
1645
|
-
ToolCompleted: "tool_completed",
|
|
1646
|
-
ToolEnd: "tool_end",
|
|
1647
|
-
AgentStart: "agent_start",
|
|
1648
|
-
AgentEnd: "agent_end",
|
|
1649
|
-
ThinkingDelta: "thinking_delta",
|
|
1650
|
-
ThinkingComplete: "thinking_complete",
|
|
1565
|
+
TodoUpdate: "todo_update",
|
|
1566
|
+
ContextUpdate: "context_update",
|
|
1651
1567
|
SubagentStart: "subagent_start",
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1568
|
+
SubagentStop: "subagent_stop",
|
|
1569
|
+
ContextWarning: "context_warning",
|
|
1570
|
+
MemoryRecall: "memory_recall",
|
|
1571
|
+
MemoryUpdate: "memory_update",
|
|
1572
|
+
DesktopStream: "desktop_stream",
|
|
1573
|
+
AttachmentStaged: "attachment_staged",
|
|
1574
|
+
WorkspaceReady: "workspace_ready",
|
|
1658
1575
|
AssetCreated: "asset_created",
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1576
|
+
ToolApprovalRequested: "tool_approval_requested",
|
|
1577
|
+
ToolApprovalGranted: "tool_approval_granted",
|
|
1578
|
+
ToolPermissionDenied: "tool_permission_denied",
|
|
1579
|
+
ToolHarnessWarning: "tool_harness_warning",
|
|
1580
|
+
UserUnavailable: "user_unavailable",
|
|
1581
|
+
PromptSuggestion: "prompt_suggestion",
|
|
1582
|
+
StateChanged: "state_changed",
|
|
1583
|
+
// Generic fallthrough for unknown custom events
|
|
1584
|
+
Custom: "custom"
|
|
1667
1585
|
};
|
|
1668
1586
|
|
|
1669
1587
|
// src/stream-manager.ts
|
|
@@ -1718,22 +1636,12 @@ var StreamManager = class {
|
|
|
1718
1636
|
}
|
|
1719
1637
|
onSessionEvent(event) {
|
|
1720
1638
|
const convId = this.session.conversationId;
|
|
1721
|
-
if (event.type === ChatEventType.BlocksChanged) {
|
|
1722
|
-
if (this._state === "streaming" && convId) {
|
|
1723
|
-
this.emit({
|
|
1724
|
-
type: "blocksChanged",
|
|
1725
|
-
conversationId: convId,
|
|
1726
|
-
blocks: event.blocks
|
|
1727
|
-
});
|
|
1728
|
-
}
|
|
1729
|
-
return;
|
|
1730
|
-
}
|
|
1731
1639
|
this.emit({
|
|
1732
1640
|
type: "event",
|
|
1733
1641
|
conversationId: convId,
|
|
1734
1642
|
event
|
|
1735
1643
|
});
|
|
1736
|
-
if (event.type === ChatEventType.
|
|
1644
|
+
if (event.type === ChatEventType.MessageStop) {
|
|
1737
1645
|
if (this._state === "streaming") {
|
|
1738
1646
|
this.setState("idle");
|
|
1739
1647
|
}
|
|
@@ -1746,13 +1654,13 @@ var StreamManager = class {
|
|
|
1746
1654
|
const id = await this.session.createNewConversation();
|
|
1747
1655
|
this.setActiveConversation(id);
|
|
1748
1656
|
}
|
|
1749
|
-
this.prepareUserBlock(content);
|
|
1750
1657
|
this.setState("streaming");
|
|
1751
1658
|
try {
|
|
1752
1659
|
await this.session.send(content, {
|
|
1753
1660
|
enableSearch: options?.enableSearch,
|
|
1754
1661
|
agentName: options?.agentName,
|
|
1755
|
-
uploadIds: options?.uploadIds
|
|
1662
|
+
uploadIds: options?.uploadIds,
|
|
1663
|
+
planMode: options?.planMode
|
|
1756
1664
|
});
|
|
1757
1665
|
} catch {
|
|
1758
1666
|
}
|
|
@@ -1766,7 +1674,6 @@ var StreamManager = class {
|
|
|
1766
1674
|
);
|
|
1767
1675
|
const lastUserMsg = userMsgs[userMsgs.length - 1];
|
|
1768
1676
|
if (!lastUserMsg) return;
|
|
1769
|
-
this.prepareUserBlock(lastUserMsg.content);
|
|
1770
1677
|
this.setState("streaming");
|
|
1771
1678
|
try {
|
|
1772
1679
|
await this.session.resendFromCheckpoint(
|
|
@@ -1830,14 +1737,6 @@ var StreamManager = class {
|
|
|
1830
1737
|
this.handlers = [];
|
|
1831
1738
|
}
|
|
1832
1739
|
// ── Internal: helpers ──────────────────────────────────────────
|
|
1833
|
-
prepareUserBlock(content) {
|
|
1834
|
-
this.session.blockBuilder.reset();
|
|
1835
|
-
this.session.blockBuilder.addBlock({
|
|
1836
|
-
type: "user",
|
|
1837
|
-
id: this.session.blockBuilder.nextId(),
|
|
1838
|
-
content
|
|
1839
|
-
});
|
|
1840
|
-
}
|
|
1841
1740
|
finalizeStream() {
|
|
1842
1741
|
if (this._state === "streaming") {
|
|
1843
1742
|
this.setState("idle");
|
|
@@ -1848,8 +1747,8 @@ var StreamManager = class {
|
|
|
1848
1747
|
this.setState("restoring");
|
|
1849
1748
|
let activeJobId = null;
|
|
1850
1749
|
try {
|
|
1851
|
-
const res = await this.session.client.
|
|
1852
|
-
activeJobId = res.
|
|
1750
|
+
const res = await this.session.client.getActiveJob(conversationId);
|
|
1751
|
+
activeJobId = res.jobId;
|
|
1853
1752
|
} catch {
|
|
1854
1753
|
}
|
|
1855
1754
|
if (activeJobId) {
|
|
@@ -1869,15 +1768,19 @@ var StreamManager = class {
|
|
|
1869
1768
|
const completedJobs = jobs.filter(
|
|
1870
1769
|
(j) => j.status === "completed"
|
|
1871
1770
|
);
|
|
1872
|
-
|
|
1873
|
-
|
|
1771
|
+
const userMessages = this.session.messages.filter(
|
|
1772
|
+
(m) => m.role === "user"
|
|
1773
|
+
);
|
|
1774
|
+
for (let i = 0; i < completedJobs.length; i++) {
|
|
1775
|
+
const job = completedJobs[i];
|
|
1776
|
+
const userContent = userMessages[i]?.content;
|
|
1777
|
+
await this.session.switchConversation(
|
|
1778
|
+
conversationId,
|
|
1779
|
+
job.job_id,
|
|
1780
|
+
userContent
|
|
1781
|
+
);
|
|
1874
1782
|
}
|
|
1875
1783
|
if (completedJobs.length > 0) {
|
|
1876
|
-
this.emit({
|
|
1877
|
-
type: "blocksChanged",
|
|
1878
|
-
conversationId,
|
|
1879
|
-
blocks: this.session.blockBuilder.getBlocks()
|
|
1880
|
-
});
|
|
1881
1784
|
this.emit({
|
|
1882
1785
|
type: "versionsReady",
|
|
1883
1786
|
conversationId,
|
|
@@ -1895,23 +1798,93 @@ var StreamManager = class {
|
|
|
1895
1798
|
this.emit({ type: "conversationChanged", conversationId: id });
|
|
1896
1799
|
}
|
|
1897
1800
|
};
|
|
1801
|
+
|
|
1802
|
+
// src/replay.ts
|
|
1803
|
+
function toWireEvent(raw) {
|
|
1804
|
+
const type = raw.data.type || raw.event;
|
|
1805
|
+
if (!type || type === "done") return null;
|
|
1806
|
+
return { ...raw.data, type };
|
|
1807
|
+
}
|
|
1808
|
+
function mapSseToChat(raw) {
|
|
1809
|
+
const wire = toWireEvent(raw);
|
|
1810
|
+
if (!wire) return [];
|
|
1811
|
+
const event = translateWireEvent(wire);
|
|
1812
|
+
return event ? [event] : [];
|
|
1813
|
+
}
|
|
1814
|
+
function replayEvents(sseEvents, userMessages, handleEvent, addBlock) {
|
|
1815
|
+
const userMsgs = userMessages.filter((m) => m.role === "user");
|
|
1816
|
+
let userIdx = 0;
|
|
1817
|
+
let expectingUserMessage = true;
|
|
1818
|
+
for (const raw of sseEvents) {
|
|
1819
|
+
const type = raw.data.type || raw.event;
|
|
1820
|
+
if (type === "message_stop") {
|
|
1821
|
+
expectingUserMessage = true;
|
|
1822
|
+
}
|
|
1823
|
+
if (type === "message_start" && expectingUserMessage && userIdx < userMsgs.length) {
|
|
1824
|
+
const userMsg = userMsgs[userIdx];
|
|
1825
|
+
addBlock({
|
|
1826
|
+
type: "user",
|
|
1827
|
+
id: `replay_user_${userIdx}`,
|
|
1828
|
+
content: userMsg.content
|
|
1829
|
+
});
|
|
1830
|
+
userIdx++;
|
|
1831
|
+
expectingUserMessage = false;
|
|
1832
|
+
}
|
|
1833
|
+
for (const ce of mapSseToChat(raw)) {
|
|
1834
|
+
handleEvent(ce);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
// src/embedded-resource.ts
|
|
1840
|
+
function isEmbeddedResource(value) {
|
|
1841
|
+
return typeof value === "object" && value !== null && value._embedded_resource === true;
|
|
1842
|
+
}
|
|
1843
|
+
function parseEmbeddedResource(value) {
|
|
1844
|
+
let candidate = value;
|
|
1845
|
+
if (typeof candidate === "string") {
|
|
1846
|
+
const trimmed = candidate.trim();
|
|
1847
|
+
if (!trimmed.startsWith("{")) return null;
|
|
1848
|
+
try {
|
|
1849
|
+
candidate = JSON.parse(trimmed);
|
|
1850
|
+
} catch {
|
|
1851
|
+
return null;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
if (!isEmbeddedResource(candidate)) return null;
|
|
1855
|
+
const mimeType = candidate.mime_type;
|
|
1856
|
+
const uri = candidate.uri;
|
|
1857
|
+
const payload = candidate.payload;
|
|
1858
|
+
if (typeof mimeType !== "string" || !mimeType) return null;
|
|
1859
|
+
if (typeof uri !== "string" || !uri) return null;
|
|
1860
|
+
if (!payload || typeof payload !== "object") return null;
|
|
1861
|
+
return {
|
|
1862
|
+
mimeType,
|
|
1863
|
+
uri,
|
|
1864
|
+
payload
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1898
1867
|
export {
|
|
1899
1868
|
AstralformClient,
|
|
1900
1869
|
AstralformError,
|
|
1901
1870
|
AuthenticationError,
|
|
1902
|
-
BlockBuilder,
|
|
1903
1871
|
ChatEventType,
|
|
1904
1872
|
ChatSession,
|
|
1905
1873
|
ConnectionError,
|
|
1906
1874
|
InMemoryStorage,
|
|
1907
1875
|
LLMNotConfiguredError,
|
|
1876
|
+
ProtocolRegistry,
|
|
1908
1877
|
RateLimitError,
|
|
1909
1878
|
ServerError,
|
|
1910
1879
|
StreamAbortedError,
|
|
1911
1880
|
StreamManager,
|
|
1912
1881
|
ToolRegistry,
|
|
1913
1882
|
generateId,
|
|
1914
|
-
|
|
1915
|
-
|
|
1883
|
+
isEmbeddedResource,
|
|
1884
|
+
mapSseToChat,
|
|
1885
|
+
parseEmbeddedResource,
|
|
1886
|
+
replayEvents,
|
|
1887
|
+
streamJobSSE,
|
|
1888
|
+
translateDelta
|
|
1916
1889
|
};
|
|
1917
1890
|
//# sourceMappingURL=index.js.map
|