@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 +123 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +97 -9
- package/dist/index.d.ts +97 -9
- package/dist/index.js +123 -33
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -258,7 +258,7 @@ declare const ChatEventType: {
|
|
|
258
258
|
type ChatEventTypeValue = (typeof ChatEventType)[keyof typeof ChatEventType];
|
|
259
259
|
type WireBlockKind = "text" | "thinking" | "tool_use";
|
|
260
260
|
type WireBlockStatus = "streaming" | "awaiting_client_result" | "ok" | "error" | "denied" | "cancelled";
|
|
261
|
-
type WireStopReason = "end_turn" | "tool_use" | "max_tokens" | "context_overflow" | "error";
|
|
261
|
+
type WireStopReason = "end_turn" | "tool_use" | "max_tokens" | "context_overflow" | "incomplete" | "error";
|
|
262
262
|
interface WireTextDelta {
|
|
263
263
|
channel: "text";
|
|
264
264
|
text: string;
|
|
@@ -506,10 +506,23 @@ type ChatEvent = {
|
|
|
506
506
|
type: "subagent_start";
|
|
507
507
|
agent: AgentIdentity;
|
|
508
508
|
taskCallId?: string | null;
|
|
509
|
+
/** The run's id, when the frame carries one (`agent_start`). */
|
|
510
|
+
runId?: string;
|
|
509
511
|
} | {
|
|
510
512
|
type: "subagent_stop";
|
|
511
513
|
agent: AgentIdentity;
|
|
512
514
|
taskCallId?: string | null;
|
|
515
|
+
/** The run's id, when the frame carries one (`agent_stop`). */
|
|
516
|
+
runId?: string;
|
|
517
|
+
/**
|
|
518
|
+
* The child's OWN outcome and spend, surfaced separately from the turn
|
|
519
|
+
* totals (`message_stop.usage` still folds them in). Absent on the older
|
|
520
|
+
* `subagent_stop` frames, which carried only the identity.
|
|
521
|
+
*/
|
|
522
|
+
stopReason?: string;
|
|
523
|
+
inputTokens?: number;
|
|
524
|
+
outputTokens?: number;
|
|
525
|
+
durationMs?: number;
|
|
513
526
|
} | {
|
|
514
527
|
type: "context_warning";
|
|
515
528
|
/** Known values: "info" | "warning" | "critical". Typed as string for forward compat. */
|
|
@@ -644,6 +657,19 @@ interface Conversation {
|
|
|
644
657
|
* null) from an Astralform older than 0.69.46.
|
|
645
658
|
*/
|
|
646
659
|
repository?: string | null;
|
|
660
|
+
/**
|
|
661
|
+
* The live job for this conversation, or `null`/absent when nothing is
|
|
662
|
+
* running. The sidebar's per-row liveness signal: without it a client cannot
|
|
663
|
+
* know that a conversation it has never opened is running, because its only
|
|
664
|
+
* other liveness signals are the per-conversation streams it is attached to.
|
|
665
|
+
*
|
|
666
|
+
* A JOB ID rather than a state string, deliberately: the backend can cheaply
|
|
667
|
+
* say "a job is live here", while "waiting for the user" lives in the
|
|
668
|
+
* worker's in-memory state and reaches only the open conversation's stream.
|
|
669
|
+
* `null` from a server that projects the field; `undefined` from one older
|
|
670
|
+
* than it (same convention as `repository`), so treat both as "not running".
|
|
671
|
+
*/
|
|
672
|
+
activeJobId?: string | null;
|
|
647
673
|
/**
|
|
648
674
|
* The code group this task is filed under, or `null`/absent when it is not in
|
|
649
675
|
* one. MUTABLE — unlike `repository`, which is written once on the first turn:
|
|
@@ -1120,7 +1146,11 @@ interface VoiceConfig {
|
|
|
1120
1146
|
* (or compare against `"raw"`) before polishing.
|
|
1121
1147
|
*/
|
|
1122
1148
|
defaultMode: VoicePolishMode;
|
|
1123
|
-
/**
|
|
1149
|
+
/**
|
|
1150
|
+
* @deprecated Always `2`. The server no longer sends the field and no
|
|
1151
|
+
* shipped client implements pause detection; kept so existing code compiles,
|
|
1152
|
+
* slated for removal in the next major.
|
|
1153
|
+
*/
|
|
1124
1154
|
silenceAutoStopSeconds: number;
|
|
1125
1155
|
/**
|
|
1126
1156
|
* @deprecated Always `false`. Auto-send was removed from the platform
|
|
@@ -1966,12 +1996,6 @@ type ChatEventHandler = (event: ChatEvent) => void;
|
|
|
1966
1996
|
* would leave the second page's offset pointing at the wrong row.
|
|
1967
1997
|
*/
|
|
1968
1998
|
declare const CONVERSATION_PAGE_SIZE = 50;
|
|
1969
|
-
/**
|
|
1970
|
-
* ChatSession — translates the backend wire protocol into typed ChatEvents
|
|
1971
|
-
* for consumers. Owns HTTP + SSE plumbing, conversation state, and the
|
|
1972
|
-
* client-tool round-trip. Does NOT own block construction — consumers
|
|
1973
|
-
* build their own block state from the typed events.
|
|
1974
|
-
*/
|
|
1975
1999
|
declare class ChatSession {
|
|
1976
2000
|
readonly client: AstralformClient;
|
|
1977
2001
|
readonly toolRegistry: ToolRegistry;
|
|
@@ -2111,6 +2135,33 @@ declare class ChatSession {
|
|
|
2111
2135
|
private processStream;
|
|
2112
2136
|
/** Last received sequence number for resumable reconnection */
|
|
2113
2137
|
private lastSeq;
|
|
2138
|
+
/**
|
|
2139
|
+
* Advance the resume cursor from a frame's `seq` — the position the next
|
|
2140
|
+
* reconnect asks the backend to replay after (`?after=`).
|
|
2141
|
+
*
|
|
2142
|
+
* Two guards, because getting this wrong is silent and expensive: the whole
|
|
2143
|
+
* turn replays over blocks the consumer is already rendering.
|
|
2144
|
+
*
|
|
2145
|
+
* - **A keepalive is not an event.** The backend sends it as an ordinary
|
|
2146
|
+
* `data:` frame with `seq: 0` BY DESIGN (``WireKeepalive(seq=0, ts=0.0)``
|
|
2147
|
+
* — an idle tick, not a position), and it arrives exactly when a turn has
|
|
2148
|
+
* been quiet for 45 s: the long-tool-run case. Harvesting it rewound the
|
|
2149
|
+
* cursor to 0, so a reconnect after a blip replayed the turn from its
|
|
2150
|
+
* start — deltas and all.
|
|
2151
|
+
* - **The cursor only advances.** `send` and `reconnectToJob` reset it to
|
|
2152
|
+
* -1 per job, so a new job's `seq: 0` still moves it forward; a frame
|
|
2153
|
+
* carrying an older (or zero) position cannot move it back.
|
|
2154
|
+
*
|
|
2155
|
+
* The one other frame on the wire with a non-position `seq` is the
|
|
2156
|
+
* subscriber's SYNTHETIC terminal — a back-filled `error` for a job whose
|
|
2157
|
+
* worker died, built without a `seq` so Pydantic's default `0` reaches the
|
|
2158
|
+
* wire. Deliberately not named here: the advance-only rule already ignores it
|
|
2159
|
+
* whenever the client has drained anything (0 never exceeds a real cursor),
|
|
2160
|
+
* and when it is the FIRST frame the pump ends on `sawTerminal` before any
|
|
2161
|
+
* reconnect could use the cursor. Naming it would add a rule that no
|
|
2162
|
+
* reachable input exercises.
|
|
2163
|
+
*/
|
|
2164
|
+
private harvestResumeCursor;
|
|
2114
2165
|
/**
|
|
2115
2166
|
* Client-tool call_ids whose result was already submitted this turn. On a
|
|
2116
2167
|
* reconnect the resumed stream can replay a tool request we already handled;
|
|
@@ -2178,7 +2229,15 @@ declare class ChatSession {
|
|
|
2178
2229
|
* Reconnect to a running job's SSE stream (e.g. after page reload).
|
|
2179
2230
|
* Replays all events from the beginning and continues live.
|
|
2180
2231
|
*/
|
|
2181
|
-
|
|
2232
|
+
/**
|
|
2233
|
+
* @param afterSeq Resume the job's event stream AFTER this sequence number.
|
|
2234
|
+
* Omitted (the reconnect default) replays the job from the beginning. A
|
|
2235
|
+
* caller that holds the job's events up to `afterSeq` — the transcript
|
|
2236
|
+
* cache — passes it so the backend replays only the delta and then goes
|
|
2237
|
+
* live; the backend's `?after=` handles a job that already ended by
|
|
2238
|
+
* back-filling its terminal event.
|
|
2239
|
+
*/
|
|
2240
|
+
reconnectToJob(jobId: string, afterSeq?: number): Promise<void>;
|
|
2182
2241
|
/** Detach from the SSE stream without cancelling the job. */
|
|
2183
2242
|
detach(): void;
|
|
2184
2243
|
/**
|
|
@@ -2601,6 +2660,35 @@ declare class StreamManager {
|
|
|
2601
2660
|
*/
|
|
2602
2661
|
switchTo(conversationId: string, opts?: {
|
|
2603
2662
|
skipHistoryReplay?: boolean;
|
|
2663
|
+
/**
|
|
2664
|
+
* Jobs whose events the caller ALREADY holds — the transcript-cache
|
|
2665
|
+
* resume. A COMPLETED job named here is neither fetched nor replayed: its
|
|
2666
|
+
* blocks are in the caller's store, and replaying them would draw a
|
|
2667
|
+
* second copy.
|
|
2668
|
+
*
|
|
2669
|
+
* Two limits, both deliberate and both stating what a review proved the
|
|
2670
|
+
* wider contract could not:
|
|
2671
|
+
*
|
|
2672
|
+
* - **The newest turn page only.** The marks are matched against the page
|
|
2673
|
+
* of jobs this restore walks, so a mark for a turn OLDER than that page
|
|
2674
|
+
* is not consulted. The message window (40 rows) and the turn page (10)
|
|
2675
|
+
* are independent spans, so a cache deeper than one page would need its
|
|
2676
|
+
* own cursor design — a prompt whose turn is not on the page cannot be
|
|
2677
|
+
* recognized as held, and would come back as a steer bubble.
|
|
2678
|
+
* - **Completed jobs only.** A mark for the RUNNING job is ignored (its
|
|
2679
|
+
* stream starts from the beginning, as it always did). Resuming a live
|
|
2680
|
+
* turn's stream from a mark would deliver the delta but NOT the prompt
|
|
2681
|
+
* — a prompt is a message-table row, not a stream event — so the plan
|
|
2682
|
+
* would still draw a bubble the caller's cache already renders.
|
|
2683
|
+
*
|
|
2684
|
+
* Absent → today's behaviour exactly. A job NOT named is fetched whole, as
|
|
2685
|
+
* now, so a stale mark loses at most one completed turn's skip, never a
|
|
2686
|
+
* turn.
|
|
2687
|
+
*/
|
|
2688
|
+
have?: ReadonlyArray<{
|
|
2689
|
+
jobId: string;
|
|
2690
|
+
lastSeq: number;
|
|
2691
|
+
}>;
|
|
2604
2692
|
}): Promise<void>;
|
|
2605
2693
|
/**
|
|
2606
2694
|
* Re-attach to whatever the server says is live for the ACTIVE conversation.
|
package/dist/index.d.ts
CHANGED
|
@@ -258,7 +258,7 @@ declare const ChatEventType: {
|
|
|
258
258
|
type ChatEventTypeValue = (typeof ChatEventType)[keyof typeof ChatEventType];
|
|
259
259
|
type WireBlockKind = "text" | "thinking" | "tool_use";
|
|
260
260
|
type WireBlockStatus = "streaming" | "awaiting_client_result" | "ok" | "error" | "denied" | "cancelled";
|
|
261
|
-
type WireStopReason = "end_turn" | "tool_use" | "max_tokens" | "context_overflow" | "error";
|
|
261
|
+
type WireStopReason = "end_turn" | "tool_use" | "max_tokens" | "context_overflow" | "incomplete" | "error";
|
|
262
262
|
interface WireTextDelta {
|
|
263
263
|
channel: "text";
|
|
264
264
|
text: string;
|
|
@@ -506,10 +506,23 @@ type ChatEvent = {
|
|
|
506
506
|
type: "subagent_start";
|
|
507
507
|
agent: AgentIdentity;
|
|
508
508
|
taskCallId?: string | null;
|
|
509
|
+
/** The run's id, when the frame carries one (`agent_start`). */
|
|
510
|
+
runId?: string;
|
|
509
511
|
} | {
|
|
510
512
|
type: "subagent_stop";
|
|
511
513
|
agent: AgentIdentity;
|
|
512
514
|
taskCallId?: string | null;
|
|
515
|
+
/** The run's id, when the frame carries one (`agent_stop`). */
|
|
516
|
+
runId?: string;
|
|
517
|
+
/**
|
|
518
|
+
* The child's OWN outcome and spend, surfaced separately from the turn
|
|
519
|
+
* totals (`message_stop.usage` still folds them in). Absent on the older
|
|
520
|
+
* `subagent_stop` frames, which carried only the identity.
|
|
521
|
+
*/
|
|
522
|
+
stopReason?: string;
|
|
523
|
+
inputTokens?: number;
|
|
524
|
+
outputTokens?: number;
|
|
525
|
+
durationMs?: number;
|
|
513
526
|
} | {
|
|
514
527
|
type: "context_warning";
|
|
515
528
|
/** Known values: "info" | "warning" | "critical". Typed as string for forward compat. */
|
|
@@ -644,6 +657,19 @@ interface Conversation {
|
|
|
644
657
|
* null) from an Astralform older than 0.69.46.
|
|
645
658
|
*/
|
|
646
659
|
repository?: string | null;
|
|
660
|
+
/**
|
|
661
|
+
* The live job for this conversation, or `null`/absent when nothing is
|
|
662
|
+
* running. The sidebar's per-row liveness signal: without it a client cannot
|
|
663
|
+
* know that a conversation it has never opened is running, because its only
|
|
664
|
+
* other liveness signals are the per-conversation streams it is attached to.
|
|
665
|
+
*
|
|
666
|
+
* A JOB ID rather than a state string, deliberately: the backend can cheaply
|
|
667
|
+
* say "a job is live here", while "waiting for the user" lives in the
|
|
668
|
+
* worker's in-memory state and reaches only the open conversation's stream.
|
|
669
|
+
* `null` from a server that projects the field; `undefined` from one older
|
|
670
|
+
* than it (same convention as `repository`), so treat both as "not running".
|
|
671
|
+
*/
|
|
672
|
+
activeJobId?: string | null;
|
|
647
673
|
/**
|
|
648
674
|
* The code group this task is filed under, or `null`/absent when it is not in
|
|
649
675
|
* one. MUTABLE — unlike `repository`, which is written once on the first turn:
|
|
@@ -1120,7 +1146,11 @@ interface VoiceConfig {
|
|
|
1120
1146
|
* (or compare against `"raw"`) before polishing.
|
|
1121
1147
|
*/
|
|
1122
1148
|
defaultMode: VoicePolishMode;
|
|
1123
|
-
/**
|
|
1149
|
+
/**
|
|
1150
|
+
* @deprecated Always `2`. The server no longer sends the field and no
|
|
1151
|
+
* shipped client implements pause detection; kept so existing code compiles,
|
|
1152
|
+
* slated for removal in the next major.
|
|
1153
|
+
*/
|
|
1124
1154
|
silenceAutoStopSeconds: number;
|
|
1125
1155
|
/**
|
|
1126
1156
|
* @deprecated Always `false`. Auto-send was removed from the platform
|
|
@@ -1966,12 +1996,6 @@ type ChatEventHandler = (event: ChatEvent) => void;
|
|
|
1966
1996
|
* would leave the second page's offset pointing at the wrong row.
|
|
1967
1997
|
*/
|
|
1968
1998
|
declare const CONVERSATION_PAGE_SIZE = 50;
|
|
1969
|
-
/**
|
|
1970
|
-
* ChatSession — translates the backend wire protocol into typed ChatEvents
|
|
1971
|
-
* for consumers. Owns HTTP + SSE plumbing, conversation state, and the
|
|
1972
|
-
* client-tool round-trip. Does NOT own block construction — consumers
|
|
1973
|
-
* build their own block state from the typed events.
|
|
1974
|
-
*/
|
|
1975
1999
|
declare class ChatSession {
|
|
1976
2000
|
readonly client: AstralformClient;
|
|
1977
2001
|
readonly toolRegistry: ToolRegistry;
|
|
@@ -2111,6 +2135,33 @@ declare class ChatSession {
|
|
|
2111
2135
|
private processStream;
|
|
2112
2136
|
/** Last received sequence number for resumable reconnection */
|
|
2113
2137
|
private lastSeq;
|
|
2138
|
+
/**
|
|
2139
|
+
* Advance the resume cursor from a frame's `seq` — the position the next
|
|
2140
|
+
* reconnect asks the backend to replay after (`?after=`).
|
|
2141
|
+
*
|
|
2142
|
+
* Two guards, because getting this wrong is silent and expensive: the whole
|
|
2143
|
+
* turn replays over blocks the consumer is already rendering.
|
|
2144
|
+
*
|
|
2145
|
+
* - **A keepalive is not an event.** The backend sends it as an ordinary
|
|
2146
|
+
* `data:` frame with `seq: 0` BY DESIGN (``WireKeepalive(seq=0, ts=0.0)``
|
|
2147
|
+
* — an idle tick, not a position), and it arrives exactly when a turn has
|
|
2148
|
+
* been quiet for 45 s: the long-tool-run case. Harvesting it rewound the
|
|
2149
|
+
* cursor to 0, so a reconnect after a blip replayed the turn from its
|
|
2150
|
+
* start — deltas and all.
|
|
2151
|
+
* - **The cursor only advances.** `send` and `reconnectToJob` reset it to
|
|
2152
|
+
* -1 per job, so a new job's `seq: 0` still moves it forward; a frame
|
|
2153
|
+
* carrying an older (or zero) position cannot move it back.
|
|
2154
|
+
*
|
|
2155
|
+
* The one other frame on the wire with a non-position `seq` is the
|
|
2156
|
+
* subscriber's SYNTHETIC terminal — a back-filled `error` for a job whose
|
|
2157
|
+
* worker died, built without a `seq` so Pydantic's default `0` reaches the
|
|
2158
|
+
* wire. Deliberately not named here: the advance-only rule already ignores it
|
|
2159
|
+
* whenever the client has drained anything (0 never exceeds a real cursor),
|
|
2160
|
+
* and when it is the FIRST frame the pump ends on `sawTerminal` before any
|
|
2161
|
+
* reconnect could use the cursor. Naming it would add a rule that no
|
|
2162
|
+
* reachable input exercises.
|
|
2163
|
+
*/
|
|
2164
|
+
private harvestResumeCursor;
|
|
2114
2165
|
/**
|
|
2115
2166
|
* Client-tool call_ids whose result was already submitted this turn. On a
|
|
2116
2167
|
* reconnect the resumed stream can replay a tool request we already handled;
|
|
@@ -2178,7 +2229,15 @@ declare class ChatSession {
|
|
|
2178
2229
|
* Reconnect to a running job's SSE stream (e.g. after page reload).
|
|
2179
2230
|
* Replays all events from the beginning and continues live.
|
|
2180
2231
|
*/
|
|
2181
|
-
|
|
2232
|
+
/**
|
|
2233
|
+
* @param afterSeq Resume the job's event stream AFTER this sequence number.
|
|
2234
|
+
* Omitted (the reconnect default) replays the job from the beginning. A
|
|
2235
|
+
* caller that holds the job's events up to `afterSeq` — the transcript
|
|
2236
|
+
* cache — passes it so the backend replays only the delta and then goes
|
|
2237
|
+
* live; the backend's `?after=` handles a job that already ended by
|
|
2238
|
+
* back-filling its terminal event.
|
|
2239
|
+
*/
|
|
2240
|
+
reconnectToJob(jobId: string, afterSeq?: number): Promise<void>;
|
|
2182
2241
|
/** Detach from the SSE stream without cancelling the job. */
|
|
2183
2242
|
detach(): void;
|
|
2184
2243
|
/**
|
|
@@ -2601,6 +2660,35 @@ declare class StreamManager {
|
|
|
2601
2660
|
*/
|
|
2602
2661
|
switchTo(conversationId: string, opts?: {
|
|
2603
2662
|
skipHistoryReplay?: boolean;
|
|
2663
|
+
/**
|
|
2664
|
+
* Jobs whose events the caller ALREADY holds — the transcript-cache
|
|
2665
|
+
* resume. A COMPLETED job named here is neither fetched nor replayed: its
|
|
2666
|
+
* blocks are in the caller's store, and replaying them would draw a
|
|
2667
|
+
* second copy.
|
|
2668
|
+
*
|
|
2669
|
+
* Two limits, both deliberate and both stating what a review proved the
|
|
2670
|
+
* wider contract could not:
|
|
2671
|
+
*
|
|
2672
|
+
* - **The newest turn page only.** The marks are matched against the page
|
|
2673
|
+
* of jobs this restore walks, so a mark for a turn OLDER than that page
|
|
2674
|
+
* is not consulted. The message window (40 rows) and the turn page (10)
|
|
2675
|
+
* are independent spans, so a cache deeper than one page would need its
|
|
2676
|
+
* own cursor design — a prompt whose turn is not on the page cannot be
|
|
2677
|
+
* recognized as held, and would come back as a steer bubble.
|
|
2678
|
+
* - **Completed jobs only.** A mark for the RUNNING job is ignored (its
|
|
2679
|
+
* stream starts from the beginning, as it always did). Resuming a live
|
|
2680
|
+
* turn's stream from a mark would deliver the delta but NOT the prompt
|
|
2681
|
+
* — a prompt is a message-table row, not a stream event — so the plan
|
|
2682
|
+
* would still draw a bubble the caller's cache already renders.
|
|
2683
|
+
*
|
|
2684
|
+
* Absent → today's behaviour exactly. A job NOT named is fetched whole, as
|
|
2685
|
+
* now, so a stale mark loses at most one completed turn's skip, never a
|
|
2686
|
+
* turn.
|
|
2687
|
+
*/
|
|
2688
|
+
have?: ReadonlyArray<{
|
|
2689
|
+
jobId: string;
|
|
2690
|
+
lastSeq: number;
|
|
2691
|
+
}>;
|
|
2604
2692
|
}): Promise<void>;
|
|
2605
2693
|
/**
|
|
2606
2694
|
* Re-attach to whatever the server says is live for the ACTIVE conversation.
|
package/dist/index.js
CHANGED
|
@@ -1081,7 +1081,11 @@ var AstralformClient = class {
|
|
|
1081
1081
|
// A mode this SDK does not know must not reach a `switch` typed as
|
|
1082
1082
|
// `VoicePolishMode`; `structured` is the server's own default.
|
|
1083
1083
|
defaultMode: isVoicePolishMode(raw.default_mode) ? raw.default_mode : "structured",
|
|
1084
|
-
|
|
1084
|
+
// Deliberately not read from the wire. The field was retired from the
|
|
1085
|
+
// platform (2026-10-09): no shipped client implements pause detection
|
|
1086
|
+
// and the server no longer sends it, so an older server's stored value
|
|
1087
|
+
// must not resurrect it.
|
|
1088
|
+
silenceAutoStopSeconds: 2,
|
|
1085
1089
|
// Deliberately not read from the wire. Auto-send was removed from the
|
|
1086
1090
|
// platform (2026-09-14) and the server pins `auto_send` to false; an
|
|
1087
1091
|
// older server still sending `true` must not make a client auto-send, so
|
|
@@ -1488,13 +1492,29 @@ function translateDelta(wire) {
|
|
|
1488
1492
|
}
|
|
1489
1493
|
}
|
|
1490
1494
|
function translateAgentIdentity(raw) {
|
|
1495
|
+
const source = raw !== null && typeof raw === "object" ? { ...raw } : {};
|
|
1496
|
+
const read = (key) => source[key];
|
|
1491
1497
|
return {
|
|
1492
|
-
name:
|
|
1493
|
-
displayName:
|
|
1494
|
-
avatarUrl:
|
|
1495
|
-
description:
|
|
1498
|
+
name: read("name") ?? "",
|
|
1499
|
+
displayName: read("display_name") ?? null,
|
|
1500
|
+
avatarUrl: read("avatar_url") ?? null,
|
|
1501
|
+
description: read("description") ?? null
|
|
1496
1502
|
};
|
|
1497
1503
|
}
|
|
1504
|
+
function agentIdentityFromEvent(data) {
|
|
1505
|
+
if ("agent" in data) return translateAgentIdentity(data.agent);
|
|
1506
|
+
return translateAgentIdentity({
|
|
1507
|
+
name: data.agent_name,
|
|
1508
|
+
display_name: data.agent_display_name,
|
|
1509
|
+
avatar_url: data.agent_avatar_url
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
function optionalString(value) {
|
|
1513
|
+
return typeof value === "string" ? value : void 0;
|
|
1514
|
+
}
|
|
1515
|
+
function optionalNumber(value) {
|
|
1516
|
+
return typeof value === "number" ? value : void 0;
|
|
1517
|
+
}
|
|
1498
1518
|
function translateTodoItem(raw) {
|
|
1499
1519
|
return {
|
|
1500
1520
|
id: raw.id ?? 0,
|
|
@@ -1545,21 +1565,33 @@ function translateCustomEvent(name, data) {
|
|
|
1545
1565
|
phase: data.phase ?? null,
|
|
1546
1566
|
updatedAt: data.updated_at ?? null
|
|
1547
1567
|
};
|
|
1568
|
+
// Two names for one event: the backend renamed these to `agent_*` (a nested
|
|
1569
|
+
// run is not necessarily a "subagent" in the product's vocabulary) while
|
|
1570
|
+
// older servers still emit `subagent_*`. Both are translated, so a client
|
|
1571
|
+
// works across the rename.
|
|
1572
|
+
case "agent_start":
|
|
1548
1573
|
case "subagent_start":
|
|
1549
1574
|
return {
|
|
1550
1575
|
type: "subagent_start",
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1576
|
+
runId: optionalString(data.run_id),
|
|
1577
|
+
agent: agentIdentityFromEvent(data),
|
|
1578
|
+
// `parent_call_id` is the spawn block's call_id — the same key a client
|
|
1579
|
+
// uses to find the tool block whose subtree is this child's work.
|
|
1580
|
+
taskCallId: optionalString(data.parent_call_id) ?? optionalString(data.task_call_id) ?? null
|
|
1555
1581
|
};
|
|
1582
|
+
case "agent_stop":
|
|
1556
1583
|
case "subagent_stop":
|
|
1557
1584
|
return {
|
|
1558
1585
|
type: "subagent_stop",
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
),
|
|
1562
|
-
|
|
1586
|
+
runId: optionalString(data.run_id),
|
|
1587
|
+
agent: agentIdentityFromEvent(data),
|
|
1588
|
+
taskCallId: optionalString(data.parent_call_id) ?? optionalString(data.task_call_id) ?? null,
|
|
1589
|
+
// The child's OWN spend and outcome, which the wire surfaces separately
|
|
1590
|
+
// from the turn totals (`message_stop.usage` folds them in too).
|
|
1591
|
+
stopReason: optionalString(data.stop_reason),
|
|
1592
|
+
inputTokens: optionalNumber(data.input_tokens),
|
|
1593
|
+
outputTokens: optionalNumber(data.output_tokens),
|
|
1594
|
+
durationMs: optionalNumber(data.duration_ms)
|
|
1563
1595
|
};
|
|
1564
1596
|
case "context_warning":
|
|
1565
1597
|
return {
|
|
@@ -1738,22 +1770,24 @@ function translateWireEvent(wire) {
|
|
|
1738
1770
|
status: wire.status,
|
|
1739
1771
|
final: wire.final
|
|
1740
1772
|
};
|
|
1741
|
-
case "message_stop":
|
|
1773
|
+
case "message_stop": {
|
|
1774
|
+
const usage = wire.usage ?? {};
|
|
1742
1775
|
return {
|
|
1743
1776
|
type: "message_stop",
|
|
1744
1777
|
turnId: wire.turn_id,
|
|
1745
1778
|
jobId: wire.job_id,
|
|
1746
1779
|
stopReason: wire.stop_reason,
|
|
1747
1780
|
usage: {
|
|
1748
|
-
inputTokens:
|
|
1749
|
-
outputTokens:
|
|
1750
|
-
cachedTokens:
|
|
1751
|
-
cacheCreationTokens:
|
|
1781
|
+
inputTokens: usage.input_tokens ?? 0,
|
|
1782
|
+
outputTokens: usage.output_tokens ?? 0,
|
|
1783
|
+
cachedTokens: usage.cached_tokens ?? 0,
|
|
1784
|
+
cacheCreationTokens: usage.cache_creation_tokens ?? 0
|
|
1752
1785
|
},
|
|
1753
1786
|
ttfbMs: wire.ttfb_ms,
|
|
1754
1787
|
totalMs: wire.total_ms,
|
|
1755
1788
|
stallCount: wire.stall_count
|
|
1756
1789
|
};
|
|
1790
|
+
}
|
|
1757
1791
|
case "stall":
|
|
1758
1792
|
return {
|
|
1759
1793
|
type: "stall",
|
|
@@ -1807,6 +1841,11 @@ function pathEquals(a, b) {
|
|
|
1807
1841
|
}
|
|
1808
1842
|
return true;
|
|
1809
1843
|
}
|
|
1844
|
+
function frameSeq(frame) {
|
|
1845
|
+
if (typeof frame !== "object" || frame === null) return void 0;
|
|
1846
|
+
if (!("seq" in frame)) return void 0;
|
|
1847
|
+
return frame.seq;
|
|
1848
|
+
}
|
|
1810
1849
|
var ChatSession = class {
|
|
1811
1850
|
constructor(config, storage) {
|
|
1812
1851
|
/**
|
|
@@ -2105,7 +2144,7 @@ var ChatSession = class {
|
|
|
2105
2144
|
if (!(err instanceof DOMException && err.name === "AbortError")) {
|
|
2106
2145
|
this.emit({
|
|
2107
2146
|
type: "error",
|
|
2108
|
-
code: "connection_error",
|
|
2147
|
+
code: err instanceof AstralformError ? err.code : "connection_error",
|
|
2109
2148
|
message: err instanceof Error ? err.message : String(err),
|
|
2110
2149
|
blockPath: null
|
|
2111
2150
|
});
|
|
@@ -2117,6 +2156,37 @@ var ChatSession = class {
|
|
|
2117
2156
|
}
|
|
2118
2157
|
}
|
|
2119
2158
|
}
|
|
2159
|
+
/**
|
|
2160
|
+
* Advance the resume cursor from a frame's `seq` — the position the next
|
|
2161
|
+
* reconnect asks the backend to replay after (`?after=`).
|
|
2162
|
+
*
|
|
2163
|
+
* Two guards, because getting this wrong is silent and expensive: the whole
|
|
2164
|
+
* turn replays over blocks the consumer is already rendering.
|
|
2165
|
+
*
|
|
2166
|
+
* - **A keepalive is not an event.** The backend sends it as an ordinary
|
|
2167
|
+
* `data:` frame with `seq: 0` BY DESIGN (``WireKeepalive(seq=0, ts=0.0)``
|
|
2168
|
+
* — an idle tick, not a position), and it arrives exactly when a turn has
|
|
2169
|
+
* been quiet for 45 s: the long-tool-run case. Harvesting it rewound the
|
|
2170
|
+
* cursor to 0, so a reconnect after a blip replayed the turn from its
|
|
2171
|
+
* start — deltas and all.
|
|
2172
|
+
* - **The cursor only advances.** `send` and `reconnectToJob` reset it to
|
|
2173
|
+
* -1 per job, so a new job's `seq: 0` still moves it forward; a frame
|
|
2174
|
+
* carrying an older (or zero) position cannot move it back.
|
|
2175
|
+
*
|
|
2176
|
+
* The one other frame on the wire with a non-position `seq` is the
|
|
2177
|
+
* subscriber's SYNTHETIC terminal — a back-filled `error` for a job whose
|
|
2178
|
+
* worker died, built without a `seq` so Pydantic's default `0` reaches the
|
|
2179
|
+
* wire. Deliberately not named here: the advance-only rule already ignores it
|
|
2180
|
+
* whenever the client has drained anything (0 never exceeds a real cursor),
|
|
2181
|
+
* and when it is the FIRST frame the pump ends on `sawTerminal` before any
|
|
2182
|
+
* reconnect could use the cursor. Naming it would add a rule that no
|
|
2183
|
+
* reachable input exercises.
|
|
2184
|
+
*/
|
|
2185
|
+
harvestResumeCursor(seq, type) {
|
|
2186
|
+
if (type === "keepalive") return;
|
|
2187
|
+
if (typeof seq !== "number") return;
|
|
2188
|
+
if (seq > this.lastSeq) this.lastSeq = seq;
|
|
2189
|
+
}
|
|
2120
2190
|
async consumeJobStream(request, wire) {
|
|
2121
2191
|
const job = await this.client.createJob(request);
|
|
2122
2192
|
if (wire) wire.reached = true;
|
|
@@ -2254,15 +2324,14 @@ var ChatSession = class {
|
|
|
2254
2324
|
try {
|
|
2255
2325
|
const data = JSON.parse(raw.data);
|
|
2256
2326
|
if (typeof data !== "object" || data === null || typeof data.type !== "string") {
|
|
2257
|
-
|
|
2258
|
-
|
|
2327
|
+
this.harvestResumeCursor(frameSeq(data), void 0);
|
|
2328
|
+
if (typeof data === "object" && data !== null && "data" in data && data.data === "[DONE]") {
|
|
2329
|
+
sawTerminal = true;
|
|
2259
2330
|
}
|
|
2260
2331
|
continue;
|
|
2261
2332
|
}
|
|
2262
2333
|
parsed = data;
|
|
2263
|
-
|
|
2264
|
-
this.lastSeq = data.seq;
|
|
2265
|
-
}
|
|
2334
|
+
this.harvestResumeCursor(frameSeq(data), parsed.type);
|
|
2266
2335
|
} catch {
|
|
2267
2336
|
continue;
|
|
2268
2337
|
}
|
|
@@ -2386,7 +2455,7 @@ var ChatSession = class {
|
|
|
2386
2455
|
}
|
|
2387
2456
|
return;
|
|
2388
2457
|
case "block_delta":
|
|
2389
|
-
if (ownsTurnState && wire.delta
|
|
2458
|
+
if (ownsTurnState && wire.delta?.channel === "text" && this.currentTextPath !== null && pathEquals(this.currentTextPath, wire.path)) {
|
|
2390
2459
|
this.accumulatedText += wire.delta.text;
|
|
2391
2460
|
}
|
|
2392
2461
|
return;
|
|
@@ -2491,11 +2560,19 @@ var ChatSession = class {
|
|
|
2491
2560
|
* Reconnect to a running job's SSE stream (e.g. after page reload).
|
|
2492
2561
|
* Replays all events from the beginning and continues live.
|
|
2493
2562
|
*/
|
|
2494
|
-
|
|
2563
|
+
/**
|
|
2564
|
+
* @param afterSeq Resume the job's event stream AFTER this sequence number.
|
|
2565
|
+
* Omitted (the reconnect default) replays the job from the beginning. A
|
|
2566
|
+
* caller that holds the job's events up to `afterSeq` — the transcript
|
|
2567
|
+
* cache — passes it so the backend replays only the delta and then goes
|
|
2568
|
+
* live; the backend's `?after=` handles a job that already ended by
|
|
2569
|
+
* back-filling its terminal event.
|
|
2570
|
+
*/
|
|
2571
|
+
async reconnectToJob(jobId, afterSeq) {
|
|
2495
2572
|
if (this.isStreaming) return;
|
|
2496
2573
|
this.isStreaming = true;
|
|
2497
2574
|
this.currentJobId = jobId;
|
|
2498
|
-
this.lastSeq = -1;
|
|
2575
|
+
this.lastSeq = afterSeq ?? -1;
|
|
2499
2576
|
this.submittedToolCallIds.clear();
|
|
2500
2577
|
this.resetStreamingState();
|
|
2501
2578
|
const controller = new AbortController();
|
|
@@ -2786,7 +2863,10 @@ function planRestore(args) {
|
|
|
2786
2863
|
messageId: msgs[i]?.id
|
|
2787
2864
|
}));
|
|
2788
2865
|
if (jobs.length === 0) {
|
|
2789
|
-
|
|
2866
|
+
const held = new Set(
|
|
2867
|
+
(args.heldMessageIds ?? []).filter((id) => !!id)
|
|
2868
|
+
);
|
|
2869
|
+
return userMessages.filter((m) => !(m.id && held.has(m.id))).map((m) => ({
|
|
2790
2870
|
kind: "steer",
|
|
2791
2871
|
content: m.content,
|
|
2792
2872
|
messageId: m.id
|
|
@@ -3065,7 +3145,7 @@ var StreamManager = class _StreamManager {
|
|
|
3065
3145
|
}
|
|
3066
3146
|
let tookOver = false;
|
|
3067
3147
|
try {
|
|
3068
|
-
await this.restore(conversationId, gen);
|
|
3148
|
+
await this.restore(conversationId, gen, opts?.have);
|
|
3069
3149
|
tookOver = gen === this.generation;
|
|
3070
3150
|
} finally {
|
|
3071
3151
|
const supersededOntoSame = gen !== this.generation && this._activeConversationId === conversationId;
|
|
@@ -3283,7 +3363,10 @@ var StreamManager = class _StreamManager {
|
|
|
3283
3363
|
...before ? { before } : {}
|
|
3284
3364
|
});
|
|
3285
3365
|
}
|
|
3286
|
-
async restore(conversationId, gen) {
|
|
3366
|
+
async restore(conversationId, gen, have) {
|
|
3367
|
+
const haveByJobId = new Map(
|
|
3368
|
+
(have ?? []).map((h) => [h.jobId, h.lastSeq])
|
|
3369
|
+
);
|
|
3287
3370
|
const superseded = () => gen !== this.generation;
|
|
3288
3371
|
if (superseded()) return;
|
|
3289
3372
|
const turn = this.turnCounter;
|
|
@@ -3310,7 +3393,8 @@ var StreamManager = class _StreamManager {
|
|
|
3310
3393
|
gen,
|
|
3311
3394
|
activeJobId,
|
|
3312
3395
|
turn,
|
|
3313
|
-
jobsRequest
|
|
3396
|
+
jobsRequest,
|
|
3397
|
+
haveByJobId
|
|
3314
3398
|
))
|
|
3315
3399
|
return;
|
|
3316
3400
|
if (activeJobId) {
|
|
@@ -3507,7 +3591,7 @@ var StreamManager = class _StreamManager {
|
|
|
3507
3591
|
session.isLoadingEarlierTurns = false;
|
|
3508
3592
|
}
|
|
3509
3593
|
}
|
|
3510
|
-
async replayHistory(conversationId, gen, activeJobId, turn, jobsRequest) {
|
|
3594
|
+
async replayHistory(conversationId, gen, activeJobId, turn, jobsRequest, haveByJobId) {
|
|
3511
3595
|
const stopReplay = () => gen !== this.generation || this.viewTakenOverByLiveTurn() || this.turnStarted(turn);
|
|
3512
3596
|
let complete = false;
|
|
3513
3597
|
try {
|
|
@@ -3515,7 +3599,7 @@ var StreamManager = class _StreamManager {
|
|
|
3515
3599
|
const jobs = page.jobs;
|
|
3516
3600
|
if (stopReplay()) return false;
|
|
3517
3601
|
const replayableJobs = jobs.filter(
|
|
3518
|
-
(j) => j.job_id !== activeJobId
|
|
3602
|
+
(j) => j.job_id !== activeJobId && !haveByJobId.has(j.job_id)
|
|
3519
3603
|
);
|
|
3520
3604
|
const userMessages = this.session.messages.filter(
|
|
3521
3605
|
(m) => m.role === "user"
|
|
@@ -3549,6 +3633,12 @@ var StreamManager = class _StreamManager {
|
|
|
3549
3633
|
// them, silently. Deriving both from `jobs` makes that impossible to
|
|
3550
3634
|
// get half-right.
|
|
3551
3635
|
claimedMessageIds: jobs.map((j) => j.message_id),
|
|
3636
|
+
// The prompts whose turn the CALLER holds (see `switchTo`'s `have`).
|
|
3637
|
+
// The walk below is empty for exactly those jobs — which is the
|
|
3638
|
+
// primary cache scenario, a SETTLED conversation whose turns are all
|
|
3639
|
+
// held — and the empty-walk rescue would otherwise re-emit every one of
|
|
3640
|
+
// them as a steer bubble.
|
|
3641
|
+
heldMessageIds: jobs.filter((j) => haveByJobId.has(j.job_id)).map((j) => j.message_id),
|
|
3552
3642
|
userMessages: userMessages.map((m) => ({
|
|
3553
3643
|
id: m.id,
|
|
3554
3644
|
content: m.content
|