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