@astralform/js 9.4.0 → 9.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1134,7 +1134,11 @@ var AstralformClient = class {
1134
1134
  // A mode this SDK does not know must not reach a `switch` typed as
1135
1135
  // `VoicePolishMode`; `structured` is the server's own default.
1136
1136
  defaultMode: isVoicePolishMode(raw.default_mode) ? raw.default_mode : "structured",
1137
- silenceAutoStopSeconds: raw.silence_auto_stop_seconds ?? 2,
1137
+ // Deliberately not read from the wire. The field was retired from the
1138
+ // platform (2026-10-09): no shipped client implements pause detection
1139
+ // and the server no longer sends it, so an older server's stored value
1140
+ // must not resurrect it.
1141
+ silenceAutoStopSeconds: 2,
1138
1142
  // Deliberately not read from the wire. Auto-send was removed from the
1139
1143
  // platform (2026-09-14) and the server pins `auto_send` to false; an
1140
1144
  // older server still sending `true` must not make a client auto-send, so
@@ -1541,13 +1545,29 @@ function translateDelta(wire) {
1541
1545
  }
1542
1546
  }
1543
1547
  function translateAgentIdentity(raw) {
1548
+ const source = raw !== null && typeof raw === "object" ? { ...raw } : {};
1549
+ const read = (key) => source[key];
1544
1550
  return {
1545
- name: raw.name ?? "",
1546
- displayName: raw.display_name ?? null,
1547
- avatarUrl: raw.avatar_url ?? null,
1548
- description: raw.description ?? null
1551
+ name: read("name") ?? "",
1552
+ displayName: read("display_name") ?? null,
1553
+ avatarUrl: read("avatar_url") ?? null,
1554
+ description: read("description") ?? null
1549
1555
  };
1550
1556
  }
1557
+ function agentIdentityFromEvent(data) {
1558
+ if ("agent" in data) return translateAgentIdentity(data.agent);
1559
+ return translateAgentIdentity({
1560
+ name: data.agent_name,
1561
+ display_name: data.agent_display_name,
1562
+ avatar_url: data.agent_avatar_url
1563
+ });
1564
+ }
1565
+ function optionalString(value) {
1566
+ return typeof value === "string" ? value : void 0;
1567
+ }
1568
+ function optionalNumber(value) {
1569
+ return typeof value === "number" ? value : void 0;
1570
+ }
1551
1571
  function translateTodoItem(raw) {
1552
1572
  return {
1553
1573
  id: raw.id ?? 0,
@@ -1598,21 +1618,33 @@ function translateCustomEvent(name, data) {
1598
1618
  phase: data.phase ?? null,
1599
1619
  updatedAt: data.updated_at ?? null
1600
1620
  };
1621
+ // Two names for one event: the backend renamed these to `agent_*` (a nested
1622
+ // run is not necessarily a "subagent" in the product's vocabulary) while
1623
+ // older servers still emit `subagent_*`. Both are translated, so a client
1624
+ // works across the rename.
1625
+ case "agent_start":
1601
1626
  case "subagent_start":
1602
1627
  return {
1603
1628
  type: "subagent_start",
1604
- agent: translateAgentIdentity(
1605
- data.agent ?? {}
1606
- ),
1607
- taskCallId: data.task_call_id ?? null
1629
+ runId: optionalString(data.run_id),
1630
+ agent: agentIdentityFromEvent(data),
1631
+ // `parent_call_id` is the spawn block's call_id — the same key a client
1632
+ // uses to find the tool block whose subtree is this child's work.
1633
+ taskCallId: optionalString(data.parent_call_id) ?? optionalString(data.task_call_id) ?? null
1608
1634
  };
1635
+ case "agent_stop":
1609
1636
  case "subagent_stop":
1610
1637
  return {
1611
1638
  type: "subagent_stop",
1612
- agent: translateAgentIdentity(
1613
- data.agent ?? {}
1614
- ),
1615
- taskCallId: data.task_call_id ?? null
1639
+ runId: optionalString(data.run_id),
1640
+ agent: agentIdentityFromEvent(data),
1641
+ taskCallId: optionalString(data.parent_call_id) ?? optionalString(data.task_call_id) ?? null,
1642
+ // The child's OWN spend and outcome, which the wire surfaces separately
1643
+ // from the turn totals (`message_stop.usage` folds them in too).
1644
+ stopReason: optionalString(data.stop_reason),
1645
+ inputTokens: optionalNumber(data.input_tokens),
1646
+ outputTokens: optionalNumber(data.output_tokens),
1647
+ durationMs: optionalNumber(data.duration_ms)
1616
1648
  };
1617
1649
  case "context_warning":
1618
1650
  return {
@@ -1791,22 +1823,24 @@ function translateWireEvent(wire) {
1791
1823
  status: wire.status,
1792
1824
  final: wire.final
1793
1825
  };
1794
- case "message_stop":
1826
+ case "message_stop": {
1827
+ const usage = wire.usage ?? {};
1795
1828
  return {
1796
1829
  type: "message_stop",
1797
1830
  turnId: wire.turn_id,
1798
1831
  jobId: wire.job_id,
1799
1832
  stopReason: wire.stop_reason,
1800
1833
  usage: {
1801
- inputTokens: wire.usage.input_tokens ?? 0,
1802
- outputTokens: wire.usage.output_tokens ?? 0,
1803
- cachedTokens: wire.usage.cached_tokens ?? 0,
1804
- cacheCreationTokens: wire.usage.cache_creation_tokens ?? 0
1834
+ inputTokens: usage.input_tokens ?? 0,
1835
+ outputTokens: usage.output_tokens ?? 0,
1836
+ cachedTokens: usage.cached_tokens ?? 0,
1837
+ cacheCreationTokens: usage.cache_creation_tokens ?? 0
1805
1838
  },
1806
1839
  ttfbMs: wire.ttfb_ms,
1807
1840
  totalMs: wire.total_ms,
1808
1841
  stallCount: wire.stall_count
1809
1842
  };
1843
+ }
1810
1844
  case "stall":
1811
1845
  return {
1812
1846
  type: "stall",
@@ -1860,6 +1894,11 @@ function pathEquals(a, b) {
1860
1894
  }
1861
1895
  return true;
1862
1896
  }
1897
+ function frameSeq(frame) {
1898
+ if (typeof frame !== "object" || frame === null) return void 0;
1899
+ if (!("seq" in frame)) return void 0;
1900
+ return frame.seq;
1901
+ }
1863
1902
  var ChatSession = class {
1864
1903
  constructor(config, storage) {
1865
1904
  /**
@@ -2158,7 +2197,7 @@ var ChatSession = class {
2158
2197
  if (!(err instanceof DOMException && err.name === "AbortError")) {
2159
2198
  this.emit({
2160
2199
  type: "error",
2161
- code: "connection_error",
2200
+ code: err instanceof AstralformError ? err.code : "connection_error",
2162
2201
  message: err instanceof Error ? err.message : String(err),
2163
2202
  blockPath: null
2164
2203
  });
@@ -2170,6 +2209,37 @@ var ChatSession = class {
2170
2209
  }
2171
2210
  }
2172
2211
  }
2212
+ /**
2213
+ * Advance the resume cursor from a frame's `seq` — the position the next
2214
+ * reconnect asks the backend to replay after (`?after=`).
2215
+ *
2216
+ * Two guards, because getting this wrong is silent and expensive: the whole
2217
+ * turn replays over blocks the consumer is already rendering.
2218
+ *
2219
+ * - **A keepalive is not an event.** The backend sends it as an ordinary
2220
+ * `data:` frame with `seq: 0` BY DESIGN (``WireKeepalive(seq=0, ts=0.0)``
2221
+ * — an idle tick, not a position), and it arrives exactly when a turn has
2222
+ * been quiet for 45 s: the long-tool-run case. Harvesting it rewound the
2223
+ * cursor to 0, so a reconnect after a blip replayed the turn from its
2224
+ * start — deltas and all.
2225
+ * - **The cursor only advances.** `send` and `reconnectToJob` reset it to
2226
+ * -1 per job, so a new job's `seq: 0` still moves it forward; a frame
2227
+ * carrying an older (or zero) position cannot move it back.
2228
+ *
2229
+ * The one other frame on the wire with a non-position `seq` is the
2230
+ * subscriber's SYNTHETIC terminal — a back-filled `error` for a job whose
2231
+ * worker died, built without a `seq` so Pydantic's default `0` reaches the
2232
+ * wire. Deliberately not named here: the advance-only rule already ignores it
2233
+ * whenever the client has drained anything (0 never exceeds a real cursor),
2234
+ * and when it is the FIRST frame the pump ends on `sawTerminal` before any
2235
+ * reconnect could use the cursor. Naming it would add a rule that no
2236
+ * reachable input exercises.
2237
+ */
2238
+ harvestResumeCursor(seq, type) {
2239
+ if (type === "keepalive") return;
2240
+ if (typeof seq !== "number") return;
2241
+ if (seq > this.lastSeq) this.lastSeq = seq;
2242
+ }
2173
2243
  async consumeJobStream(request, wire) {
2174
2244
  const job = await this.client.createJob(request);
2175
2245
  if (wire) wire.reached = true;
@@ -2307,15 +2377,14 @@ var ChatSession = class {
2307
2377
  try {
2308
2378
  const data = JSON.parse(raw.data);
2309
2379
  if (typeof data !== "object" || data === null || typeof data.type !== "string") {
2310
- if (typeof data?.seq === "number") {
2311
- this.lastSeq = data.seq;
2380
+ this.harvestResumeCursor(frameSeq(data), void 0);
2381
+ if (typeof data === "object" && data !== null && "data" in data && data.data === "[DONE]") {
2382
+ sawTerminal = true;
2312
2383
  }
2313
2384
  continue;
2314
2385
  }
2315
2386
  parsed = data;
2316
- if (typeof data.seq === "number") {
2317
- this.lastSeq = data.seq;
2318
- }
2387
+ this.harvestResumeCursor(frameSeq(data), parsed.type);
2319
2388
  } catch {
2320
2389
  continue;
2321
2390
  }
@@ -2439,7 +2508,7 @@ var ChatSession = class {
2439
2508
  }
2440
2509
  return;
2441
2510
  case "block_delta":
2442
- if (ownsTurnState && wire.delta.channel === "text" && this.currentTextPath !== null && pathEquals(this.currentTextPath, wire.path)) {
2511
+ if (ownsTurnState && wire.delta?.channel === "text" && this.currentTextPath !== null && pathEquals(this.currentTextPath, wire.path)) {
2443
2512
  this.accumulatedText += wire.delta.text;
2444
2513
  }
2445
2514
  return;
@@ -2544,11 +2613,19 @@ var ChatSession = class {
2544
2613
  * Reconnect to a running job's SSE stream (e.g. after page reload).
2545
2614
  * Replays all events from the beginning and continues live.
2546
2615
  */
2547
- async reconnectToJob(jobId) {
2616
+ /**
2617
+ * @param afterSeq Resume the job's event stream AFTER this sequence number.
2618
+ * Omitted (the reconnect default) replays the job from the beginning. A
2619
+ * caller that holds the job's events up to `afterSeq` — the transcript
2620
+ * cache — passes it so the backend replays only the delta and then goes
2621
+ * live; the backend's `?after=` handles a job that already ended by
2622
+ * back-filling its terminal event.
2623
+ */
2624
+ async reconnectToJob(jobId, afterSeq) {
2548
2625
  if (this.isStreaming) return;
2549
2626
  this.isStreaming = true;
2550
2627
  this.currentJobId = jobId;
2551
- this.lastSeq = -1;
2628
+ this.lastSeq = afterSeq ?? -1;
2552
2629
  this.submittedToolCallIds.clear();
2553
2630
  this.resetStreamingState();
2554
2631
  const controller = new AbortController();
@@ -2839,7 +2916,10 @@ function planRestore(args) {
2839
2916
  messageId: msgs[i]?.id
2840
2917
  }));
2841
2918
  if (jobs.length === 0) {
2842
- return userMessages.map((m) => ({
2919
+ const held = new Set(
2920
+ (args.heldMessageIds ?? []).filter((id) => !!id)
2921
+ );
2922
+ return userMessages.filter((m) => !(m.id && held.has(m.id))).map((m) => ({
2843
2923
  kind: "steer",
2844
2924
  content: m.content,
2845
2925
  messageId: m.id
@@ -3118,7 +3198,7 @@ var StreamManager = class _StreamManager {
3118
3198
  }
3119
3199
  let tookOver = false;
3120
3200
  try {
3121
- await this.restore(conversationId, gen);
3201
+ await this.restore(conversationId, gen, opts?.have);
3122
3202
  tookOver = gen === this.generation;
3123
3203
  } finally {
3124
3204
  const supersededOntoSame = gen !== this.generation && this._activeConversationId === conversationId;
@@ -3336,7 +3416,10 @@ var StreamManager = class _StreamManager {
3336
3416
  ...before ? { before } : {}
3337
3417
  });
3338
3418
  }
3339
- async restore(conversationId, gen) {
3419
+ async restore(conversationId, gen, have) {
3420
+ const haveByJobId = new Map(
3421
+ (have ?? []).map((h) => [h.jobId, h.lastSeq])
3422
+ );
3340
3423
  const superseded = () => gen !== this.generation;
3341
3424
  if (superseded()) return;
3342
3425
  const turn = this.turnCounter;
@@ -3363,7 +3446,8 @@ var StreamManager = class _StreamManager {
3363
3446
  gen,
3364
3447
  activeJobId,
3365
3448
  turn,
3366
- jobsRequest
3449
+ jobsRequest,
3450
+ haveByJobId
3367
3451
  ))
3368
3452
  return;
3369
3453
  if (activeJobId) {
@@ -3560,7 +3644,7 @@ var StreamManager = class _StreamManager {
3560
3644
  session.isLoadingEarlierTurns = false;
3561
3645
  }
3562
3646
  }
3563
- async replayHistory(conversationId, gen, activeJobId, turn, jobsRequest) {
3647
+ async replayHistory(conversationId, gen, activeJobId, turn, jobsRequest, haveByJobId) {
3564
3648
  const stopReplay = () => gen !== this.generation || this.viewTakenOverByLiveTurn() || this.turnStarted(turn);
3565
3649
  let complete = false;
3566
3650
  try {
@@ -3568,7 +3652,7 @@ var StreamManager = class _StreamManager {
3568
3652
  const jobs = page.jobs;
3569
3653
  if (stopReplay()) return false;
3570
3654
  const replayableJobs = jobs.filter(
3571
- (j) => j.job_id !== activeJobId
3655
+ (j) => j.job_id !== activeJobId && !haveByJobId.has(j.job_id)
3572
3656
  );
3573
3657
  const userMessages = this.session.messages.filter(
3574
3658
  (m) => m.role === "user"
@@ -3602,6 +3686,12 @@ var StreamManager = class _StreamManager {
3602
3686
  // them, silently. Deriving both from `jobs` makes that impossible to
3603
3687
  // get half-right.
3604
3688
  claimedMessageIds: jobs.map((j) => j.message_id),
3689
+ // The prompts whose turn the CALLER holds (see `switchTo`'s `have`).
3690
+ // The walk below is empty for exactly those jobs — which is the
3691
+ // primary cache scenario, a SETTLED conversation whose turns are all
3692
+ // held — and the empty-walk rescue would otherwise re-emit every one of
3693
+ // them as a steer bubble.
3694
+ heldMessageIds: jobs.filter((j) => haveByJobId.has(j.job_id)).map((j) => j.message_id),
3605
3695
  userMessages: userMessages.map((m) => ({
3606
3696
  id: m.id,
3607
3697
  content: m.content