@astralform/js 4.3.0 → 4.4.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.d.cts CHANGED
@@ -420,6 +420,12 @@ type ChatEvent = {
420
420
  type: "user_message";
421
421
  content: string;
422
422
  createdAt?: number;
423
+ id?: string;
424
+ /** This message was steered into a run already in flight — it started
425
+ * no turn of its own. Consumers that badge a steer (a "waiting to be
426
+ * read" notice) key off this; without it a replayed steer is
427
+ * indistinguishable from an ordinary prompt. */
428
+ steer?: boolean;
423
429
  } | {
424
430
  type: "title_generated";
425
431
  title: string;
@@ -1139,7 +1145,7 @@ declare class ChatSession {
1139
1145
  * ``message_start`` in the stream (e.g. ``memory_recall`` from prompt prep),
1140
1146
  * so leading with the prompt keeps the turn in order.
1141
1147
  */
1142
- replayTurn(id: string, events: ConversationEvent[], userMessageContent?: string): void;
1148
+ replayTurn(id: string, events: ConversationEvent[], userMessageContent?: string, userMessageId?: string, isSteer?: boolean): void;
1143
1149
  /**
1144
1150
  * Load a conversation's messages and replay its persisted history.
1145
1151
  *
@@ -1236,26 +1242,6 @@ declare function generateId(): string;
1236
1242
  */
1237
1243
  declare function streamJobSSE(options: StreamJobSSEOptions): AsyncGenerator<ChatStreamEvent>;
1238
1244
 
1239
- /**
1240
- * StreamManager — high-level conversation lifecycle coordinator.
1241
- *
1242
- * Sits on top of ChatSession and manages the state machine for
1243
- * multi-conversation SSE streaming. Framework-agnostic: emits
1244
- * typed events to registered handlers. Block construction is NOT
1245
- * the SDK's concern — consumers build their own block tree from
1246
- * the forwarded ``ChatEvent`` instances.
1247
- *
1248
- * import { ChatSession, StreamManager } from "@astralform/js";
1249
- * const session = new ChatSession({ ... });
1250
- * const manager = new StreamManager(session);
1251
- * manager.on((event) => {
1252
- * if (event.type === "event") {
1253
- * // event.event is a typed ChatEvent — dispatch to your reducer
1254
- * }
1255
- * });
1256
- * await manager.send("Hello");
1257
- */
1258
-
1259
1245
  type StreamState = "idle" | "streaming" | "restoring" | "detached";
1260
1246
  interface SendOptions extends ModelChoiceOptions {
1261
1247
  agentName?: string;
package/dist/index.d.ts CHANGED
@@ -420,6 +420,12 @@ type ChatEvent = {
420
420
  type: "user_message";
421
421
  content: string;
422
422
  createdAt?: number;
423
+ id?: string;
424
+ /** This message was steered into a run already in flight — it started
425
+ * no turn of its own. Consumers that badge a steer (a "waiting to be
426
+ * read" notice) key off this; without it a replayed steer is
427
+ * indistinguishable from an ordinary prompt. */
428
+ steer?: boolean;
423
429
  } | {
424
430
  type: "title_generated";
425
431
  title: string;
@@ -1139,7 +1145,7 @@ declare class ChatSession {
1139
1145
  * ``message_start`` in the stream (e.g. ``memory_recall`` from prompt prep),
1140
1146
  * so leading with the prompt keeps the turn in order.
1141
1147
  */
1142
- replayTurn(id: string, events: ConversationEvent[], userMessageContent?: string): void;
1148
+ replayTurn(id: string, events: ConversationEvent[], userMessageContent?: string, userMessageId?: string, isSteer?: boolean): void;
1143
1149
  /**
1144
1150
  * Load a conversation's messages and replay its persisted history.
1145
1151
  *
@@ -1236,26 +1242,6 @@ declare function generateId(): string;
1236
1242
  */
1237
1243
  declare function streamJobSSE(options: StreamJobSSEOptions): AsyncGenerator<ChatStreamEvent>;
1238
1244
 
1239
- /**
1240
- * StreamManager — high-level conversation lifecycle coordinator.
1241
- *
1242
- * Sits on top of ChatSession and manages the state machine for
1243
- * multi-conversation SSE streaming. Framework-agnostic: emits
1244
- * typed events to registered handlers. Block construction is NOT
1245
- * the SDK's concern — consumers build their own block tree from
1246
- * the forwarded ``ChatEvent`` instances.
1247
- *
1248
- * import { ChatSession, StreamManager } from "@astralform/js";
1249
- * const session = new ChatSession({ ... });
1250
- * const manager = new StreamManager(session);
1251
- * manager.on((event) => {
1252
- * if (event.type === "event") {
1253
- * // event.event is a typed ChatEvent — dispatch to your reducer
1254
- * }
1255
- * });
1256
- * await manager.send("Hello");
1257
- */
1258
-
1259
1245
  type StreamState = "idle" | "streaming" | "restoring" | "detached";
1260
1246
  interface SendOptions extends ModelChoiceOptions {
1261
1247
  agentName?: string;
package/dist/index.js CHANGED
@@ -1773,11 +1773,16 @@ var ChatSession = class {
1773
1773
  * ``message_start`` in the stream (e.g. ``memory_recall`` from prompt prep),
1774
1774
  * so leading with the prompt keeps the turn in order.
1775
1775
  */
1776
- replayTurn(id, events, userMessageContent) {
1776
+ replayTurn(id, events, userMessageContent, userMessageId, isSteer = false) {
1777
1777
  this.conversationId = id;
1778
1778
  this.resetStreamingState();
1779
1779
  if (userMessageContent) {
1780
- this.emit({ type: "user_message", content: userMessageContent });
1780
+ this.emit({
1781
+ type: "user_message",
1782
+ content: userMessageContent,
1783
+ ...userMessageId ? { id: userMessageId } : {},
1784
+ ...isSteer ? { steer: true } : {}
1785
+ });
1781
1786
  }
1782
1787
  for (const ev of events) {
1783
1788
  const type = ev.data.type || ev.event;
@@ -1890,6 +1895,64 @@ var ChatSession = class {
1890
1895
  }
1891
1896
  };
1892
1897
 
1898
+ // src/restore-plan.ts
1899
+ function planRestore(args) {
1900
+ const { completedJobs, userMessages } = args;
1901
+ const byId = /* @__PURE__ */ new Map();
1902
+ userMessages.forEach((m, i) => {
1903
+ if (m.id) byId.set(m.id, i);
1904
+ });
1905
+ const linkOf = (j) => j.message_id ? byId.get(j.message_id) : void 0;
1906
+ const positional = (jobs, msgs) => jobs.map((job, i) => ({
1907
+ kind: "turn",
1908
+ jobId: job.job_id,
1909
+ content: msgs[i]?.content,
1910
+ messageId: msgs[i]?.id
1911
+ }));
1912
+ const firstLinked = completedJobs.findIndex((j) => linkOf(j) !== void 0);
1913
+ if (firstLinked === -1) {
1914
+ return positional(completedJobs, userMessages);
1915
+ }
1916
+ const cutover = linkOf(completedJobs[firstLinked]);
1917
+ const steps = positional(
1918
+ completedJobs.slice(0, firstLinked),
1919
+ userMessages.slice(0, cutover)
1920
+ );
1921
+ let cursor = cutover;
1922
+ const isSteer = (m) => !!m?.id && !completedJobs.some((j) => j.message_id === m.id);
1923
+ const drainTo = (stopAt) => {
1924
+ while (cursor < stopAt) {
1925
+ const m = userMessages[cursor++];
1926
+ if (isSteer(m)) {
1927
+ steps.push({ kind: "steer", content: m.content, messageId: m.id });
1928
+ }
1929
+ }
1930
+ cursor = stopAt + 1;
1931
+ };
1932
+ for (const job of completedJobs.slice(firstLinked)) {
1933
+ const at = linkOf(job);
1934
+ if (at !== void 0) {
1935
+ drainTo(at);
1936
+ const prompt = userMessages[at];
1937
+ steps.push({
1938
+ kind: "turn",
1939
+ jobId: job.job_id,
1940
+ content: prompt.content,
1941
+ messageId: prompt.id
1942
+ });
1943
+ continue;
1944
+ }
1945
+ steps.push({ kind: "turn", jobId: job.job_id });
1946
+ }
1947
+ for (let i = cursor; i < userMessages.length; i++) {
1948
+ const m = userMessages[i];
1949
+ if (isSteer(m)) {
1950
+ steps.push({ kind: "steer", content: m.content, messageId: m.id });
1951
+ }
1952
+ }
1953
+ return steps;
1954
+ }
1955
+
1893
1956
  // src/types.ts
1894
1957
  var ChatEventType = {
1895
1958
  // Connection lifecycle (SDK-local, not wire)
@@ -2158,16 +2221,40 @@ var StreamManager = class {
2158
2221
  const userMessages = this.session.messages.filter(
2159
2222
  (m) => m.role === "user"
2160
2223
  );
2224
+ const plan = planRestore({
2225
+ completedJobs: completedJobs.map((j) => ({
2226
+ job_id: j.job_id,
2227
+ message_id: j.message_id
2228
+ })),
2229
+ userMessages: userMessages.map((m) => ({
2230
+ id: m.id,
2231
+ content: m.content
2232
+ }))
2233
+ });
2161
2234
  const eventLists = await Promise.all(
2162
2235
  completedJobs.map(
2163
2236
  (job) => this.session.client.getConversationEvents(conversationId, job.job_id).catch(() => [])
2164
2237
  )
2165
2238
  );
2166
- for (let i = 0; i < completedJobs.length; i++) {
2239
+ const eventsByJobId = new Map(
2240
+ completedJobs.map((job, i) => [job.job_id, eventLists[i] ?? []])
2241
+ );
2242
+ for (const step of plan) {
2243
+ if (step.kind === "steer") {
2244
+ this.session.replayTurn(
2245
+ conversationId,
2246
+ [],
2247
+ step.content,
2248
+ step.messageId,
2249
+ true
2250
+ );
2251
+ continue;
2252
+ }
2167
2253
  this.session.replayTurn(
2168
2254
  conversationId,
2169
- eventLists[i] ?? [],
2170
- userMessages[i]?.content
2255
+ eventsByJobId.get(step.jobId) ?? [],
2256
+ step.content,
2257
+ step.messageId
2171
2258
  );
2172
2259
  }
2173
2260
  if (completedJobs.length > 0) {