@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.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
|
-
|
|
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:
|
|
1546
|
-
displayName:
|
|
1547
|
-
avatarUrl:
|
|
1548
|
-
description:
|
|
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
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
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
|
-
|
|
1613
|
-
|
|
1614
|
-
),
|
|
1615
|
-
|
|
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:
|
|
1802
|
-
outputTokens:
|
|
1803
|
-
cachedTokens:
|
|
1804
|
-
cacheCreationTokens:
|
|
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
|
-
|
|
2311
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|