@chroxify/eunoe 1.3.0 → 1.3.1
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 +19 -12
- package/dist/core/index.js +285 -166
- package/dist/types/context/fold/strategies.d.ts +1 -1
- package/dist/types/context/fold/turns.d.ts +2 -0
- package/dist/types/context/recall/index.d.ts +3 -0
- package/dist/types/core/config.d.ts +1 -0
- package/dist/types/core/recall.d.ts +1 -0
- package/dist/types/core/rewrite.d.ts +3 -3
- package/dist/types/core/state.d.ts +3 -0
- package/dist/types/core/usage.d.ts +2 -1
- package/dist/types/core/window.d.ts +5 -2
- package/package.json +1 -1
- package/src/context/fold/strategies.ts +23 -15
- package/src/context/fold/turns.ts +12 -0
- package/src/context/recall/index.ts +6 -1
- package/src/core/compaction.ts +55 -30
- package/src/core/config.ts +1 -1
- package/src/core/fold.ts +19 -17
- package/src/core/forward.ts +4 -2
- package/src/core/recall.ts +27 -3
- package/src/core/rewrite.ts +19 -7
- package/src/core/state.ts +10 -0
- package/src/core/usage.ts +17 -8
- package/src/core/window.ts +40 -10
|
@@ -8,7 +8,7 @@ export interface CompactOptions extends ReduceOptions {
|
|
|
8
8
|
reduce?: (turn: Message[], options: ReduceOptions) => Message[];
|
|
9
9
|
pinned?: ReadonlySet<string>;
|
|
10
10
|
}
|
|
11
|
-
export declare const countPinned: (
|
|
11
|
+
export declare const countPinned: (keys: string[], pinned: ReadonlySet<string>) => number;
|
|
12
12
|
export declare function trimMessages(messages: Message[], options?: ReduceOptions): Message[];
|
|
13
13
|
export declare const CARRIED_CHARS = 80000;
|
|
14
14
|
export declare const foldsOpening: (opening: Message[], keptFrom: number) => boolean;
|
|
@@ -9,6 +9,8 @@ export declare function splitTurns(messages: Message[]): {
|
|
|
9
9
|
turns: Message[][];
|
|
10
10
|
};
|
|
11
11
|
export declare function turnFingerprint(turn: Message[]): string;
|
|
12
|
+
export declare function turnKeys(turns: Message[][]): string[];
|
|
13
|
+
export declare const fingerprintOfKey: (key: string) => string;
|
|
12
14
|
export declare function isHarness(message: Message): boolean;
|
|
13
15
|
export declare function hasContent(message: Message): boolean;
|
|
14
16
|
export declare function reduceTurn(turn: Message[], options?: ReduceOptions): Message[];
|
|
@@ -12,6 +12,8 @@ export interface RecallOptions {
|
|
|
12
12
|
minTerms?: number;
|
|
13
13
|
maxChars?: number;
|
|
14
14
|
maxCharsMulti?: number;
|
|
15
|
+
maxCharsTotal?: number;
|
|
16
|
+
charsInUse?: number;
|
|
15
17
|
narrative?: boolean;
|
|
16
18
|
callWeight?: number;
|
|
17
19
|
salientExcerpt?: boolean;
|
|
@@ -22,6 +24,7 @@ export interface RecallPick {
|
|
|
22
24
|
query: Set<string>;
|
|
23
25
|
}
|
|
24
26
|
export type Reranker = (query: string, chunks: RecallChunk[], candidates: number[]) => Promise<number[]>;
|
|
27
|
+
export declare const RECALL_CHARS_TOTAL = 30000;
|
|
25
28
|
export declare function terms(text: string): string[];
|
|
26
29
|
export declare function recallChunks(messages: Message[], keptIds: Map<string, string>, options?: RecallOptions): RecallChunk[];
|
|
27
30
|
interface RankedChunk {
|
|
@@ -4,5 +4,6 @@ export declare const wantsRecall: (input: RewriteInput) => boolean;
|
|
|
4
4
|
export declare const activeRecalls: (recalls?: Record<string, string>) => {
|
|
5
5
|
[k: string]: string;
|
|
6
6
|
};
|
|
7
|
+
export declare function dropLiveRecall(input: RewriteInput, threadKey: string): void;
|
|
7
8
|
export declare function applyRecall(input: RewriteInput, result: Rewrite): Rewrite;
|
|
8
9
|
export declare function prefetchFor(input: RewriteInput, result: Rewrite, rerank: Reranker): Promise<number | null>;
|
|
@@ -54,10 +54,10 @@ export interface Rewrite {
|
|
|
54
54
|
note: RewriteNote;
|
|
55
55
|
}
|
|
56
56
|
export declare function threadKeyOf(threadId: string, messages: Message[]): string | null;
|
|
57
|
-
export declare function rewrite(
|
|
58
|
-
export declare function prefetchRecall(
|
|
57
|
+
export declare function rewrite(given: RewriteInput, before?: Rewrite): Rewrite;
|
|
58
|
+
export declare function prefetchRecall(given: RewriteInput, rerank: Reranker): Promise<{
|
|
59
59
|
reranked: boolean;
|
|
60
60
|
rerankError: null;
|
|
61
61
|
recalled: number;
|
|
62
62
|
} | null>;
|
|
63
|
-
export declare function rewriteAsync(
|
|
63
|
+
export declare function rewriteAsync(given: RewriteInput, rerank?: Reranker): Promise<Rewrite>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Message } from "../context/content.js";
|
|
1
2
|
export interface Handoff {
|
|
2
3
|
label: string;
|
|
3
4
|
source: string;
|
|
@@ -12,6 +13,7 @@ export interface ThreadState {
|
|
|
12
13
|
host?: boolean;
|
|
13
14
|
pins?: string[];
|
|
14
15
|
ratio?: number;
|
|
16
|
+
limits?: Record<string, number>;
|
|
15
17
|
recalls?: Record<string, string>;
|
|
16
18
|
}
|
|
17
19
|
export interface ThreadStore {
|
|
@@ -19,4 +21,5 @@ export interface ThreadStore {
|
|
|
19
21
|
save: (key: string, state: ThreadState) => void;
|
|
20
22
|
}
|
|
21
23
|
export declare function memoryStore(initial?: Record<string, ThreadState>): ThreadStore;
|
|
24
|
+
export declare function retireRecalls(state: ThreadState, folded: Message[][]): void;
|
|
22
25
|
export declare function resetCut(state: ThreadState): void;
|
|
@@ -4,4 +4,5 @@ export type Usage = Record<string, number>;
|
|
|
4
4
|
export declare function watchUsage(response: Response, onUsage: (usage: Usage) => void): Response;
|
|
5
5
|
export declare function inputTokens(usage: Usage): number;
|
|
6
6
|
export declare function recordUsage(store: ThreadStore, threadKey: string, sent: Body | number, usage: Usage): boolean;
|
|
7
|
-
export declare
|
|
7
|
+
export declare const limitKey: (model: unknown) => string;
|
|
8
|
+
export declare function recordOverflow(store: ThreadStore, threadKey: string, sent: Body | number, errorText: string, model?: unknown): number | null;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export type WindowLookup = (model: unknown, requestHeaders: Headers, upstream?: string) => Promise<number | null>;
|
|
2
|
-
export
|
|
2
|
+
export interface WindowLookupOptions {
|
|
3
3
|
known?: Map<string, number>;
|
|
4
4
|
onLearn?: (windows: Map<string, number>) => void;
|
|
5
|
-
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
retryAfterMs?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function createWindowLookup({ known, onLearn, timeoutMs, retryAfterMs }?: WindowLookupOptions): WindowLookup;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Block, type Body, blocks, clean, type Message, truncate, withoutCacheControl } from "../content"
|
|
2
2
|
import { clearedInput, compactionNotice, truncationNotice } from "../prompts"
|
|
3
|
-
import { hasContent, isHarness, isStaleNote, type ReduceOptions, reduceTurn, splitTurns, turnFingerprint } from "./turns"
|
|
3
|
+
import { fingerprintOfKey, hasContent, isHarness, isStaleNote, type ReduceOptions, reduceTurn, splitTurns, turnFingerprint, turnKeys } from "./turns"
|
|
4
4
|
|
|
5
5
|
export interface CompactOptions extends ReduceOptions {
|
|
6
6
|
keepTurns: number
|
|
@@ -11,12 +11,19 @@ export interface CompactOptions extends ReduceOptions {
|
|
|
11
11
|
pinned?: ReadonlySet<string>
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
(
|
|
16
|
-
(
|
|
17
|
-
|
|
14
|
+
function pinnedAt(turns: Message[][], { pinned }: CompactOptions): (index: number) => boolean {
|
|
15
|
+
if (pinned === undefined || pinned.size === 0) return () => false
|
|
16
|
+
const keys = turnKeys(turns)
|
|
17
|
+
return (index) => pinned.has(keys[index])
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function pinnedPrompt({ pinned }: CompactOptions): (turn: Message[]) => boolean {
|
|
21
|
+
if (pinned === undefined || pinned.size === 0) return () => false
|
|
22
|
+
const fingerprints = new Set([...pinned].map(fingerprintOfKey))
|
|
23
|
+
return (turn) => fingerprints.has(turnFingerprint(turn))
|
|
24
|
+
}
|
|
18
25
|
|
|
19
|
-
export const countPinned = (
|
|
26
|
+
export const countPinned = (keys: string[], pinned: ReadonlySet<string>) => (pinned.size > 0 ? keys.filter((key) => pinned.has(key)).length : 0)
|
|
20
27
|
|
|
21
28
|
export function trimMessages(messages: Message[], options: ReduceOptions = {}): Message[] {
|
|
22
29
|
const { preamble, turns } = splitTurns(messages)
|
|
@@ -50,18 +57,19 @@ export function compactMessages(messages: Message[], cutIndex: number, options:
|
|
|
50
57
|
const { preamble, turns } = splitTurns(messages)
|
|
51
58
|
if (cutIndex <= 0 || cutIndex >= turns.length) return messages
|
|
52
59
|
const reduce = options.reduce ?? reduceTurn
|
|
53
|
-
const isPinned =
|
|
60
|
+
const isPinned = pinnedAt(turns, options)
|
|
61
|
+
const indexed = turns.map((turn, index) => ({ turn, pinned: isPinned(index) }))
|
|
54
62
|
const keptFrom = Math.max(1, cutIndex - options.keepTurns)
|
|
55
63
|
const [first, ...rest] = turns[0]
|
|
56
|
-
const openingPinned =
|
|
64
|
+
const openingPinned = indexed[0].pinned
|
|
57
65
|
const openingFolded = !openingPinned && foldsOpening(turns[0], keptFrom)
|
|
58
|
-
const dropped =
|
|
59
|
-
const whole = dropped.filter(
|
|
60
|
-
const pinned = whole.length +
|
|
66
|
+
const dropped = indexed.slice(1, keptFrom)
|
|
67
|
+
const whole = dropped.filter((t) => t.pinned).map((t) => t.turn)
|
|
68
|
+
const pinned = whole.length + indexed.slice(keptFrom, cutIndex).filter((t) => t.pinned).length + (openingPinned ? 1 : 0)
|
|
61
69
|
const kept = cutIndex - keptFrom + (openingFolded ? 1 : 0)
|
|
62
70
|
const notice = compactionNotice({ omitted: dropped.length - whole.length + (options.omittedBefore ?? 0), kept, pinned, guide: options.guide })
|
|
63
|
-
const removed = [...(openingPinned || openingFolded ? [] : rest), ...dropped.filter((
|
|
64
|
-
const shown = (turn: Message[]) => (
|
|
71
|
+
const removed = [...(openingPinned || openingFolded ? [] : rest), ...dropped.filter((t) => !t.pinned).flatMap((t) => t.turn)]
|
|
72
|
+
const shown = (t: { turn: Message[]; pinned: boolean }) => (t.pinned ? t.turn.map(clean) : reduce(t.turn, options))
|
|
65
73
|
return [
|
|
66
74
|
...preamble,
|
|
67
75
|
first,
|
|
@@ -69,7 +77,7 @@ export function compactMessages(messages: Message[], cutIndex: number, options:
|
|
|
69
77
|
{ role: "user", content: [{ type: "text", text: notice }, ...carriedHarness(removed)] },
|
|
70
78
|
...(openingFolded ? reduce(turns[0], options).slice(1) : []),
|
|
71
79
|
...whole.flatMap((turn) => turn.map(clean)),
|
|
72
|
-
...
|
|
80
|
+
...indexed.slice(keptFrom, cutIndex).flatMap(shown),
|
|
73
81
|
...turns.slice(cutIndex).flat(),
|
|
74
82
|
]
|
|
75
83
|
}
|
|
@@ -80,7 +88,7 @@ export function capTurns(messages: Message[], options: CompactOptions): Message[
|
|
|
80
88
|
const { preamble, turns } = splitTurns(messages)
|
|
81
89
|
if (turns.length < 3) return messages
|
|
82
90
|
const live = turns.length - 1
|
|
83
|
-
const isPinned =
|
|
91
|
+
const isPinned = pinnedPrompt(options)
|
|
84
92
|
const over = (turn: Message[], i: number) => i > 0 && i < live && !isPinned(turn) && JSON.stringify(turn).length > capChars
|
|
85
93
|
if (!turns.some(over)) return messages
|
|
86
94
|
const reduce = options.reduce ?? reduceTurn
|
|
@@ -37,6 +37,18 @@ export function turnFingerprint(turn: Message[]): string {
|
|
|
37
37
|
return shortHash(JSON.stringify(blocks(turn[0]).map(withoutCacheControl)), 24)
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
export function turnKeys(turns: Message[][]): string[] {
|
|
41
|
+
const copies = new Map<string, number>()
|
|
42
|
+
return turns.map((turn) => {
|
|
43
|
+
const fingerprint = turnFingerprint(turn)
|
|
44
|
+
const earlier = copies.get(fingerprint) ?? 0
|
|
45
|
+
copies.set(fingerprint, earlier + 1)
|
|
46
|
+
return earlier === 0 ? fingerprint : `${fingerprint}~${earlier}`
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const fingerprintOfKey = (key: string) => key.split("~")[0]
|
|
51
|
+
|
|
40
52
|
export function isHarness(message: Message) {
|
|
41
53
|
return message.role === "system"
|
|
42
54
|
}
|
|
@@ -17,6 +17,8 @@ export interface RecallOptions {
|
|
|
17
17
|
minTerms?: number
|
|
18
18
|
maxChars?: number
|
|
19
19
|
maxCharsMulti?: number
|
|
20
|
+
maxCharsTotal?: number
|
|
21
|
+
charsInUse?: number
|
|
20
22
|
narrative?: boolean
|
|
21
23
|
callWeight?: number
|
|
22
24
|
salientExcerpt?: boolean
|
|
@@ -30,6 +32,8 @@ export interface RecallPick {
|
|
|
30
32
|
|
|
31
33
|
export type Reranker = (query: string, chunks: RecallChunk[], candidates: number[]) => Promise<number[]>
|
|
32
34
|
|
|
35
|
+
export const RECALL_CHARS_TOTAL = 30_000
|
|
36
|
+
|
|
33
37
|
const SHOWN_LINES = 8
|
|
34
38
|
const BM25_K1 = 1.2
|
|
35
39
|
const BM25_B = 0.75
|
|
@@ -388,7 +392,8 @@ export async function pickRecallWith(prompt: string, chunks: RecallChunk[], rera
|
|
|
388
392
|
|
|
389
393
|
export function renderRecall(prompt: string, chunks: RecallChunk[], picks: RecallPick[], options: RecallOptions = {}): string | null {
|
|
390
394
|
if (picks.length === 0) return null
|
|
391
|
-
const
|
|
395
|
+
const own = queriesOf(prompt).length > 1 ? (options.maxCharsMulti ?? 30_000) : (options.maxChars ?? 18_000)
|
|
396
|
+
const budget = Math.min(own, (options.maxCharsTotal ?? RECALL_CHARS_TOTAL) - (options.charsInUse ?? 0))
|
|
392
397
|
const entries: string[] = []
|
|
393
398
|
let used = 0
|
|
394
399
|
for (const { index, query } of [...picks].sort((a, b) => chunks[a.index].turn - chunks[b.index].turn || a.index - b.index)) {
|
package/src/core/compaction.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { type Body, type Message, quickHash } from "../context/content"
|
|
2
2
|
import { type CompactOptions, capTurns, clearToolResults, compactMessages, countPinned, countToolResults, foldsOpening } from "../context/fold/strategies"
|
|
3
|
-
import { type ReduceOptions, reduceTurn, splitTurns,
|
|
3
|
+
import { type ReduceOptions, reduceTurn, splitTurns, turnKeys } from "../context/fold/turns"
|
|
4
4
|
import { clearedOutput } from "../context/prompts"
|
|
5
5
|
import type { CoreConfig } from "./config"
|
|
6
6
|
import { DEFAULT_RATIO, type Estimator, estimator, targetSize } from "./estimate"
|
|
7
7
|
import type { RewriteNote } from "./rewrite"
|
|
8
|
-
import { resetCut, type ThreadState } from "./state"
|
|
8
|
+
import { resetCut, retireRecalls, type ThreadState } from "./state"
|
|
9
9
|
|
|
10
10
|
export interface CompactInput {
|
|
11
11
|
body: Body
|
|
@@ -51,16 +51,14 @@ interface Rolling {
|
|
|
51
51
|
|
|
52
52
|
const PROTECTED_OUTPUTS = 5
|
|
53
53
|
|
|
54
|
-
const STUCK_STEP = 0.1
|
|
55
|
-
|
|
56
54
|
function settledCut(turns: Message[][], index: number) {
|
|
57
55
|
let cut = index
|
|
58
56
|
while (cut > 1 && JSON.stringify(turns[cut][0].content).includes("[Request interrupted")) cut -= 1
|
|
59
57
|
return cut
|
|
60
58
|
}
|
|
61
59
|
|
|
62
|
-
function resolveCut(turns: Message[][], state: ThreadState, config: CoreConfig, rollingKeep: number | undefined) {
|
|
63
|
-
let cutIndex = state.cut ?
|
|
60
|
+
function resolveCut(turns: Message[][], keys: string[], state: ThreadState, config: CoreConfig, rollingKeep: number | undefined) {
|
|
61
|
+
let cutIndex = state.cut ? keys.indexOf(state.cut) : -1
|
|
64
62
|
let stateChanged = false
|
|
65
63
|
if (state.cut && cutIndex === -1) {
|
|
66
64
|
resetCut(state)
|
|
@@ -68,7 +66,8 @@ function resolveCut(turns: Message[][], state: ThreadState, config: CoreConfig,
|
|
|
68
66
|
}
|
|
69
67
|
if (cutIndex <= 0 && config.eval?.forceCut && turns.length >= 3) {
|
|
70
68
|
cutIndex = settledCut(turns, Math.max(1, turns.length - 1 - (rollingKeep ?? 1)))
|
|
71
|
-
state.cut =
|
|
69
|
+
state.cut = keys[cutIndex]
|
|
70
|
+
retireRecalls(state, turns.slice(0, cutIndex))
|
|
72
71
|
stateChanged = true
|
|
73
72
|
}
|
|
74
73
|
return { cutIndex, stateChanged }
|
|
@@ -92,39 +91,69 @@ function dueRoll(turns: Message[][], cutIndex: number, rolling: Rolling, size: E
|
|
|
92
91
|
return next >= 1 && next > cutIndex && backlog > rolling.budget ? next : null
|
|
93
92
|
}
|
|
94
93
|
|
|
95
|
-
function shrinkToTarget(over: () => boolean, step: () => string, actions: string[], rebuild: () => void)
|
|
94
|
+
function shrinkToTarget(over: () => boolean, step: () => string, actions: string[], rebuild: () => void) {
|
|
96
95
|
while (over()) {
|
|
97
96
|
const action = step()
|
|
98
97
|
actions.push(action)
|
|
99
|
-
if (action === "stuck") return
|
|
98
|
+
if (action === "stuck") return
|
|
100
99
|
rebuild()
|
|
101
100
|
}
|
|
102
|
-
return false
|
|
103
101
|
}
|
|
104
102
|
|
|
105
|
-
|
|
103
|
+
interface Shrink {
|
|
104
|
+
estimate: () => number
|
|
105
|
+
step: () => string
|
|
106
|
+
rebuild: () => void
|
|
107
|
+
target: number
|
|
108
|
+
limit: number
|
|
109
|
+
window: number | null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function shrinkOrHold(state: ThreadState, actions: string[], shrink: Shrink) {
|
|
113
|
+
const overWindow = (tokens: number) => shrink.window !== null && tokens > shrink.window
|
|
114
|
+
const held = state.stuck === undefined ? null : { keep: state.keep, cleared: state.cleared, actions: actions.length, overWindow: overWindow(shrink.estimate()) }
|
|
115
|
+
shrinkToTarget(() => shrink.estimate() > shrink.target, shrink.step, actions, shrink.rebuild)
|
|
116
|
+
const helped = (estimate: number) => estimate <= shrink.limit || (held?.overWindow === true && !overWindow(estimate))
|
|
117
|
+
if (held && !helped(shrink.estimate())) {
|
|
118
|
+
restoreShrink(state, held)
|
|
119
|
+
actions.length = held.actions
|
|
120
|
+
shrink.rebuild()
|
|
121
|
+
}
|
|
122
|
+
if (shrink.estimate() > shrink.limit) state.stuck = shrink.estimate()
|
|
123
|
+
else {
|
|
124
|
+
delete state.stuck
|
|
125
|
+
if (actions.at(-1) === "stuck") actions.pop()
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function restoreShrink(state: ThreadState, held: Pick<ThreadState, "keep" | "cleared">) {
|
|
130
|
+
if (held.keep === undefined) delete state.keep
|
|
131
|
+
else state.keep = held.keep
|
|
132
|
+
if (held.cleared === undefined) delete state.cleared
|
|
133
|
+
else state.cleared = held.cleared
|
|
134
|
+
}
|
|
106
135
|
|
|
107
136
|
export type StuckSizes = ReturnType<typeof stuckSizes>
|
|
108
137
|
|
|
109
|
-
const headLength = (preamble: Message[], first: Message[], state: ThreadState) => preamble.length + 1 + (countPinned([
|
|
138
|
+
const headLength = (preamble: Message[], first: Message[], firstKey: string, state: ThreadState) => preamble.length + 1 + (countPinned([firstKey], new Set(state.pins)) > 0 ? first.length : 1)
|
|
110
139
|
|
|
111
140
|
function stuckSizes(messages: Message[], headEnd: number, liveFrom: number, size: Estimator) {
|
|
112
141
|
const of = (list: Message[]) => size.of(list) - size.fixed
|
|
113
142
|
return { fixed: size.fixed, head: of(messages.slice(0, headEnd)), folded: of(messages.slice(headEnd, liveFrom)), live: of(messages.slice(liveFrom)) }
|
|
114
143
|
}
|
|
115
144
|
|
|
116
|
-
function cutNote(state: ThreadState, turns: Message[][], cutIndex: number, configuredKeep: number, actions: string[]) {
|
|
145
|
+
function cutNote(state: ThreadState, turns: Message[][], keys: string[], cutIndex: number, configuredKeep: number, actions: string[]) {
|
|
117
146
|
const pins = new Set(state.pins)
|
|
118
147
|
const keptFrom = cutIndex > 0 ? Math.max(1, cutIndex - (state.keep ?? configuredKeep)) : 0
|
|
119
|
-
const omitted = cutIndex > 0 ? keptFrom - 1 - countPinned(
|
|
148
|
+
const omitted = cutIndex > 0 ? keptFrom - 1 - countPinned(keys.slice(1, keptFrom), pins) : 0
|
|
120
149
|
return {
|
|
121
150
|
action: cutIndex > 0 ? ("compact" as const) : ("full" as const),
|
|
122
151
|
compacted: actions.includes("cut") || actions.includes("roll"),
|
|
123
152
|
actions: actions.length > 0 ? actions : undefined,
|
|
124
153
|
cutTurn: cutIndex > 0 ? cutIndex : null,
|
|
125
154
|
omitted,
|
|
126
|
-
folded: cutIndex > 0 ? cutIndex - keptFrom - countPinned(
|
|
127
|
-
pinned: cutIndex > 0 ? countPinned(
|
|
155
|
+
folded: cutIndex > 0 ? cutIndex - keptFrom - countPinned(keys.slice(keptFrom, cutIndex), pins) + (foldsOpening(turns[0], keptFrom) && countPinned([keys[0]], pins) === 0 ? 1 : 0) : 0,
|
|
156
|
+
pinned: cutIndex > 0 ? countPinned(keys.slice(0, cutIndex), pins) : 0,
|
|
128
157
|
host: state.host,
|
|
129
158
|
keep: state.keep,
|
|
130
159
|
cleared: state.cleared,
|
|
@@ -134,7 +163,8 @@ function cutNote(state: ThreadState, turns: Message[][], cutIndex: number, confi
|
|
|
134
163
|
export function compact({ body, config, state, window, guide, transcriptDir, mayCompact, omittedBefore = 0, decorate = (m) => m, reduce }: CompactInput): CompactResult {
|
|
135
164
|
const { preamble, turns } = splitTurns(body.messages)
|
|
136
165
|
const rolling = config.eval?.rolling ?? (config.mode === "default" ? { keep: config.keep, budget: config.budget } : undefined)
|
|
137
|
-
const
|
|
166
|
+
const keys = turnKeys(turns)
|
|
167
|
+
const resolved = resolveCut(turns, keys, state, config, rolling?.keep)
|
|
138
168
|
let cutIndex = resolved.cutIndex
|
|
139
169
|
let stateChanged = resolved.stateChanged
|
|
140
170
|
|
|
@@ -170,7 +200,8 @@ export function compact({ body, config, state, window, guide, transcriptDir, may
|
|
|
170
200
|
const moveCut = (index: number, action: string) => {
|
|
171
201
|
cutIndex = index
|
|
172
202
|
resetCut(state)
|
|
173
|
-
state.cut =
|
|
203
|
+
state.cut = keys[index]
|
|
204
|
+
retireRecalls(state, turns.slice(0, index))
|
|
174
205
|
actions.push(action)
|
|
175
206
|
rebuild()
|
|
176
207
|
}
|
|
@@ -184,16 +215,10 @@ export function compact({ body, config, state, window, guide, transcriptDir, may
|
|
|
184
215
|
if (mayCompact && estimate > limit) {
|
|
185
216
|
const current = turns.length - 1
|
|
186
217
|
if (current > 0 && current > cutIndex) moveCut(current, "cut")
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
actions,
|
|
192
|
-
rebuild,
|
|
193
|
-
)
|
|
194
|
-
if (stuck) state.stuck = estimate
|
|
195
|
-
else delete state.stuck
|
|
196
|
-
}
|
|
218
|
+
shrinkOrHold(state, actions, { estimate: () => estimate, step: () => shrinkOnce(state, cutIndex, configuredKeep, liveResults), rebuild, target, limit, window })
|
|
219
|
+
stateChanged = true
|
|
220
|
+
} else if (state.stuck !== undefined) {
|
|
221
|
+
delete state.stuck
|
|
197
222
|
stateChanged = true
|
|
198
223
|
}
|
|
199
224
|
|
|
@@ -201,8 +226,8 @@ export function compact({ body, config, state, window, guide, transcriptDir, may
|
|
|
201
226
|
messages,
|
|
202
227
|
stateChanged,
|
|
203
228
|
note: {
|
|
204
|
-
...cutNote(state, turns, cutIndex, configuredKeep, actions),
|
|
205
|
-
stuck: state.stuck === undefined ? undefined : stuckSizes(messages, headLength(preamble, turns[0], state), liveStart(messages), size),
|
|
229
|
+
...cutNote(state, turns, keys, cutIndex, configuredKeep, actions),
|
|
230
|
+
stuck: state.stuck === undefined ? undefined : stuckSizes(messages, headLength(preamble, turns[0], keys[0], state), liveStart(messages), size),
|
|
206
231
|
turns: turns.length,
|
|
207
232
|
messagesIn: body.messages.length,
|
|
208
233
|
messagesOut: messages.length,
|
package/src/core/config.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type Mode = "off" | "compact" | "default"
|
|
2
2
|
|
|
3
3
|
interface EvalConfig {
|
|
4
|
-
recallOptions?: { maxChars?: number; maxCharsMulti?: number; perQuery?: number }
|
|
4
|
+
recallOptions?: { maxChars?: number; maxCharsMulti?: number; maxCharsTotal?: number; perQuery?: number }
|
|
5
5
|
strategy?: "tail" | "guide" | "trim"
|
|
6
6
|
rolling?: { keep: number; budget: number }
|
|
7
7
|
forceCut?: boolean
|
package/src/core/fold.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Message } from "../context/content"
|
|
2
|
-
import { splitTurns,
|
|
2
|
+
import { splitTurns, turnKeys } from "../context/fold/turns"
|
|
3
3
|
import { threadKeyOf } from "./rewrite"
|
|
4
|
-
import { resetCut, type ThreadState, type ThreadStore } from "./state"
|
|
4
|
+
import { resetCut, retireRecalls, type ThreadState, type ThreadStore } from "./state"
|
|
5
5
|
|
|
6
6
|
export interface FoldTarget {
|
|
7
7
|
store: ThreadStore
|
|
@@ -23,7 +23,8 @@ export interface Folded {
|
|
|
23
23
|
function located({ store, threadId, messages }: FoldTarget) {
|
|
24
24
|
const threadKey = threadKeyOf(threadId, messages)
|
|
25
25
|
if (!threadKey) throw new RangeError("eunoe can only fold a thread that has at least one user turn")
|
|
26
|
-
|
|
26
|
+
const turns = splitTurns(messages).turns
|
|
27
|
+
return { threadKey, turns, keys: turnKeys(turns), state: store.read(threadKey) }
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
function checkedTurn(turn: number, turns: Message[][], from: number): number {
|
|
@@ -32,38 +33,39 @@ function checkedTurn(turn: number, turns: Message[][], from: number): number {
|
|
|
32
33
|
return turn
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
function folded(threadKey: string,
|
|
36
|
-
const cutTurn = state.cut ?
|
|
36
|
+
function folded(threadKey: string, keys: string[], state: ThreadState): Folded {
|
|
37
|
+
const cutTurn = state.cut ? keys.indexOf(state.cut) : -1
|
|
37
38
|
const pins = new Set(state.pins)
|
|
38
|
-
return { threadKey, cutTurn: cutTurn > 0 ? cutTurn : null, pinned:
|
|
39
|
+
return { threadKey, cutTurn: cutTurn > 0 ? cutTurn : null, pinned: keys.flatMap((key, i) => (pins.has(key) ? [i] : [])) }
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
function withPins(state: ThreadState, turns: Message[][], indices: number[], pinned: boolean) {
|
|
42
|
+
function withPins(state: ThreadState, turns: Message[][], keys: string[], indices: number[], pinned: boolean) {
|
|
42
43
|
const pins = new Set(state.pins)
|
|
43
44
|
for (const index of indices) {
|
|
44
|
-
const
|
|
45
|
-
if (pinned) pins.add(
|
|
46
|
-
else pins.delete(
|
|
45
|
+
const key = keys[checkedTurn(index, turns, 0)]
|
|
46
|
+
if (pinned) pins.add(key)
|
|
47
|
+
else pins.delete(key)
|
|
47
48
|
}
|
|
48
49
|
if (pins.size > 0) state.pins = [...pins].sort()
|
|
49
50
|
else delete state.pins
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
export function foldThread(target: FoldTarget, { at = "latest", pin = [] }: FoldOptions = {}): Folded {
|
|
53
|
-
const { threadKey, turns, state } = located(target)
|
|
54
|
+
const { threadKey, turns, keys, state } = located(target)
|
|
54
55
|
const cut = checkedTurn(at === "latest" ? turns.length - 1 : at, turns, 1)
|
|
55
|
-
withPins(state, turns, pin, true)
|
|
56
|
+
withPins(state, turns, keys, pin, true)
|
|
56
57
|
resetCut(state)
|
|
57
|
-
state.cut =
|
|
58
|
+
state.cut = keys[cut]
|
|
59
|
+
retireRecalls(state, turns.slice(0, cut))
|
|
58
60
|
state.keep = cut - 1
|
|
59
61
|
state.host = true
|
|
60
62
|
target.store.save(threadKey, state)
|
|
61
|
-
return folded(threadKey,
|
|
63
|
+
return folded(threadKey, keys, state)
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
export function pinTurns(target: FoldTarget, indices: number[], pinned = true): Folded {
|
|
65
|
-
const { threadKey, turns, state } = located(target)
|
|
66
|
-
withPins(state, turns, indices, pinned)
|
|
67
|
+
const { threadKey, turns, keys, state } = located(target)
|
|
68
|
+
withPins(state, turns, keys, indices, pinned)
|
|
67
69
|
target.store.save(threadKey, state)
|
|
68
|
-
return folded(threadKey,
|
|
70
|
+
return folded(threadKey, keys, state)
|
|
69
71
|
}
|
package/src/core/forward.ts
CHANGED
|
@@ -26,6 +26,7 @@ export interface ForwarderOptions {
|
|
|
26
26
|
|
|
27
27
|
interface Outgoing {
|
|
28
28
|
note: ForwardNote
|
|
29
|
+
model: unknown
|
|
29
30
|
threadKey: string | null
|
|
30
31
|
bodyText: string | undefined
|
|
31
32
|
sentSize: number
|
|
@@ -71,7 +72,7 @@ function defaultPrepare(options: ForwarderOptions, upstream: string): NonNullabl
|
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
const passthrough = (bodyText: string | undefined): Outgoing => ({ note: { action: "pass" }, threadKey: null, bodyText, sentSize: 0, givenSize: 0, fixedSize: 0 })
|
|
75
|
+
const passthrough = (bodyText: string | undefined): Outgoing => ({ note: { action: "pass" }, model: undefined, threadKey: null, bodyText, sentSize: 0, givenSize: 0, fixedSize: 0 })
|
|
75
76
|
|
|
76
77
|
async function outgoing(prepare: NonNullable<ForwarderOptions["prepare"]>, request: Request, givenText: string, mayCompact: boolean): Promise<Outgoing> {
|
|
77
78
|
try {
|
|
@@ -80,6 +81,7 @@ async function outgoing(prepare: NonNullable<ForwarderOptions["prepare"]>, reque
|
|
|
80
81
|
const sentSize = sizeOf(result.body)
|
|
81
82
|
return {
|
|
82
83
|
note: result.note,
|
|
84
|
+
model: given.model,
|
|
83
85
|
threadKey: result.threadKey,
|
|
84
86
|
bodyText: result.body !== given ? JSON.stringify(result.body, wellFormed) : givenText,
|
|
85
87
|
sentSize,
|
|
@@ -161,7 +163,7 @@ export function createForwarder(options: ForwarderOptions): (request: Request) =
|
|
|
161
163
|
if (answer.status >= 400 && out.note.action !== "pass") {
|
|
162
164
|
const errorText = await answer.text()
|
|
163
165
|
const logged = { path: url.pathname, status: answer.status, thread: out.threadKey, ...out.note }
|
|
164
|
-
const overflow = out.threadKey && !isCountTokens ? recordOverflow(options.store, out.threadKey, out.sentSize, errorText) : null
|
|
166
|
+
const overflow = out.threadKey && !isCountTokens ? recordOverflow(options.store, out.threadKey, out.sentSize, errorText, out.model) : null
|
|
165
167
|
if (!overflow) {
|
|
166
168
|
log({ ...logged, error: clipped(errorText) })
|
|
167
169
|
return errorResponse(errorText)
|
package/src/core/recall.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Body, blocks, type Message, resultText } from "../context/content"
|
|
2
|
-
import {
|
|
2
|
+
import { isPrompt } from "../context/fold/turns"
|
|
3
|
+
import { livePrompt, RECALL_CHARS_TOTAL, type RecallChunk, type RecallOptions, type Reranker, recallChunks, recallFor, recallForWith, withRecall } from "../context/recall"
|
|
3
4
|
import { messageFingerprint } from "./fingerprint"
|
|
4
5
|
import { activeHandoffs, handoffChunks } from "./handoff"
|
|
5
6
|
import type { Rewrite, RewriteInput } from "./rewrite"
|
|
@@ -27,6 +28,29 @@ function recallTarget(original: Body): { key: string; text: string } | null {
|
|
|
27
28
|
return live ? { key: messageFingerprint(original.messages[live.index]), text: live.text } : null
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
function charsInUse(messages: Message[], recalls: Record<string, string>): number {
|
|
32
|
+
return messages.reduce((sum, message) => sum + (isPrompt(message) ? (recalls[messageFingerprint(message)]?.length ?? 0) : 0), 0)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const RECALL_SHARE = 0.25
|
|
36
|
+
|
|
37
|
+
const RECALL_FLOOR = 6_000
|
|
38
|
+
|
|
39
|
+
function withinTotal(input: RewriteInput, result: Rewrite, recalls: Record<string, string>): RecallOptions {
|
|
40
|
+
const options = input.config.eval?.recallOptions
|
|
41
|
+
const share = Math.max(RECALL_FLOOR, RECALL_SHARE * JSON.stringify(result.body.messages).length)
|
|
42
|
+
return { ...options, maxCharsTotal: Math.min(options?.maxCharsTotal ?? RECALL_CHARS_TOTAL, share), charsInUse: charsInUse(result.body.messages, recalls) }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function dropLiveRecall(input: RewriteInput, threadKey: string) {
|
|
46
|
+
const target = recallTarget(input.body)
|
|
47
|
+
if (!target) return
|
|
48
|
+
const state = input.store.read(threadKey)
|
|
49
|
+
if (state.recalls?.[target.key] === "") return
|
|
50
|
+
state.recalls = { ...state.recalls, [target.key]: "" }
|
|
51
|
+
input.store.save(threadKey, state)
|
|
52
|
+
}
|
|
53
|
+
|
|
30
54
|
export function applyRecall(input: RewriteInput, result: Rewrite): Rewrite {
|
|
31
55
|
if (!touched(input, result) || !result.threadKey) return result
|
|
32
56
|
const state = input.store.read(result.threadKey)
|
|
@@ -34,7 +58,7 @@ export function applyRecall(input: RewriteInput, result: Rewrite): Rewrite {
|
|
|
34
58
|
const target = recallTarget(input.body)
|
|
35
59
|
let note = result.note
|
|
36
60
|
if (target && !(target.key in state.recalls)) {
|
|
37
|
-
state.recalls[target.key] = recallFor(target.text, droppedChunks(input, result, result.threadKey), input.
|
|
61
|
+
state.recalls[target.key] = recallFor(target.text, droppedChunks(input, result, result.threadKey), withinTotal(input, result, state.recalls)) ?? ""
|
|
38
62
|
input.store.save(result.threadKey, state)
|
|
39
63
|
note = { ...note, recalled: state.recalls[target.key].length }
|
|
40
64
|
}
|
|
@@ -49,7 +73,7 @@ export async function prefetchFor(input: RewriteInput, result: Rewrite, rerank:
|
|
|
49
73
|
const state = input.store.read(result.threadKey)
|
|
50
74
|
state.recalls ??= {}
|
|
51
75
|
if (target.key in state.recalls) return null
|
|
52
|
-
const options = { candidates: input.config.eval?.rerank?.candidates, ...input.
|
|
76
|
+
const options = { candidates: input.config.eval?.rerank?.candidates, ...withinTotal(input, result, state.recalls) }
|
|
53
77
|
state.recalls[target.key] = (await recallForWith(target.text, droppedChunks(input, result, result.threadKey), rerank, options)) ?? ""
|
|
54
78
|
input.store.save(result.threadKey, state)
|
|
55
79
|
return state.recalls[target.key].length
|