dsh-context 0.46.1 → 0.48.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/README.md +2 -1
- package/lib/client.js +549 -295
- package/lib/index.d.ts +87 -4
- package/lib/index.js +350 -35
- package/package.json +6 -3
package/lib/index.d.ts
CHANGED
|
@@ -56,6 +56,15 @@ interface HeadersState {
|
|
|
56
56
|
headers: StoredHeaderRecord[];
|
|
57
57
|
}
|
|
58
58
|
//#endregion
|
|
59
|
+
//#region src/host/logShapes.d.ts
|
|
60
|
+
/**
|
|
61
|
+
* The decode bucket a stream's `block-start.blockType` names: the model's
|
|
62
|
+
* thinking, the answer text, or the tool-call arguments. Undefined for an
|
|
63
|
+
* unknown/hostile marker, whose interval then stays unattributed rather than
|
|
64
|
+
* poisoning a bucket.
|
|
65
|
+
*/
|
|
66
|
+
type DecodeKind = 'reasoning' | 'text' | 'toolarg';
|
|
67
|
+
//#endregion
|
|
59
68
|
//#region src/host/fold.d.ts
|
|
60
69
|
/**
|
|
61
70
|
* History retention bounds (configurable since 0.11 — see config.ts; these
|
|
@@ -72,6 +81,27 @@ interface TimelineState {
|
|
|
72
81
|
surface: SurfaceNode[];
|
|
73
82
|
sums: Record<Category, number>;
|
|
74
83
|
systemTokens: number;
|
|
84
|
+
/**
|
|
85
|
+
* The live system-prompt nodes, oldest first — a V3 log's `system/message`
|
|
86
|
+
* surface nodes, or the single entry a V0/V2 `request/header.header.system`
|
|
87
|
+
* envelope defines. `systemTokens` is the LAST entry with tokens > 0 (the
|
|
88
|
+
* harness's own "last nonempty surviving system" rule), so an empty dormant
|
|
89
|
+
* node keeps its position without clearing the prompt. Bounded by
|
|
90
|
+
* SYSTEM_NODES_MAX. ABSENT on rows folded before this field existed — the
|
|
91
|
+
* wire then serves no `systems` and the client falls back to the header
|
|
92
|
+
* epoch's own envelope figure.
|
|
93
|
+
*/
|
|
94
|
+
systems?: SystemPromptNode[];
|
|
95
|
+
/**
|
|
96
|
+
* Whether `systems` was built from the V0/V2 request ENVELOPE
|
|
97
|
+
* (`header.system`) rather than from V3 `system/message` events. Only then
|
|
98
|
+
* may a system-less header CLEAR the list: its canonical V0 meaning is
|
|
99
|
+
* "this request has no system prompt", while a V3 header never carries one
|
|
100
|
+
* (its prompt lives in the message history). Absent = log-sourced, and
|
|
101
|
+
* never materialized as an `undefined`-valued property (plain-JSON
|
|
102
|
+
* precondition — see the note above `model`).
|
|
103
|
+
*/
|
|
104
|
+
systemsFromHeader?: true;
|
|
75
105
|
toolsTokens: number;
|
|
76
106
|
/**
|
|
77
107
|
* The projection-cache precondition is plain JSON: a property whose value
|
|
@@ -132,10 +162,21 @@ interface TimelineState {
|
|
|
132
162
|
* one those events close — a hostile interleaved log degrades to skipped
|
|
133
163
|
* durations, never to unbounded state. Same arm/remove lifecycle as
|
|
134
164
|
* `pendingShadowedSeqs`.
|
|
165
|
+
*
|
|
166
|
+
* `decode` and `block` carry the generation split (reasoning / answer text /
|
|
167
|
+
* tool arguments — see TimingTotals): a V0 log's `assistant/chunk`
|
|
168
|
+
* `block-start` markers open `block` and close the previous one into
|
|
169
|
+
* `decode`; a V2+ log carries no such events, so `decode` stays absent and
|
|
170
|
+
* `assistant/message` reads the spans off its embedded stream instead.
|
|
135
171
|
*/
|
|
136
172
|
stepStart?: {
|
|
137
173
|
time: number;
|
|
138
174
|
firstToken?: number;
|
|
175
|
+
decode?: Record<DecodeKind, number>;
|
|
176
|
+
block?: {
|
|
177
|
+
kind: DecodeKind;
|
|
178
|
+
since: number;
|
|
179
|
+
};
|
|
139
180
|
};
|
|
140
181
|
/**
|
|
141
182
|
* Tool callId → the call's name, start instant, and raw arguments, armed by
|
|
@@ -221,6 +262,19 @@ declare module '@deepseek-ai/dsh-session-projection/types' {
|
|
|
221
262
|
}
|
|
222
263
|
}
|
|
223
264
|
type Category = 'user' | 'inject' | 'assistant' | 'tool';
|
|
265
|
+
/**
|
|
266
|
+
* One live system-prompt node (Snapshot.systems) — the harness models the
|
|
267
|
+
* system prompt as a surface node, so its TEXT is fetched on demand from the
|
|
268
|
+
* event at `seq`: a V3 `system/message` event, or the V0/V2 `request/header`
|
|
269
|
+
* whose envelope carried `header.system`. `tokens` is the node's heuristic
|
|
270
|
+
* price (0 for a dormant empty node, which the harness reads as "no system
|
|
271
|
+
* prompt"); the effective figure is the LAST node with `tokens > 0`.
|
|
272
|
+
*/
|
|
273
|
+
interface SystemPromptNode {
|
|
274
|
+
seq: number;
|
|
275
|
+
time: number;
|
|
276
|
+
tokens: number;
|
|
277
|
+
}
|
|
224
278
|
/**
|
|
225
279
|
* The stats board's count figures, precomputed host-side over the RETAINED
|
|
226
280
|
* request/event records (the same set the detail payload serves). Carried by
|
|
@@ -316,6 +370,13 @@ interface Snapshot {
|
|
|
316
370
|
* one — clients treat absence as an empty timing card).
|
|
317
371
|
*/
|
|
318
372
|
timing?: TimingTotals;
|
|
373
|
+
/**
|
|
374
|
+
* The live system-prompt nodes, oldest first — the browser's per-step source
|
|
375
|
+
* for the System section. Absent when the log carried no system prompt, and
|
|
376
|
+
* on older plugin builds (the client then falls back to the header epoch's
|
|
377
|
+
* own `systemTokens`, the pre-V3 shape).
|
|
378
|
+
*/
|
|
379
|
+
systems?: SystemPromptNode[];
|
|
319
380
|
/**
|
|
320
381
|
* The served live surface: the newest `maxNodes` tail PLUS every live inject node older than the tail (injections land first and are
|
|
321
382
|
* few,
|
|
@@ -450,14 +511,30 @@ interface ToolTimingTotals {
|
|
|
450
511
|
}
|
|
451
512
|
/**
|
|
452
513
|
* Whole-session timing totals, host-folded from the durable `step/start` /
|
|
453
|
-
`step/end` / `
|
|
454
|
-
* (running totals over the COMPLETE session log — the same
|
|
455
|
-
* framing as `cost`).
|
|
514
|
+
* `step/end` / `tool/call` / `tool/result` lifecycle plus the model call's
|
|
515
|
+
* first token (running totals over the COMPLETE session log — the same
|
|
516
|
+
* never-trimmed framing as `cost`). The first token comes from a V0
|
|
517
|
+
* `assistant/chunk` delta or from the call's own embedded stream
|
|
518
|
+
* (`assistant/message.data.stream` / `assistant/attempt.data.stream`, the
|
|
519
|
+
* V2+ settlement) — whichever the log carries, matching the harness's own
|
|
520
|
+
* session-stats fold. Durations are wall-clock milliseconds: `wallMs` sums
|
|
456
521
|
* whole steps, `ttftMs` the step-start → first-token slice (the model wait)
|
|
457
522
|
* and `genMs` the first-token → assistant-message slice (the generation) —
|
|
458
523
|
* both only over calls whose stream carried a token delta, `toolsMs` the sum
|
|
459
524
|
* of per-call tool durations (parallel calls each count, so it can overlap).
|
|
460
525
|
* Absent until the first step lifecycle completes in the log.
|
|
526
|
+
*
|
|
527
|
+
* The generation window itself splits by WHAT was being decoded, off the
|
|
528
|
+
* stream's `block-start` framing (`blockType`): `reasoningMs` (the model's
|
|
529
|
+
* thinking), `textMs` (the answer text), and `toolArgMs` (the tool-call
|
|
530
|
+
* arguments). Each marker owns the interval up to the next one (the last one
|
|
531
|
+
* up to the assistant message), so the three tile the marker span and together
|
|
532
|
+
* account for essentially all of `genMs` — the span opens at the first marker,
|
|
533
|
+
* which can sit marginally before the first token, so it is not an exact
|
|
534
|
+
* partition. They are ADDITIVE-OPTIONAL: cached projection rows written before
|
|
535
|
+
* the split carry `genMs` without them, so the card falls back to the
|
|
536
|
+
* un-split shape instead of the cache row being discarded (the
|
|
537
|
+
* stateVersion-15 rationale in host/timeline.ts).
|
|
461
538
|
*/
|
|
462
539
|
interface TimingTotals {
|
|
463
540
|
/** Summed wall time of completed steps (the session's active time). */
|
|
@@ -466,6 +543,12 @@ interface TimingTotals {
|
|
|
466
543
|
ttftMs: number;
|
|
467
544
|
/** Summed first-token → assistant-message time (the generation). */
|
|
468
545
|
genMs: number;
|
|
546
|
+
/** Reasoning-decode slice of `genMs` (the model's thinking). */
|
|
547
|
+
reasoningMs?: number;
|
|
548
|
+
/** Answer-text decode slice of `genMs`. */
|
|
549
|
+
textMs?: number;
|
|
550
|
+
/** Tool-call-argument decode slice of `genMs`. */
|
|
551
|
+
toolArgMs?: number;
|
|
469
552
|
/** Completed model calls (assistant messages folded). */
|
|
470
553
|
calls: number;
|
|
471
554
|
/** Summed per-call durations of completed tool calls. */
|
|
@@ -485,7 +568,7 @@ interface CostFamilyUsage {
|
|
|
485
568
|
* token totals per DeepSeek V4 model family (matched on the model NAME,
|
|
486
569
|
* provider-agnostic) and pricing period. The Client prices these with its
|
|
487
570
|
* hardcoded list-price table in the locale's currency. Absent until a
|
|
488
|
-
* deepseek-v4-flash / deepseek-v4-pro request reports usage.
|
|
571
|
+
* deepseek-v4.1-flash / deepseek-v4-pro request reports usage.
|
|
489
572
|
*/
|
|
490
573
|
interface SessionCostUsage {
|
|
491
574
|
flash?: CostFamilyUsage;
|
package/lib/index.js
CHANGED
|
@@ -308,6 +308,30 @@ function estimateSystemTokens(text) {
|
|
|
308
308
|
if (typeof text !== "string" || text.length === 0) return 0;
|
|
309
309
|
return Math.ceil(text.length / CHARS_PER_TOKEN$1) + ROLE_OVERHEAD$1;
|
|
310
310
|
}
|
|
311
|
+
/**
|
|
312
|
+
* Price a `system/message` payload's content exactly like the harness's
|
|
313
|
+
* token-meter (`estimateSystemMessage`): text density over EVERY text block
|
|
314
|
+
* plus role framing, with no per-block overhead — an adapter serializes the
|
|
315
|
+
* prompt as plain text, so a text block costs its characters alone. Any other
|
|
316
|
+
* block (or a hostile element) falls back to its JSON length. 0 for empty
|
|
317
|
+
* content, which the harness reads as "no system prompt".
|
|
318
|
+
*/
|
|
319
|
+
function estimateSystemContent(blocks) {
|
|
320
|
+
if (!Array.isArray(blocks) || blocks.length === 0) return 0;
|
|
321
|
+
let characters = 0;
|
|
322
|
+
for (const block of blocks) {
|
|
323
|
+
const text = block !== null && typeof block === "object" && block.type === "text" ? block.text : void 0;
|
|
324
|
+
if (typeof text === "string") {
|
|
325
|
+
characters += text.length;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
const json = JSON.stringify(block);
|
|
330
|
+
if (typeof json === "string") characters += json.length;
|
|
331
|
+
} catch {}
|
|
332
|
+
}
|
|
333
|
+
return Math.ceil(characters / CHARS_PER_TOKEN$1) + ROLE_OVERHEAD$1;
|
|
334
|
+
}
|
|
311
335
|
//#endregion
|
|
312
336
|
//#region src/shared/imageTokens.ts
|
|
313
337
|
/**
|
|
@@ -565,6 +589,150 @@ function isInjection(source) {
|
|
|
565
589
|
return source !== null && source !== void 0 && (typeof source.kind === "string" && source.kind !== "" && source.kind !== "user" || typeof source.form === "string");
|
|
566
590
|
}
|
|
567
591
|
//#endregion
|
|
592
|
+
//#region src/host/logShapes.ts
|
|
593
|
+
/**
|
|
594
|
+
* Shape-driven readers over the durable session-event vocabulary — the ONE
|
|
595
|
+
* place the plugin reconciles the two supported log generations:
|
|
596
|
+
*
|
|
597
|
+
* - V0 (dsh 0.1.2-rc.1): `request/header.header.system`, `assistant/chunk`
|
|
598
|
+
* stream events, `SurfaceOp { start, end }`, `tool/code-dispatch`.
|
|
599
|
+
* - V3 (dsh 0.1.5-alpha.x+): `system/message` surface nodes,
|
|
600
|
+
* `assistant/message.data.stream` / `assistant/attempt.data.stream`,
|
|
601
|
+
* `SurfaceOp { startSeq, endSeq }`, `tool/ptc-dispatch`.
|
|
602
|
+
*
|
|
603
|
+
* The fold reads SHAPES, never a detected harness version: a session log is
|
|
604
|
+
* written by exactly one generation, the two spellings are mutually
|
|
605
|
+
* exclusive within it, and a deployment's version probe can be wrong (a
|
|
606
|
+
* healed profile mirror may name a different release than the running
|
|
607
|
+
* harness). Every reader is total over untrusted input — a malformed record
|
|
608
|
+
* yields "nothing here", never a throw (the projection registry drives the
|
|
609
|
+
* fold without an error boundary; one throw stalls the unit's push feed and
|
|
610
|
+
* the browser waits on "loading" forever).
|
|
611
|
+
*
|
|
612
|
+
* @module dsh-context/host/log-shapes
|
|
613
|
+
*/
|
|
614
|
+
/**
|
|
615
|
+
* Whether one raw stream chunk carries a token delta — the first-token marker
|
|
616
|
+
* both generations share. Mirrors dsh-llm's `isTokenDelta` (a non-empty text
|
|
617
|
+
* or reasoning fragment, or any Tool-call delta carrying arguments or a name);
|
|
618
|
+
* a malformed chunk is simply not a token.
|
|
619
|
+
*/
|
|
620
|
+
function isTokenChunk(chunk) {
|
|
621
|
+
if (chunk === null || typeof chunk !== "object") return false;
|
|
622
|
+
const c = chunk;
|
|
623
|
+
switch (c.type) {
|
|
624
|
+
case "text-delta":
|
|
625
|
+
case "reasoning-delta": return typeof c.text === "string" && c.text !== "";
|
|
626
|
+
case "tool-call-delta": return typeof c.argumentsDelta === "string" && c.argumentsDelta !== "" || c.name !== void 0;
|
|
627
|
+
default: return false;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
/** Map one `blockType` to its timing bucket (see {@link DecodeKind}). */
|
|
631
|
+
function decodeKindOfBlock(blockType) {
|
|
632
|
+
if (blockType === "reasoning") return "reasoning";
|
|
633
|
+
if (blockType === "text") return "text";
|
|
634
|
+
if (blockType === "tool-call") return "toolarg";
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Per-kind decode spans inside one embedded assistant stream, tiling
|
|
638
|
+
* [first block-start, endTime]: each `block-start` record owns the interval up
|
|
639
|
+
* to the next one, the last one up to `endTime`. This is the V2+ shape, whose
|
|
640
|
+
* timed stream rides the settlement (`assistant/message.data.stream`) instead
|
|
641
|
+
* of separate `assistant/chunk` events. Total over untrusted input — a
|
|
642
|
+
* malformed record is skipped, a non-finite boundary yields zero, and a stream
|
|
643
|
+
* with no marker (or not an array) yields all zeros.
|
|
644
|
+
*/
|
|
645
|
+
function decodeSpansOfStream(stream, endTime) {
|
|
646
|
+
const spans = {
|
|
647
|
+
reasoning: 0,
|
|
648
|
+
text: 0,
|
|
649
|
+
toolarg: 0
|
|
650
|
+
};
|
|
651
|
+
if (!Array.isArray(stream) || !Number.isFinite(endTime)) return spans;
|
|
652
|
+
let kind;
|
|
653
|
+
let since = 0;
|
|
654
|
+
for (const record of stream) {
|
|
655
|
+
if (record === null || typeof record !== "object") continue;
|
|
656
|
+
const r = record;
|
|
657
|
+
if (r.type !== "chunk" || r.chunk === null || typeof r.chunk !== "object") continue;
|
|
658
|
+
const chunk = r.chunk;
|
|
659
|
+
if (chunk.type !== "block-start") continue;
|
|
660
|
+
const time = r.time;
|
|
661
|
+
if (typeof time !== "number" || !Number.isFinite(time)) continue;
|
|
662
|
+
if (kind !== void 0) spans[kind] += Math.max(0, time - since);
|
|
663
|
+
kind = decodeKindOfBlock(chunk.blockType);
|
|
664
|
+
since = time;
|
|
665
|
+
}
|
|
666
|
+
if (kind !== void 0) spans[kind] += Math.max(0, endTime - since);
|
|
667
|
+
return spans;
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* The first token's instant inside one PACKED delta run (`text-chunks` /
|
|
671
|
+
* `reasoning-chunks` / `tool-call-chunks`): the run's base time plus the
|
|
672
|
+
* accumulated inter-member deltas, taken at the first qualifying member —
|
|
673
|
+
* a name-bearing Tool-call run starts at its first member. Mirrors dsh-llm's
|
|
674
|
+
* `runFirstTokenTime`; a non-finite base or delta yields undefined rather
|
|
675
|
+
* than a NaN instant.
|
|
676
|
+
*/
|
|
677
|
+
function runFirstTokenTime(record) {
|
|
678
|
+
const time0 = record.time0;
|
|
679
|
+
if (typeof time0 !== "number" || !Number.isFinite(time0)) return void 0;
|
|
680
|
+
if (record.type === "tool-call-chunks" && record.name !== void 0) return time0;
|
|
681
|
+
const fragments = record.type === "tool-call-chunks" ? record.args : record.texts;
|
|
682
|
+
if (!Array.isArray(fragments)) return void 0;
|
|
683
|
+
const dt = Array.isArray(record.dt) ? record.dt : [];
|
|
684
|
+
let time = time0;
|
|
685
|
+
for (const [index, fragment] of fragments.entries()) {
|
|
686
|
+
if (index > 0) {
|
|
687
|
+
const step = dt[index - 1];
|
|
688
|
+
if (typeof step !== "number" || !Number.isFinite(step)) return void 0;
|
|
689
|
+
time += step;
|
|
690
|
+
}
|
|
691
|
+
if (typeof fragment === "string" && fragment !== "") return time;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* The first token's instant inside an embedded assistant stream
|
|
696
|
+
* (`assistant/message.data.stream`, `assistant/attempt.data.stream` — the V2+
|
|
697
|
+
* settlement that replaced the V0 `assistant/chunk` events), or undefined
|
|
698
|
+
* when the stream carries no token. Mirrors dsh-llm's
|
|
699
|
+
* `assistantStreamFirstTokenTime` over the compact record union.
|
|
700
|
+
*/
|
|
701
|
+
function firstTokenTimeOfStream(stream) {
|
|
702
|
+
if (!Array.isArray(stream)) return void 0;
|
|
703
|
+
for (const record of stream) {
|
|
704
|
+
if (record === null || typeof record !== "object") continue;
|
|
705
|
+
const r = record;
|
|
706
|
+
if (r.type === "chunk") {
|
|
707
|
+
const time = r.time;
|
|
708
|
+
if (typeof time === "number" && Number.isFinite(time) && isTokenChunk(r.chunk)) return time;
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
const time = runFirstTokenTime(r);
|
|
712
|
+
if (time !== void 0) return time;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* The inclusive surface range a replacement op covers, or null for `append`
|
|
717
|
+
* and for any unrecognized/hostile op (which the fold treats as an append).
|
|
718
|
+
* Reads BOTH endpoint spellings: V3's `startSeq`/`endSeq` first, then V0's
|
|
719
|
+
* `start`/`end` — each accepted only as a finite number, so a hostile op
|
|
720
|
+
* with one good and one malformed endpoint degrades to append.
|
|
721
|
+
*/
|
|
722
|
+
function replaceRangeOf(surfaceOp) {
|
|
723
|
+
if (surfaceOp === null || typeof surfaceOp !== "object") return null;
|
|
724
|
+
const op = surfaceOp;
|
|
725
|
+
if (op.op !== "replace") return null;
|
|
726
|
+
const start = typeof op.startSeq === "number" ? op.startSeq : op.start;
|
|
727
|
+
const end = typeof op.endSeq === "number" ? op.endSeq : op.end;
|
|
728
|
+
if (typeof start !== "number" || !Number.isFinite(start)) return null;
|
|
729
|
+
if (typeof end !== "number" || !Number.isFinite(end)) return null;
|
|
730
|
+
return {
|
|
731
|
+
start,
|
|
732
|
+
end
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
//#endregion
|
|
568
736
|
//#region src/shared/fileOps.ts
|
|
569
737
|
/** Parse a call's raw JSON arguments; non-string/malformed/non-record inputs yield null. */
|
|
570
738
|
function parseCallArgs(raw) {
|
|
@@ -873,6 +1041,24 @@ function bumpDetailRev(st) {
|
|
|
873
1041
|
st.detailRev = (st.detailRev ?? 0) + 1;
|
|
874
1042
|
}
|
|
875
1043
|
/**
|
|
1044
|
+
* Bound on the live system-prompt nodes (TimelineState.systems). The
|
|
1045
|
+
* effective figure is the LAST nonempty node, so dropping the oldest can only
|
|
1046
|
+
* under-report a pathological log whose newest SYSTEM_NODES_MAX nodes are all
|
|
1047
|
+
* empty while an older one still carried text.
|
|
1048
|
+
*/
|
|
1049
|
+
const SYSTEM_NODES_MAX = 8;
|
|
1050
|
+
/** The effective system-prompt price: the last nonempty node, else 0 (the harness's own rule). */
|
|
1051
|
+
function systemTokensOf(systems) {
|
|
1052
|
+
for (let i = systems.length - 1; i >= 0; i--) if (systems[i].tokens > 0) return systems[i].tokens;
|
|
1053
|
+
return 0;
|
|
1054
|
+
}
|
|
1055
|
+
/** Append one system-prompt node, bounding the list (see SYSTEM_NODES_MAX). */
|
|
1056
|
+
function pushSystem(st, node) {
|
|
1057
|
+
const systems = [...st.systems ?? [], node];
|
|
1058
|
+
st.systems = systems.length > SYSTEM_NODES_MAX ? systems.slice(-8) : systems;
|
|
1059
|
+
st.systemTokens = systemTokensOf(st.systems);
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
876
1062
|
* Bound on the buffered nested Code-Mode ops (TimelineState.pendingCodeOps)
|
|
877
1063
|
* — a hostile log that dispatches without settling the parent run_code
|
|
878
1064
|
* cannot grow the persisted state past this.
|
|
@@ -919,6 +1105,35 @@ function archiveRemoved(st, removed, goneSeq) {
|
|
|
919
1105
|
});
|
|
920
1106
|
}
|
|
921
1107
|
/**
|
|
1108
|
+
* Remove every live surface node whose seq the replacement claims, keeping the
|
|
1109
|
+
* per-category sums equal to the surviving nodes and archiving the removals.
|
|
1110
|
+
* Removal follows the SEQ list, not the declared range: pruned replacement
|
|
1111
|
+
* nodes keep their own seqs beyond the range end, so a range-based removal
|
|
1112
|
+
* would leave them behind and overcount. Returns the removed nodes.
|
|
1113
|
+
*/
|
|
1114
|
+
function removeSurfaceSeqs(st, claimed, goneSeq) {
|
|
1115
|
+
if (claimed.size === 0) return [];
|
|
1116
|
+
const kept = [];
|
|
1117
|
+
const removed = [];
|
|
1118
|
+
for (const n of st.surface) if (claimed.has(n.seq)) {
|
|
1119
|
+
st.sums[n.cat] -= n.tokens;
|
|
1120
|
+
removed.push(n);
|
|
1121
|
+
} else kept.push(n);
|
|
1122
|
+
archiveRemoved(st, removed, goneSeq);
|
|
1123
|
+
st.surface = kept;
|
|
1124
|
+
return removed;
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* The message nested under an event payload's `message` field
|
|
1128
|
+
* (`system/message`, `assistant/message`, `tool/result`) — read structurally
|
|
1129
|
+
* rather than through `deriveEventMessage`, whose 0.1.2-rc.1 generation knows
|
|
1130
|
+
* nothing of the V3 `system/message` variant. A malformed payload reads null.
|
|
1131
|
+
*/
|
|
1132
|
+
function messageOf(data) {
|
|
1133
|
+
const message = data?.message;
|
|
1134
|
+
return message !== null && typeof message === "object" ? message : null;
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
922
1137
|
* The first full text block, recursing through nested content blocks (a tool
|
|
923
1138
|
* result wraps its text in a `tool-result` block). Unlike `firstText` this
|
|
924
1139
|
* must NOT truncate/normalize: the skill name is matched off the raw
|
|
@@ -1002,18 +1217,10 @@ function applySurface(st, ev, type, data, message) {
|
|
|
1002
1217
|
const shadowEventSeq = st.pendingShadowEventSeq;
|
|
1003
1218
|
delete st.pendingShadowedSeqs;
|
|
1004
1219
|
delete st.pendingShadowEventSeq;
|
|
1005
|
-
const op = ev.surfaceOp;
|
|
1006
|
-
if (op !== null
|
|
1220
|
+
const op = replaceRangeOf(ev.surfaceOp);
|
|
1221
|
+
if (op !== null) {
|
|
1007
1222
|
if (Array.isArray(shadowedSeqs) && shadowedSeqs.length > 0) {
|
|
1008
|
-
const
|
|
1009
|
-
const kept = [];
|
|
1010
|
-
const removed = [];
|
|
1011
|
-
for (const n of st.surface) if (shadowed.has(n.seq)) {
|
|
1012
|
-
st.sums[n.cat] -= n.tokens;
|
|
1013
|
-
removed.push(n);
|
|
1014
|
-
} else kept.push(n);
|
|
1015
|
-
archiveRemoved(st, removed, ev.seq);
|
|
1016
|
-
st.surface = kept;
|
|
1223
|
+
const removed = removeSurfaceSeqs(st, new Set(shadowedSeqs), ev.seq);
|
|
1017
1224
|
st.sums[cat] += node.tokens;
|
|
1018
1225
|
st.surface.push(node);
|
|
1019
1226
|
if (shadowEventSeq !== void 0) {
|
|
@@ -1069,7 +1276,7 @@ function tokenCountOf(value) {
|
|
|
1069
1276
|
/**
|
|
1070
1277
|
* The DeepSeek V4 model family a model name prices as — matched on the NAME
|
|
1071
1278
|
* alone (provider-agnostic: official API, proxies, OpenRouter spellings like
|
|
1072
|
-
* `deepseek/deepseek-v4-flash` all land here). Null for any other model:
|
|
1279
|
+
* `deepseek/deepseek-v4.1-flash` all land here). Null for any other model:
|
|
1073
1280
|
* non-V4 usage is simply not priced.
|
|
1074
1281
|
*/
|
|
1075
1282
|
function costFamilyOf(model) {
|
|
@@ -1132,27 +1339,18 @@ function accumulateCost(st, time, usage) {
|
|
|
1132
1339
|
*/
|
|
1133
1340
|
/** The timing card's per-tool ranking cap: the busiest 16 names are kept. */
|
|
1134
1341
|
const TOOL_TIMING_CAP = 16;
|
|
1342
|
+
/** The decode buckets of the generation split, in card order (see TimingTotals). */
|
|
1343
|
+
const DECODE_KINDS = [
|
|
1344
|
+
"reasoning",
|
|
1345
|
+
"text",
|
|
1346
|
+
"toolarg"
|
|
1347
|
+
];
|
|
1135
1348
|
/** Non-negative, NaN-proof duration between two instants (hostile times degrade to 0). */
|
|
1136
1349
|
function durOf(from, to) {
|
|
1137
1350
|
if (!Number.isFinite(from) || !Number.isFinite(to)) return 0;
|
|
1138
1351
|
return Math.max(0, to - from);
|
|
1139
1352
|
}
|
|
1140
1353
|
/**
|
|
1141
|
-
* Whether a stream chunk carries a non-empty token delta — the first-token
|
|
1142
|
-
* marker the TTFT fold waits for (the same rule as the harness's own
|
|
1143
|
-
* session-stats fold). Shape-guarded: a malformed chunk is just not a token.
|
|
1144
|
-
*/
|
|
1145
|
-
function isTokenDelta(chunk) {
|
|
1146
|
-
if (chunk === null || typeof chunk !== "object") return false;
|
|
1147
|
-
const c = chunk;
|
|
1148
|
-
switch (c.type) {
|
|
1149
|
-
case "text-delta":
|
|
1150
|
-
case "reasoning-delta": return typeof c.text === "string" && c.text !== "";
|
|
1151
|
-
case "tool-call-delta": return typeof c.argumentsDelta === "string" && c.argumentsDelta !== "" || c.name !== void 0;
|
|
1152
|
-
default: return false;
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
/**
|
|
1156
1354
|
* The fold's private timing accumulator: created on first use, and CLONED on
|
|
1157
1355
|
* every later ensure() (see `applyTimeline`) — the object left in the
|
|
1158
1356
|
* persisted previous state is never written into in place.
|
|
@@ -1170,6 +1368,18 @@ function ensureTiming(st) {
|
|
|
1170
1368
|
return st.timing;
|
|
1171
1369
|
}
|
|
1172
1370
|
/**
|
|
1371
|
+
* Fold one block's decode span into the totals' generation split (see
|
|
1372
|
+
* TimingTotals). A zero span stays ABSENT — the field then carries the
|
|
1373
|
+
* "no time was decoded in this bucket" fact without adding dead properties to
|
|
1374
|
+
* every pre-split-shaped state, and the card reads absence as 0.
|
|
1375
|
+
*/
|
|
1376
|
+
function addDecode(timing, kind, ms) {
|
|
1377
|
+
if (!(ms > 0)) return;
|
|
1378
|
+
if (kind === "reasoning") timing.reasoningMs = (timing.reasoningMs ?? 0) + ms;
|
|
1379
|
+
else if (kind === "text") timing.textMs = (timing.textMs ?? 0) + ms;
|
|
1380
|
+
else timing.toolArgMs = (timing.toolArgMs ?? 0) + ms;
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1173
1383
|
* Tally one completed tool call into the per-name ranking, bounded to
|
|
1174
1384
|
* TOOL_TIMING_CAP names: repeated names update in place, a new name beyond
|
|
1175
1385
|
* the cap evicts the smallest tally first (the ranking's tail), so state
|
|
@@ -1225,7 +1435,20 @@ function applyTimeline(state, event, bounds) {
|
|
|
1225
1435
|
const tools = Array.isArray(header.tools) ? header.tools : [];
|
|
1226
1436
|
const s = ensure();
|
|
1227
1437
|
s.toolsTokens = estimateToolsTotal(tools);
|
|
1228
|
-
|
|
1438
|
+
const systemText = header.system;
|
|
1439
|
+
if (typeof systemText === "string" && systemText !== "") {
|
|
1440
|
+
s.systems = [{
|
|
1441
|
+
seq: event.seq,
|
|
1442
|
+
time: event.time,
|
|
1443
|
+
tokens: estimateSystemTokens(systemText)
|
|
1444
|
+
}];
|
|
1445
|
+
s.systemsFromHeader = true;
|
|
1446
|
+
s.systemTokens = systemTokensOf(s.systems);
|
|
1447
|
+
} else if (s.systemsFromHeader === true) {
|
|
1448
|
+
s.systems = [];
|
|
1449
|
+
delete s.systemsFromHeader;
|
|
1450
|
+
s.systemTokens = 0;
|
|
1451
|
+
}
|
|
1229
1452
|
if (header.config && typeof header.config.model === "string") s.model = header.config.model;
|
|
1230
1453
|
if (header.config && typeof header.config.provider === "string") s.provider = header.config.provider;
|
|
1231
1454
|
if ((data?.reason === "change" || data?.reason === "resume") && s.model && s.lastModel && s.model !== s.lastModel) {
|
|
@@ -1241,6 +1464,25 @@ function applyTimeline(state, event, bounds) {
|
|
|
1241
1464
|
if (s.model) s.lastModel = s.model;
|
|
1242
1465
|
break;
|
|
1243
1466
|
}
|
|
1467
|
+
case "system/message": {
|
|
1468
|
+
const s = ensure();
|
|
1469
|
+
delete s.pendingShadowedSeqs;
|
|
1470
|
+
delete s.pendingShadowEventSeq;
|
|
1471
|
+
const op = replaceRangeOf(event.surfaceOp);
|
|
1472
|
+
if (op !== null) {
|
|
1473
|
+
s.systems = (s.systems ?? []).filter((n) => n.seq < op.start || n.seq > op.end);
|
|
1474
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
1475
|
+
for (const n of s.surface) if (n.seq >= op.start && n.seq <= op.end) claimed.add(n.seq);
|
|
1476
|
+
if (removeSurfaceSeqs(s, claimed, event.seq).length > 0) bumpDetailRev(s);
|
|
1477
|
+
}
|
|
1478
|
+
delete s.systemsFromHeader;
|
|
1479
|
+
pushSystem(s, {
|
|
1480
|
+
seq: event.seq,
|
|
1481
|
+
time: event.time,
|
|
1482
|
+
tokens: estimateSystemContent(messageOf(data)?.content)
|
|
1483
|
+
});
|
|
1484
|
+
break;
|
|
1485
|
+
}
|
|
1244
1486
|
case "request/context": {
|
|
1245
1487
|
const s = ensure();
|
|
1246
1488
|
if (data && typeof data.contextWindow === "number") s.contextWindow = data.contextWindow;
|
|
@@ -1259,7 +1501,8 @@ function applyTimeline(state, event, bounds) {
|
|
|
1259
1501
|
};
|
|
1260
1502
|
}
|
|
1261
1503
|
break;
|
|
1262
|
-
case "tool/code-dispatch":
|
|
1504
|
+
case "tool/code-dispatch":
|
|
1505
|
+
case "tool/ptc-dispatch": {
|
|
1263
1506
|
const rootCallId = data?.rootCallId;
|
|
1264
1507
|
const name = data?.name;
|
|
1265
1508
|
if (typeof rootCallId === "string" && typeof name === "string") {
|
|
@@ -1275,13 +1518,48 @@ function applyTimeline(state, event, bounds) {
|
|
|
1275
1518
|
break;
|
|
1276
1519
|
}
|
|
1277
1520
|
case "assistant/chunk": {
|
|
1521
|
+
const start = state.stepStart;
|
|
1522
|
+
if (start === void 0) return state;
|
|
1523
|
+
const chunk = data?.chunk;
|
|
1524
|
+
if (chunk !== null && typeof chunk === "object" && chunk.type === "block-start") {
|
|
1525
|
+
const kind = decodeKindOfBlock(chunk.blockType);
|
|
1526
|
+
if (start.block === void 0 && kind === void 0) return state;
|
|
1527
|
+
const s = ensure();
|
|
1528
|
+
const decode = { ...start.decode ?? {
|
|
1529
|
+
reasoning: 0,
|
|
1530
|
+
text: 0,
|
|
1531
|
+
toolarg: 0
|
|
1532
|
+
} };
|
|
1533
|
+
if (start.block !== void 0) decode[start.block.kind] += durOf(start.block.since, event.time);
|
|
1534
|
+
s.stepStart = {
|
|
1535
|
+
time: start.time,
|
|
1536
|
+
...start.firstToken !== void 0 ? { firstToken: start.firstToken } : {},
|
|
1537
|
+
decode,
|
|
1538
|
+
...kind !== void 0 ? { block: {
|
|
1539
|
+
kind,
|
|
1540
|
+
since: event.time
|
|
1541
|
+
} } : {}
|
|
1542
|
+
};
|
|
1543
|
+
break;
|
|
1544
|
+
}
|
|
1545
|
+
if (start.firstToken !== void 0) return state;
|
|
1546
|
+
if (!isTokenChunk(data?.chunk)) return state;
|
|
1547
|
+
const s = ensure();
|
|
1548
|
+
s.stepStart = {
|
|
1549
|
+
...start,
|
|
1550
|
+
firstToken: event.time
|
|
1551
|
+
};
|
|
1552
|
+
break;
|
|
1553
|
+
}
|
|
1554
|
+
case "assistant/attempt": {
|
|
1278
1555
|
const start = state.stepStart;
|
|
1279
1556
|
if (start === void 0 || start.firstToken !== void 0) return state;
|
|
1280
|
-
|
|
1557
|
+
const first = firstTokenTimeOfStream(data?.stream);
|
|
1558
|
+
if (first === void 0) return state;
|
|
1281
1559
|
const s = ensure();
|
|
1282
1560
|
s.stepStart = {
|
|
1283
1561
|
time: start.time,
|
|
1284
|
-
firstToken:
|
|
1562
|
+
firstToken: first
|
|
1285
1563
|
};
|
|
1286
1564
|
break;
|
|
1287
1565
|
}
|
|
@@ -1410,9 +1688,20 @@ function applyTimeline(state, event, bounds) {
|
|
|
1410
1688
|
const timing = ensureTiming(s);
|
|
1411
1689
|
timing.calls += 1;
|
|
1412
1690
|
const stepStart = state.stepStart;
|
|
1413
|
-
if (stepStart !== void 0
|
|
1414
|
-
|
|
1415
|
-
|
|
1691
|
+
if (stepStart !== void 0) {
|
|
1692
|
+
const firstToken = stepStart.firstToken ?? firstTokenTimeOfStream(data?.stream);
|
|
1693
|
+
if (firstToken !== void 0) {
|
|
1694
|
+
timing.ttftMs += durOf(stepStart.time, firstToken);
|
|
1695
|
+
timing.genMs += durOf(firstToken, event.time);
|
|
1696
|
+
if (stepStart.decode !== void 0) {
|
|
1697
|
+
const decode = { ...stepStart.decode };
|
|
1698
|
+
if (stepStart.block !== void 0) decode[stepStart.block.kind] += durOf(stepStart.block.since, event.time);
|
|
1699
|
+
for (const kind of DECODE_KINDS) addDecode(timing, kind, decode[kind]);
|
|
1700
|
+
} else {
|
|
1701
|
+
const spans = decodeSpansOfStream(data?.stream, event.time);
|
|
1702
|
+
for (const kind of DECODE_KINDS) addDecode(timing, kind, spans[kind]);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1416
1705
|
}
|
|
1417
1706
|
const asstMsg = deriveEventMessage(event);
|
|
1418
1707
|
applySurface(s, event, event.type, data, asstMsg);
|
|
@@ -1514,6 +1803,7 @@ function headFieldsOf(state) {
|
|
|
1514
1803
|
tools
|
|
1515
1804
|
};
|
|
1516
1805
|
}
|
|
1806
|
+
if (state.systems !== void 0 && state.systems.length > 0) result.systems = state.systems.map((n) => ({ ...n }));
|
|
1517
1807
|
return result;
|
|
1518
1808
|
}
|
|
1519
1809
|
/**
|
|
@@ -1951,6 +2241,12 @@ const surfaceNodeSchema = z.object({
|
|
|
1951
2241
|
skill: z.string().optional(),
|
|
1952
2242
|
calls: z.array(z.string()).optional()
|
|
1953
2243
|
}).strict();
|
|
2244
|
+
/** One live system-prompt node (shared/types.ts SystemPromptNode). */
|
|
2245
|
+
const systemPromptNodeSchema = z.object({
|
|
2246
|
+
seq: z.number().int().nonnegative(),
|
|
2247
|
+
time: z.number(),
|
|
2248
|
+
tokens: z.number().int().nonnegative()
|
|
2249
|
+
}).strict();
|
|
1954
2250
|
const requestRecordSchema = z.object({
|
|
1955
2251
|
turn: z.number().optional(),
|
|
1956
2252
|
step: z.number().optional(),
|
|
@@ -2046,6 +2342,9 @@ const timingTotalsSchema = z.object({
|
|
|
2046
2342
|
wallMs: z.number().nonnegative(),
|
|
2047
2343
|
ttftMs: z.number().nonnegative(),
|
|
2048
2344
|
genMs: z.number().nonnegative(),
|
|
2345
|
+
reasoningMs: z.number().nonnegative().optional(),
|
|
2346
|
+
textMs: z.number().nonnegative().optional(),
|
|
2347
|
+
toolArgMs: z.number().nonnegative().optional(),
|
|
2049
2348
|
calls: z.number().int().nonnegative(),
|
|
2050
2349
|
toolsMs: z.number().nonnegative(),
|
|
2051
2350
|
toolCalls: z.number().int().nonnegative(),
|
|
@@ -2097,6 +2396,7 @@ const contextTimelineSchema = z.object({
|
|
|
2097
2396
|
pro: costFamilySchema.optional()
|
|
2098
2397
|
}).strict().optional(),
|
|
2099
2398
|
timing: timingTotalsSchema.optional(),
|
|
2399
|
+
systems: z.array(systemPromptNodeSchema).optional(),
|
|
2100
2400
|
nodes: z.array(surfaceNodeSchema).optional(),
|
|
2101
2401
|
droppedNodes: z.number().int().nonnegative().optional(),
|
|
2102
2402
|
archive: z.array(surfaceNodeSchema).optional(),
|
|
@@ -2120,6 +2420,8 @@ const timelineStateSchema = z.object({
|
|
|
2120
2420
|
tool: z.number().int().nonnegative()
|
|
2121
2421
|
}).strict(),
|
|
2122
2422
|
systemTokens: z.number().int().nonnegative(),
|
|
2423
|
+
systems: z.array(systemPromptNodeSchema).optional(),
|
|
2424
|
+
systemsFromHeader: z.literal(true).optional(),
|
|
2123
2425
|
toolsTokens: z.number().int().nonnegative(),
|
|
2124
2426
|
model: z.string().optional(),
|
|
2125
2427
|
provider: z.string().optional(),
|
|
@@ -2136,7 +2438,20 @@ const timelineStateSchema = z.object({
|
|
|
2136
2438
|
timing: timingTotalsSchema.optional(),
|
|
2137
2439
|
stepStart: z.object({
|
|
2138
2440
|
time: z.number(),
|
|
2139
|
-
firstToken: z.number().optional()
|
|
2441
|
+
firstToken: z.number().optional(),
|
|
2442
|
+
decode: z.object({
|
|
2443
|
+
reasoning: z.number(),
|
|
2444
|
+
text: z.number(),
|
|
2445
|
+
toolarg: z.number()
|
|
2446
|
+
}).strict().optional(),
|
|
2447
|
+
block: z.object({
|
|
2448
|
+
kind: z.enum([
|
|
2449
|
+
"reasoning",
|
|
2450
|
+
"text",
|
|
2451
|
+
"toolarg"
|
|
2452
|
+
]),
|
|
2453
|
+
since: z.number()
|
|
2454
|
+
}).strict().optional()
|
|
2140
2455
|
}).strict().optional(),
|
|
2141
2456
|
callNames: z.record(z.string(), z.object({
|
|
2142
2457
|
name: z.string(),
|