@animalabs/context-manager 0.5.5 → 0.5.9
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/src/adaptive/kv-control.d.ts +69 -32
- package/dist/src/adaptive/kv-control.d.ts.map +1 -1
- package/dist/src/adaptive/kv-control.js +471 -172
- package/dist/src/adaptive/kv-control.js.map +1 -1
- package/dist/src/adaptive/picker.d.ts +8 -0
- package/dist/src/adaptive/picker.d.ts.map +1 -1
- package/dist/src/adaptive/picker.js.map +1 -1
- package/dist/src/adaptive/strategies/kv-stable.d.ts +12 -4
- package/dist/src/adaptive/strategies/kv-stable.d.ts.map +1 -1
- package/dist/src/adaptive/strategies/kv-stable.js +14 -5
- package/dist/src/adaptive/strategies/kv-stable.js.map +1 -1
- package/dist/src/context-log.d.ts.map +1 -1
- package/dist/src/context-log.js +11 -0
- package/dist/src/context-log.js.map +1 -1
- package/dist/src/context-manager.d.ts +28 -1
- package/dist/src/context-manager.d.ts.map +1 -1
- package/dist/src/context-manager.js +42 -0
- package/dist/src/context-manager.js.map +1 -1
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/message-store.d.ts +60 -0
- package/dist/src/message-store.d.ts.map +1 -1
- package/dist/src/message-store.js +165 -10
- package/dist/src/message-store.js.map +1 -1
- package/dist/src/strategies/autobiographical.d.ts +207 -2
- package/dist/src/strategies/autobiographical.d.ts.map +1 -1
- package/dist/src/strategies/autobiographical.js +1283 -124
- package/dist/src/strategies/autobiographical.js.map +1 -1
- package/dist/src/strategies/index.d.ts +1 -1
- package/dist/src/strategies/index.d.ts.map +1 -1
- package/dist/src/strategies/index.js.map +1 -1
- package/dist/src/strategies/knowledge.d.ts +8 -0
- package/dist/src/strategies/knowledge.d.ts.map +1 -1
- package/dist/src/strategies/knowledge.js +23 -1
- package/dist/src/strategies/knowledge.js.map +1 -1
- package/dist/src/types/strategy.d.ts +82 -5
- package/dist/src/types/strategy.d.ts.map +1 -1
- package/dist/src/types/strategy.js.map +1 -1
- package/dist/test/adaptive/doc-plus-chat.test.js +7 -3
- package/dist/test/adaptive/doc-plus-chat.test.js.map +1 -1
- package/dist/test/adaptive/hard-fail.test.js +22 -0
- package/dist/test/adaptive/hard-fail.test.js.map +1 -1
- package/dist/test/adaptive/ingestion.test.js +3 -1
- package/dist/test/adaptive/ingestion.test.js.map +1 -1
- package/dist/test/adaptive/kv-control.test.js +17 -11
- package/dist/test/adaptive/kv-control.test.js.map +1 -1
- package/dist/test/adaptive/single-path-solve.test.d.ts +16 -0
- package/dist/test/adaptive/single-path-solve.test.d.ts.map +1 -0
- package/dist/test/adaptive/single-path-solve.test.js +204 -0
- package/dist/test/adaptive/single-path-solve.test.js.map +1 -0
- package/dist/test/anti-redundancy-budget.test.d.ts +17 -0
- package/dist/test/anti-redundancy-budget.test.d.ts.map +1 -0
- package/dist/test/anti-redundancy-budget.test.js +197 -0
- package/dist/test/anti-redundancy-budget.test.js.map +1 -0
- package/dist/test/chunk-persistence.test.d.ts +29 -0
- package/dist/test/chunk-persistence.test.d.ts.map +1 -0
- package/dist/test/chunk-persistence.test.js +290 -0
- package/dist/test/chunk-persistence.test.js.map +1 -0
- package/dist/test/compression-dedup.test.d.ts +20 -0
- package/dist/test/compression-dedup.test.d.ts.map +1 -0
- package/dist/test/compression-dedup.test.js +92 -0
- package/dist/test/compression-dedup.test.js.map +1 -0
- package/dist/test/compression-shape-invariants.test.js +40 -3
- package/dist/test/compression-shape-invariants.test.js.map +1 -1
- package/dist/test/head-window-order.test.d.ts +19 -0
- package/dist/test/head-window-order.test.d.ts.map +1 -0
- package/dist/test/head-window-order.test.js +159 -0
- package/dist/test/head-window-order.test.js.map +1 -0
- package/dist/test/image-strip-render-stats.test.js +25 -3
- package/dist/test/image-strip-render-stats.test.js.map +1 -1
- package/dist/test/knowledge-tool-boundary.test.d.ts +14 -0
- package/dist/test/knowledge-tool-boundary.test.d.ts.map +1 -0
- package/dist/test/knowledge-tool-boundary.test.js +93 -0
- package/dist/test/knowledge-tool-boundary.test.js.map +1 -0
- package/dist/test/l1-holdback.test.d.ts +15 -0
- package/dist/test/l1-holdback.test.d.ts.map +1 -0
- package/dist/test/l1-holdback.test.js +119 -0
- package/dist/test/l1-holdback.test.js.map +1 -0
- package/dist/test/merge-contiguity.test.d.ts +11 -0
- package/dist/test/merge-contiguity.test.d.ts.map +1 -0
- package/dist/test/merge-contiguity.test.js +74 -0
- package/dist/test/merge-contiguity.test.js.map +1 -0
- package/dist/test/message-store-window.test.d.ts +13 -0
- package/dist/test/message-store-window.test.d.ts.map +1 -0
- package/dist/test/message-store-window.test.js +177 -0
- package/dist/test/message-store-window.test.js.map +1 -0
- package/dist/test/speculation-cap.test.js +4 -0
- package/dist/test/speculation-cap.test.js.map +1 -1
- package/dist/test/summary-log-consistency.test.d.ts +18 -0
- package/dist/test/summary-log-consistency.test.d.ts.map +1 -0
- package/dist/test/summary-log-consistency.test.js +97 -0
- package/dist/test/summary-log-consistency.test.js.map +1 -0
- package/dist/test/tool-pairing-invariant.test.d.ts +15 -0
- package/dist/test/tool-pairing-invariant.test.d.ts.map +1 -0
- package/dist/test/tool-pairing-invariant.test.js +225 -0
- package/dist/test/tool-pairing-invariant.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +11 -3
- package/src/adaptive/kv-control.ts +509 -184
- package/src/adaptive/picker.ts +8 -0
- package/src/adaptive/strategies/kv-stable.ts +22 -10
- package/src/context-log.ts +11 -0
- package/src/context-manager.ts +48 -2
- package/src/index.ts +3 -2
- package/src/message-store.ts +202 -9
- package/src/strategies/autobiographical.ts +1343 -148
- package/src/strategies/index.ts +1 -1
- package/src/strategies/knowledge.ts +25 -1
- package/src/types/strategy.ts +89 -5
package/src/adaptive/picker.ts
CHANGED
|
@@ -99,6 +99,14 @@ export interface PickerChunk {
|
|
|
99
99
|
* by walking parentId pointers in the summary tree.
|
|
100
100
|
*/
|
|
101
101
|
l1Id?: SummaryId;
|
|
102
|
+
/**
|
|
103
|
+
* Salience ∈ [0,1] — the coefficient on this chunk's information loss
|
|
104
|
+
* (design §13.3). Lower = folds earlier and costs less when folded:
|
|
105
|
+
* content whose payload is externalized (code on disk, tool output,
|
|
106
|
+
* images, link drops) vs conversation that exists nowhere else. Default 1.
|
|
107
|
+
* Honored by the kv-stable controller; never overrides hard protections.
|
|
108
|
+
*/
|
|
109
|
+
salience?: number;
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
export interface PickerInputs {
|
|
@@ -30,14 +30,16 @@ import type {
|
|
|
30
30
|
} from '../folding-strategy.js';
|
|
31
31
|
import type { PickerInputs } from '../picker.js';
|
|
32
32
|
import { SummaryTree } from '../summary-tree.js';
|
|
33
|
-
import { planControlledFrontier } from '../kv-control.js';
|
|
33
|
+
import { planControlledFrontier, type ControlPlan } from '../kv-control.js';
|
|
34
34
|
|
|
35
35
|
export interface KvStableOptions {
|
|
36
|
-
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
36
|
+
/** Trust region P on per-turn perturbation, in tokens the provider would
|
|
37
|
+
* re-read (exact kvCost — see design §13.4). Within it the solver adopts
|
|
38
|
+
* the relevance-ideal cut; beyond it it amortizes via suffix adoption or
|
|
39
|
+
* overrides with cause. Default: the hard budget (never binds). */
|
|
40
40
|
reachTokens?: number;
|
|
41
|
+
/** Quality-gap override threshold (design §13.4). Default 0.35. */
|
|
42
|
+
qualityGapRatio?: number;
|
|
41
43
|
/** Base-k summary grouping (matches the strategy's mergeThreshold). Default 6. */
|
|
42
44
|
mergeThreshold?: number;
|
|
43
45
|
}
|
|
@@ -49,6 +51,7 @@ export class KvStableStrategy implements FoldingStrategy {
|
|
|
49
51
|
private readonly opts: KvStableOptions;
|
|
50
52
|
private readonly fPrev: Map<ChunkId, number>;
|
|
51
53
|
private target: Map<ChunkId, number> | null = null;
|
|
54
|
+
private _lastPlan: ControlPlan | null = null;
|
|
52
55
|
|
|
53
56
|
constructor(inputs: PickerInputs, opts: KvStableOptions = {}) {
|
|
54
57
|
this.inputs = inputs;
|
|
@@ -66,6 +69,13 @@ export class KvStableStrategy implements FoldingStrategy {
|
|
|
66
69
|
return this.target;
|
|
67
70
|
}
|
|
68
71
|
|
|
72
|
+
/** The full control plan behind the target (perturbation, override) — for
|
|
73
|
+
* observability at the call site (`[kv-escalation]` logging). Null before
|
|
74
|
+
* the first `selectNextFold`. */
|
|
75
|
+
lastPlan(): ControlPlan | null {
|
|
76
|
+
return this._lastPlan;
|
|
77
|
+
}
|
|
78
|
+
|
|
69
79
|
private solve(budget: FoldingBudget): Map<ChunkId, number> {
|
|
70
80
|
const tree = new SummaryTree(this.inputs);
|
|
71
81
|
|
|
@@ -119,23 +129,25 @@ export class KvStableStrategy implements FoldingStrategy {
|
|
|
119
129
|
}
|
|
120
130
|
}
|
|
121
131
|
|
|
122
|
-
|
|
132
|
+
const plan = planControlledFrontier(this.inputs, tree, {
|
|
123
133
|
previous: this.fPrev,
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
// [targetBudget, totalBudget] is the quiet dead band.
|
|
134
|
+
// Single-path solve (design §13.4): the [targetBudget, totalBudget] band
|
|
135
|
+
// is the quiet dead band; the trust region and overrides do the rest.
|
|
127
136
|
foldAtTokens: budget.totalBudget,
|
|
128
137
|
expandAtTokens: budget.targetBudget,
|
|
129
138
|
targetTokens: budget.targetBudget,
|
|
130
139
|
windowTokens: budget.totalBudget,
|
|
131
140
|
reachTokens: this.opts.reachTokens,
|
|
141
|
+
qualityGapRatio: this.opts.qualityGapRatio,
|
|
132
142
|
rawZone,
|
|
133
143
|
frozen,
|
|
134
144
|
fixedLevels,
|
|
135
145
|
pinCaps,
|
|
136
146
|
now,
|
|
137
147
|
mergeThreshold: this.opts.mergeThreshold,
|
|
138
|
-
})
|
|
148
|
+
});
|
|
149
|
+
this._lastPlan = plan;
|
|
150
|
+
return plan.resolutions;
|
|
139
151
|
}
|
|
140
152
|
|
|
141
153
|
/** Emit one op moving the live state toward the target frontier. */
|
package/src/context-log.ts
CHANGED
|
@@ -323,6 +323,17 @@ export class ContextLog {
|
|
|
323
323
|
}
|
|
324
324
|
|
|
325
325
|
private getInternal(index: number): ContextEntryInternal | null {
|
|
326
|
+
// Point lookup through chronicle's per-item cache — O(item size).
|
|
327
|
+
// See MessageStore.getInternal for why this must never fetch the
|
|
328
|
+
// full state per index.
|
|
329
|
+
//
|
|
330
|
+
// Feature-detect: getStateItemJson landed in chronicle 0.2.2; boxes
|
|
331
|
+
// still on <= 0.2.1 (npm copies) fall back to the full-materialization
|
|
332
|
+
// path so a routine `git pull` of this package can never crash them.
|
|
333
|
+
if (typeof (this.store as { getStateItemJson?: unknown }).getStateItemJson === 'function') {
|
|
334
|
+
const item = this.store.getStateItemJson(this.stateId, index);
|
|
335
|
+
return (item as ContextEntryInternal | null) ?? null;
|
|
336
|
+
}
|
|
326
337
|
const all = this.getAllInternal();
|
|
327
338
|
return all[index] ?? null;
|
|
328
339
|
}
|
package/src/context-manager.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { JsStore } from '@animalabs/chronicle';
|
|
2
|
-
import type { Membrane, NormalizedMessage, ContentBlock } from '@animalabs/membrane';
|
|
2
|
+
import type { Membrane, NormalizedMessage, ContentBlock, ToolDefinition } from '@animalabs/membrane';
|
|
3
3
|
import type {
|
|
4
4
|
MessageId,
|
|
5
5
|
Sequence,
|
|
@@ -23,7 +23,7 @@ import type {
|
|
|
23
23
|
} from './types/index.js';
|
|
24
24
|
import { isResettableStrategy, isPinnableStrategy, isSearchableStrategy, isRenderStatsCapable } from './types/index.js';
|
|
25
25
|
import type { RenderStats } from './types/index.js';
|
|
26
|
-
import { MessageStore, MessageStoreEvent } from './message-store.js';
|
|
26
|
+
import { MessageStore, MessageStoreEvent, MessageStoreListener, MessageWindow, MessageWindowOptions } from './message-store.js';
|
|
27
27
|
import { ContextLog } from './context-log.js';
|
|
28
28
|
import { PassthroughStrategy } from './strategies/passthrough.js';
|
|
29
29
|
import { splitMixedToolMessages } from './normalize-tool-messages.js';
|
|
@@ -301,6 +301,31 @@ export class ContextManager {
|
|
|
301
301
|
return this.messageStore.getAll();
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Get the total number of messages — O(1).
|
|
306
|
+
*/
|
|
307
|
+
getMessageCount(): number {
|
|
308
|
+
return this.messageStore.length();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Get a window of messages by slot index — O(window), not O(all).
|
|
313
|
+
* See MessageStore.getWindow for options (blob resolution, bodyGroup
|
|
314
|
+
* alignment). Intended for viewers/paginated UIs.
|
|
315
|
+
*/
|
|
316
|
+
getMessageWindow(offset: number, limit: number, opts?: MessageWindowOptions): MessageWindow {
|
|
317
|
+
return this.messageStore.getWindow(offset, limit, opts);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Subscribe to message-store mutations (add/edit/remove/removeRange).
|
|
322
|
+
* Returns a detacher. Unlike trace events, this fires for ALL stored
|
|
323
|
+
* messages including assistant turns and tool results.
|
|
324
|
+
*/
|
|
325
|
+
onMessage(listener: MessageStoreListener): () => void {
|
|
326
|
+
return this.messageStore.addListener(listener);
|
|
327
|
+
}
|
|
328
|
+
|
|
304
329
|
/**
|
|
305
330
|
* Query messages by filter criteria.
|
|
306
331
|
* Useful for finding messages from external sources, by participant, etc.
|
|
@@ -769,7 +794,22 @@ export class ContextManager {
|
|
|
769
794
|
}
|
|
770
795
|
}
|
|
771
796
|
|
|
797
|
+
/**
|
|
798
|
+
* Live tool definitions for the owning agent, refreshed by the host on
|
|
799
|
+
* every activation (Agent.buildActivationRequest in agent-framework).
|
|
800
|
+
* Threaded into StrategyContext so compression/summarizer LLM calls can
|
|
801
|
+
* declare the same tools as the live instance — required to avoid
|
|
802
|
+
* reasoning_extraction refusals on transcripts containing tool blocks.
|
|
803
|
+
*/
|
|
804
|
+
private toolDefinitions?: ToolDefinition[];
|
|
805
|
+
|
|
806
|
+
/** Host hook: record the agent's current tool definitions (see above). */
|
|
807
|
+
setToolDefinitions(tools: ToolDefinition[] | undefined): void {
|
|
808
|
+
if (tools && tools.length > 0) this.toolDefinitions = tools;
|
|
809
|
+
}
|
|
810
|
+
|
|
772
811
|
private createStrategyContext(): StrategyContext {
|
|
812
|
+
const self = this;
|
|
773
813
|
return {
|
|
774
814
|
messageStore: this.messageStore.createView(),
|
|
775
815
|
contextLog: this.contextLog.createView(),
|
|
@@ -777,6 +817,12 @@ export class ContextManager {
|
|
|
777
817
|
currentSequence: this.store.currentSequence(),
|
|
778
818
|
store: this.store,
|
|
779
819
|
namespace: this.strategyNamespace,
|
|
820
|
+
// Live getter, not a snapshot: strategies capture a ctx object once and
|
|
821
|
+
// reuse it across a long-running drain (driveSpeculativeDrain recurses
|
|
822
|
+
// with the same ctx). A snapshot taken before the session's first
|
|
823
|
+
// activation would freeze `tools` as undefined for the drain's entire
|
|
824
|
+
// lifetime — the getter always reflects the latest activation.
|
|
825
|
+
get tools() { return self.toolDefinitions; },
|
|
780
826
|
};
|
|
781
827
|
}
|
|
782
828
|
|
package/src/index.ts
CHANGED
|
@@ -7,13 +7,14 @@ export { phaseChannel, enterPhase, withPhase, withPhaseAsync } from './phase-cha
|
|
|
7
7
|
|
|
8
8
|
// Storage
|
|
9
9
|
export { MessageStore } from './message-store.js';
|
|
10
|
-
export type { MessageStoreEvent, MessageStoreListener } from './message-store.js';
|
|
10
|
+
export type { MessageStoreEvent, MessageStoreListener, MessageWindow, MessageWindowOptions } from './message-store.js';
|
|
11
|
+
export { concatBodyGroups } from './adaptive/render.js';
|
|
11
12
|
export { ContextLog } from './context-log.js';
|
|
12
13
|
export { BlobManager } from './blob-manager.js';
|
|
13
14
|
|
|
14
15
|
// Strategies
|
|
15
16
|
export { PassthroughStrategy } from './strategies/passthrough.js';
|
|
16
|
-
export { AutobiographicalStrategy, type AutobiographicalProgressSnapshot } from './strategies/autobiographical.js';
|
|
17
|
+
export { AutobiographicalStrategy, type AutobiographicalProgressSnapshot, type Chunk } from './strategies/autobiographical.js';
|
|
17
18
|
export { KnowledgeStrategy } from './strategies/knowledge.js';
|
|
18
19
|
|
|
19
20
|
// Utilities
|
package/src/message-store.ts
CHANGED
|
@@ -28,6 +28,39 @@ export type MessageStoreEvent =
|
|
|
28
28
|
*/
|
|
29
29
|
export type MessageStoreListener = (event: MessageStoreEvent) => void;
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Options for windowed message reads.
|
|
33
|
+
*/
|
|
34
|
+
export interface MessageWindowOptions {
|
|
35
|
+
/**
|
|
36
|
+
* Re-inline blob media (images/documents) into content blocks, matching
|
|
37
|
+
* the behavior of get()/getAll(). Default true. Viewers that only need
|
|
38
|
+
* text/thinking/tool blocks should pass false to avoid inflating large
|
|
39
|
+
* base64 payloads.
|
|
40
|
+
*/
|
|
41
|
+
resolveBlobs?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Extend the window edges outward so that no bodyGroup (shard run of a
|
|
44
|
+
* single large message) is split across the window boundary. Default
|
|
45
|
+
* false (exact offset/limit semantics).
|
|
46
|
+
*/
|
|
47
|
+
alignToBodyGroups?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A window of messages plus enough metadata to page through the store.
|
|
52
|
+
*/
|
|
53
|
+
export interface MessageWindow {
|
|
54
|
+
messages: StoredMessage[];
|
|
55
|
+
/**
|
|
56
|
+
* Actual first slot index of the returned window. May be lower than the
|
|
57
|
+
* requested offset when alignToBodyGroups extended the window backward.
|
|
58
|
+
*/
|
|
59
|
+
startIndex: number;
|
|
60
|
+
/** Total number of messages in the store at read time. */
|
|
61
|
+
totalCount: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
31
64
|
/**
|
|
32
65
|
* Options for token estimation.
|
|
33
66
|
*/
|
|
@@ -358,19 +391,72 @@ export class MessageStore {
|
|
|
358
391
|
);
|
|
359
392
|
}
|
|
360
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Get a window of messages by slot index — O(window), not O(all).
|
|
396
|
+
*
|
|
397
|
+
* Backed by chronicle's `getStateSlice` (0.2.2+) with a
|
|
398
|
+
* full-materialization fallback for older chronicle copies, mirroring
|
|
399
|
+
* the feature-detect in getInternal().
|
|
400
|
+
*/
|
|
401
|
+
getWindow(offset: number, limit: number, opts: MessageWindowOptions = {}): MessageWindow {
|
|
402
|
+
const totalCount = this.length();
|
|
403
|
+
let start = Math.max(0, Math.min(offset, totalCount));
|
|
404
|
+
let end = Math.min(start + Math.max(0, limit), totalCount);
|
|
405
|
+
|
|
406
|
+
if (start >= end) {
|
|
407
|
+
return { messages: [], startIndex: Math.min(start, totalCount), totalCount };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (opts.alignToBodyGroups) {
|
|
411
|
+
// Shards of a bodyGroup are contiguous by construction (removeRange
|
|
412
|
+
// refuses to bisect a group). Walk edges outward with O(item) point
|
|
413
|
+
// lookups until the group boundary.
|
|
414
|
+
const first = this.getInternal(start);
|
|
415
|
+
if (first?.bodyGroupId !== undefined) {
|
|
416
|
+
while (start > 0) {
|
|
417
|
+
const prev = this.getInternal(start - 1);
|
|
418
|
+
if (prev?.bodyGroupId !== first.bodyGroupId) break;
|
|
419
|
+
start--;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const last = this.getInternal(end - 1);
|
|
423
|
+
if (last?.bodyGroupId !== undefined) {
|
|
424
|
+
while (end < totalCount) {
|
|
425
|
+
const next = this.getInternal(end);
|
|
426
|
+
if (next?.bodyGroupId !== last.bodyGroupId) break;
|
|
427
|
+
end++;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const internals = this.getSliceInternal(start, end - start);
|
|
433
|
+
const resolveBlobs = opts.resolveBlobs !== false;
|
|
434
|
+
return {
|
|
435
|
+
messages: internals.map((internal, i) =>
|
|
436
|
+
this.internalToStored(internal, internal.id, start + i, resolveBlobs)
|
|
437
|
+
),
|
|
438
|
+
startIndex: start,
|
|
439
|
+
totalCount,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
361
443
|
/**
|
|
362
444
|
* Get messages from a specific index.
|
|
445
|
+
* Negative indices count from the end, matching Array.prototype.slice.
|
|
363
446
|
*/
|
|
364
447
|
getFrom(index: number): StoredMessage[] {
|
|
365
|
-
|
|
448
|
+
const len = this.length();
|
|
449
|
+
const start = index < 0 ? Math.max(0, len + index) : Math.min(index, len);
|
|
450
|
+
return this.getWindow(start, len - start).messages;
|
|
366
451
|
}
|
|
367
452
|
|
|
368
453
|
/**
|
|
369
454
|
* Get the last N messages.
|
|
370
455
|
*/
|
|
371
456
|
getTail(count: number): StoredMessage[] {
|
|
372
|
-
const
|
|
373
|
-
|
|
457
|
+
const len = this.length();
|
|
458
|
+
const n = Math.max(0, Math.min(count, len));
|
|
459
|
+
return this.getWindow(len - n, n).messages;
|
|
374
460
|
}
|
|
375
461
|
|
|
376
462
|
/**
|
|
@@ -391,21 +477,62 @@ export class MessageStore {
|
|
|
391
477
|
return tokens;
|
|
392
478
|
}
|
|
393
479
|
|
|
480
|
+
/**
|
|
481
|
+
* Closed-loop calibration multiplier applied to every estimate (default 1).
|
|
482
|
+
* Owned by the strategy: it compares real `usage` totals against the
|
|
483
|
+
* compile-time estimate and feeds the EMA back here, so the store's
|
|
484
|
+
* numbers track the live model/content mix instead of a fixed heuristic.
|
|
485
|
+
*/
|
|
486
|
+
private tokenCalibration = 1;
|
|
487
|
+
|
|
488
|
+
setTokenCalibration(factor: number): void {
|
|
489
|
+
if (Number.isFinite(factor) && factor > 0.25 && factor < 4) this.tokenCalibration = factor;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
getTokenCalibration(): number {
|
|
493
|
+
return this.tokenCalibration;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Hidden-CoT price for a signed thinking block whose text was summarized/
|
|
498
|
+
* redacted away (2026-07-12): the block estimates as EMPTY client-side, but
|
|
499
|
+
* signed replay bills the FULL original chain of thought. Measured on
|
|
500
|
+
* mythos production turns: median ~608 tokens/block, mean ~590, p90 ~1210.
|
|
501
|
+
* A per-block `tokenEstimate` stamped at creation (from usage residuals)
|
|
502
|
+
* takes precedence; this constant is the fallback for unstamped history.
|
|
503
|
+
*/
|
|
504
|
+
static readonly HIDDEN_THINKING_TOKENS_DEFAULT = 600;
|
|
505
|
+
|
|
394
506
|
private estimateBlockTokens(block: ContentBlock): number {
|
|
507
|
+
return Math.round(this.estimateBlockTokensRaw(block) * this.tokenCalibration);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private estimateBlockTokensRaw(block: ContentBlock): number {
|
|
395
511
|
switch (block.type) {
|
|
396
512
|
case 'text':
|
|
397
513
|
return this.tokenEstimator(block.text);
|
|
398
|
-
case 'thinking':
|
|
514
|
+
case 'thinking': {
|
|
515
|
+
// Stamped price wins; a signed-but-empty block is a HIDDEN full CoT
|
|
516
|
+
// (never "no thinking") — price it at the measured default.
|
|
517
|
+
const stamped = (block as { tokenEstimate?: number }).tokenEstimate;
|
|
518
|
+
if (typeof stamped === 'number') return stamped;
|
|
519
|
+
const hasSignature =
|
|
520
|
+
typeof (block as { signature?: string }).signature === 'string' &&
|
|
521
|
+
((block as { signature?: string }).signature as string).length > 0;
|
|
522
|
+
if (hasSignature && (!block.thinking || block.thinking.length === 0)) {
|
|
523
|
+
return MessageStore.HIDDEN_THINKING_TOKENS_DEFAULT;
|
|
524
|
+
}
|
|
399
525
|
return this.tokenEstimator(block.thinking);
|
|
526
|
+
}
|
|
400
527
|
case 'tool_use':
|
|
401
|
-
return
|
|
528
|
+
return jsonTokenEstimator(JSON.stringify(block.input)) + 20; // overhead for name, id
|
|
402
529
|
case 'tool_result':
|
|
403
530
|
if (!block.content) return 0;
|
|
404
531
|
if (typeof block.content === 'string') {
|
|
405
|
-
return
|
|
532
|
+
return jsonTokenEstimator(block.content);
|
|
406
533
|
}
|
|
407
534
|
if (Array.isArray(block.content)) {
|
|
408
|
-
return block.content.reduce((sum, b) => sum + this.
|
|
535
|
+
return block.content.reduce((sum, b) => sum + this.estimateBlockTokensRaw(b), 0);
|
|
409
536
|
}
|
|
410
537
|
return 0;
|
|
411
538
|
case 'image':
|
|
@@ -429,6 +556,8 @@ export class MessageStore {
|
|
|
429
556
|
getFrom: (index) => this.getFrom(index),
|
|
430
557
|
getTail: (count) => this.getTail(count),
|
|
431
558
|
length: () => this.length(),
|
|
559
|
+
setTokenCalibration: (f: number) => this.setTokenCalibration(f),
|
|
560
|
+
getTokenCalibration: () => this.getTokenCalibration(),
|
|
432
561
|
estimateTokens: (msg) => this.estimateTokens(msg),
|
|
433
562
|
};
|
|
434
563
|
}
|
|
@@ -545,7 +674,35 @@ export class MessageStore {
|
|
|
545
674
|
return state as StoredMessageInternal[];
|
|
546
675
|
}
|
|
547
676
|
|
|
677
|
+
private getSliceInternal(offset: number, limit: number): StoredMessageInternal[] {
|
|
678
|
+
// Windowed read — O(window) JSON conversion instead of materializing
|
|
679
|
+
// the entire state slot. getStateSlice landed in chronicle 0.2.2 and
|
|
680
|
+
// returns the window as a JSON-array Buffer; feature-detect so boxes
|
|
681
|
+
// on <= 0.2.1 fall back to full materialization (same pattern as
|
|
682
|
+
// getInternal below).
|
|
683
|
+
const s = this.store as { getStateSlice?: (id: string, offset: number, limit: number) => Buffer | null };
|
|
684
|
+
if (typeof s.getStateSlice === 'function') {
|
|
685
|
+
const buf = s.getStateSlice(this.stateId, offset, limit);
|
|
686
|
+
if (!buf) return [];
|
|
687
|
+
return JSON.parse(buf.toString('utf-8')) as StoredMessageInternal[];
|
|
688
|
+
}
|
|
689
|
+
return this.getAllInternal().slice(offset, offset + limit);
|
|
690
|
+
}
|
|
691
|
+
|
|
548
692
|
private getInternal(index: number): StoredMessageInternal | null {
|
|
693
|
+
// Point lookup through chronicle's per-item cache — O(item size).
|
|
694
|
+
// Never fetch the full state for a single index: with a 4.6k-message
|
|
695
|
+
// session, each full `getStateJson` materialization cost ~15ms, and
|
|
696
|
+
// per-entry get() loops turned renders into minutes (observed on
|
|
697
|
+
// Lena, 2026-07-02, /debug/context at 51–108s).
|
|
698
|
+
//
|
|
699
|
+
// Feature-detect: getStateItemJson landed in chronicle 0.2.2; boxes
|
|
700
|
+
// still on <= 0.2.1 (npm copies) fall back to the full-materialization
|
|
701
|
+
// path so a routine `git pull` of this package can never crash them.
|
|
702
|
+
if (typeof (this.store as { getStateItemJson?: unknown }).getStateItemJson === 'function') {
|
|
703
|
+
const item = this.store.getStateItemJson(this.stateId, index);
|
|
704
|
+
return (item as StoredMessageInternal | null) ?? null;
|
|
705
|
+
}
|
|
549
706
|
const all = this.getAllInternal();
|
|
550
707
|
return all[index] ?? null;
|
|
551
708
|
}
|
|
@@ -554,6 +711,7 @@ export class MessageStore {
|
|
|
554
711
|
internal: StoredMessageInternal,
|
|
555
712
|
id: MessageId,
|
|
556
713
|
_index: number,
|
|
714
|
+
resolveBlobs: boolean = true,
|
|
557
715
|
): StoredMessage {
|
|
558
716
|
const stored: StoredMessage = {
|
|
559
717
|
id,
|
|
@@ -570,7 +728,12 @@ export class MessageStore {
|
|
|
570
728
|
// so /undo of a 800-message conversation forked ~400 messages back.
|
|
571
729
|
sequence: internal.sequence,
|
|
572
730
|
participant: internal.participant,
|
|
573
|
-
|
|
731
|
+
// When resolveBlobs is false, blob_ref placeholder blocks are passed
|
|
732
|
+
// through un-inflated (StoredContentBlock ⊂ wire-safe superset of
|
|
733
|
+
// ContentBlock for viewer purposes).
|
|
734
|
+
content: resolveBlobs
|
|
735
|
+
? this.blobManager.resolveBlobs(internal.content)
|
|
736
|
+
: (internal.content as unknown as ContentBlock[]),
|
|
574
737
|
metadata: internal.metadata,
|
|
575
738
|
timestamp: new Date(internal.timestamp),
|
|
576
739
|
causedBy: internal.causedBy,
|
|
@@ -587,6 +750,36 @@ export class MessageStore {
|
|
|
587
750
|
/**
|
|
588
751
|
* Default token estimator: chars / 4
|
|
589
752
|
*/
|
|
753
|
+
/**
|
|
754
|
+
* Content-class token rates (2026-07-12, measured on mythos production
|
|
755
|
+
* requests by reconciling real `usage` against per-class char counts):
|
|
756
|
+
* - prose (Discord multiparty, markdown, emoji) ≈ 2.9 chars/token
|
|
757
|
+
* - JSON / tool i/o / code ≈ 2.3 chars/token
|
|
758
|
+
* The old flat chars/4 under-priced real windows by ~1.7-1.9x (a 183.6k
|
|
759
|
+
* "hard budget" compiled to a 344k request — straight into the refusal
|
|
760
|
+
* band). Rates are deliberately slightly conservative; the closed-loop
|
|
761
|
+
* calibration multiplier trims the residual per agent.
|
|
762
|
+
*/
|
|
763
|
+
const PROSE_CHARS_PER_TOKEN = 2.9;
|
|
764
|
+
const DENSE_CHARS_PER_TOKEN = 2.3;
|
|
765
|
+
|
|
590
766
|
function defaultTokenEstimator(text: string): number {
|
|
591
|
-
|
|
767
|
+
if (!text) return 0;
|
|
768
|
+
// Cheap density probe: JSON/code punctuation and non-ASCII share.
|
|
769
|
+
let dense = 0;
|
|
770
|
+
const n = Math.min(text.length, 2000);
|
|
771
|
+
for (let i = 0; i < n; i++) {
|
|
772
|
+
const c = text.charCodeAt(i);
|
|
773
|
+
if (c > 126) { dense++; continue; } // non-ASCII (emoji, accents, CJK)
|
|
774
|
+
const ch = text[i];
|
|
775
|
+
if (ch === '{' || ch === '}' || ch === '[' || ch === ']' || ch === '"' || ch === ':' || ch === '_' || ch === '/' || ch === '=' || ch === '`') dense++;
|
|
776
|
+
}
|
|
777
|
+
const rate = dense / n > 0.12 ? DENSE_CHARS_PER_TOKEN : PROSE_CHARS_PER_TOKEN;
|
|
778
|
+
return Math.ceil(text.length / rate);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/** JSON-ish payloads (tool inputs/results) always use the dense rate. */
|
|
782
|
+
function jsonTokenEstimator(text: string): number {
|
|
783
|
+
if (!text) return 0;
|
|
784
|
+
return Math.ceil(text.length / DENSE_CHARS_PER_TOKEN);
|
|
592
785
|
}
|