@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
package/src/core/rewrite.ts
CHANGED
|
@@ -8,8 +8,9 @@ import type { CoreConfig } from "./config"
|
|
|
8
8
|
import { DEFAULT_RATIO, estimator, sizeOf, targetSize } from "./estimate"
|
|
9
9
|
import { messageFingerprint } from "./fingerprint"
|
|
10
10
|
import { activeHandoffs, attachHandoffs, withHandoffs } from "./handoff"
|
|
11
|
-
import { activeRecalls, applyRecall, prefetchFor, wantsRecall } from "./recall"
|
|
11
|
+
import { activeRecalls, applyRecall, dropLiveRecall, prefetchFor, wantsRecall } from "./recall"
|
|
12
12
|
import type { ThreadStore } from "./state"
|
|
13
|
+
import { limitKey } from "./usage"
|
|
13
14
|
|
|
14
15
|
export interface ThreadSources {
|
|
15
16
|
guide?: string | null
|
|
@@ -119,7 +120,7 @@ function strategize(input: RewriteInput): Rewrite {
|
|
|
119
120
|
const based = isAgentThread(body) ? { ...body, system: withSystemPrompt(body.system) } : body
|
|
120
121
|
if (body.messages.length < 2) return { body: based, threadKey, note: { action: "full", turns: turns.length } }
|
|
121
122
|
const handoffs = activeHandoffs(state.handoffs)
|
|
122
|
-
const
|
|
123
|
+
const recalling = wantsRecall(input)
|
|
123
124
|
const result = compact({
|
|
124
125
|
body: based,
|
|
125
126
|
config,
|
|
@@ -129,7 +130,7 @@ function strategize(input: RewriteInput): Rewrite {
|
|
|
129
130
|
transcriptDir: sources.transcriptDir ?? null,
|
|
130
131
|
mayCompact: input.mayCompact && !off,
|
|
131
132
|
omittedBefore: input.omittedBefore,
|
|
132
|
-
decorate: (messages) => withRecall(withHandoffs(messages, handoffs), messageFingerprint, recalls),
|
|
133
|
+
decorate: (messages) => withRecall(withHandoffs(messages, handoffs), messageFingerprint, recalling ? activeRecalls(state.recalls) : {}),
|
|
133
134
|
})
|
|
134
135
|
if (result.stateChanged) store.save(threadKey, state)
|
|
135
136
|
return { body: result.messages === body.messages ? based : { ...based, messages: result.messages }, threadKey, note: result.note }
|
|
@@ -165,22 +166,33 @@ function carried(from: Rewrite, to: Rewrite): Rewrite {
|
|
|
165
166
|
return { ...to, note: { ...to.note, compacted: Boolean(to.note.compacted || from.note.compacted), actions } }
|
|
166
167
|
}
|
|
167
168
|
|
|
168
|
-
|
|
169
|
+
function withLearnedLimit(input: RewriteInput): RewriteInput {
|
|
170
|
+
const threadKey = input.threadId && Array.isArray(input.body.messages) ? threadKeyOf(input.threadId, input.body.messages) : null
|
|
171
|
+
const limit = threadKey ? input.store.read(threadKey).limits?.[limitKey(input.body.model)] : undefined
|
|
172
|
+
if (!limit || (input.window !== null && input.window <= limit)) return input
|
|
173
|
+
return { ...input, window: limit }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function rewrite(given: RewriteInput, before?: Rewrite): Rewrite {
|
|
177
|
+
const input = withLearnedLimit(given)
|
|
169
178
|
const result = before ? carried(before, once(input)) : once(input)
|
|
170
179
|
if (!overLimit(input, result)) return result
|
|
171
180
|
const recut = carried(result, once(input))
|
|
172
181
|
if (!overLimit(input, recut)) return { ...recut, note: { ...recut.note, fit: "recut" } }
|
|
173
|
-
|
|
182
|
+
if (recut.threadKey && wantsRecall(input)) dropLiveRecall(input, recut.threadKey)
|
|
183
|
+
const bare = carried(recut, once(input))
|
|
174
184
|
return { ...bare, note: { ...bare.note, fit: overLimit(input, bare) ? "over" : "bare", fitEstimate: sentEstimate(input, bare) } }
|
|
175
185
|
}
|
|
176
186
|
|
|
177
|
-
export async function prefetchRecall(
|
|
187
|
+
export async function prefetchRecall(given: RewriteInput, rerank: Reranker): Promise<{ reranked: boolean; rerankError: null; recalled: number } | null> {
|
|
188
|
+
const input = withLearnedLimit(given)
|
|
178
189
|
if (!wantsRecall(input)) return null
|
|
179
190
|
const recalled = await prefetchFor(input, strategize(input), rerank)
|
|
180
191
|
return recalled === null ? null : { reranked: rerank !== identityReranker, rerankError: null, recalled }
|
|
181
192
|
}
|
|
182
193
|
|
|
183
|
-
export async function rewriteAsync(
|
|
194
|
+
export async function rewriteAsync(given: RewriteInput, rerank: Reranker = identityReranker): Promise<Rewrite> {
|
|
195
|
+
const input = withLearnedLimit(given)
|
|
184
196
|
if (!wantsRecall(input) || rerank === identityReranker) return rewrite(input)
|
|
185
197
|
const first = strategize(input)
|
|
186
198
|
const recalled = await prefetchFor(input, first, rerank)
|
package/src/core/state.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type { Message } from "../context/content"
|
|
2
|
+
import { isPrompt } from "../context/fold/turns"
|
|
3
|
+
import { messageFingerprint } from "./fingerprint"
|
|
4
|
+
|
|
1
5
|
export interface Handoff {
|
|
2
6
|
label: string
|
|
3
7
|
source: string
|
|
@@ -13,6 +17,7 @@ export interface ThreadState {
|
|
|
13
17
|
host?: boolean
|
|
14
18
|
pins?: string[]
|
|
15
19
|
ratio?: number
|
|
20
|
+
limits?: Record<string, number>
|
|
16
21
|
recalls?: Record<string, string>
|
|
17
22
|
}
|
|
18
23
|
|
|
@@ -31,6 +36,11 @@ export function memoryStore(initial: Record<string, ThreadState> = {}): ThreadSt
|
|
|
31
36
|
}
|
|
32
37
|
}
|
|
33
38
|
|
|
39
|
+
export function retireRecalls(state: ThreadState, folded: Message[][]) {
|
|
40
|
+
if (!state.recalls) return
|
|
41
|
+
for (const message of folded.flat()) if (isPrompt(message)) delete state.recalls[messageFingerprint(message)]
|
|
42
|
+
}
|
|
43
|
+
|
|
34
44
|
export function resetCut(state: ThreadState) {
|
|
35
45
|
delete state.cut
|
|
36
46
|
delete state.keep
|
package/src/core/usage.ts
CHANGED
|
@@ -67,9 +67,12 @@ const OVERFLOW_MARGIN = 1.1
|
|
|
67
67
|
|
|
68
68
|
const charsOf = (sent: Body | number) => (typeof sent === "number" ? sent : sizeOf(sent))
|
|
69
69
|
|
|
70
|
-
function
|
|
71
|
-
const
|
|
72
|
-
|
|
70
|
+
function overflowOf(errorText: string): { tokens: number; maximum: number | null } | null {
|
|
71
|
+
const match = errorText.match(/prompt is too long[^0-9]*(\d+)\s*tokens?\s*>\s*(\d+)/i)
|
|
72
|
+
const tokens = Number(match?.[1])
|
|
73
|
+
const maximum = Number(match?.[2])
|
|
74
|
+
if (!Number.isFinite(tokens) || tokens <= 0) return null
|
|
75
|
+
return { tokens, maximum: Number.isFinite(maximum) && maximum > 0 ? maximum : null }
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
export function recordUsage(store: ThreadStore, threadKey: string, sent: Body | number, usage: Usage): boolean {
|
|
@@ -82,12 +85,18 @@ export function recordUsage(store: ThreadStore, threadKey: string, sent: Body |
|
|
|
82
85
|
return true
|
|
83
86
|
}
|
|
84
87
|
|
|
85
|
-
export
|
|
86
|
-
|
|
88
|
+
export const limitKey = (model: unknown) => String(model ?? "")
|
|
89
|
+
|
|
90
|
+
export function recordOverflow(store: ThreadStore, threadKey: string, sent: Body | number, errorText: string, model: unknown = typeof sent === "number" ? undefined : sent.model): number | null {
|
|
91
|
+
const overflow = overflowOf(errorText)
|
|
87
92
|
const chars = charsOf(sent)
|
|
88
|
-
if (!
|
|
93
|
+
if (!overflow || chars <= 0) return null
|
|
89
94
|
const state = store.read(threadKey)
|
|
90
|
-
state.ratio = Math.max(state.ratio ?? 0, tokens / chars) * OVERFLOW_MARGIN
|
|
95
|
+
state.ratio = Math.max(state.ratio ?? 0, overflow.tokens / chars) * OVERFLOW_MARGIN
|
|
96
|
+
if (overflow.maximum) {
|
|
97
|
+
const key = limitKey(model)
|
|
98
|
+
state.limits = { ...state.limits, [key]: Math.min(state.limits?.[key] ?? Number.POSITIVE_INFINITY, overflow.maximum) }
|
|
99
|
+
}
|
|
91
100
|
store.save(threadKey, state)
|
|
92
|
-
return tokens
|
|
101
|
+
return overflow.tokens
|
|
93
102
|
}
|
package/src/core/window.ts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
export type WindowLookup = (model: unknown, requestHeaders: Headers, upstream?: string) => Promise<number | null>
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
export interface WindowLookupOptions {
|
|
4
|
+
known?: Map<string, number>
|
|
5
|
+
onLearn?: (windows: Map<string, number>) => void
|
|
6
|
+
timeoutMs?: number
|
|
7
|
+
retryAfterMs?: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const LOOKUP_TIMEOUT_MS = 1_500
|
|
11
|
+
|
|
12
|
+
const RETRY_AFTER_MS = 10 * 60_000
|
|
13
|
+
|
|
14
|
+
const LONG_SUFFIX = /\[1m\]$/i
|
|
15
|
+
|
|
16
|
+
const SUFFIX = /\[[^\]]*\]$/
|
|
4
17
|
|
|
5
18
|
function lookupHeaders(requestHeaders: Headers): Headers {
|
|
6
19
|
const headers = new Headers()
|
|
@@ -12,10 +25,19 @@ function lookupHeaders(requestHeaders: Headers): Headers {
|
|
|
12
25
|
return headers
|
|
13
26
|
}
|
|
14
27
|
|
|
15
|
-
|
|
28
|
+
function familyWindow(model: string): number | null {
|
|
29
|
+
if (LONG_SUFFIX.test(model)) return 1_000_000
|
|
30
|
+
const id = model.replace(SUFFIX, "")
|
|
31
|
+
if (/^claude-(?:(?:opus|sonnet|haiku|fable)-5|opus-4-8)/.test(id)) return 1_000_000
|
|
32
|
+
if (id.startsWith("claude-")) return 200_000
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createWindowLookup({ known = new Map<string, number>(), onLearn, timeoutMs = LOOKUP_TIMEOUT_MS, retryAfterMs = RETRY_AFTER_MS }: WindowLookupOptions = {}): WindowLookup {
|
|
16
37
|
const lookups = new Map<string, Promise<number | null>>()
|
|
38
|
+
const failedAt = new Map<string, number>()
|
|
17
39
|
const lookup = async (id: string, requestHeaders: Headers, upstream: string): Promise<number | null> => {
|
|
18
|
-
const response = await fetch(`${upstream}/v1/models/${encodeURIComponent(id)}`, { headers: lookupHeaders(requestHeaders), signal: AbortSignal.timeout(
|
|
40
|
+
const response = await fetch(`${upstream}/v1/models/${encodeURIComponent(id)}`, { headers: lookupHeaders(requestHeaders), signal: AbortSignal.timeout(timeoutMs) })
|
|
19
41
|
if (!response.ok) return null
|
|
20
42
|
const info = (await response.json()) as { max_input_tokens?: number }
|
|
21
43
|
if (typeof info.max_input_tokens !== "number" || info.max_input_tokens <= 0) return null
|
|
@@ -24,16 +46,24 @@ export function createWindowLookup({ known = new Map<string, number>(), onLearn
|
|
|
24
46
|
return info.max_input_tokens
|
|
25
47
|
}
|
|
26
48
|
return (model, requestHeaders, upstream = "https://api.anthropic.com") => {
|
|
27
|
-
const
|
|
49
|
+
const given = String(model ?? "")
|
|
50
|
+
const id = given.replace(SUFFIX, "")
|
|
28
51
|
if (!id) return Promise.resolve(null)
|
|
29
52
|
const window = known.get(id)
|
|
30
53
|
if (window) return Promise.resolve(window)
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
54
|
+
const fallback = familyWindow(given)
|
|
55
|
+
const failed = failedAt.get(id)
|
|
56
|
+
if (failed !== undefined && Date.now() - failed < retryAfterMs) return Promise.resolve(fallback)
|
|
57
|
+
const pending =
|
|
58
|
+
lookups.get(id) ??
|
|
59
|
+
lookup(id, requestHeaders, upstream)
|
|
60
|
+
.catch(() => null)
|
|
61
|
+
.then((found) => {
|
|
62
|
+
if (found === null) failedAt.set(id, Date.now())
|
|
63
|
+
return found
|
|
64
|
+
})
|
|
65
|
+
.finally(() => lookups.delete(id))
|
|
36
66
|
lookups.set(id, pending)
|
|
37
|
-
return pending
|
|
67
|
+
return pending.then((found) => found ?? fallback)
|
|
38
68
|
}
|
|
39
69
|
}
|