@astralform/js 0.2.3 → 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/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,9 @@ 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
+ }
300
557
  // --- Conversation Assets ---
301
558
  mapAsset(raw) {
302
559
  return {
@@ -318,10 +575,7 @@ var AstralformClient = class {
318
575
  `${this.baseURL}/v1/conversations/${encodeURIComponent(conversationId)}/uploads`,
319
576
  {
320
577
  method: "POST",
321
- headers: {
322
- Authorization: `Bearer ${this.apiKey}`,
323
- "X-End-User-ID": this.userId
324
- },
578
+ headers: this.authHeaders,
325
579
  body: formData
326
580
  }
327
581
  ).catch((err) => {
@@ -345,6 +599,31 @@ var AstralformClient = class {
345
599
  );
346
600
  return raw.map((r) => this.mapAsset(r));
347
601
  }
602
+ // --- Account-scoped discovery (user-token mode) ---
603
+ //
604
+ // Lets a signed-in user pick which team/project they want to act on.
605
+ // Backend gates these on OIDC user context (no X-Project-ID required) —
606
+ // sending them in API-key mode yields 401.
607
+ async listTeams() {
608
+ const raw = await this.get("/v1/teams");
609
+ return raw.map((t) => ({
610
+ id: t.id,
611
+ name: t.name,
612
+ slug: t.slug,
613
+ isDefault: t.is_default,
614
+ role: t.role
615
+ }));
616
+ }
617
+ async listProjects(teamId) {
618
+ const raw = await this.get(`/v1/teams/${encodeURIComponent(teamId)}/projects`);
619
+ return raw.map((p) => ({
620
+ id: p.id,
621
+ name: p.name,
622
+ teamId: p.team_id,
623
+ createdAt: p.created_at,
624
+ updatedAt: p.updated_at
625
+ }));
626
+ }
348
627
  // --- Jobs API ---
349
628
  async createJob(request) {
350
629
  return this.post("/v1/jobs", request);
@@ -361,90 +640,50 @@ var AstralformClient = class {
361
640
  async cancelJob(jobId) {
362
641
  await this.post(`/v1/jobs/${encodeURIComponent(jobId)}/cancel`, {});
363
642
  }
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();
643
+ async getJob(jobId) {
644
+ const raw = await this.get(`/v1/jobs/${encodeURIComponent(jobId)}`);
645
+ return {
646
+ jobId: raw.job_id,
647
+ status: raw.status,
648
+ createdAt: raw.created_at ?? null,
649
+ startedAt: raw.started_at ?? null,
650
+ completedAt: raw.completed_at ?? null,
651
+ errorMessage: raw.error_message ?? null,
652
+ inputTokens: raw.input_tokens ?? 0,
653
+ outputTokens: raw.output_tokens ?? 0
654
+ };
434
655
  }
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;
656
+ async submitFeedback(jobId, request) {
657
+ const body = {
658
+ rating: request.rating
659
+ };
660
+ if (request.comment != null) body.comment = request.comment;
661
+ const raw = await this.post(`/v1/jobs/${encodeURIComponent(jobId)}/feedback`, body);
662
+ return {
663
+ id: raw.id,
664
+ jobId: raw.job_id,
665
+ rating: raw.rating,
666
+ comment: raw.comment,
667
+ createdAt: raw.created_at
668
+ };
441
669
  }
442
- nextId() {
443
- return `blk_${++_idCounter}`;
670
+ async getActiveJob(conversationId) {
671
+ const raw = await this.get(`/v1/conversations/${encodeURIComponent(conversationId)}/active-job`);
672
+ return {
673
+ jobId: raw.job_id ?? null,
674
+ status: raw.status
675
+ };
444
676
  }
445
- // ── Internal ──────────────────────────────────────────────────
446
- _notify() {
447
- this._onChange?.();
677
+ async listJobs(conversationId) {
678
+ const raw = await this.get(`/v1/conversations/${encodeURIComponent(conversationId)}/jobs`);
679
+ return raw.map((j) => ({
680
+ jobId: j.job_id,
681
+ status: j.status,
682
+ replacesJobId: j.replaces_job_id ?? null,
683
+ responseContent: j.response_content ?? null,
684
+ metrics: j.metrics ?? null,
685
+ createdAt: j.created_at ?? null
686
+ }));
448
687
  }
449
688
  };
450
689
 
@@ -598,40 +837,348 @@ var ToolRegistry = class {
598
837
  }
599
838
  };
600
839
 
601
- // src/utils.ts
602
- function generateId() {
603
- if (typeof crypto !== "undefined" && crypto.randomUUID) {
604
- return crypto.randomUUID();
840
+ // src/protocol-registry.ts
841
+ var ProtocolRegistry = class {
842
+ constructor() {
843
+ this.adapters = /* @__PURE__ */ new Map();
844
+ }
845
+ /** Register or replace the adapter for a MIME type. */
846
+ register(adapter) {
847
+ this.adapters.set(adapter.mimeType, adapter);
848
+ }
849
+ /** Remove the adapter for a MIME type. No-op if not registered. */
850
+ unregister(mimeType) {
851
+ this.adapters.delete(mimeType);
852
+ }
853
+ /** Returns the adapter for a MIME type, or ``null`` if none is registered. */
854
+ get(mimeType) {
855
+ return this.adapters.get(mimeType) ?? null;
856
+ }
857
+ has(mimeType) {
858
+ return this.adapters.has(mimeType);
859
+ }
860
+ /** Drop every adapter. Called when a session disconnects. */
861
+ clear() {
862
+ this.adapters.clear();
863
+ }
864
+ listMimeTypes() {
865
+ return Array.from(this.adapters.keys());
866
+ }
867
+ };
868
+
869
+ // src/translate.ts
870
+ function translateDelta(wire) {
871
+ switch (wire.channel) {
872
+ case "text":
873
+ return { channel: "text", text: wire.text };
874
+ case "thinking":
875
+ return { channel: "thinking", text: wire.text };
876
+ case "signature":
877
+ return { channel: "signature", signature: wire.signature };
878
+ case "input":
879
+ return { channel: "input", partialJson: wire.partial_json };
880
+ case "input_arg":
881
+ return {
882
+ channel: "inputArg",
883
+ argName: wire.arg_name,
884
+ text: wire.text
885
+ };
886
+ case "output":
887
+ return { channel: "output", stream: wire.stream, chunk: wire.chunk };
888
+ case "status":
889
+ return {
890
+ channel: "status",
891
+ status: wire.status,
892
+ note: wire.note
893
+ };
894
+ default:
895
+ return null;
896
+ }
897
+ }
898
+ function translateAgentIdentity(raw) {
899
+ return {
900
+ name: raw.name ?? "",
901
+ displayName: raw.display_name ?? null,
902
+ avatarUrl: raw.avatar_url ?? null,
903
+ description: raw.description ?? null
904
+ };
905
+ }
906
+ function translateCustomEvent(name, data) {
907
+ switch (name) {
908
+ case "user_message":
909
+ return {
910
+ type: "user_message",
911
+ content: data.content ?? "",
912
+ createdAt: data.created_at
913
+ };
914
+ case "title_generated":
915
+ return {
916
+ type: "title_generated",
917
+ title: data.title ?? ""
918
+ };
919
+ case "todo_update":
920
+ return {
921
+ type: "todo_update",
922
+ todos: data.todos ?? []
923
+ };
924
+ case "context_update":
925
+ return {
926
+ type: "context_update",
927
+ context: data.context ?? {},
928
+ phase: data.phase ?? null,
929
+ updatedAt: data.updated_at ?? null
930
+ };
931
+ case "subagent_start":
932
+ return {
933
+ type: "subagent_start",
934
+ agent: translateAgentIdentity(
935
+ data.agent ?? {}
936
+ ),
937
+ taskCallId: data.task_call_id ?? null
938
+ };
939
+ case "subagent_stop":
940
+ return {
941
+ type: "subagent_stop",
942
+ agent: translateAgentIdentity(
943
+ data.agent ?? {}
944
+ ),
945
+ taskCallId: data.task_call_id ?? null
946
+ };
947
+ case "context_warning":
948
+ return {
949
+ type: "context_warning",
950
+ severity: data.severity ?? "warning",
951
+ utilizationPct: data.utilization_pct ?? 0,
952
+ remainingTokens: data.remaining_tokens ?? 0,
953
+ windowTokens: data.window_tokens ?? 0,
954
+ inputTokens: data.input_tokens ?? 0,
955
+ message: data.message ?? ""
956
+ };
957
+ case "memory_recall":
958
+ return {
959
+ type: "memory_recall",
960
+ memories: data.memories ?? []
961
+ };
962
+ case "memory_update":
963
+ return {
964
+ type: "memory_update",
965
+ action: data.action ?? "",
966
+ memoryId: data.memory_id ?? null,
967
+ key: data.key ?? null,
968
+ namespace: data.namespace ?? null
969
+ };
970
+ case "desktop_stream":
971
+ return {
972
+ type: "desktop_stream",
973
+ url: data.url ?? "",
974
+ sandboxId: data.sandbox_id ?? null
975
+ };
976
+ case "attachment_staged":
977
+ return {
978
+ type: "attachment_staged",
979
+ attachmentId: data.attachment_id ?? "",
980
+ filename: data.filename ?? "",
981
+ contentType: data.content_type ?? null,
982
+ sizeBytes: data.size_bytes ?? null
983
+ };
984
+ case "workspace_ready":
985
+ return {
986
+ type: "workspace_ready",
987
+ sandboxId: data.sandbox_id ?? "",
988
+ workspacePath: data.workspace_path ?? null
989
+ };
990
+ case "asset_created":
991
+ return {
992
+ type: "asset_created",
993
+ assetId: data.asset_id ?? "",
994
+ filename: data.filename ?? "",
995
+ url: data.url ?? null,
996
+ contentType: data.content_type ?? null
997
+ };
998
+ case "tool_approval_requested":
999
+ return {
1000
+ type: "tool_approval_requested",
1001
+ toolName: data.tool_name ?? "",
1002
+ callId: data.call_id ?? "",
1003
+ arguments: data.arguments ?? {},
1004
+ riskLevel: data.risk_level ?? null,
1005
+ reason: data.reason ?? null
1006
+ };
1007
+ case "tool_approval_granted":
1008
+ return {
1009
+ type: "tool_approval_granted",
1010
+ toolName: data.tool_name ?? "",
1011
+ callId: data.call_id ?? ""
1012
+ };
1013
+ case "tool_permission_denied":
1014
+ return {
1015
+ type: "tool_permission_denied",
1016
+ toolName: data.tool_name ?? "",
1017
+ callId: data.call_id ?? "",
1018
+ reason: data.reason ?? null,
1019
+ deniedBy: data.denied_by ?? null
1020
+ };
1021
+ case "tool_harness_warning":
1022
+ return {
1023
+ type: "tool_harness_warning",
1024
+ toolName: data.tool_name ?? "",
1025
+ callId: data.call_id ?? "",
1026
+ message: data.message ?? null,
1027
+ details: data.details ?? null
1028
+ };
1029
+ case "user_unavailable":
1030
+ return {
1031
+ type: "user_unavailable",
1032
+ consecutiveTimeouts: data.consecutive_timeouts ?? 0,
1033
+ toolName: data.tool_name ?? null
1034
+ };
1035
+ case "prompt_suggestion":
1036
+ return {
1037
+ type: "prompt_suggestion",
1038
+ suggestions: data.suggestions ?? []
1039
+ };
1040
+ case "state_changed":
1041
+ return {
1042
+ type: "state_changed",
1043
+ state: data.state ?? ""
1044
+ };
1045
+ default:
1046
+ return { type: "custom", name, data };
1047
+ }
1048
+ }
1049
+ function legacyCustomEventData(wire) {
1050
+ return wire;
1051
+ }
1052
+ function translateWireEvent(wire) {
1053
+ if (wire.type === "prompt_suggestion") {
1054
+ return translateCustomEvent(
1055
+ "prompt_suggestion",
1056
+ legacyCustomEventData(wire)
1057
+ );
1058
+ }
1059
+ switch (wire.type) {
1060
+ case "message_start":
1061
+ return {
1062
+ type: "message_start",
1063
+ turnId: wire.turn_id,
1064
+ model: wire.model,
1065
+ agentName: wire.agent_name,
1066
+ agentDisplayName: wire.agent_display_name,
1067
+ agentAvatarUrl: wire.agent_avatar_url
1068
+ };
1069
+ case "block_start":
1070
+ return {
1071
+ type: "block_start",
1072
+ turnId: wire.turn_id,
1073
+ path: wire.path,
1074
+ parentPath: wire.parent_path ?? null,
1075
+ kind: wire.kind,
1076
+ metadata: wire.metadata
1077
+ };
1078
+ case "block_delta": {
1079
+ const delta = translateDelta(wire.delta);
1080
+ if (!delta) return null;
1081
+ return {
1082
+ type: "block_delta",
1083
+ turnId: wire.turn_id,
1084
+ path: wire.path,
1085
+ delta
1086
+ };
1087
+ }
1088
+ case "block_stop":
1089
+ return {
1090
+ type: "block_stop",
1091
+ turnId: wire.turn_id,
1092
+ path: wire.path,
1093
+ status: wire.status,
1094
+ final: wire.final
1095
+ };
1096
+ case "message_stop":
1097
+ return {
1098
+ type: "message_stop",
1099
+ turnId: wire.turn_id,
1100
+ jobId: wire.job_id,
1101
+ stopReason: wire.stop_reason,
1102
+ usage: {
1103
+ inputTokens: wire.usage.input_tokens ?? 0,
1104
+ outputTokens: wire.usage.output_tokens ?? 0,
1105
+ cachedTokens: wire.usage.cached_tokens ?? 0
1106
+ },
1107
+ ttfbMs: wire.ttfb_ms,
1108
+ totalMs: wire.total_ms,
1109
+ stallCount: wire.stall_count
1110
+ };
1111
+ case "stall":
1112
+ return {
1113
+ type: "stall",
1114
+ sinceLastEventMs: wire.since_last_event_ms,
1115
+ stallCount: wire.stall_count
1116
+ };
1117
+ case "retry":
1118
+ return {
1119
+ type: "retry",
1120
+ attempt: wire.attempt,
1121
+ reason: wire.reason,
1122
+ backoffMs: wire.backoff_ms,
1123
+ strategy: wire.strategy ?? null,
1124
+ maxAttempts: wire.max_attempts ?? null,
1125
+ contextRecovery: wire.context_recovery ?? null
1126
+ };
1127
+ case "error":
1128
+ return {
1129
+ type: "error",
1130
+ code: wire.code,
1131
+ message: wire.message,
1132
+ blockPath: wire.block_path ?? null
1133
+ };
1134
+ case "keepalive":
1135
+ return {
1136
+ type: "keepalive",
1137
+ sinceLastEventMs: wire.since_last_event_ms
1138
+ };
1139
+ case "custom":
1140
+ return translateCustomEvent(wire.name, wire.data);
1141
+ default: {
1142
+ const _exhaustive = wire;
1143
+ void _exhaustive;
1144
+ return null;
1145
+ }
605
1146
  }
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
1147
  }
612
1148
 
613
1149
  // src/session.ts
1150
+ function pathEquals(a, b) {
1151
+ if (a.length !== b.length) return false;
1152
+ for (let i = 0; i < a.length; i++) {
1153
+ if (a[i] !== b[i]) return false;
1154
+ }
1155
+ return true;
1156
+ }
614
1157
  var ChatSession = class {
615
- constructor(config, storage, blockBuilder) {
1158
+ constructor(config, storage) {
1159
+ /**
1160
+ * Pluggable UI protocol adapters. Consumers register a framework-
1161
+ * specific adapter (e.g. React) for each MIME type they can render,
1162
+ * typically gated on ``session.projectStatus.uiComponents.protocol``.
1163
+ * ``ToolBlock``-style consumers look up the adapter for an incoming
1164
+ * embedded resource and hand off rendering.
1165
+ */
1166
+ this.protocols = new ProtocolRegistry();
616
1167
  // State
617
1168
  this.conversationId = null;
618
1169
  this.conversations = [];
619
1170
  this.messages = [];
620
- this.streamingContent = "";
621
1171
  this.isStreaming = false;
622
- this.executingTool = null;
623
1172
  this.projectStatus = null;
624
1173
  this.agents = [];
625
1174
  this.skills = [];
626
1175
  this.enabledClientTools = /* @__PURE__ */ new Set();
627
1176
  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();
1177
+ // Minimal in-session accumulation for the assistant message record.
1178
+ // Only top-level ``text`` blocks contribute; subagent / tool output
1179
+ // is tracked by the consumer's own block store.
1180
+ this.accumulatedText = "";
1181
+ this.currentTextPath = null;
635
1182
  this.handlers = /* @__PURE__ */ new Set();
636
1183
  this.abortController = null;
637
1184
  /** Last received sequence number for resumable reconnection */
@@ -641,13 +1188,6 @@ var ChatSession = class {
641
1188
  this.client = new AstralformClient(config);
642
1189
  this.toolRegistry = new ToolRegistry();
643
1190
  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
1191
  }
652
1192
  on(handler) {
653
1193
  this.handlers.add(handler);
@@ -656,9 +1196,6 @@ var ChatSession = class {
656
1196
  };
657
1197
  }
658
1198
  emit(event) {
659
- if (event.type !== "blocks_changed" && event.type !== "connected") {
660
- this.blockBuilder.processEvent(event);
661
- }
662
1199
  for (const handler of this.handlers) {
663
1200
  try {
664
1201
  handler(event);
@@ -711,7 +1248,8 @@ var ChatSession = class {
711
1248
  ),
712
1249
  upload_ids: options?.uploadIds,
713
1250
  agent_name: options?.agentName,
714
- enable_search: options?.enableSearch
1251
+ enable_search: options?.enableSearch,
1252
+ plan_mode: options?.planMode
715
1253
  };
716
1254
  await this.processStream(request);
717
1255
  }
@@ -728,13 +1266,8 @@ var ChatSession = class {
728
1266
  await this.processStream(request);
729
1267
  }
730
1268
  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();
1269
+ this.accumulatedText = "";
1270
+ this.currentTextPath = null;
738
1271
  }
739
1272
  async processStream(request) {
740
1273
  this.isStreaming = true;
@@ -746,22 +1279,42 @@ var ChatSession = class {
746
1279
  if (!(err instanceof DOMException && err.name === "AbortError")) {
747
1280
  this.emit({
748
1281
  type: "error",
749
- error: err instanceof Error ? err : new ConnectionError(String(err))
1282
+ code: "connection_error",
1283
+ message: err instanceof Error ? err.message : String(err),
1284
+ blockPath: null
750
1285
  });
751
1286
  }
752
1287
  } finally {
753
1288
  this.isStreaming = false;
754
- this.executingTool = null;
755
1289
  this.abortController = null;
756
1290
  }
757
1291
  }
758
1292
  async consumeJobStream(request) {
759
1293
  const job = await this.client.createJob(request);
760
1294
  this.currentJobId = job.job_id;
761
- let conversationId = job.conversation_id;
1295
+ const conversationId = job.conversation_id;
762
1296
  if (!this.conversationId) {
763
1297
  this.conversationId = conversationId;
764
1298
  }
1299
+ if (!this.conversations.some((c) => c.id === conversationId)) {
1300
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1301
+ const conv = {
1302
+ id: conversationId,
1303
+ title: "",
1304
+ messageCount: 0,
1305
+ createdAt: now,
1306
+ updatedAt: now
1307
+ };
1308
+ this.conversations.unshift(conv);
1309
+ await this.storage.createConversation(conversationId, "").catch(() => {
1310
+ });
1311
+ }
1312
+ const lastMsg = this.messages[this.messages.length - 1];
1313
+ if (lastMsg?.role === "user" && !lastMsg.conversationId) {
1314
+ lastMsg.conversationId = conversationId;
1315
+ await this.storage.addMessage(lastMsg, conversationId).catch(() => {
1316
+ });
1317
+ }
765
1318
  const messageId = job.message_id;
766
1319
  this.lastSeq = -1;
767
1320
  const stream = this.client.streamJobEvents(
@@ -778,7 +1331,8 @@ var ChatSession = class {
778
1331
  );
779
1332
  }
780
1333
  /**
781
- * Shared event consumption loop used by both consumeJobStream and reconnectToJob.
1334
+ * Shared event consumption loop. Parses each wire event, updates
1335
+ * minimal session state, and emits typed ChatEvents to consumers.
782
1336
  */
783
1337
  async consumeEventStream(stream, conversationId, messageId, executeClientTools) {
784
1338
  for await (const raw of stream) {
@@ -798,419 +1352,112 @@ var ChatSession = class {
798
1352
  } catch {
799
1353
  continue;
800
1354
  }
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
- }
1355
+ await this.dispatchWireEvent(
1356
+ parsed,
1357
+ conversationId,
1358
+ messageId,
1359
+ executeClientTools
1360
+ );
1361
+ }
1362
+ }
1363
+ async dispatchWireEvent(wire, conversationId, messageId, executeClientTools) {
1364
+ this.applyWireSideEffects(wire, conversationId, messageId);
1365
+ const event = translateWireEvent(wire);
1366
+ if (event) {
1367
+ this.emit(event);
1368
+ }
1369
+ if (executeClientTools && wire.type === "block_stop" && wire.status === "awaiting_client_result" && wire.final?.call_id) {
1370
+ const f = wire.final;
1371
+ const request = {
1372
+ callId: f.call_id ?? "",
1373
+ toolName: f.tool_name ?? "",
1374
+ arguments: f.input ?? {},
1375
+ isClientTool: true
1376
+ };
1377
+ const results = await this.executeClientTools([request]);
1378
+ await this.client.submitToolResult({
1379
+ conversation_id: conversationId,
1380
+ message_id: messageId,
1381
+ tool_results: results
1382
+ });
895
1383
  }
896
1384
  }
897
1385
  /**
898
- * Apply a single SSE event to session state and notify consumers.
899
- * Shared between live streaming and historical event replay.
1386
+ * State mutations driven by wire events. Kept separate from translation so
1387
+ * the pure wire → ChatEvent mapping can live in translate.ts and be reused
1388
+ * by the replay path.
1389
+ *
1390
+ * ``messageId`` is the server-assigned assistant message id for the current
1391
+ * turn; empty in the reconnect and conversation-switch replay paths where
1392
+ * messages have already been loaded from REST and shouldn't be re-pushed.
900
1393
  */
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
- }
1394
+ applyWireSideEffects(wire, conversationId, messageId) {
1395
+ switch (wire.type) {
922
1396
  case "message_start":
923
- if (event.conversation_id && !this.conversationId) {
924
- this.conversationId = event.conversation_id;
1397
+ this.resetStreamingState();
1398
+ if (wire.model) {
1399
+ this.modelDisplayName = wire.model;
925
1400
  }
926
- if (event.model_display_name) {
927
- this.modelDisplayName = event.model_display_name;
928
- this.emit({ type: "model_info", name: event.model_display_name });
1401
+ return;
1402
+ case "block_start":
1403
+ if (wire.kind === "text" && (!wire.parent_path || wire.parent_path.length === 0)) {
1404
+ this.currentTextPath = wire.path;
929
1405
  }
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";
1406
+ return;
1407
+ case "block_delta":
1408
+ if (wire.delta.channel === "text" && this.currentTextPath !== null && pathEquals(this.currentTextPath, wire.path)) {
1409
+ this.accumulatedText += wire.delta.text;
994
1410
  }
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;
1411
+ return;
1412
+ case "block_stop":
1413
+ if (this.currentTextPath !== null && pathEquals(this.currentTextPath, wire.path)) {
1414
+ this.currentTextPath = null;
1039
1415
  }
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;
1416
+ return;
1417
+ case "message_stop":
1418
+ if (messageId) {
1419
+ const assistantMessage = {
1420
+ id: messageId,
1421
+ conversationId,
1422
+ role: "assistant",
1423
+ content: this.accumulatedText,
1424
+ status: "complete",
1425
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1426
+ };
1427
+ this.messages.push(assistantMessage);
1428
+ this.storage.addMessage(assistantMessage, conversationId).catch(() => {
1429
+ });
1052
1430
  }
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;
1431
+ this.isStreaming = false;
1432
+ this.currentJobId = null;
1433
+ return;
1434
+ case "custom":
1435
+ if (wire.name === "title_generated") {
1436
+ const title = wire.data.title ?? "";
1437
+ if (this.conversationId && title) {
1438
+ const conv = this.conversations.find(
1439
+ (c) => c.id === this.conversationId
1440
+ );
1441
+ if (conv) {
1442
+ conv.title = title;
1443
+ }
1444
+ this.storage.updateConversationTitle(this.conversationId, title).catch(() => {
1445
+ });
1446
+ }
1447
+ }
1448
+ return;
1449
+ default:
1450
+ return;
1161
1451
  }
1162
1452
  }
1163
1453
  async executeClientTools(toolCalls) {
1164
1454
  const results = [];
1165
1455
  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
1456
  const result = await this.toolRegistry.executeTool(call);
1173
1457
  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
1458
  }
1183
- this.executingTool = null;
1184
1459
  return results;
1185
1460
  }
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
1461
  /**
1215
1462
  * Load conversation context (messages) without replaying events.
1216
1463
  * Used before reconnectToJob — SSE replay handles event replay.
@@ -1218,13 +1465,11 @@ var ChatSession = class {
1218
1465
  async loadConversation(id) {
1219
1466
  this.conversationId = id;
1220
1467
  this.resetStreamingState();
1221
- this.blockBuilder.reset();
1222
1468
  this.messages = await this.client.getMessages(id).catch(() => this.storage.fetchMessages(id));
1223
1469
  }
1224
1470
  /**
1225
1471
  * Reconnect to a running job's SSE stream (e.g. after page reload).
1226
1472
  * Replays all events from the beginning and continues live.
1227
- * Does NOT reset BlockBuilder — caller controls reset.
1228
1473
  */
1229
1474
  async reconnectToJob(jobId) {
1230
1475
  if (this.isStreaming) return;
@@ -1249,23 +1494,21 @@ var ChatSession = class {
1249
1494
  } catch (err) {
1250
1495
  this.emit({
1251
1496
  type: "error",
1252
- error: err instanceof Error ? err : new ConnectionError(String(err))
1497
+ code: "connection_error",
1498
+ message: err instanceof Error ? err.message : String(err),
1499
+ blockPath: null
1253
1500
  });
1254
1501
  } finally {
1255
1502
  this.isStreaming = false;
1256
- this.executingTool = null;
1257
1503
  this.abortController = null;
1258
1504
  }
1259
1505
  }
1260
- /** Detach from the SSE stream without cancelling the job.
1261
- * The backend job keeps running — caller can reconnect later. */
1506
+ /** Detach from the SSE stream without cancelling the job. */
1262
1507
  detach() {
1263
1508
  this.abortController?.abort();
1264
1509
  this.abortController = null;
1265
1510
  this.isStreaming = false;
1266
- this.streamingContent = "";
1267
- this.executingTool = null;
1268
- this.blockBuilder.reset();
1511
+ this.resetStreamingState();
1269
1512
  this.emit({ type: "disconnected" });
1270
1513
  }
1271
1514
  /** Stop the job and disconnect (explicit user action). */
@@ -1276,6 +1519,7 @@ var ChatSession = class {
1276
1519
  }
1277
1520
  this.detach();
1278
1521
  this.currentJobId = null;
1522
+ this.protocols.clear();
1279
1523
  }
1280
1524
  async createNewConversation() {
1281
1525
  const id = generateId();
@@ -1286,21 +1530,39 @@ var ChatSession = class {
1286
1530
  this.conversations.unshift(conversation);
1287
1531
  this.conversationId = id;
1288
1532
  this.messages = [];
1289
- this.streamingContent = "";
1290
1533
  return id;
1291
1534
  }
1292
- async switchConversation(id, jobId) {
1535
+ async switchConversation(id, jobId, userMessageContent) {
1293
1536
  this.conversationId = id;
1294
1537
  this.resetStreamingState();
1295
- this.blockBuilder.reset();
1296
1538
  const [messagesResult, eventsResult] = await Promise.allSettled([
1297
1539
  this.client.getMessages(id).catch(() => this.storage.fetchMessages(id)),
1298
1540
  this.client.getConversationEvents(id, jobId)
1299
1541
  ]);
1300
1542
  this.messages = messagesResult.status === "fulfilled" ? messagesResult.value : [];
1301
1543
  if (eventsResult.status === "fulfilled") {
1544
+ let userMessageEmitted = !userMessageContent;
1302
1545
  for (const ev of eventsResult.value) {
1303
- this.applyEvent({ type: ev.event, ...ev.data });
1546
+ const type = ev.data.type || ev.event;
1547
+ if (!type || type === "done") continue;
1548
+ if (!userMessageEmitted && type === "message_start") {
1549
+ this.emit({
1550
+ type: "user_message",
1551
+ content: userMessageContent
1552
+ });
1553
+ userMessageEmitted = true;
1554
+ }
1555
+ const wire = { ...ev.data, type };
1556
+ try {
1557
+ await this.dispatchWireEvent(
1558
+ wire,
1559
+ id,
1560
+ "",
1561
+ false
1562
+ // don't execute client tools on replay
1563
+ );
1564
+ } catch {
1565
+ }
1304
1566
  }
1305
1567
  }
1306
1568
  }
@@ -1326,456 +1588,46 @@ var ChatSession = class {
1326
1588
  }
1327
1589
  };
1328
1590
 
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
1591
  // src/types.ts
1743
1592
  var ChatEventType = {
1593
+ // Connection lifecycle (SDK-local, not wire)
1744
1594
  Connected: "connected",
1745
- BlocksChanged: "blocks_changed",
1595
+ Disconnected: "disconnected",
1596
+ // Turn lifecycle
1597
+ MessageStart: "message_start",
1598
+ MessageStop: "message_stop",
1599
+ // Block lifecycle
1600
+ BlockStart: "block_start",
1601
+ BlockDelta: "block_delta",
1602
+ BlockStop: "block_stop",
1603
+ // Reliability
1604
+ Stall: "stall",
1605
+ Retry: "retry",
1606
+ Error: "error",
1607
+ Keepalive: "keepalive",
1608
+ // Conversation-level (typed custom events)
1746
1609
  UserMessage: "user_message",
1747
1610
  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
1611
  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
1612
  ContextUpdate: "context_update",
1613
+ SubagentStart: "subagent_start",
1614
+ SubagentStop: "subagent_stop",
1615
+ ContextWarning: "context_warning",
1616
+ MemoryRecall: "memory_recall",
1617
+ MemoryUpdate: "memory_update",
1776
1618
  DesktopStream: "desktop_stream",
1777
1619
  AttachmentStaged: "attachment_staged",
1778
- WorkspaceReady: "workspace_ready"
1620
+ WorkspaceReady: "workspace_ready",
1621
+ AssetCreated: "asset_created",
1622
+ ToolApprovalRequested: "tool_approval_requested",
1623
+ ToolApprovalGranted: "tool_approval_granted",
1624
+ ToolPermissionDenied: "tool_permission_denied",
1625
+ ToolHarnessWarning: "tool_harness_warning",
1626
+ UserUnavailable: "user_unavailable",
1627
+ PromptSuggestion: "prompt_suggestion",
1628
+ StateChanged: "state_changed",
1629
+ // Generic fallthrough for unknown custom events
1630
+ Custom: "custom"
1779
1631
  };
1780
1632
 
1781
1633
  // src/stream-manager.ts
@@ -1830,22 +1682,12 @@ var StreamManager = class {
1830
1682
  }
1831
1683
  onSessionEvent(event) {
1832
1684
  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
1685
  this.emit({
1844
1686
  type: "event",
1845
1687
  conversationId: convId,
1846
1688
  event
1847
1689
  });
1848
- if (event.type === ChatEventType.Complete) {
1690
+ if (event.type === ChatEventType.MessageStop) {
1849
1691
  if (this._state === "streaming") {
1850
1692
  this.setState("idle");
1851
1693
  }
@@ -1858,13 +1700,13 @@ var StreamManager = class {
1858
1700
  const id = await this.session.createNewConversation();
1859
1701
  this.setActiveConversation(id);
1860
1702
  }
1861
- this.prepareUserBlock(content);
1862
1703
  this.setState("streaming");
1863
1704
  try {
1864
1705
  await this.session.send(content, {
1865
1706
  enableSearch: options?.enableSearch,
1866
1707
  agentName: options?.agentName,
1867
- uploadIds: options?.uploadIds
1708
+ uploadIds: options?.uploadIds,
1709
+ planMode: options?.planMode
1868
1710
  });
1869
1711
  } catch {
1870
1712
  }
@@ -1878,7 +1720,6 @@ var StreamManager = class {
1878
1720
  );
1879
1721
  const lastUserMsg = userMsgs[userMsgs.length - 1];
1880
1722
  if (!lastUserMsg) return;
1881
- this.prepareUserBlock(lastUserMsg.content);
1882
1723
  this.setState("streaming");
1883
1724
  try {
1884
1725
  await this.session.resendFromCheckpoint(
@@ -1942,14 +1783,6 @@ var StreamManager = class {
1942
1783
  this.handlers = [];
1943
1784
  }
1944
1785
  // ── 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
1786
  finalizeStream() {
1954
1787
  if (this._state === "streaming") {
1955
1788
  this.setState("idle");
@@ -1960,8 +1793,8 @@ var StreamManager = class {
1960
1793
  this.setState("restoring");
1961
1794
  let activeJobId = null;
1962
1795
  try {
1963
- const res = await this.session.client.get(`/v1/conversations/${encodeURIComponent(conversationId)}/active-job`);
1964
- activeJobId = res.job_id ?? null;
1796
+ const res = await this.session.client.getActiveJob(conversationId);
1797
+ activeJobId = res.jobId;
1965
1798
  } catch {
1966
1799
  }
1967
1800
  if (activeJobId) {
@@ -1981,15 +1814,19 @@ var StreamManager = class {
1981
1814
  const completedJobs = jobs.filter(
1982
1815
  (j) => j.status === "completed"
1983
1816
  );
1984
- for (const job of completedJobs) {
1985
- await this.session.switchConversation(conversationId, job.job_id);
1817
+ const userMessages = this.session.messages.filter(
1818
+ (m) => m.role === "user"
1819
+ );
1820
+ for (let i = 0; i < completedJobs.length; i++) {
1821
+ const job = completedJobs[i];
1822
+ const userContent = userMessages[i]?.content;
1823
+ await this.session.switchConversation(
1824
+ conversationId,
1825
+ job.job_id,
1826
+ userContent
1827
+ );
1986
1828
  }
1987
1829
  if (completedJobs.length > 0) {
1988
- this.emit({
1989
- type: "blocksChanged",
1990
- conversationId,
1991
- blocks: this.session.blockBuilder.getBlocks()
1992
- });
1993
1830
  this.emit({
1994
1831
  type: "versionsReady",
1995
1832
  conversationId,
@@ -2007,24 +1844,94 @@ var StreamManager = class {
2007
1844
  this.emit({ type: "conversationChanged", conversationId: id });
2008
1845
  }
2009
1846
  };
1847
+
1848
+ // src/replay.ts
1849
+ function toWireEvent(raw) {
1850
+ const type = raw.data.type || raw.event;
1851
+ if (!type || type === "done") return null;
1852
+ return { ...raw.data, type };
1853
+ }
1854
+ function mapSseToChat(raw) {
1855
+ const wire = toWireEvent(raw);
1856
+ if (!wire) return [];
1857
+ const event = translateWireEvent(wire);
1858
+ return event ? [event] : [];
1859
+ }
1860
+ function replayEvents(sseEvents, userMessages, handleEvent, addBlock) {
1861
+ const userMsgs = userMessages.filter((m) => m.role === "user");
1862
+ let userIdx = 0;
1863
+ let expectingUserMessage = true;
1864
+ for (const raw of sseEvents) {
1865
+ const type = raw.data.type || raw.event;
1866
+ if (type === "message_stop") {
1867
+ expectingUserMessage = true;
1868
+ }
1869
+ if (type === "message_start" && expectingUserMessage && userIdx < userMsgs.length) {
1870
+ const userMsg = userMsgs[userIdx];
1871
+ addBlock({
1872
+ type: "user",
1873
+ id: `replay_user_${userIdx}`,
1874
+ content: userMsg.content
1875
+ });
1876
+ userIdx++;
1877
+ expectingUserMessage = false;
1878
+ }
1879
+ for (const ce of mapSseToChat(raw)) {
1880
+ handleEvent(ce);
1881
+ }
1882
+ }
1883
+ }
1884
+
1885
+ // src/embedded-resource.ts
1886
+ function isEmbeddedResource(value) {
1887
+ return typeof value === "object" && value !== null && value._embedded_resource === true;
1888
+ }
1889
+ function parseEmbeddedResource(value) {
1890
+ let candidate = value;
1891
+ if (typeof candidate === "string") {
1892
+ const trimmed = candidate.trim();
1893
+ if (!trimmed.startsWith("{")) return null;
1894
+ try {
1895
+ candidate = JSON.parse(trimmed);
1896
+ } catch {
1897
+ return null;
1898
+ }
1899
+ }
1900
+ if (!isEmbeddedResource(candidate)) return null;
1901
+ const mimeType = candidate.mime_type;
1902
+ const uri = candidate.uri;
1903
+ const payload = candidate.payload;
1904
+ if (typeof mimeType !== "string" || !mimeType) return null;
1905
+ if (typeof uri !== "string" || !uri) return null;
1906
+ if (!payload || typeof payload !== "object") return null;
1907
+ return {
1908
+ mimeType,
1909
+ uri,
1910
+ payload
1911
+ };
1912
+ }
2010
1913
  // Annotate the CommonJS export names for ESM import in node:
2011
1914
  0 && (module.exports = {
2012
1915
  AstralformClient,
2013
1916
  AstralformError,
2014
1917
  AuthenticationError,
2015
- BlockBuilder,
2016
1918
  ChatEventType,
2017
1919
  ChatSession,
2018
1920
  ConnectionError,
2019
1921
  InMemoryStorage,
2020
1922
  LLMNotConfiguredError,
1923
+ ProtocolRegistry,
2021
1924
  RateLimitError,
2022
1925
  ServerError,
2023
1926
  StreamAbortedError,
2024
1927
  StreamManager,
2025
1928
  ToolRegistry,
2026
1929
  generateId,
2027
- standardHandlers,
2028
- streamJobSSE
1930
+ isEmbeddedResource,
1931
+ mapSseToChat,
1932
+ parseEmbeddedResource,
1933
+ replayEvents,
1934
+ streamJobSSE,
1935
+ translateDelta
2029
1936
  });
2030
1937
  //# sourceMappingURL=index.cjs.map