@astralform/js 0.2.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,385 +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 "asset_created":
1054
- this.emit({
1055
- type: "asset_created",
1056
- assetId: event.asset_id,
1057
- name: event.name,
1058
- url: event.url,
1059
- mediaType: event.media_type,
1060
- sizeBytes: event.size_bytes
1061
- });
1062
- break;
1063
- case "editor_content_start":
1064
- this.emit({
1065
- type: "editor_content_start",
1066
- callId: event.call_id,
1067
- path: event.path,
1068
- language: event.language
1069
- });
1070
- break;
1071
- case "editor_content_delta":
1072
- this.emit({
1073
- type: "editor_content_delta",
1074
- callId: event.call_id,
1075
- path: event.path,
1076
- delta: event.delta
1077
- });
1078
- break;
1079
- case "editor_content_end":
1080
- this.emit({
1081
- type: "editor_content_end",
1082
- callId: event.call_id
1083
- });
1084
- 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;
1085
1405
  }
1086
1406
  }
1087
1407
  async executeClientTools(toolCalls) {
1088
1408
  const results = [];
1089
1409
  for (const call of toolCalls) {
1090
- this.executingTool = call.toolName;
1091
- const toolState = this.activeTools.get(call.callId);
1092
- if (toolState) {
1093
- toolState.status = "executing";
1094
- }
1095
- this.emit({ type: "tool_executing", name: call.toolName });
1096
1410
  const result = await this.toolRegistry.executeTool(call);
1097
1411
  results.push(result);
1098
- if (toolState) {
1099
- toolState.status = "completed";
1100
- }
1101
- this.emit({
1102
- type: "tool_completed",
1103
- name: call.toolName,
1104
- result: result.result
1105
- });
1106
1412
  }
1107
- this.executingTool = null;
1108
1413
  return results;
1109
1414
  }
1110
- async completeStream(conversationId, messageId, title, metrics, jobId) {
1111
- const assistantMessage = {
1112
- id: messageId || generateId(),
1113
- conversationId,
1114
- role: "assistant",
1115
- content: this.streamingContent,
1116
- status: "complete",
1117
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
1118
- };
1119
- this.messages.push(assistantMessage);
1120
- await this.storage.addMessage(assistantMessage, conversationId);
1121
- if (title && conversationId) {
1122
- await this.storage.updateConversationTitle(conversationId, title);
1123
- const conv = this.conversations.find((c) => c.id === conversationId);
1124
- if (conv) {
1125
- conv.title = title;
1126
- }
1127
- }
1128
- this.emit({
1129
- type: "complete",
1130
- content: this.streamingContent,
1131
- conversationId,
1132
- messageId: assistantMessage.id,
1133
- title,
1134
- metrics,
1135
- job_id: jobId
1136
- });
1137
- }
1138
1415
  /**
1139
1416
  * Load conversation context (messages) without replaying events.
1140
1417
  * Used before reconnectToJob — SSE replay handles event replay.
@@ -1142,13 +1419,11 @@ var ChatSession = class {
1142
1419
  async loadConversation(id) {
1143
1420
  this.conversationId = id;
1144
1421
  this.resetStreamingState();
1145
- this.blockBuilder.reset();
1146
1422
  this.messages = await this.client.getMessages(id).catch(() => this.storage.fetchMessages(id));
1147
1423
  }
1148
1424
  /**
1149
1425
  * Reconnect to a running job's SSE stream (e.g. after page reload).
1150
1426
  * Replays all events from the beginning and continues live.
1151
- * Does NOT reset BlockBuilder — caller controls reset.
1152
1427
  */
1153
1428
  async reconnectToJob(jobId) {
1154
1429
  if (this.isStreaming) return;
@@ -1173,23 +1448,21 @@ var ChatSession = class {
1173
1448
  } catch (err) {
1174
1449
  this.emit({
1175
1450
  type: "error",
1176
- 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
1177
1454
  });
1178
1455
  } finally {
1179
1456
  this.isStreaming = false;
1180
- this.executingTool = null;
1181
1457
  this.abortController = null;
1182
1458
  }
1183
1459
  }
1184
- /** Detach from the SSE stream without cancelling the job.
1185
- * The backend job keeps running — caller can reconnect later. */
1460
+ /** Detach from the SSE stream without cancelling the job. */
1186
1461
  detach() {
1187
1462
  this.abortController?.abort();
1188
1463
  this.abortController = null;
1189
1464
  this.isStreaming = false;
1190
- this.streamingContent = "";
1191
- this.executingTool = null;
1192
- this.blockBuilder.reset();
1465
+ this.resetStreamingState();
1193
1466
  this.emit({ type: "disconnected" });
1194
1467
  }
1195
1468
  /** Stop the job and disconnect (explicit user action). */
@@ -1200,6 +1473,7 @@ var ChatSession = class {
1200
1473
  }
1201
1474
  this.detach();
1202
1475
  this.currentJobId = null;
1476
+ this.protocols.clear();
1203
1477
  }
1204
1478
  async createNewConversation() {
1205
1479
  const id = generateId();
@@ -1210,21 +1484,39 @@ var ChatSession = class {
1210
1484
  this.conversations.unshift(conversation);
1211
1485
  this.conversationId = id;
1212
1486
  this.messages = [];
1213
- this.streamingContent = "";
1214
1487
  return id;
1215
1488
  }
1216
- async switchConversation(id, jobId) {
1489
+ async switchConversation(id, jobId, userMessageContent) {
1217
1490
  this.conversationId = id;
1218
1491
  this.resetStreamingState();
1219
- this.blockBuilder.reset();
1220
1492
  const [messagesResult, eventsResult] = await Promise.allSettled([
1221
1493
  this.client.getMessages(id).catch(() => this.storage.fetchMessages(id)),
1222
1494
  this.client.getConversationEvents(id, jobId)
1223
1495
  ]);
1224
1496
  this.messages = messagesResult.status === "fulfilled" ? messagesResult.value : [];
1225
1497
  if (eventsResult.status === "fulfilled") {
1498
+ let userMessageEmitted = !userMessageContent;
1226
1499
  for (const ev of eventsResult.value) {
1227
- 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
+ }
1228
1520
  }
1229
1521
  }
1230
1522
  }
@@ -1250,420 +1542,46 @@ var ChatSession = class {
1250
1542
  }
1251
1543
  };
1252
1544
 
1253
- // src/standard-handlers.ts
1254
- function finalizeText(builder) {
1255
- if (builder.activeTextId) {
1256
- builder.patchBlock(builder.activeTextId, {
1257
- isStreaming: false
1258
- });
1259
- builder.activeTextId = null;
1260
- }
1261
- }
1262
- function finalizeThinking(builder) {
1263
- if (builder.activeThinkingId) {
1264
- builder.patchBlock(builder.activeThinkingId, {
1265
- isActive: false
1266
- });
1267
- builder.activeThinkingId = null;
1268
- builder.thinkingStartMs = null;
1269
- }
1270
- }
1271
- function finalizeEditor(builder) {
1272
- if (builder.activeEditorId) {
1273
- builder.patchBlock(builder.activeEditorId, {
1274
- isStreaming: false
1275
- });
1276
- builder.activeEditorId = null;
1277
- }
1278
- }
1279
- var handleUserMessage = (event, builder) => {
1280
- const e = event;
1281
- const existing = builder.findBlock((b) => b.type === "user");
1282
- if (existing) {
1283
- if (e.createdAt) {
1284
- builder.patchBlock(existing.id, {
1285
- createdAt: e.createdAt
1286
- });
1287
- }
1288
- return;
1289
- }
1290
- builder.addBlock({
1291
- type: "user",
1292
- id: builder.nextId(),
1293
- content: e.content,
1294
- createdAt: e.createdAt
1295
- });
1296
- };
1297
- var handleChunk = (event, builder) => {
1298
- const e = event;
1299
- if (!builder.activeTextId) {
1300
- const id = builder.nextId();
1301
- builder.activeTextId = id;
1302
- builder.addBlock({
1303
- type: "text",
1304
- id,
1305
- content: e.text,
1306
- isStreaming: true
1307
- });
1308
- } else {
1309
- const id = builder.activeTextId;
1310
- const existing = builder.findBlock((b) => b.id === id);
1311
- if (existing && existing.type === "text") {
1312
- builder.patchBlock(id, {
1313
- content: existing.content + e.text
1314
- });
1315
- }
1316
- }
1317
- };
1318
- var handleToolCall = (event, builder) => {
1319
- const e = event;
1320
- finalizeText(builder);
1321
- builder.addBlock({
1322
- type: "tool",
1323
- id: builder.nextId(),
1324
- callId: e.request.callId,
1325
- toolName: e.request.toolName,
1326
- displayName: e.request.displayName,
1327
- description: e.request.description,
1328
- arguments: e.request.arguments,
1329
- toolCategory: e.request.toolCategory,
1330
- iconUrl: e.request.iconUrl,
1331
- status: "calling"
1332
- });
1333
- };
1334
- var handleToolExecuting = (event, builder) => {
1335
- const e = event;
1336
- const block = builder.findBlock(
1337
- (b) => b.type === "tool" && (e.call_id ? b.callId === e.call_id : b.toolName === e.name) && b.status === "calling"
1338
- );
1339
- if (block) {
1340
- builder.patchBlock(block.id, { status: "executing" });
1341
- }
1342
- };
1343
- var handleToolProgress = (event, builder) => {
1344
- const e = event;
1345
- const block = builder.findBlock(
1346
- (b) => b.type === "tool" && b.callId === e.callId
1347
- );
1348
- if (block && block.type === "tool") {
1349
- const sources = block.sources ? [...block.sources] : [];
1350
- sources.push(e.item);
1351
- builder.patchBlock(block.id, {
1352
- sources,
1353
- status: "executing"
1354
- });
1355
- }
1356
- };
1357
- var handleToolEnd = (event, builder) => {
1358
- const e = event;
1359
- const callId = e.type === "tool_end" ? e.callId : void 0;
1360
- const name = e.type === "tool_end" ? e.toolName : e.name;
1361
- const block = builder.findBlock(
1362
- (b) => b.type === "tool" && (callId ? b.callId === callId : b.toolName === name) && b.status !== "completed"
1363
- );
1364
- if (block) {
1365
- const toolEnd = e.type === "tool_end" ? e : null;
1366
- builder.patchBlock(block.id, {
1367
- status: "completed",
1368
- ...toolEnd?.sources ? { sources: toolEnd.sources } : {},
1369
- ...toolEnd?.durationMs != null ? { durationMs: toolEnd.durationMs } : {},
1370
- ...toolEnd?.result ? { result: toolEnd.result } : {}
1371
- });
1372
- }
1373
- };
1374
- var handleAgentStart = (event, builder) => {
1375
- const e = event;
1376
- finalizeText(builder);
1377
- builder.addBlock({
1378
- type: "agent",
1379
- id: builder.nextId(),
1380
- agentName: e.agentName,
1381
- displayName: e.agentDisplayName,
1382
- avatarUrl: e.avatarUrl
1383
- });
1384
- };
1385
- var handleThinkingDelta = (event, builder) => {
1386
- const e = event;
1387
- if (!builder.activeThinkingId) {
1388
- const id = builder.nextId();
1389
- builder.activeThinkingId = id;
1390
- builder.thinkingStartMs = Date.now();
1391
- builder.addBlock({
1392
- type: "thinking",
1393
- id,
1394
- content: e.text,
1395
- isActive: true
1396
- });
1397
- } else {
1398
- const id = builder.activeThinkingId;
1399
- const existing = builder.findBlock((b) => b.id === id);
1400
- if (existing && existing.type === "thinking") {
1401
- builder.patchBlock(id, {
1402
- content: existing.content + e.text
1403
- });
1404
- }
1405
- }
1406
- };
1407
- var handleThinkingComplete = (_event, builder) => {
1408
- if (builder.activeThinkingId) {
1409
- const durationMs = builder.thinkingStartMs ? Math.max(0, Date.now() - builder.thinkingStartMs) : void 0;
1410
- builder.patchBlock(builder.activeThinkingId, {
1411
- isActive: false,
1412
- durationMs
1413
- });
1414
- builder.activeThinkingId = null;
1415
- builder.thinkingStartMs = null;
1416
- }
1417
- };
1418
- var handleSubagentStart = (event, builder) => {
1419
- const e = event;
1420
- finalizeText(builder);
1421
- builder.addBlock({
1422
- type: "subagent",
1423
- id: builder.nextId(),
1424
- agentName: e.agentName,
1425
- displayName: e.displayName,
1426
- toolCallId: e.toolCallId,
1427
- avatarUrl: e.avatarUrl,
1428
- description: e.description,
1429
- content: "",
1430
- isActive: true
1431
- });
1432
- };
1433
- var handleSubagentChunk = (event, builder) => {
1434
- const e = event;
1435
- const block = builder.findBlock(
1436
- (b) => b.type === "subagent" && b.toolCallId === e.toolCallId
1437
- );
1438
- if (block && block.type === "subagent") {
1439
- builder.patchBlock(block.id, {
1440
- content: block.content + e.text
1441
- });
1442
- }
1443
- };
1444
- var handleSubagentUpdate = (event, builder) => {
1445
- const e = event;
1446
- const block = builder.findBlock(
1447
- (b) => b.type === "subagent" && b.toolCallId === e.toolCallId
1448
- );
1449
- if (block) {
1450
- builder.patchBlock(block.id, {
1451
- displayName: e.displayName
1452
- });
1453
- }
1454
- };
1455
- var handleSubagentEnd = (event, builder) => {
1456
- const e = event;
1457
- const block = builder.findBlock(
1458
- (b) => b.type === "subagent" && b.toolCallId === e.toolCallId
1459
- );
1460
- if (block) {
1461
- builder.patchBlock(block.id, {
1462
- isActive: false
1463
- });
1464
- }
1465
- };
1466
- var handleComplete = (_event, builder) => {
1467
- finalizeText(builder);
1468
- finalizeThinking(builder);
1469
- finalizeEditor(builder);
1470
- for (const b of builder.getBlocks()) {
1471
- if (b.type === "tool" && b.status !== "completed") {
1472
- builder.patchBlock(b.id, { status: "completed" });
1473
- }
1474
- }
1475
- };
1476
- var handleError = (event, builder) => {
1477
- const e = event;
1478
- finalizeText(builder);
1479
- finalizeThinking(builder);
1480
- builder.addBlock({
1481
- type: "error",
1482
- id: builder.nextId(),
1483
- message: e.error.message
1484
- });
1485
- };
1486
- var handleDisconnected = (_event, builder) => {
1487
- finalizeText(builder);
1488
- finalizeThinking(builder);
1489
- finalizeEditor(builder);
1490
- };
1491
- var handleCapsuleOutputChunk = (event, builder) => {
1492
- const e = event;
1493
- const block = builder.findBlock(
1494
- (b) => b.type === "capsule" && b.callId === e.callId
1495
- );
1496
- if (block && block.type === "capsule") {
1497
- builder.patchBlock(block.id, {
1498
- output: block.output + e.chunk
1499
- });
1500
- } else {
1501
- builder.addBlock({
1502
- type: "capsule",
1503
- id: builder.nextId(),
1504
- callId: e.callId,
1505
- toolName: "",
1506
- output: e.chunk,
1507
- isActive: true
1508
- });
1509
- }
1510
- };
1511
- var handleCapsuleOutput = (event, builder) => {
1512
- const e = event;
1513
- const block = builder.findBlock(
1514
- (b) => b.type === "capsule" && b.callId === (e.callId ?? "")
1515
- );
1516
- if (block) {
1517
- builder.patchBlock(block.id, {
1518
- output: e.output,
1519
- command: e.command,
1520
- toolName: e.toolName,
1521
- durationMs: e.durationMs,
1522
- isActive: false
1523
- });
1524
- } else {
1525
- builder.addBlock({
1526
- type: "capsule",
1527
- id: builder.nextId(),
1528
- callId: e.callId ?? "",
1529
- toolName: e.toolName,
1530
- command: e.command,
1531
- output: e.output,
1532
- durationMs: e.durationMs,
1533
- isActive: false
1534
- });
1535
- }
1536
- };
1537
- var handleAssetCreated = (event, builder) => {
1538
- const e = event;
1539
- builder.addBlock({
1540
- type: "asset",
1541
- id: builder.nextId(),
1542
- assetId: e.assetId,
1543
- name: e.name,
1544
- url: e.url,
1545
- mediaType: e.mediaType,
1546
- sizeBytes: e.sizeBytes
1547
- });
1548
- };
1549
- var handleTodoUpdate = (event, builder) => {
1550
- const e = event;
1551
- if (builder.activeTodoId) {
1552
- builder.patchBlock(builder.activeTodoId, {
1553
- todos: e.todos
1554
- });
1555
- } else {
1556
- const id = builder.nextId();
1557
- builder.activeTodoId = id;
1558
- builder.addBlock({
1559
- type: "todo",
1560
- id,
1561
- todos: e.todos
1562
- });
1563
- }
1564
- };
1565
- var handleEditorContentStart = (event, builder) => {
1566
- const e = event;
1567
- const id = builder.nextId();
1568
- builder.activeEditorId = id;
1569
- builder.addBlock({
1570
- type: "editor",
1571
- id,
1572
- callId: e.callId,
1573
- path: e.path,
1574
- language: e.language,
1575
- content: "",
1576
- isStreaming: true
1577
- });
1578
- };
1579
- var handleEditorContentDelta = (event, builder) => {
1580
- const e = event;
1581
- const block = builder.findBlock(
1582
- (b) => b.type === "editor" && b.callId === e.callId
1583
- );
1584
- if (block && block.type === "editor") {
1585
- builder.patchBlock(block.id, {
1586
- content: block.content + e.delta
1587
- });
1588
- }
1589
- };
1590
- var handleEditorContentEnd = (event, builder) => {
1591
- const e = event;
1592
- const block = builder.findBlock(
1593
- (b) => b.type === "editor" && b.callId === e.callId
1594
- );
1595
- if (block) {
1596
- builder.patchBlock(block.id, {
1597
- isStreaming: false
1598
- });
1599
- }
1600
- builder.activeEditorId = null;
1601
- };
1602
- var noop = () => {
1603
- };
1604
- var standardHandlers = {
1605
- user_message: handleUserMessage,
1606
- chunk: handleChunk,
1607
- tool_call: handleToolCall,
1608
- tool_executing: handleToolExecuting,
1609
- tool_progress: handleToolProgress,
1610
- tool_completed: handleToolEnd,
1611
- tool_end: handleToolEnd,
1612
- agent_start: handleAgentStart,
1613
- agent_end: noop,
1614
- thinking_delta: handleThinkingDelta,
1615
- thinking_complete: handleThinkingComplete,
1616
- subagent_start: handleSubagentStart,
1617
- subagent_chunk: handleSubagentChunk,
1618
- subagent_update: handleSubagentUpdate,
1619
- subagent_end: handleSubagentEnd,
1620
- subagent_tool_use: noop,
1621
- capsule_output: handleCapsuleOutput,
1622
- capsule_output_chunk: handleCapsuleOutputChunk,
1623
- asset_created: handleAssetCreated,
1624
- todo_update: handleTodoUpdate,
1625
- editor_content_start: handleEditorContentStart,
1626
- editor_content_delta: handleEditorContentDelta,
1627
- editor_content_end: handleEditorContentEnd,
1628
- retry: noop,
1629
- complete: handleComplete,
1630
- error: handleError,
1631
- disconnected: handleDisconnected
1632
- };
1633
-
1634
1545
  // src/types.ts
1635
1546
  var ChatEventType = {
1547
+ // Connection lifecycle (SDK-local, not wire)
1636
1548
  Connected: "connected",
1637
- 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)
1638
1563
  UserMessage: "user_message",
1639
1564
  TitleGenerated: "title_generated",
1640
- ModelInfo: "model_info",
1641
- Chunk: "chunk",
1642
- ToolCall: "tool_call",
1643
- ToolExecuting: "tool_executing",
1644
- ToolProgress: "tool_progress",
1645
- ToolCompleted: "tool_completed",
1646
- ToolEnd: "tool_end",
1647
- AgentStart: "agent_start",
1648
- AgentEnd: "agent_end",
1649
- ThinkingDelta: "thinking_delta",
1650
- ThinkingComplete: "thinking_complete",
1565
+ TodoUpdate: "todo_update",
1566
+ ContextUpdate: "context_update",
1651
1567
  SubagentStart: "subagent_start",
1652
- SubagentChunk: "subagent_chunk",
1653
- SubagentUpdate: "subagent_update",
1654
- SubagentEnd: "subagent_end",
1655
- SubagentToolUse: "subagent_tool_use",
1656
- CapsuleOutput: "capsule_output",
1657
- CapsuleOutputChunk: "capsule_output_chunk",
1568
+ SubagentStop: "subagent_stop",
1569
+ ContextWarning: "context_warning",
1570
+ MemoryRecall: "memory_recall",
1571
+ MemoryUpdate: "memory_update",
1572
+ DesktopStream: "desktop_stream",
1573
+ AttachmentStaged: "attachment_staged",
1574
+ WorkspaceReady: "workspace_ready",
1658
1575
  AssetCreated: "asset_created",
1659
- TodoUpdate: "todo_update",
1660
- EditorContentStart: "editor_content_start",
1661
- EditorContentDelta: "editor_content_delta",
1662
- EditorContentEnd: "editor_content_end",
1663
- Complete: "complete",
1664
- Error: "error",
1665
- Disconnected: "disconnected",
1666
- Retry: "retry"
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"
1667
1585
  };
1668
1586
 
1669
1587
  // src/stream-manager.ts
@@ -1718,22 +1636,12 @@ var StreamManager = class {
1718
1636
  }
1719
1637
  onSessionEvent(event) {
1720
1638
  const convId = this.session.conversationId;
1721
- if (event.type === ChatEventType.BlocksChanged) {
1722
- if (this._state === "streaming" && convId) {
1723
- this.emit({
1724
- type: "blocksChanged",
1725
- conversationId: convId,
1726
- blocks: event.blocks
1727
- });
1728
- }
1729
- return;
1730
- }
1731
1639
  this.emit({
1732
1640
  type: "event",
1733
1641
  conversationId: convId,
1734
1642
  event
1735
1643
  });
1736
- if (event.type === ChatEventType.Complete) {
1644
+ if (event.type === ChatEventType.MessageStop) {
1737
1645
  if (this._state === "streaming") {
1738
1646
  this.setState("idle");
1739
1647
  }
@@ -1746,13 +1654,13 @@ var StreamManager = class {
1746
1654
  const id = await this.session.createNewConversation();
1747
1655
  this.setActiveConversation(id);
1748
1656
  }
1749
- this.prepareUserBlock(content);
1750
1657
  this.setState("streaming");
1751
1658
  try {
1752
1659
  await this.session.send(content, {
1753
1660
  enableSearch: options?.enableSearch,
1754
1661
  agentName: options?.agentName,
1755
- uploadIds: options?.uploadIds
1662
+ uploadIds: options?.uploadIds,
1663
+ planMode: options?.planMode
1756
1664
  });
1757
1665
  } catch {
1758
1666
  }
@@ -1766,7 +1674,6 @@ var StreamManager = class {
1766
1674
  );
1767
1675
  const lastUserMsg = userMsgs[userMsgs.length - 1];
1768
1676
  if (!lastUserMsg) return;
1769
- this.prepareUserBlock(lastUserMsg.content);
1770
1677
  this.setState("streaming");
1771
1678
  try {
1772
1679
  await this.session.resendFromCheckpoint(
@@ -1830,14 +1737,6 @@ var StreamManager = class {
1830
1737
  this.handlers = [];
1831
1738
  }
1832
1739
  // ── Internal: helpers ──────────────────────────────────────────
1833
- prepareUserBlock(content) {
1834
- this.session.blockBuilder.reset();
1835
- this.session.blockBuilder.addBlock({
1836
- type: "user",
1837
- id: this.session.blockBuilder.nextId(),
1838
- content
1839
- });
1840
- }
1841
1740
  finalizeStream() {
1842
1741
  if (this._state === "streaming") {
1843
1742
  this.setState("idle");
@@ -1848,8 +1747,8 @@ var StreamManager = class {
1848
1747
  this.setState("restoring");
1849
1748
  let activeJobId = null;
1850
1749
  try {
1851
- const res = await this.session.client.get(`/v1/conversations/${encodeURIComponent(conversationId)}/active-job`);
1852
- activeJobId = res.job_id ?? null;
1750
+ const res = await this.session.client.getActiveJob(conversationId);
1751
+ activeJobId = res.jobId;
1853
1752
  } catch {
1854
1753
  }
1855
1754
  if (activeJobId) {
@@ -1869,15 +1768,19 @@ var StreamManager = class {
1869
1768
  const completedJobs = jobs.filter(
1870
1769
  (j) => j.status === "completed"
1871
1770
  );
1872
- for (const job of completedJobs) {
1873
- 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
+ );
1874
1782
  }
1875
1783
  if (completedJobs.length > 0) {
1876
- this.emit({
1877
- type: "blocksChanged",
1878
- conversationId,
1879
- blocks: this.session.blockBuilder.getBlocks()
1880
- });
1881
1784
  this.emit({
1882
1785
  type: "versionsReady",
1883
1786
  conversationId,
@@ -1895,23 +1798,93 @@ var StreamManager = class {
1895
1798
  this.emit({ type: "conversationChanged", conversationId: id });
1896
1799
  }
1897
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
+ }
1898
1867
  export {
1899
1868
  AstralformClient,
1900
1869
  AstralformError,
1901
1870
  AuthenticationError,
1902
- BlockBuilder,
1903
1871
  ChatEventType,
1904
1872
  ChatSession,
1905
1873
  ConnectionError,
1906
1874
  InMemoryStorage,
1907
1875
  LLMNotConfiguredError,
1876
+ ProtocolRegistry,
1908
1877
  RateLimitError,
1909
1878
  ServerError,
1910
1879
  StreamAbortedError,
1911
1880
  StreamManager,
1912
1881
  ToolRegistry,
1913
1882
  generateId,
1914
- standardHandlers,
1915
- streamJobSSE
1883
+ isEmbeddedResource,
1884
+ mapSseToChat,
1885
+ parseEmbeddedResource,
1886
+ replayEvents,
1887
+ streamJobSSE,
1888
+ translateDelta
1916
1889
  };
1917
1890
  //# sourceMappingURL=index.js.map