@astralform/js 0.2.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,385 +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 "asset_created":
1096
- this.emit({
1097
- type: "asset_created",
1098
- assetId: event.asset_id,
1099
- name: event.name,
1100
- url: event.url,
1101
- mediaType: event.media_type,
1102
- sizeBytes: event.size_bytes
1103
- });
1104
- break;
1105
- case "editor_content_start":
1106
- this.emit({
1107
- type: "editor_content_start",
1108
- callId: event.call_id,
1109
- path: event.path,
1110
- language: event.language
1111
- });
1112
- break;
1113
- case "editor_content_delta":
1114
- this.emit({
1115
- type: "editor_content_delta",
1116
- callId: event.call_id,
1117
- path: event.path,
1118
- delta: event.delta
1119
- });
1120
- break;
1121
- case "editor_content_end":
1122
- this.emit({
1123
- type: "editor_content_end",
1124
- callId: event.call_id
1125
- });
1126
- 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;
1127
1451
  }
1128
1452
  }
1129
1453
  async executeClientTools(toolCalls) {
1130
1454
  const results = [];
1131
1455
  for (const call of toolCalls) {
1132
- this.executingTool = call.toolName;
1133
- const toolState = this.activeTools.get(call.callId);
1134
- if (toolState) {
1135
- toolState.status = "executing";
1136
- }
1137
- this.emit({ type: "tool_executing", name: call.toolName });
1138
1456
  const result = await this.toolRegistry.executeTool(call);
1139
1457
  results.push(result);
1140
- if (toolState) {
1141
- toolState.status = "completed";
1142
- }
1143
- this.emit({
1144
- type: "tool_completed",
1145
- name: call.toolName,
1146
- result: result.result
1147
- });
1148
1458
  }
1149
- this.executingTool = null;
1150
1459
  return results;
1151
1460
  }
1152
- async completeStream(conversationId, messageId, title, metrics, jobId) {
1153
- const assistantMessage = {
1154
- id: messageId || generateId(),
1155
- conversationId,
1156
- role: "assistant",
1157
- content: this.streamingContent,
1158
- status: "complete",
1159
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
1160
- };
1161
- this.messages.push(assistantMessage);
1162
- await this.storage.addMessage(assistantMessage, conversationId);
1163
- if (title && conversationId) {
1164
- await this.storage.updateConversationTitle(conversationId, title);
1165
- const conv = this.conversations.find((c) => c.id === conversationId);
1166
- if (conv) {
1167
- conv.title = title;
1168
- }
1169
- }
1170
- this.emit({
1171
- type: "complete",
1172
- content: this.streamingContent,
1173
- conversationId,
1174
- messageId: assistantMessage.id,
1175
- title,
1176
- metrics,
1177
- job_id: jobId
1178
- });
1179
- }
1180
1461
  /**
1181
1462
  * Load conversation context (messages) without replaying events.
1182
1463
  * Used before reconnectToJob — SSE replay handles event replay.
@@ -1184,13 +1465,11 @@ var ChatSession = class {
1184
1465
  async loadConversation(id) {
1185
1466
  this.conversationId = id;
1186
1467
  this.resetStreamingState();
1187
- this.blockBuilder.reset();
1188
1468
  this.messages = await this.client.getMessages(id).catch(() => this.storage.fetchMessages(id));
1189
1469
  }
1190
1470
  /**
1191
1471
  * Reconnect to a running job's SSE stream (e.g. after page reload).
1192
1472
  * Replays all events from the beginning and continues live.
1193
- * Does NOT reset BlockBuilder — caller controls reset.
1194
1473
  */
1195
1474
  async reconnectToJob(jobId) {
1196
1475
  if (this.isStreaming) return;
@@ -1215,23 +1494,21 @@ var ChatSession = class {
1215
1494
  } catch (err) {
1216
1495
  this.emit({
1217
1496
  type: "error",
1218
- 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
1219
1500
  });
1220
1501
  } finally {
1221
1502
  this.isStreaming = false;
1222
- this.executingTool = null;
1223
1503
  this.abortController = null;
1224
1504
  }
1225
1505
  }
1226
- /** Detach from the SSE stream without cancelling the job.
1227
- * The backend job keeps running — caller can reconnect later. */
1506
+ /** Detach from the SSE stream without cancelling the job. */
1228
1507
  detach() {
1229
1508
  this.abortController?.abort();
1230
1509
  this.abortController = null;
1231
1510
  this.isStreaming = false;
1232
- this.streamingContent = "";
1233
- this.executingTool = null;
1234
- this.blockBuilder.reset();
1511
+ this.resetStreamingState();
1235
1512
  this.emit({ type: "disconnected" });
1236
1513
  }
1237
1514
  /** Stop the job and disconnect (explicit user action). */
@@ -1242,6 +1519,7 @@ var ChatSession = class {
1242
1519
  }
1243
1520
  this.detach();
1244
1521
  this.currentJobId = null;
1522
+ this.protocols.clear();
1245
1523
  }
1246
1524
  async createNewConversation() {
1247
1525
  const id = generateId();
@@ -1252,21 +1530,39 @@ var ChatSession = class {
1252
1530
  this.conversations.unshift(conversation);
1253
1531
  this.conversationId = id;
1254
1532
  this.messages = [];
1255
- this.streamingContent = "";
1256
1533
  return id;
1257
1534
  }
1258
- async switchConversation(id, jobId) {
1535
+ async switchConversation(id, jobId, userMessageContent) {
1259
1536
  this.conversationId = id;
1260
1537
  this.resetStreamingState();
1261
- this.blockBuilder.reset();
1262
1538
  const [messagesResult, eventsResult] = await Promise.allSettled([
1263
1539
  this.client.getMessages(id).catch(() => this.storage.fetchMessages(id)),
1264
1540
  this.client.getConversationEvents(id, jobId)
1265
1541
  ]);
1266
1542
  this.messages = messagesResult.status === "fulfilled" ? messagesResult.value : [];
1267
1543
  if (eventsResult.status === "fulfilled") {
1544
+ let userMessageEmitted = !userMessageContent;
1268
1545
  for (const ev of eventsResult.value) {
1269
- 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
+ }
1270
1566
  }
1271
1567
  }
1272
1568
  }
@@ -1292,420 +1588,46 @@ var ChatSession = class {
1292
1588
  }
1293
1589
  };
1294
1590
 
1295
- // src/standard-handlers.ts
1296
- function finalizeText(builder) {
1297
- if (builder.activeTextId) {
1298
- builder.patchBlock(builder.activeTextId, {
1299
- isStreaming: false
1300
- });
1301
- builder.activeTextId = null;
1302
- }
1303
- }
1304
- function finalizeThinking(builder) {
1305
- if (builder.activeThinkingId) {
1306
- builder.patchBlock(builder.activeThinkingId, {
1307
- isActive: false
1308
- });
1309
- builder.activeThinkingId = null;
1310
- builder.thinkingStartMs = null;
1311
- }
1312
- }
1313
- function finalizeEditor(builder) {
1314
- if (builder.activeEditorId) {
1315
- builder.patchBlock(builder.activeEditorId, {
1316
- isStreaming: false
1317
- });
1318
- builder.activeEditorId = null;
1319
- }
1320
- }
1321
- var handleUserMessage = (event, builder) => {
1322
- const e = event;
1323
- const existing = builder.findBlock((b) => b.type === "user");
1324
- if (existing) {
1325
- if (e.createdAt) {
1326
- builder.patchBlock(existing.id, {
1327
- createdAt: e.createdAt
1328
- });
1329
- }
1330
- return;
1331
- }
1332
- builder.addBlock({
1333
- type: "user",
1334
- id: builder.nextId(),
1335
- content: e.content,
1336
- createdAt: e.createdAt
1337
- });
1338
- };
1339
- var handleChunk = (event, builder) => {
1340
- const e = event;
1341
- if (!builder.activeTextId) {
1342
- const id = builder.nextId();
1343
- builder.activeTextId = id;
1344
- builder.addBlock({
1345
- type: "text",
1346
- id,
1347
- content: e.text,
1348
- isStreaming: true
1349
- });
1350
- } else {
1351
- const id = builder.activeTextId;
1352
- const existing = builder.findBlock((b) => b.id === id);
1353
- if (existing && existing.type === "text") {
1354
- builder.patchBlock(id, {
1355
- content: existing.content + e.text
1356
- });
1357
- }
1358
- }
1359
- };
1360
- var handleToolCall = (event, builder) => {
1361
- const e = event;
1362
- finalizeText(builder);
1363
- builder.addBlock({
1364
- type: "tool",
1365
- id: builder.nextId(),
1366
- callId: e.request.callId,
1367
- toolName: e.request.toolName,
1368
- displayName: e.request.displayName,
1369
- description: e.request.description,
1370
- arguments: e.request.arguments,
1371
- toolCategory: e.request.toolCategory,
1372
- iconUrl: e.request.iconUrl,
1373
- status: "calling"
1374
- });
1375
- };
1376
- var handleToolExecuting = (event, builder) => {
1377
- const e = event;
1378
- const block = builder.findBlock(
1379
- (b) => b.type === "tool" && (e.call_id ? b.callId === e.call_id : b.toolName === e.name) && b.status === "calling"
1380
- );
1381
- if (block) {
1382
- builder.patchBlock(block.id, { status: "executing" });
1383
- }
1384
- };
1385
- var handleToolProgress = (event, builder) => {
1386
- const e = event;
1387
- const block = builder.findBlock(
1388
- (b) => b.type === "tool" && b.callId === e.callId
1389
- );
1390
- if (block && block.type === "tool") {
1391
- const sources = block.sources ? [...block.sources] : [];
1392
- sources.push(e.item);
1393
- builder.patchBlock(block.id, {
1394
- sources,
1395
- status: "executing"
1396
- });
1397
- }
1398
- };
1399
- var handleToolEnd = (event, builder) => {
1400
- const e = event;
1401
- const callId = e.type === "tool_end" ? e.callId : void 0;
1402
- const name = e.type === "tool_end" ? e.toolName : e.name;
1403
- const block = builder.findBlock(
1404
- (b) => b.type === "tool" && (callId ? b.callId === callId : b.toolName === name) && b.status !== "completed"
1405
- );
1406
- if (block) {
1407
- const toolEnd = e.type === "tool_end" ? e : null;
1408
- builder.patchBlock(block.id, {
1409
- status: "completed",
1410
- ...toolEnd?.sources ? { sources: toolEnd.sources } : {},
1411
- ...toolEnd?.durationMs != null ? { durationMs: toolEnd.durationMs } : {},
1412
- ...toolEnd?.result ? { result: toolEnd.result } : {}
1413
- });
1414
- }
1415
- };
1416
- var handleAgentStart = (event, builder) => {
1417
- const e = event;
1418
- finalizeText(builder);
1419
- builder.addBlock({
1420
- type: "agent",
1421
- id: builder.nextId(),
1422
- agentName: e.agentName,
1423
- displayName: e.agentDisplayName,
1424
- avatarUrl: e.avatarUrl
1425
- });
1426
- };
1427
- var handleThinkingDelta = (event, builder) => {
1428
- const e = event;
1429
- if (!builder.activeThinkingId) {
1430
- const id = builder.nextId();
1431
- builder.activeThinkingId = id;
1432
- builder.thinkingStartMs = Date.now();
1433
- builder.addBlock({
1434
- type: "thinking",
1435
- id,
1436
- content: e.text,
1437
- isActive: true
1438
- });
1439
- } else {
1440
- const id = builder.activeThinkingId;
1441
- const existing = builder.findBlock((b) => b.id === id);
1442
- if (existing && existing.type === "thinking") {
1443
- builder.patchBlock(id, {
1444
- content: existing.content + e.text
1445
- });
1446
- }
1447
- }
1448
- };
1449
- var handleThinkingComplete = (_event, builder) => {
1450
- if (builder.activeThinkingId) {
1451
- const durationMs = builder.thinkingStartMs ? Math.max(0, Date.now() - builder.thinkingStartMs) : void 0;
1452
- builder.patchBlock(builder.activeThinkingId, {
1453
- isActive: false,
1454
- durationMs
1455
- });
1456
- builder.activeThinkingId = null;
1457
- builder.thinkingStartMs = null;
1458
- }
1459
- };
1460
- var handleSubagentStart = (event, builder) => {
1461
- const e = event;
1462
- finalizeText(builder);
1463
- builder.addBlock({
1464
- type: "subagent",
1465
- id: builder.nextId(),
1466
- agentName: e.agentName,
1467
- displayName: e.displayName,
1468
- toolCallId: e.toolCallId,
1469
- avatarUrl: e.avatarUrl,
1470
- description: e.description,
1471
- content: "",
1472
- isActive: true
1473
- });
1474
- };
1475
- var handleSubagentChunk = (event, builder) => {
1476
- const e = event;
1477
- const block = builder.findBlock(
1478
- (b) => b.type === "subagent" && b.toolCallId === e.toolCallId
1479
- );
1480
- if (block && block.type === "subagent") {
1481
- builder.patchBlock(block.id, {
1482
- content: block.content + e.text
1483
- });
1484
- }
1485
- };
1486
- var handleSubagentUpdate = (event, builder) => {
1487
- const e = event;
1488
- const block = builder.findBlock(
1489
- (b) => b.type === "subagent" && b.toolCallId === e.toolCallId
1490
- );
1491
- if (block) {
1492
- builder.patchBlock(block.id, {
1493
- displayName: e.displayName
1494
- });
1495
- }
1496
- };
1497
- var handleSubagentEnd = (event, builder) => {
1498
- const e = event;
1499
- const block = builder.findBlock(
1500
- (b) => b.type === "subagent" && b.toolCallId === e.toolCallId
1501
- );
1502
- if (block) {
1503
- builder.patchBlock(block.id, {
1504
- isActive: false
1505
- });
1506
- }
1507
- };
1508
- var handleComplete = (_event, builder) => {
1509
- finalizeText(builder);
1510
- finalizeThinking(builder);
1511
- finalizeEditor(builder);
1512
- for (const b of builder.getBlocks()) {
1513
- if (b.type === "tool" && b.status !== "completed") {
1514
- builder.patchBlock(b.id, { status: "completed" });
1515
- }
1516
- }
1517
- };
1518
- var handleError = (event, builder) => {
1519
- const e = event;
1520
- finalizeText(builder);
1521
- finalizeThinking(builder);
1522
- builder.addBlock({
1523
- type: "error",
1524
- id: builder.nextId(),
1525
- message: e.error.message
1526
- });
1527
- };
1528
- var handleDisconnected = (_event, builder) => {
1529
- finalizeText(builder);
1530
- finalizeThinking(builder);
1531
- finalizeEditor(builder);
1532
- };
1533
- var handleCapsuleOutputChunk = (event, builder) => {
1534
- const e = event;
1535
- const block = builder.findBlock(
1536
- (b) => b.type === "capsule" && b.callId === e.callId
1537
- );
1538
- if (block && block.type === "capsule") {
1539
- builder.patchBlock(block.id, {
1540
- output: block.output + e.chunk
1541
- });
1542
- } else {
1543
- builder.addBlock({
1544
- type: "capsule",
1545
- id: builder.nextId(),
1546
- callId: e.callId,
1547
- toolName: "",
1548
- output: e.chunk,
1549
- isActive: true
1550
- });
1551
- }
1552
- };
1553
- var handleCapsuleOutput = (event, builder) => {
1554
- const e = event;
1555
- const block = builder.findBlock(
1556
- (b) => b.type === "capsule" && b.callId === (e.callId ?? "")
1557
- );
1558
- if (block) {
1559
- builder.patchBlock(block.id, {
1560
- output: e.output,
1561
- command: e.command,
1562
- toolName: e.toolName,
1563
- durationMs: e.durationMs,
1564
- isActive: false
1565
- });
1566
- } else {
1567
- builder.addBlock({
1568
- type: "capsule",
1569
- id: builder.nextId(),
1570
- callId: e.callId ?? "",
1571
- toolName: e.toolName,
1572
- command: e.command,
1573
- output: e.output,
1574
- durationMs: e.durationMs,
1575
- isActive: false
1576
- });
1577
- }
1578
- };
1579
- var handleAssetCreated = (event, builder) => {
1580
- const e = event;
1581
- builder.addBlock({
1582
- type: "asset",
1583
- id: builder.nextId(),
1584
- assetId: e.assetId,
1585
- name: e.name,
1586
- url: e.url,
1587
- mediaType: e.mediaType,
1588
- sizeBytes: e.sizeBytes
1589
- });
1590
- };
1591
- var handleTodoUpdate = (event, builder) => {
1592
- const e = event;
1593
- if (builder.activeTodoId) {
1594
- builder.patchBlock(builder.activeTodoId, {
1595
- todos: e.todos
1596
- });
1597
- } else {
1598
- const id = builder.nextId();
1599
- builder.activeTodoId = id;
1600
- builder.addBlock({
1601
- type: "todo",
1602
- id,
1603
- todos: e.todos
1604
- });
1605
- }
1606
- };
1607
- var handleEditorContentStart = (event, builder) => {
1608
- const e = event;
1609
- const id = builder.nextId();
1610
- builder.activeEditorId = id;
1611
- builder.addBlock({
1612
- type: "editor",
1613
- id,
1614
- callId: e.callId,
1615
- path: e.path,
1616
- language: e.language,
1617
- content: "",
1618
- isStreaming: true
1619
- });
1620
- };
1621
- var handleEditorContentDelta = (event, builder) => {
1622
- const e = event;
1623
- const block = builder.findBlock(
1624
- (b) => b.type === "editor" && b.callId === e.callId
1625
- );
1626
- if (block && block.type === "editor") {
1627
- builder.patchBlock(block.id, {
1628
- content: block.content + e.delta
1629
- });
1630
- }
1631
- };
1632
- var handleEditorContentEnd = (event, builder) => {
1633
- const e = event;
1634
- const block = builder.findBlock(
1635
- (b) => b.type === "editor" && b.callId === e.callId
1636
- );
1637
- if (block) {
1638
- builder.patchBlock(block.id, {
1639
- isStreaming: false
1640
- });
1641
- }
1642
- builder.activeEditorId = null;
1643
- };
1644
- var noop = () => {
1645
- };
1646
- var standardHandlers = {
1647
- user_message: handleUserMessage,
1648
- chunk: handleChunk,
1649
- tool_call: handleToolCall,
1650
- tool_executing: handleToolExecuting,
1651
- tool_progress: handleToolProgress,
1652
- tool_completed: handleToolEnd,
1653
- tool_end: handleToolEnd,
1654
- agent_start: handleAgentStart,
1655
- agent_end: noop,
1656
- thinking_delta: handleThinkingDelta,
1657
- thinking_complete: handleThinkingComplete,
1658
- subagent_start: handleSubagentStart,
1659
- subagent_chunk: handleSubagentChunk,
1660
- subagent_update: handleSubagentUpdate,
1661
- subagent_end: handleSubagentEnd,
1662
- subagent_tool_use: noop,
1663
- capsule_output: handleCapsuleOutput,
1664
- capsule_output_chunk: handleCapsuleOutputChunk,
1665
- asset_created: handleAssetCreated,
1666
- todo_update: handleTodoUpdate,
1667
- editor_content_start: handleEditorContentStart,
1668
- editor_content_delta: handleEditorContentDelta,
1669
- editor_content_end: handleEditorContentEnd,
1670
- retry: noop,
1671
- complete: handleComplete,
1672
- error: handleError,
1673
- disconnected: handleDisconnected
1674
- };
1675
-
1676
1591
  // src/types.ts
1677
1592
  var ChatEventType = {
1593
+ // Connection lifecycle (SDK-local, not wire)
1678
1594
  Connected: "connected",
1679
- 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)
1680
1609
  UserMessage: "user_message",
1681
1610
  TitleGenerated: "title_generated",
1682
- ModelInfo: "model_info",
1683
- Chunk: "chunk",
1684
- ToolCall: "tool_call",
1685
- ToolExecuting: "tool_executing",
1686
- ToolProgress: "tool_progress",
1687
- ToolCompleted: "tool_completed",
1688
- ToolEnd: "tool_end",
1689
- AgentStart: "agent_start",
1690
- AgentEnd: "agent_end",
1691
- ThinkingDelta: "thinking_delta",
1692
- ThinkingComplete: "thinking_complete",
1611
+ TodoUpdate: "todo_update",
1612
+ ContextUpdate: "context_update",
1693
1613
  SubagentStart: "subagent_start",
1694
- SubagentChunk: "subagent_chunk",
1695
- SubagentUpdate: "subagent_update",
1696
- SubagentEnd: "subagent_end",
1697
- SubagentToolUse: "subagent_tool_use",
1698
- CapsuleOutput: "capsule_output",
1699
- CapsuleOutputChunk: "capsule_output_chunk",
1614
+ SubagentStop: "subagent_stop",
1615
+ ContextWarning: "context_warning",
1616
+ MemoryRecall: "memory_recall",
1617
+ MemoryUpdate: "memory_update",
1618
+ DesktopStream: "desktop_stream",
1619
+ AttachmentStaged: "attachment_staged",
1620
+ WorkspaceReady: "workspace_ready",
1700
1621
  AssetCreated: "asset_created",
1701
- TodoUpdate: "todo_update",
1702
- EditorContentStart: "editor_content_start",
1703
- EditorContentDelta: "editor_content_delta",
1704
- EditorContentEnd: "editor_content_end",
1705
- Complete: "complete",
1706
- Error: "error",
1707
- Disconnected: "disconnected",
1708
- Retry: "retry"
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"
1709
1631
  };
1710
1632
 
1711
1633
  // src/stream-manager.ts
@@ -1760,22 +1682,12 @@ var StreamManager = class {
1760
1682
  }
1761
1683
  onSessionEvent(event) {
1762
1684
  const convId = this.session.conversationId;
1763
- if (event.type === ChatEventType.BlocksChanged) {
1764
- if (this._state === "streaming" && convId) {
1765
- this.emit({
1766
- type: "blocksChanged",
1767
- conversationId: convId,
1768
- blocks: event.blocks
1769
- });
1770
- }
1771
- return;
1772
- }
1773
1685
  this.emit({
1774
1686
  type: "event",
1775
1687
  conversationId: convId,
1776
1688
  event
1777
1689
  });
1778
- if (event.type === ChatEventType.Complete) {
1690
+ if (event.type === ChatEventType.MessageStop) {
1779
1691
  if (this._state === "streaming") {
1780
1692
  this.setState("idle");
1781
1693
  }
@@ -1788,13 +1700,13 @@ var StreamManager = class {
1788
1700
  const id = await this.session.createNewConversation();
1789
1701
  this.setActiveConversation(id);
1790
1702
  }
1791
- this.prepareUserBlock(content);
1792
1703
  this.setState("streaming");
1793
1704
  try {
1794
1705
  await this.session.send(content, {
1795
1706
  enableSearch: options?.enableSearch,
1796
1707
  agentName: options?.agentName,
1797
- uploadIds: options?.uploadIds
1708
+ uploadIds: options?.uploadIds,
1709
+ planMode: options?.planMode
1798
1710
  });
1799
1711
  } catch {
1800
1712
  }
@@ -1808,7 +1720,6 @@ var StreamManager = class {
1808
1720
  );
1809
1721
  const lastUserMsg = userMsgs[userMsgs.length - 1];
1810
1722
  if (!lastUserMsg) return;
1811
- this.prepareUserBlock(lastUserMsg.content);
1812
1723
  this.setState("streaming");
1813
1724
  try {
1814
1725
  await this.session.resendFromCheckpoint(
@@ -1872,14 +1783,6 @@ var StreamManager = class {
1872
1783
  this.handlers = [];
1873
1784
  }
1874
1785
  // ── Internal: helpers ──────────────────────────────────────────
1875
- prepareUserBlock(content) {
1876
- this.session.blockBuilder.reset();
1877
- this.session.blockBuilder.addBlock({
1878
- type: "user",
1879
- id: this.session.blockBuilder.nextId(),
1880
- content
1881
- });
1882
- }
1883
1786
  finalizeStream() {
1884
1787
  if (this._state === "streaming") {
1885
1788
  this.setState("idle");
@@ -1890,8 +1793,8 @@ var StreamManager = class {
1890
1793
  this.setState("restoring");
1891
1794
  let activeJobId = null;
1892
1795
  try {
1893
- const res = await this.session.client.get(`/v1/conversations/${encodeURIComponent(conversationId)}/active-job`);
1894
- activeJobId = res.job_id ?? null;
1796
+ const res = await this.session.client.getActiveJob(conversationId);
1797
+ activeJobId = res.jobId;
1895
1798
  } catch {
1896
1799
  }
1897
1800
  if (activeJobId) {
@@ -1911,15 +1814,19 @@ var StreamManager = class {
1911
1814
  const completedJobs = jobs.filter(
1912
1815
  (j) => j.status === "completed"
1913
1816
  );
1914
- for (const job of completedJobs) {
1915
- 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
+ );
1916
1828
  }
1917
1829
  if (completedJobs.length > 0) {
1918
- this.emit({
1919
- type: "blocksChanged",
1920
- conversationId,
1921
- blocks: this.session.blockBuilder.getBlocks()
1922
- });
1923
1830
  this.emit({
1924
1831
  type: "versionsReady",
1925
1832
  conversationId,
@@ -1937,24 +1844,94 @@ var StreamManager = class {
1937
1844
  this.emit({ type: "conversationChanged", conversationId: id });
1938
1845
  }
1939
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
+ }
1940
1913
  // Annotate the CommonJS export names for ESM import in node:
1941
1914
  0 && (module.exports = {
1942
1915
  AstralformClient,
1943
1916
  AstralformError,
1944
1917
  AuthenticationError,
1945
- BlockBuilder,
1946
1918
  ChatEventType,
1947
1919
  ChatSession,
1948
1920
  ConnectionError,
1949
1921
  InMemoryStorage,
1950
1922
  LLMNotConfiguredError,
1923
+ ProtocolRegistry,
1951
1924
  RateLimitError,
1952
1925
  ServerError,
1953
1926
  StreamAbortedError,
1954
1927
  StreamManager,
1955
1928
  ToolRegistry,
1956
1929
  generateId,
1957
- standardHandlers,
1958
- streamJobSSE
1930
+ isEmbeddedResource,
1931
+ mapSseToChat,
1932
+ parseEmbeddedResource,
1933
+ replayEvents,
1934
+ streamJobSSE,
1935
+ translateDelta
1959
1936
  });
1960
1937
  //# sourceMappingURL=index.cjs.map