@astralform/js 0.2.3 → 1.0.0

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