@astralform/js 4.9.0 → 5.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.d.cts CHANGED
@@ -792,6 +792,24 @@ interface ConversationEvent {
792
792
  data: Record<string, unknown>;
793
793
  }
794
794
  interface SendOptions$1 extends ModelChoiceOptions {
795
+ /**
796
+ * Which conversation this turn belongs to. Defaults to the session's current
797
+ * one.
798
+ *
799
+ * ⚠️ BEHAVIOR CHANGE: passing this now RELOCATES the session. A value
800
+ * different from the current one sets `session.conversationId`, DISCARDS
801
+ * `session.messages` (the old conversation's list is not that conversation's
802
+ * history), and invalidates any `loadConversation` still in flight.
803
+ * Previously it addressed a single turn elsewhere and left the session where
804
+ * it was. After a successful send the list holds only that turn, so load the
805
+ * conversation if you need its history.
806
+ *
807
+ * The change is deliberate: `onSessionEvent` tags every emitted event with
808
+ * `session.conversationId`, so a turn sent elsewhere without moving the
809
+ * pointer streamed its events back under the wrong conversation. Addressing
810
+ * one turn away from the session is not something the session can honestly
811
+ * represent while it has a single pointer, so the send now moves it.
812
+ */
795
813
  conversationId?: string;
796
814
  enabledClientTools?: string[];
797
815
  uploadIds?: string[];
@@ -1115,6 +1133,17 @@ declare class ChatSession {
1115
1133
  /** True while ``loadMoreConversations`` is in flight. */
1116
1134
  isLoadingConversations: boolean;
1117
1135
  messages: Message[];
1136
+ /**
1137
+ * Which conversation ``messages`` currently holds.
1138
+ *
1139
+ * Distinct from ``conversationId``, and the distinction is the point:
1140
+ * ``loadConversation`` moves the POINTER synchronously and installs the LIST
1141
+ * an await later, so for the whole duration of every load the two disagree.
1142
+ * Anything pairing a message with a conversation — ``regenerate`` above all —
1143
+ * has to read this one, or it will pair the previous conversation's last
1144
+ * message with the new conversation's id.
1145
+ */
1146
+ messagesConversationId: string | null;
1118
1147
  isStreaming: boolean;
1119
1148
  agentStatus: AgentStatus | null;
1120
1149
  agents: AgentInfo[];
@@ -1147,6 +1176,49 @@ declare class ChatSession {
1147
1176
  * is discarded instead.
1148
1177
  */
1149
1178
  private conversationsGeneration;
1179
+ /**
1180
+ * Bumped by every ``loadConversation`` call, so an out-of-order fetch can
1181
+ * tell it is no longer the newest one and drop its result. Separate from
1182
+ * ``conversationsGeneration``, which guards the conversation LIST.
1183
+ */
1184
+ private loadGeneration;
1185
+ /**
1186
+ * Ids of locally-created user messages the server has not acknowledged yet.
1187
+ *
1188
+ * Arrival time cannot answer "could the reply have included this?" on its
1189
+ * own. Every `loadConversation` in `StreamManager` sits behind the
1190
+ * active-job probe, so the ORDINARY ordering is that a send lands BEFORE the
1191
+ * load starts — and an arrival-time rule drops exactly those, losing the
1192
+ * prompt the user just sent while its stream is still running. Membership
1193
+ * here is set by `send` and cleared when a server row turns up carrying the
1194
+ * same turn, so the keep-decision no longer depends on which side of the
1195
+ * fetch the push landed on.
1196
+ *
1197
+ * The value is `serverRowsKnown` as of the `message_stop` that proved the
1198
+ * row committed, or 0 until then — including after the job response, which
1199
+ * hands back the id but starts the loop as a background task and so proves
1200
+ * nothing about the row. That stamp is what separates "the snapshot predates
1201
+ * the row" from "the server does not have this row" — see
1202
+ * `loadConversation`.
1203
+ *
1204
+ * It is an ANNOTATION ON `this.messages`: reconciliation only ever consults
1205
+ * entries of that array, so an id whose message has left it is dead weight.
1206
+ * `setMessages` is the single place the array is replaced, and it prunes.
1207
+ */
1208
+ private pendingUserMessages;
1209
+ /**
1210
+ * Bumped once per completed turn, at `message_stop`, so a fetch can record
1211
+ * what was proven when it was ISSUED. A row proven committed before the
1212
+ * fetch went out must appear in its snapshot; one proven after may
1213
+ * legitimately be missing.
1214
+ */
1215
+ private serverRowsKnown;
1216
+ /**
1217
+ * Replace the message list, keeping `pendingUserMessages` an annotation on
1218
+ * it. Every removal from the array goes through here — `push` is the only
1219
+ * other mutation and it cannot orphan an id.
1220
+ */
1221
+ private setMessages;
1150
1222
  private accumulatedText;
1151
1223
  private currentTextPath;
1152
1224
  private handlers;
@@ -1180,6 +1252,11 @@ declare class ChatSession {
1180
1252
  * Consume a single SSE stream to exhaustion. Returns whether a terminal
1181
1253
  * event (``message_stop`` / ``error``) was seen, so the caller can decide
1182
1254
  * whether an ended stream means "turn done" vs "dropped, reconnect".
1255
+ *
1256
+ * ``onStall`` aborts the per-attempt connection: if no event arrives within
1257
+ * SSE_STALL_TIMEOUT_MS (backend keepalives land every 15s), the stream is a
1258
+ * zombie — ``reader.read()`` will never settle — so we kill the fetch and
1259
+ * throw a ConnectionError, feeding the caller's reconnect-from-lastSeq loop.
1183
1260
  */
1184
1261
  private pumpStream;
1185
1262
  /** Sleep for ``ms``, resolving early if the turn is aborted mid-backoff. */
@@ -1200,9 +1277,15 @@ declare class ChatSession {
1200
1277
  * the pure wire → ChatEvent mapping can live in translate.ts and be reused
1201
1278
  * by the replay path.
1202
1279
  *
1203
- * ``messageId`` is the server-assigned assistant message id for the current
1204
- * turn; empty in the reconnect and conversation-switch replay paths where
1205
- * messages have already been loaded from REST and shouldn't be re-pushed.
1280
+ * ``promptMessageId`` is the id of the USER turn that started this job —
1281
+ * `POST /v1/jobs` returns it and the backend tags the prompt with it
1282
+ * (`HumanMessage(content=..., id=message_id)`), which is what lets a restore
1283
+ * pair a job with its prompt by id instead of by position. It was previously
1284
+ * documented here as the ASSISTANT's id and used as one; there is no
1285
+ * server-assigned assistant id on the wire, so that row gets a local one.
1286
+ * Empty in the reconnect and conversation-switch replay paths, where the
1287
+ * messages have already been loaded from REST and must not be re-pushed —
1288
+ * so it doubles as the "is this a live send?" gate.
1206
1289
  */
1207
1290
  private applyWireSideEffects;
1208
1291
  private executeClientTools;
@@ -1218,8 +1301,26 @@ declare class ChatSession {
1218
1301
  reconnectToJob(jobId: string): Promise<void>;
1219
1302
  /** Detach from the SSE stream without cancelling the job. */
1220
1303
  detach(): void;
1221
- /** Stop the job and disconnect (explicit user action). */
1304
+ /**
1305
+ * Cancel the running turn: stop the job server-side and tear the stream
1306
+ * down, WITHOUT ending the session. A turn-level cancel is not a
1307
+ * session-level teardown, so unlike `disconnect()` this leaves the protocol
1308
+ * registry alone — the SDK never auto-registers adapters, so clearing them
1309
+ * on a Stop press would silently kill embedded-resource rendering for the
1310
+ * rest of the session with nothing to re-register it.
1311
+ */
1312
+ cancelTurn(): void;
1313
+ /** Stop the job and end the session's activity (explicit user action). */
1222
1314
  disconnect(): void;
1315
+ /**
1316
+ * A pointer move happened above this layer, so any `loadConversation` in
1317
+ * flight must lose to it. `StreamManager` owns a pointer of its own and
1318
+ * moves it before this one; without this the two halves would gate on
1319
+ * counters that bump at different instants — `generation` synchronously in
1320
+ * `setActiveConversation`, `loadGeneration` only once the switch's own load
1321
+ * actually runs, which is behind the active-job probe.
1322
+ */
1323
+ invalidateLoadsInFlight(): void;
1223
1324
  createNewConversation(): Promise<string>;
1224
1325
  /**
1225
1326
  * Replay one completed turn's already-fetched events, synchronously.
@@ -1396,6 +1497,12 @@ declare class StreamManager {
1396
1497
  private _backgroundJobs;
1397
1498
  private handlers;
1398
1499
  private unsub;
1500
+ /**
1501
+ * Bumped every time the active conversation moves. An async sequence that
1502
+ * captures it can then tell, at each await boundary, whether it is still the
1503
+ * one the user is waiting on — see ``restore``.
1504
+ */
1505
+ private generation;
1399
1506
  constructor(session: ChatSession);
1400
1507
  get state(): StreamState;
1401
1508
  get activeConversationId(): string | null;
@@ -1427,6 +1534,15 @@ declare class StreamManager {
1427
1534
  switchTo(conversationId: string, opts?: {
1428
1535
  skipHistoryReplay?: boolean;
1429
1536
  }): Promise<void>;
1537
+ /**
1538
+ * Create a conversation and make it active.
1539
+ *
1540
+ * The returned id is NOT guaranteed to be the active conversation: if a
1541
+ * switch lands inside the storage round-trip, this declines the pointer move
1542
+ * so the newer one wins, and no `conversationChanged` fires for the new id.
1543
+ * A caller that routes on the return value should `switchTo(id)` rather than
1544
+ * assume it is current — that call is not a no-op in the declined case.
1545
+ */
1430
1546
  createConversation(): Promise<string>;
1431
1547
  /**
1432
1548
  * Rename a conversation. Purely a relabel — no active-conversation or
@@ -1436,6 +1552,31 @@ declare class StreamManager {
1436
1552
  deleteConversation(id: string): Promise<void>;
1437
1553
  stop(): void;
1438
1554
  destroy(): void;
1555
+ /**
1556
+ * Park a streaming turn as a background job and detach from its SSE stream.
1557
+ *
1558
+ * Every method that relocates the active conversation has to do this before
1559
+ * announcing a new state. Announcing `idle` while `session.isStreaming` is
1560
+ * still true is worse than announcing nothing: `manager.send` no longer bails
1561
+ * on the streaming state, calls `session.send`, and THAT bails on its own
1562
+ * `isStreaming` — so the message is never posted, no error is emitted, and
1563
+ * the composer looks ready the whole time.
1564
+ */
1565
+ private detachStreamingTurn;
1566
+ /**
1567
+ * Announce `idle` unless a turn is actually streaming.
1568
+ *
1569
+ * A `send` can land inside any of the switch paths — the fast path most
1570
+ * easily, since it deliberately stays out of `restoring` and so leaves the
1571
+ * composer live for the whole probe. `send` sets `streaming` and does not
1572
+ * bump the generation, so the path resumes, passes its supersession check,
1573
+ * and would announce a ready composer over a running stream. From there
1574
+ * `finalizeStream` and the `message_stop` branch both no-op (they only act
1575
+ * on `streaming`), so it stays `idle` for the whole turn — and the next send
1576
+ * reaches `session.send`, which bails on its own `isStreaming`: message
1577
+ * never posted, no error, composer ready throughout.
1578
+ */
1579
+ private settleIdle;
1439
1580
  private finalizeStream;
1440
1581
  private restore;
1441
1582
  private setActiveConversation;
package/dist/index.d.ts CHANGED
@@ -792,6 +792,24 @@ interface ConversationEvent {
792
792
  data: Record<string, unknown>;
793
793
  }
794
794
  interface SendOptions$1 extends ModelChoiceOptions {
795
+ /**
796
+ * Which conversation this turn belongs to. Defaults to the session's current
797
+ * one.
798
+ *
799
+ * ⚠️ BEHAVIOR CHANGE: passing this now RELOCATES the session. A value
800
+ * different from the current one sets `session.conversationId`, DISCARDS
801
+ * `session.messages` (the old conversation's list is not that conversation's
802
+ * history), and invalidates any `loadConversation` still in flight.
803
+ * Previously it addressed a single turn elsewhere and left the session where
804
+ * it was. After a successful send the list holds only that turn, so load the
805
+ * conversation if you need its history.
806
+ *
807
+ * The change is deliberate: `onSessionEvent` tags every emitted event with
808
+ * `session.conversationId`, so a turn sent elsewhere without moving the
809
+ * pointer streamed its events back under the wrong conversation. Addressing
810
+ * one turn away from the session is not something the session can honestly
811
+ * represent while it has a single pointer, so the send now moves it.
812
+ */
795
813
  conversationId?: string;
796
814
  enabledClientTools?: string[];
797
815
  uploadIds?: string[];
@@ -1115,6 +1133,17 @@ declare class ChatSession {
1115
1133
  /** True while ``loadMoreConversations`` is in flight. */
1116
1134
  isLoadingConversations: boolean;
1117
1135
  messages: Message[];
1136
+ /**
1137
+ * Which conversation ``messages`` currently holds.
1138
+ *
1139
+ * Distinct from ``conversationId``, and the distinction is the point:
1140
+ * ``loadConversation`` moves the POINTER synchronously and installs the LIST
1141
+ * an await later, so for the whole duration of every load the two disagree.
1142
+ * Anything pairing a message with a conversation — ``regenerate`` above all —
1143
+ * has to read this one, or it will pair the previous conversation's last
1144
+ * message with the new conversation's id.
1145
+ */
1146
+ messagesConversationId: string | null;
1118
1147
  isStreaming: boolean;
1119
1148
  agentStatus: AgentStatus | null;
1120
1149
  agents: AgentInfo[];
@@ -1147,6 +1176,49 @@ declare class ChatSession {
1147
1176
  * is discarded instead.
1148
1177
  */
1149
1178
  private conversationsGeneration;
1179
+ /**
1180
+ * Bumped by every ``loadConversation`` call, so an out-of-order fetch can
1181
+ * tell it is no longer the newest one and drop its result. Separate from
1182
+ * ``conversationsGeneration``, which guards the conversation LIST.
1183
+ */
1184
+ private loadGeneration;
1185
+ /**
1186
+ * Ids of locally-created user messages the server has not acknowledged yet.
1187
+ *
1188
+ * Arrival time cannot answer "could the reply have included this?" on its
1189
+ * own. Every `loadConversation` in `StreamManager` sits behind the
1190
+ * active-job probe, so the ORDINARY ordering is that a send lands BEFORE the
1191
+ * load starts — and an arrival-time rule drops exactly those, losing the
1192
+ * prompt the user just sent while its stream is still running. Membership
1193
+ * here is set by `send` and cleared when a server row turns up carrying the
1194
+ * same turn, so the keep-decision no longer depends on which side of the
1195
+ * fetch the push landed on.
1196
+ *
1197
+ * The value is `serverRowsKnown` as of the `message_stop` that proved the
1198
+ * row committed, or 0 until then — including after the job response, which
1199
+ * hands back the id but starts the loop as a background task and so proves
1200
+ * nothing about the row. That stamp is what separates "the snapshot predates
1201
+ * the row" from "the server does not have this row" — see
1202
+ * `loadConversation`.
1203
+ *
1204
+ * It is an ANNOTATION ON `this.messages`: reconciliation only ever consults
1205
+ * entries of that array, so an id whose message has left it is dead weight.
1206
+ * `setMessages` is the single place the array is replaced, and it prunes.
1207
+ */
1208
+ private pendingUserMessages;
1209
+ /**
1210
+ * Bumped once per completed turn, at `message_stop`, so a fetch can record
1211
+ * what was proven when it was ISSUED. A row proven committed before the
1212
+ * fetch went out must appear in its snapshot; one proven after may
1213
+ * legitimately be missing.
1214
+ */
1215
+ private serverRowsKnown;
1216
+ /**
1217
+ * Replace the message list, keeping `pendingUserMessages` an annotation on
1218
+ * it. Every removal from the array goes through here — `push` is the only
1219
+ * other mutation and it cannot orphan an id.
1220
+ */
1221
+ private setMessages;
1150
1222
  private accumulatedText;
1151
1223
  private currentTextPath;
1152
1224
  private handlers;
@@ -1180,6 +1252,11 @@ declare class ChatSession {
1180
1252
  * Consume a single SSE stream to exhaustion. Returns whether a terminal
1181
1253
  * event (``message_stop`` / ``error``) was seen, so the caller can decide
1182
1254
  * whether an ended stream means "turn done" vs "dropped, reconnect".
1255
+ *
1256
+ * ``onStall`` aborts the per-attempt connection: if no event arrives within
1257
+ * SSE_STALL_TIMEOUT_MS (backend keepalives land every 15s), the stream is a
1258
+ * zombie — ``reader.read()`` will never settle — so we kill the fetch and
1259
+ * throw a ConnectionError, feeding the caller's reconnect-from-lastSeq loop.
1183
1260
  */
1184
1261
  private pumpStream;
1185
1262
  /** Sleep for ``ms``, resolving early if the turn is aborted mid-backoff. */
@@ -1200,9 +1277,15 @@ declare class ChatSession {
1200
1277
  * the pure wire → ChatEvent mapping can live in translate.ts and be reused
1201
1278
  * by the replay path.
1202
1279
  *
1203
- * ``messageId`` is the server-assigned assistant message id for the current
1204
- * turn; empty in the reconnect and conversation-switch replay paths where
1205
- * messages have already been loaded from REST and shouldn't be re-pushed.
1280
+ * ``promptMessageId`` is the id of the USER turn that started this job —
1281
+ * `POST /v1/jobs` returns it and the backend tags the prompt with it
1282
+ * (`HumanMessage(content=..., id=message_id)`), which is what lets a restore
1283
+ * pair a job with its prompt by id instead of by position. It was previously
1284
+ * documented here as the ASSISTANT's id and used as one; there is no
1285
+ * server-assigned assistant id on the wire, so that row gets a local one.
1286
+ * Empty in the reconnect and conversation-switch replay paths, where the
1287
+ * messages have already been loaded from REST and must not be re-pushed —
1288
+ * so it doubles as the "is this a live send?" gate.
1206
1289
  */
1207
1290
  private applyWireSideEffects;
1208
1291
  private executeClientTools;
@@ -1218,8 +1301,26 @@ declare class ChatSession {
1218
1301
  reconnectToJob(jobId: string): Promise<void>;
1219
1302
  /** Detach from the SSE stream without cancelling the job. */
1220
1303
  detach(): void;
1221
- /** Stop the job and disconnect (explicit user action). */
1304
+ /**
1305
+ * Cancel the running turn: stop the job server-side and tear the stream
1306
+ * down, WITHOUT ending the session. A turn-level cancel is not a
1307
+ * session-level teardown, so unlike `disconnect()` this leaves the protocol
1308
+ * registry alone — the SDK never auto-registers adapters, so clearing them
1309
+ * on a Stop press would silently kill embedded-resource rendering for the
1310
+ * rest of the session with nothing to re-register it.
1311
+ */
1312
+ cancelTurn(): void;
1313
+ /** Stop the job and end the session's activity (explicit user action). */
1222
1314
  disconnect(): void;
1315
+ /**
1316
+ * A pointer move happened above this layer, so any `loadConversation` in
1317
+ * flight must lose to it. `StreamManager` owns a pointer of its own and
1318
+ * moves it before this one; without this the two halves would gate on
1319
+ * counters that bump at different instants — `generation` synchronously in
1320
+ * `setActiveConversation`, `loadGeneration` only once the switch's own load
1321
+ * actually runs, which is behind the active-job probe.
1322
+ */
1323
+ invalidateLoadsInFlight(): void;
1223
1324
  createNewConversation(): Promise<string>;
1224
1325
  /**
1225
1326
  * Replay one completed turn's already-fetched events, synchronously.
@@ -1396,6 +1497,12 @@ declare class StreamManager {
1396
1497
  private _backgroundJobs;
1397
1498
  private handlers;
1398
1499
  private unsub;
1500
+ /**
1501
+ * Bumped every time the active conversation moves. An async sequence that
1502
+ * captures it can then tell, at each await boundary, whether it is still the
1503
+ * one the user is waiting on — see ``restore``.
1504
+ */
1505
+ private generation;
1399
1506
  constructor(session: ChatSession);
1400
1507
  get state(): StreamState;
1401
1508
  get activeConversationId(): string | null;
@@ -1427,6 +1534,15 @@ declare class StreamManager {
1427
1534
  switchTo(conversationId: string, opts?: {
1428
1535
  skipHistoryReplay?: boolean;
1429
1536
  }): Promise<void>;
1537
+ /**
1538
+ * Create a conversation and make it active.
1539
+ *
1540
+ * The returned id is NOT guaranteed to be the active conversation: if a
1541
+ * switch lands inside the storage round-trip, this declines the pointer move
1542
+ * so the newer one wins, and no `conversationChanged` fires for the new id.
1543
+ * A caller that routes on the return value should `switchTo(id)` rather than
1544
+ * assume it is current — that call is not a no-op in the declined case.
1545
+ */
1430
1546
  createConversation(): Promise<string>;
1431
1547
  /**
1432
1548
  * Rename a conversation. Purely a relabel — no active-conversation or
@@ -1436,6 +1552,31 @@ declare class StreamManager {
1436
1552
  deleteConversation(id: string): Promise<void>;
1437
1553
  stop(): void;
1438
1554
  destroy(): void;
1555
+ /**
1556
+ * Park a streaming turn as a background job and detach from its SSE stream.
1557
+ *
1558
+ * Every method that relocates the active conversation has to do this before
1559
+ * announcing a new state. Announcing `idle` while `session.isStreaming` is
1560
+ * still true is worse than announcing nothing: `manager.send` no longer bails
1561
+ * on the streaming state, calls `session.send`, and THAT bails on its own
1562
+ * `isStreaming` — so the message is never posted, no error is emitted, and
1563
+ * the composer looks ready the whole time.
1564
+ */
1565
+ private detachStreamingTurn;
1566
+ /**
1567
+ * Announce `idle` unless a turn is actually streaming.
1568
+ *
1569
+ * A `send` can land inside any of the switch paths — the fast path most
1570
+ * easily, since it deliberately stays out of `restoring` and so leaves the
1571
+ * composer live for the whole probe. `send` sets `streaming` and does not
1572
+ * bump the generation, so the path resumes, passes its supersession check,
1573
+ * and would announce a ready composer over a running stream. From there
1574
+ * `finalizeStream` and the `message_stop` branch both no-op (they only act
1575
+ * on `streaming`), so it stays `idle` for the whole turn — and the next send
1576
+ * reaches `session.send`, which bails on its own `isStreaming`: message
1577
+ * never posted, no error, composer ready throughout.
1578
+ */
1579
+ private settleIdle;
1439
1580
  private finalizeStream;
1440
1581
  private restore;
1441
1582
  private setActiveConversation;