@astralform/js 9.4.1 → 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 +107 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +91 -7
- package/dist/index.d.ts +91 -7
- package/dist/index.js +107 -26
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1545,13 +1545,29 @@ function translateDelta(wire) {
|
|
|
1545
1545
|
}
|
|
1546
1546
|
}
|
|
1547
1547
|
function translateAgentIdentity(raw) {
|
|
1548
|
+
const source = raw !== null && typeof raw === "object" ? { ...raw } : {};
|
|
1549
|
+
const read = (key) => source[key];
|
|
1548
1550
|
return {
|
|
1549
|
-
name:
|
|
1550
|
-
displayName:
|
|
1551
|
-
avatarUrl:
|
|
1552
|
-
description:
|
|
1551
|
+
name: read("name") ?? "",
|
|
1552
|
+
displayName: read("display_name") ?? null,
|
|
1553
|
+
avatarUrl: read("avatar_url") ?? null,
|
|
1554
|
+
description: read("description") ?? null
|
|
1553
1555
|
};
|
|
1554
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
|
+
}
|
|
1555
1571
|
function translateTodoItem(raw) {
|
|
1556
1572
|
return {
|
|
1557
1573
|
id: raw.id ?? 0,
|
|
@@ -1602,21 +1618,33 @@ function translateCustomEvent(name, data) {
|
|
|
1602
1618
|
phase: data.phase ?? null,
|
|
1603
1619
|
updatedAt: data.updated_at ?? null
|
|
1604
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":
|
|
1605
1626
|
case "subagent_start":
|
|
1606
1627
|
return {
|
|
1607
1628
|
type: "subagent_start",
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
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
|
|
1612
1634
|
};
|
|
1635
|
+
case "agent_stop":
|
|
1613
1636
|
case "subagent_stop":
|
|
1614
1637
|
return {
|
|
1615
1638
|
type: "subagent_stop",
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
),
|
|
1619
|
-
|
|
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)
|
|
1620
1648
|
};
|
|
1621
1649
|
case "context_warning":
|
|
1622
1650
|
return {
|
|
@@ -1866,6 +1894,11 @@ function pathEquals(a, b) {
|
|
|
1866
1894
|
}
|
|
1867
1895
|
return true;
|
|
1868
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
|
+
}
|
|
1869
1902
|
var ChatSession = class {
|
|
1870
1903
|
constructor(config, storage) {
|
|
1871
1904
|
/**
|
|
@@ -2176,6 +2209,37 @@ var ChatSession = class {
|
|
|
2176
2209
|
}
|
|
2177
2210
|
}
|
|
2178
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
|
+
}
|
|
2179
2243
|
async consumeJobStream(request, wire) {
|
|
2180
2244
|
const job = await this.client.createJob(request);
|
|
2181
2245
|
if (wire) wire.reached = true;
|
|
@@ -2313,18 +2377,14 @@ var ChatSession = class {
|
|
|
2313
2377
|
try {
|
|
2314
2378
|
const data = JSON.parse(raw.data);
|
|
2315
2379
|
if (typeof data !== "object" || data === null || typeof data.type !== "string") {
|
|
2316
|
-
|
|
2317
|
-
this.lastSeq = data.seq;
|
|
2318
|
-
}
|
|
2380
|
+
this.harvestResumeCursor(frameSeq(data), void 0);
|
|
2319
2381
|
if (typeof data === "object" && data !== null && "data" in data && data.data === "[DONE]") {
|
|
2320
2382
|
sawTerminal = true;
|
|
2321
2383
|
}
|
|
2322
2384
|
continue;
|
|
2323
2385
|
}
|
|
2324
2386
|
parsed = data;
|
|
2325
|
-
|
|
2326
|
-
this.lastSeq = data.seq;
|
|
2327
|
-
}
|
|
2387
|
+
this.harvestResumeCursor(frameSeq(data), parsed.type);
|
|
2328
2388
|
} catch {
|
|
2329
2389
|
continue;
|
|
2330
2390
|
}
|
|
@@ -2553,11 +2613,19 @@ var ChatSession = class {
|
|
|
2553
2613
|
* Reconnect to a running job's SSE stream (e.g. after page reload).
|
|
2554
2614
|
* Replays all events from the beginning and continues live.
|
|
2555
2615
|
*/
|
|
2556
|
-
|
|
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) {
|
|
2557
2625
|
if (this.isStreaming) return;
|
|
2558
2626
|
this.isStreaming = true;
|
|
2559
2627
|
this.currentJobId = jobId;
|
|
2560
|
-
this.lastSeq = -1;
|
|
2628
|
+
this.lastSeq = afterSeq ?? -1;
|
|
2561
2629
|
this.submittedToolCallIds.clear();
|
|
2562
2630
|
this.resetStreamingState();
|
|
2563
2631
|
const controller = new AbortController();
|
|
@@ -2848,7 +2916,10 @@ function planRestore(args) {
|
|
|
2848
2916
|
messageId: msgs[i]?.id
|
|
2849
2917
|
}));
|
|
2850
2918
|
if (jobs.length === 0) {
|
|
2851
|
-
|
|
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) => ({
|
|
2852
2923
|
kind: "steer",
|
|
2853
2924
|
content: m.content,
|
|
2854
2925
|
messageId: m.id
|
|
@@ -3127,7 +3198,7 @@ var StreamManager = class _StreamManager {
|
|
|
3127
3198
|
}
|
|
3128
3199
|
let tookOver = false;
|
|
3129
3200
|
try {
|
|
3130
|
-
await this.restore(conversationId, gen);
|
|
3201
|
+
await this.restore(conversationId, gen, opts?.have);
|
|
3131
3202
|
tookOver = gen === this.generation;
|
|
3132
3203
|
} finally {
|
|
3133
3204
|
const supersededOntoSame = gen !== this.generation && this._activeConversationId === conversationId;
|
|
@@ -3345,7 +3416,10 @@ var StreamManager = class _StreamManager {
|
|
|
3345
3416
|
...before ? { before } : {}
|
|
3346
3417
|
});
|
|
3347
3418
|
}
|
|
3348
|
-
async restore(conversationId, gen) {
|
|
3419
|
+
async restore(conversationId, gen, have) {
|
|
3420
|
+
const haveByJobId = new Map(
|
|
3421
|
+
(have ?? []).map((h) => [h.jobId, h.lastSeq])
|
|
3422
|
+
);
|
|
3349
3423
|
const superseded = () => gen !== this.generation;
|
|
3350
3424
|
if (superseded()) return;
|
|
3351
3425
|
const turn = this.turnCounter;
|
|
@@ -3372,7 +3446,8 @@ var StreamManager = class _StreamManager {
|
|
|
3372
3446
|
gen,
|
|
3373
3447
|
activeJobId,
|
|
3374
3448
|
turn,
|
|
3375
|
-
jobsRequest
|
|
3449
|
+
jobsRequest,
|
|
3450
|
+
haveByJobId
|
|
3376
3451
|
))
|
|
3377
3452
|
return;
|
|
3378
3453
|
if (activeJobId) {
|
|
@@ -3569,7 +3644,7 @@ var StreamManager = class _StreamManager {
|
|
|
3569
3644
|
session.isLoadingEarlierTurns = false;
|
|
3570
3645
|
}
|
|
3571
3646
|
}
|
|
3572
|
-
async replayHistory(conversationId, gen, activeJobId, turn, jobsRequest) {
|
|
3647
|
+
async replayHistory(conversationId, gen, activeJobId, turn, jobsRequest, haveByJobId) {
|
|
3573
3648
|
const stopReplay = () => gen !== this.generation || this.viewTakenOverByLiveTurn() || this.turnStarted(turn);
|
|
3574
3649
|
let complete = false;
|
|
3575
3650
|
try {
|
|
@@ -3577,7 +3652,7 @@ var StreamManager = class _StreamManager {
|
|
|
3577
3652
|
const jobs = page.jobs;
|
|
3578
3653
|
if (stopReplay()) return false;
|
|
3579
3654
|
const replayableJobs = jobs.filter(
|
|
3580
|
-
(j) => j.job_id !== activeJobId
|
|
3655
|
+
(j) => j.job_id !== activeJobId && !haveByJobId.has(j.job_id)
|
|
3581
3656
|
);
|
|
3582
3657
|
const userMessages = this.session.messages.filter(
|
|
3583
3658
|
(m) => m.role === "user"
|
|
@@ -3611,6 +3686,12 @@ var StreamManager = class _StreamManager {
|
|
|
3611
3686
|
// them, silently. Deriving both from `jobs` makes that impossible to
|
|
3612
3687
|
// get half-right.
|
|
3613
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),
|
|
3614
3695
|
userMessages: userMessages.map((m) => ({
|
|
3615
3696
|
id: m.id,
|
|
3616
3697
|
content: m.content
|