@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.
@@ -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 recalls = wantsRecall(input) ? activeRecalls(state.recalls) : {}
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
- export function rewrite(input: RewriteInput, before?: Rewrite): Rewrite {
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
- const bare = carried(recut, once({ ...input, config: { ...input.config, eval: { ...input.config.eval, recall: false } } }))
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(input: RewriteInput, rerank: Reranker): Promise<{ reranked: boolean; rerankError: null; recalled: number } | null> {
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(input: RewriteInput, rerank: Reranker = identityReranker): Promise<Rewrite> {
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 overflowTokens(errorText: string): number | null {
71
- const tokens = Number(errorText.match(/prompt is too long[^0-9]*(\d+)\s*tokens?\s*>\s*(\d+)/i)?.[1])
72
- return Number.isFinite(tokens) && tokens > 0 ? tokens : null
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 function recordOverflow(store: ThreadStore, threadKey: string, sent: Body | number, errorText: string): number | null {
86
- const tokens = overflowTokens(errorText)
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 (!tokens || chars <= 0) return null
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
  }
@@ -1,6 +1,19 @@
1
1
  export type WindowLookup = (model: unknown, requestHeaders: Headers, upstream?: string) => Promise<number | null>
2
2
 
3
- const LOOKUP_TIMEOUT_MS = 5_000
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
- export function createWindowLookup({ known = new Map<string, number>(), onLearn }: { known?: Map<string, number>; onLearn?: (windows: Map<string, number>) => void } = {}): WindowLookup {
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(LOOKUP_TIMEOUT_MS) })
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 id = String(model ?? "").replace(/\[[^\]]*\]$/, "")
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 inFlight = lookups.get(id)
32
- if (inFlight) return inFlight
33
- const pending = lookup(id, requestHeaders, upstream)
34
- .catch(() => null)
35
- .finally(() => lookups.delete(id))
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
  }