@astralform/js 0.2.3 → 1.1.0

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