@chroxify/eunoe 1.2.1 → 1.3.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 +143 -5
- package/dist/core/index.js +2235 -0
- package/dist/types/context/content.d.ts +31 -0
- package/dist/types/context/fold/evidence.d.ts +20 -0
- package/dist/types/context/fold/index.d.ts +11 -0
- package/dist/types/context/fold/strategies.d.ts +20 -0
- package/dist/types/context/fold/turns.d.ts +14 -0
- package/dist/types/context/prompts.d.ts +24 -0
- package/dist/types/context/recall/index.d.ts +44 -0
- package/dist/types/context/recall/rerank.d.ts +3 -0
- package/dist/types/core/compaction.d.ts +32 -0
- package/dist/types/core/config.d.ts +33 -0
- package/dist/types/core/estimate.d.ts +9 -0
- package/dist/types/core/fingerprint.d.ts +10 -0
- package/dist/types/core/fold.d.ts +18 -0
- package/dist/types/core/forward.d.ts +24 -0
- package/dist/types/core/handoff.d.ts +10 -0
- package/dist/types/core/index.d.ts +14 -0
- package/dist/types/core/recall.d.ts +8 -0
- package/dist/types/core/rewrite.d.ts +63 -0
- package/dist/types/core/state.d.ts +22 -0
- package/dist/types/core/subagents.d.ts +10 -0
- package/dist/types/core/transcript.d.ts +9 -0
- package/dist/types/core/usage.d.ts +7 -0
- package/dist/types/core/window.d.ts +5 -0
- package/dist/types/transcript/format.d.ts +16 -0
- package/dist/types/transcript/parse.d.ts +45 -0
- package/package.json +15 -2
- package/src/cli/commands/mode.ts +2 -1
- package/src/cli/commands/serve.ts +2 -0
- package/src/cli/commands/status.ts +1 -1
- package/src/cli/proxy/stats.ts +1 -1
- package/src/cli/replay/index.ts +3 -3
- package/src/cli/sessions/index.ts +2 -2
- package/src/config/index.ts +4 -27
- package/src/context/content.ts +64 -1
- package/src/context/fold/strategies.ts +55 -10
- package/src/context/fold/turns.ts +22 -2
- package/src/context/prompts.ts +4 -3
- package/src/context/recall/index.ts +259 -35
- package/src/context/recall/rerank.ts +0 -8
- package/src/{proxy → core}/compaction.ts +74 -24
- package/src/core/config.ts +31 -0
- package/src/core/fold.ts +69 -0
- package/src/core/forward.ts +192 -0
- package/src/{proxy → core}/handoff.ts +23 -23
- package/src/core/index.ts +14 -0
- package/src/{proxy → core}/recall.ts +18 -20
- package/src/core/rewrite.ts +189 -0
- package/src/core/state.ts +40 -0
- package/src/core/subagents.ts +28 -0
- package/src/core/transcript.ts +44 -0
- package/src/{proxy → core}/usage.ts +34 -1
- package/src/core/window.ts +39 -0
- package/src/proxy/rewrite.ts +22 -139
- package/src/proxy/server.ts +24 -137
- package/src/proxy/sessions.ts +1 -1
- package/src/proxy/sources.ts +35 -0
- package/src/proxy/state.ts +32 -21
- package/src/proxy/warm.ts +68 -0
- package/src/proxy/window.ts +3 -33
- package/src/transcript/entries.ts +114 -0
- package/src/transcript/format.ts +1 -2
- package/src/transcript/messages.ts +14 -11
- package/src/transcript/render-worker.ts +24 -0
- package/src/transcript/render.ts +12 -3
- package/src/transcript/search.ts +46 -5
- package/src/transcript/subagents.ts +55 -34
- /package/src/{proxy → core}/estimate.ts +0 -0
- /package/src/{proxy → core}/fingerprint.ts +0 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type { Body, Message } from "../context/content"
|
|
2
|
+
import { appendSystem, tailMessages, trimMessages } from "../context/fold/strategies"
|
|
3
|
+
import { splitTurns, turnFingerprint } from "../context/fold/turns"
|
|
4
|
+
import { SYSTEM_PROMPT, searchOnlyInstructions, trimInstructions } from "../context/prompts"
|
|
5
|
+
import { identityReranker, type RecallChunk, type Reranker, withRecall } from "../context/recall"
|
|
6
|
+
import { compact, type StuckSizes } from "./compaction"
|
|
7
|
+
import type { CoreConfig } from "./config"
|
|
8
|
+
import { DEFAULT_RATIO, estimator, sizeOf, targetSize } from "./estimate"
|
|
9
|
+
import { messageFingerprint } from "./fingerprint"
|
|
10
|
+
import { activeHandoffs, attachHandoffs, withHandoffs } from "./handoff"
|
|
11
|
+
import { activeRecalls, applyRecall, prefetchFor, wantsRecall } from "./recall"
|
|
12
|
+
import type { ThreadStore } from "./state"
|
|
13
|
+
|
|
14
|
+
export interface ThreadSources {
|
|
15
|
+
guide?: string | null
|
|
16
|
+
transcriptDir?: string | null
|
|
17
|
+
readHistory?: () => Message[]
|
|
18
|
+
listSubagentChunks?: () => RecallChunk[]
|
|
19
|
+
findHandoff?: (id: string) => { label: string; source: string } | null
|
|
20
|
+
readHandoff?: (source: string) => Message[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RewriteInput {
|
|
24
|
+
body: Body
|
|
25
|
+
threadId: string | null
|
|
26
|
+
config: CoreConfig
|
|
27
|
+
store: ThreadStore
|
|
28
|
+
sources?: ThreadSources
|
|
29
|
+
window: number | null
|
|
30
|
+
mayCompact: boolean
|
|
31
|
+
omittedBefore?: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type RewriteNote = {
|
|
35
|
+
action: "pass" | "full" | "compact" | "guide" | "trim" | "tail"
|
|
36
|
+
compacted?: boolean
|
|
37
|
+
actions?: string[]
|
|
38
|
+
cutTurn?: number | null
|
|
39
|
+
omitted?: number
|
|
40
|
+
folded?: number
|
|
41
|
+
pinned?: number
|
|
42
|
+
host?: boolean
|
|
43
|
+
keep?: number
|
|
44
|
+
cleared?: number
|
|
45
|
+
stuck?: StuckSizes
|
|
46
|
+
turns?: number
|
|
47
|
+
keptTurns?: number
|
|
48
|
+
messagesIn?: number
|
|
49
|
+
messagesOut?: number
|
|
50
|
+
estimate?: number
|
|
51
|
+
window?: number | null
|
|
52
|
+
fit?: "recut" | "bare" | "over"
|
|
53
|
+
fitEstimate?: number
|
|
54
|
+
recalled?: number
|
|
55
|
+
handoff?: number
|
|
56
|
+
handoffFrom?: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface Rewrite {
|
|
60
|
+
body: Body
|
|
61
|
+
threadKey: string | null
|
|
62
|
+
note: RewriteNote
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const pass = (body: Body): Rewrite => ({ body, threadKey: null, note: { action: "pass" } })
|
|
66
|
+
|
|
67
|
+
const isAgentThread = (body: Body) => Array.isArray(body.tools) && body.tools.length > 0
|
|
68
|
+
|
|
69
|
+
const withSystemPrompt = (system: Body["system"], extra?: string | null) => appendSystem(extra ? appendSystem(system, extra) : system, SYSTEM_PROMPT)
|
|
70
|
+
|
|
71
|
+
const ratioOf = (input: RewriteInput, threadKey: string) => input.store.read(threadKey).ratio ?? DEFAULT_RATIO
|
|
72
|
+
|
|
73
|
+
export function threadKeyOf(threadId: string, messages: Message[]): string | null {
|
|
74
|
+
const first = splitTurns(messages).turns[0]
|
|
75
|
+
return first ? `${threadId}-${turnFingerprint(first)}` : null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function evalStrategy(input: RewriteInput, turns: Message[][], threadKey: string, guide: string | null): Rewrite | null {
|
|
79
|
+
const { body, config } = input
|
|
80
|
+
const strategy = config.eval?.strategy
|
|
81
|
+
if (strategy !== "guide" && strategy !== "trim" && strategy !== "tail") return null
|
|
82
|
+
if (!isAgentThread(body)) return pass(body)
|
|
83
|
+
const counts = { turns: turns.length, messagesIn: body.messages.length }
|
|
84
|
+
if (strategy === "guide") {
|
|
85
|
+
return { body: { ...body, system: withSystemPrompt(body.system, guide ? searchOnlyInstructions(guide) : null) }, threadKey, note: { action: "guide", ...counts } }
|
|
86
|
+
}
|
|
87
|
+
if (strategy === "trim") {
|
|
88
|
+
const messages = trimMessages(body.messages, { evidence: config.eval?.evidence ?? true })
|
|
89
|
+
return { body: { ...body, system: withSystemPrompt(body.system, trimInstructions(guide)), messages }, threadKey, note: { action: "trim", ...counts, messagesOut: messages.length } }
|
|
90
|
+
}
|
|
91
|
+
const based = { ...body, system: withSystemPrompt(body.system) }
|
|
92
|
+
const size = estimator(based, ratioOf(input, threadKey))
|
|
93
|
+
let from = 1
|
|
94
|
+
if (input.window) {
|
|
95
|
+
const target = targetSize(size, input.window)
|
|
96
|
+
from = turns.length - 1
|
|
97
|
+
while (from > 1 && size.of(tailMessages(body.messages, from - 1, guide)) <= target) from -= 1
|
|
98
|
+
}
|
|
99
|
+
const messages = tailMessages(body.messages, from, guide)
|
|
100
|
+
return { body: { ...based, messages }, threadKey, note: { action: "tail", keptTurns: turns.length - from, ...counts, messagesOut: messages.length, estimate: size.of(messages) } }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function strategize(input: RewriteInput): Rewrite {
|
|
104
|
+
const { body, threadId, config, store } = input
|
|
105
|
+
const sources = input.sources ?? {}
|
|
106
|
+
if (!threadId || !Array.isArray(body.messages) || body.messages.length === 0) return pass(body)
|
|
107
|
+
const threadKey = threadKeyOf(threadId, body.messages)
|
|
108
|
+
if (!threadKey) return pass(body)
|
|
109
|
+
const off = config.mode === "off"
|
|
110
|
+
const state = store.read(threadKey)
|
|
111
|
+
if (off && !state.host) return pass(body)
|
|
112
|
+
const { turns } = splitTurns(body.messages)
|
|
113
|
+
const guide = sources.guide ?? null
|
|
114
|
+
if (!off && attachHandoffs(body.messages, sources, state)) store.save(threadKey, state)
|
|
115
|
+
|
|
116
|
+
const forced = off ? null : evalStrategy(input, turns, threadKey, guide)
|
|
117
|
+
if (forced) return forced
|
|
118
|
+
|
|
119
|
+
const based = isAgentThread(body) ? { ...body, system: withSystemPrompt(body.system) } : body
|
|
120
|
+
if (body.messages.length < 2) return { body: based, threadKey, note: { action: "full", turns: turns.length } }
|
|
121
|
+
const handoffs = activeHandoffs(state.handoffs)
|
|
122
|
+
const recalls = wantsRecall(input) ? activeRecalls(state.recalls) : {}
|
|
123
|
+
const result = compact({
|
|
124
|
+
body: based,
|
|
125
|
+
config,
|
|
126
|
+
state,
|
|
127
|
+
window: input.window,
|
|
128
|
+
guide,
|
|
129
|
+
transcriptDir: sources.transcriptDir ?? null,
|
|
130
|
+
mayCompact: input.mayCompact && !off,
|
|
131
|
+
omittedBefore: input.omittedBefore,
|
|
132
|
+
decorate: (messages) => withRecall(withHandoffs(messages, handoffs), messageFingerprint, recalls),
|
|
133
|
+
})
|
|
134
|
+
if (result.stateChanged) store.save(threadKey, state)
|
|
135
|
+
return { body: result.messages === body.messages ? based : { ...based, messages: result.messages }, threadKey, note: result.note }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function withAttachedHandoffs(input: RewriteInput, result: Rewrite): Rewrite {
|
|
139
|
+
if (!result.threadKey) return result
|
|
140
|
+
const handoffs = activeHandoffs(input.store.read(result.threadKey).handoffs)
|
|
141
|
+
const messages = withHandoffs(result.body.messages, handoffs)
|
|
142
|
+
if (messages === result.body.messages) return result
|
|
143
|
+
const attached = Object.values(handoffs)
|
|
144
|
+
const note = { ...result.note, handoff: attached.reduce((sum, h) => sum + h.text.length, 0), handoffFrom: attached.map((h) => h.label).join(", ") }
|
|
145
|
+
return { ...result, body: { ...result.body, messages }, note }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function once(input: RewriteInput): Rewrite {
|
|
149
|
+
const result = withAttachedHandoffs(input, strategize(input))
|
|
150
|
+
return wantsRecall(input) ? applyRecall(input, result) : result
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function sentEstimate(input: RewriteInput, result: Rewrite): number {
|
|
154
|
+
return Math.round(sizeOf(result.body) * (result.threadKey ? ratioOf(input, result.threadKey) : DEFAULT_RATIO))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function overLimit(input: RewriteInput, result: Rewrite): boolean {
|
|
158
|
+
if (!input.window || !result.threadKey || input.mayCompact === false || result.note.action === "pass") return false
|
|
159
|
+
return sentEstimate(input, result) > input.config.compactAt * input.window
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function carried(from: Rewrite, to: Rewrite): Rewrite {
|
|
163
|
+
const actions = [...(from.note.actions ?? []), ...(to.note.actions ?? [])]
|
|
164
|
+
if (!from.note.compacted && actions.length === 0) return to
|
|
165
|
+
return { ...to, note: { ...to.note, compacted: Boolean(to.note.compacted || from.note.compacted), actions } }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function rewrite(input: RewriteInput, before?: Rewrite): Rewrite {
|
|
169
|
+
const result = before ? carried(before, once(input)) : once(input)
|
|
170
|
+
if (!overLimit(input, result)) return result
|
|
171
|
+
const recut = carried(result, once(input))
|
|
172
|
+
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 } } }))
|
|
174
|
+
return { ...bare, note: { ...bare.note, fit: overLimit(input, bare) ? "over" : "bare", fitEstimate: sentEstimate(input, bare) } }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function prefetchRecall(input: RewriteInput, rerank: Reranker): Promise<{ reranked: boolean; rerankError: null; recalled: number } | null> {
|
|
178
|
+
if (!wantsRecall(input)) return null
|
|
179
|
+
const recalled = await prefetchFor(input, strategize(input), rerank)
|
|
180
|
+
return recalled === null ? null : { reranked: rerank !== identityReranker, rerankError: null, recalled }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function rewriteAsync(input: RewriteInput, rerank: Reranker = identityReranker): Promise<Rewrite> {
|
|
184
|
+
if (!wantsRecall(input) || rerank === identityReranker) return rewrite(input)
|
|
185
|
+
const first = strategize(input)
|
|
186
|
+
const recalled = await prefetchFor(input, first, rerank)
|
|
187
|
+
const final = rewrite(input, first)
|
|
188
|
+
return recalled === null ? final : { ...final, note: { ...final.note, recalled } }
|
|
189
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface Handoff {
|
|
2
|
+
label: string
|
|
3
|
+
source: string
|
|
4
|
+
text: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ThreadState {
|
|
8
|
+
handoffs?: Record<string, Handoff | null>
|
|
9
|
+
cut?: string
|
|
10
|
+
keep?: number
|
|
11
|
+
cleared?: number
|
|
12
|
+
stuck?: number
|
|
13
|
+
host?: boolean
|
|
14
|
+
pins?: string[]
|
|
15
|
+
ratio?: number
|
|
16
|
+
recalls?: Record<string, string>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ThreadStore {
|
|
20
|
+
read: (key: string) => ThreadState
|
|
21
|
+
save: (key: string, state: ThreadState) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function memoryStore(initial: Record<string, ThreadState> = {}): ThreadStore {
|
|
25
|
+
const threads = new Map(Object.entries(initial).map(([key, state]) => [key, structuredClone(state)]))
|
|
26
|
+
return {
|
|
27
|
+
read: (key) => structuredClone(threads.get(key) ?? {}),
|
|
28
|
+
save: (key, state) => {
|
|
29
|
+
threads.set(key, structuredClone(state))
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resetCut(state: ThreadState) {
|
|
35
|
+
delete state.cut
|
|
36
|
+
delete state.keep
|
|
37
|
+
delete state.cleared
|
|
38
|
+
delete state.stuck
|
|
39
|
+
delete state.host
|
|
40
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { blocks, type Message } from "../context/content"
|
|
2
|
+
import { splitTurns } from "../context/fold/turns"
|
|
3
|
+
import { type RecallChunk, recallChunks } from "../context/recall"
|
|
4
|
+
|
|
5
|
+
export interface Subagent {
|
|
6
|
+
description: string
|
|
7
|
+
messages: Message[]
|
|
8
|
+
toolUseId?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const closingTurn: Message = { role: "user", content: "" }
|
|
12
|
+
|
|
13
|
+
export function launchTurnOf(parentMessages: Message[], toolUseId: string | undefined): number {
|
|
14
|
+
if (!toolUseId) return 0
|
|
15
|
+
const index = splitTurns(parentMessages).turns.findIndex((turn) => turn.some((message) => message.role === "assistant" && blocks(message).some((b) => b.type === "tool_use" && b.id === toolUseId)))
|
|
16
|
+
return index + 1
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const subagentLabel = (description: string) => `subagent "${description}"`
|
|
20
|
+
|
|
21
|
+
export function subagentRecall(messages: Message[], description: string, parentTurn: number): RecallChunk[] {
|
|
22
|
+
const from = subagentLabel(description)
|
|
23
|
+
return recallChunks([...messages, closingTurn], new Map()).map((chunk) => ({ ...chunk, from, turn: parentTurn }))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function listSubagentRecall(parentMessages: Message[], subagents: Subagent[]): RecallChunk[] {
|
|
27
|
+
return subagents.flatMap((subagent) => subagentRecall(subagent.messages, subagent.description, launchTurnOf(parentMessages, subagent.toolUseId)))
|
|
28
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { blocks, type Message } from "../context/content"
|
|
2
|
+
import { isPrompt } from "../context/fold/turns"
|
|
3
|
+
import { searchGuide } from "../context/prompts"
|
|
4
|
+
import { renderIndex, renderMarkdown, renderSubagentMarkdown, renderSubagentXml, renderXml, type SubagentRecord, subagentFile, turnFile } from "../transcript/format"
|
|
5
|
+
import { parseTurns, type SessionEntry, type TranscriptFormat, type Turn } from "../transcript/parse"
|
|
6
|
+
import { launchTurnOf, type Subagent } from "./subagents"
|
|
7
|
+
|
|
8
|
+
export interface TranscriptOptions {
|
|
9
|
+
format?: TranscriptFormat
|
|
10
|
+
subagents?: Subagent[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const isNote = (message: Message) => message.role === "user" && !isPrompt(message) && !blocks(message).some((b) => b.type === "tool_result")
|
|
14
|
+
|
|
15
|
+
const turnsOf = (messages: Message[], sidechain: boolean | "any" = false): Turn[] =>
|
|
16
|
+
parseTurns(
|
|
17
|
+
messages.map((message): SessionEntry => ({ type: message.role, isMeta: isNote(message), message })),
|
|
18
|
+
sidechain,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
function subagentRecords(messages: Message[], turns: Turn[], subagents: Subagent[], format: TranscriptFormat): SubagentRecord[] {
|
|
22
|
+
const perTurn = new Map<number, number>()
|
|
23
|
+
return subagents.map((subagent) => {
|
|
24
|
+
const parentTurn = launchTurnOf(messages, subagent.toolUseId)
|
|
25
|
+
const k = (perTurn.get(parentTurn) ?? 0) + 1
|
|
26
|
+
perTurn.set(parentTurn, k)
|
|
27
|
+
const file = subagentFile(parentTurn, k, format)
|
|
28
|
+
const launcher = turns[parentTurn - 1]?.parts.find((part) => part.kind === "tool" && part.id === subagent.toolUseId)
|
|
29
|
+
if (launcher?.kind === "tool") launcher.record = file
|
|
30
|
+
return { run: { description: subagent.description }, turns: turnsOf(subagent.messages, "any"), parentTurn, file }
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function renderTranscriptFiles(messages: Message[], { format = "markdown", subagents = [] }: TranscriptOptions = {}): Record<string, string> {
|
|
35
|
+
const turns = turnsOf(messages)
|
|
36
|
+
const records = subagentRecords(messages, turns, subagents, format)
|
|
37
|
+
const files: Record<string, string> = {}
|
|
38
|
+
for (const turn of turns) files[turnFile(turn, format)] = format === "xml" ? renderXml(turn) : renderMarkdown(turn)
|
|
39
|
+
for (const record of records) files[record.file] = format === "xml" ? renderSubagentXml(record) : renderSubagentMarkdown(record)
|
|
40
|
+
files["index.md"] = renderIndex(turns, format, records)
|
|
41
|
+
return files
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const transcriptGuide = (dir: string, format: TranscriptFormat = "markdown") => searchGuide({ mode: format, dir, jsonl: "" })
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
type
|
|
1
|
+
import type { Body } from "../context/content"
|
|
2
|
+
import { sizeOf } from "./estimate"
|
|
3
|
+
import type { ThreadStore } from "./state"
|
|
4
|
+
|
|
5
|
+
export type Usage = Record<string, number>
|
|
2
6
|
|
|
3
7
|
function usageIn(line: string): Usage | null {
|
|
4
8
|
if (!line.startsWith("data:") || !line.includes("message_start")) return null
|
|
@@ -58,3 +62,32 @@ export function watchUsage(response: Response, onUsage: (usage: Usage) => void):
|
|
|
58
62
|
export function inputTokens(usage: Usage) {
|
|
59
63
|
return (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0)
|
|
60
64
|
}
|
|
65
|
+
|
|
66
|
+
const OVERFLOW_MARGIN = 1.1
|
|
67
|
+
|
|
68
|
+
const charsOf = (sent: Body | number) => (typeof sent === "number" ? sent : sizeOf(sent))
|
|
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
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function recordUsage(store: ThreadStore, threadKey: string, sent: Body | number, usage: Usage): boolean {
|
|
76
|
+
const input = inputTokens(usage)
|
|
77
|
+
const chars = charsOf(sent)
|
|
78
|
+
if (input <= 0 || chars <= 0) return false
|
|
79
|
+
const state = store.read(threadKey)
|
|
80
|
+
state.ratio = input / chars
|
|
81
|
+
store.save(threadKey, state)
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function recordOverflow(store: ThreadStore, threadKey: string, sent: Body | number, errorText: string): number | null {
|
|
86
|
+
const tokens = overflowTokens(errorText)
|
|
87
|
+
const chars = charsOf(sent)
|
|
88
|
+
if (!tokens || chars <= 0) return null
|
|
89
|
+
const state = store.read(threadKey)
|
|
90
|
+
state.ratio = Math.max(state.ratio ?? 0, tokens / chars) * OVERFLOW_MARGIN
|
|
91
|
+
store.save(threadKey, state)
|
|
92
|
+
return tokens
|
|
93
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type WindowLookup = (model: unknown, requestHeaders: Headers, upstream?: string) => Promise<number | null>
|
|
2
|
+
|
|
3
|
+
const LOOKUP_TIMEOUT_MS = 5_000
|
|
4
|
+
|
|
5
|
+
function lookupHeaders(requestHeaders: Headers): Headers {
|
|
6
|
+
const headers = new Headers()
|
|
7
|
+
for (const name of ["authorization", "x-api-key", "anthropic-version", "anthropic-beta"]) {
|
|
8
|
+
const value = requestHeaders.get(name)
|
|
9
|
+
if (value) headers.set(name, value)
|
|
10
|
+
}
|
|
11
|
+
if (!headers.has("anthropic-version")) headers.set("anthropic-version", "2023-06-01")
|
|
12
|
+
return headers
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createWindowLookup({ known = new Map<string, number>(), onLearn }: { known?: Map<string, number>; onLearn?: (windows: Map<string, number>) => void } = {}): WindowLookup {
|
|
16
|
+
const lookups = new Map<string, Promise<number | null>>()
|
|
17
|
+
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) })
|
|
19
|
+
if (!response.ok) return null
|
|
20
|
+
const info = (await response.json()) as { max_input_tokens?: number }
|
|
21
|
+
if (typeof info.max_input_tokens !== "number" || info.max_input_tokens <= 0) return null
|
|
22
|
+
known.set(id, info.max_input_tokens)
|
|
23
|
+
onLearn?.(known)
|
|
24
|
+
return info.max_input_tokens
|
|
25
|
+
}
|
|
26
|
+
return (model, requestHeaders, upstream = "https://api.anthropic.com") => {
|
|
27
|
+
const id = String(model ?? "").replace(/\[[^\]]*\]$/, "")
|
|
28
|
+
if (!id) return Promise.resolve(null)
|
|
29
|
+
const window = known.get(id)
|
|
30
|
+
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))
|
|
36
|
+
lookups.set(id, pending)
|
|
37
|
+
return pending
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/proxy/rewrite.ts
CHANGED
|
@@ -1,20 +1,15 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
1
2
|
import type { Config } from "../config"
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { identityReranker, type Reranker, withRecall } from "../context/recall"
|
|
7
|
-
import { jevReranker, rerankKey } from "../context/recall/rerank"
|
|
3
|
+
import { paths } from "../config/paths"
|
|
4
|
+
import type { Reranker } from "../context/recall"
|
|
5
|
+
import { jevReranker } from "../context/recall/rerank"
|
|
6
|
+
import { type RewriteInput as CoreInput, prefetchRecall as prefetchCore, type Rewrite, rewrite as rewriteCore, rewriteAsync as rewriteCoreAsync } from "../core/rewrite"
|
|
8
7
|
import type { TranscriptRef } from "../transcript/search"
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { messageFingerprint } from "./fingerprint"
|
|
12
|
-
import { activeHandoffs, attachHandoffs, withHandoffs } from "./handoff"
|
|
13
|
-
import { activeRecalls, applyRecall, prefetchFor, wantsRecall } from "./recall"
|
|
14
|
-
import { readState, saveState } from "./state"
|
|
8
|
+
import { sessionSources } from "./sources"
|
|
9
|
+
import { fileStore } from "./state"
|
|
15
10
|
|
|
16
11
|
export interface RewriteInput {
|
|
17
|
-
body:
|
|
12
|
+
body: CoreInput["body"]
|
|
18
13
|
sessionId: string | null
|
|
19
14
|
config: Config
|
|
20
15
|
transcript: TranscriptRef | null
|
|
@@ -23,146 +18,34 @@ export interface RewriteInput {
|
|
|
23
18
|
omittedBefore?: number
|
|
24
19
|
}
|
|
25
20
|
|
|
26
|
-
export
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
21
|
+
export function rerankKey(): string | null {
|
|
22
|
+
const env = process.env.TYPESAFE_API_KEY?.trim()
|
|
23
|
+
if (env) return env
|
|
24
|
+
return existsSync(paths.typesafeKey) ? readFileSync(paths.typesafeKey, "utf8").trim() : null
|
|
30
25
|
}
|
|
31
26
|
|
|
32
|
-
interface Prefetched {
|
|
33
|
-
reranked: boolean
|
|
34
|
-
rerankError: string | null
|
|
35
|
-
recalled: number
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const pass = (body: Body): Rewrite => ({ body, threadKey: null, note: { action: "pass" } })
|
|
39
|
-
|
|
40
|
-
const isAgentThread = (body: Body) => Array.isArray(body.tools) && body.tools.length > 0
|
|
41
|
-
|
|
42
|
-
const withSystemPrompt = (system: Body["system"], extra?: string | null) => appendSystem(extra ? appendSystem(system, extra) : system, SYSTEM_PROMPT)
|
|
43
|
-
|
|
44
|
-
const ratioOf = (threadKey: string) => readState(threadKey).ratio ?? DEFAULT_RATIO
|
|
45
|
-
|
|
46
27
|
function rerankModel(config: Config): string | null {
|
|
47
28
|
const model = config.eval?.rerank?.model ?? config.rerank
|
|
48
29
|
return model && model !== "off" ? model : null
|
|
49
30
|
}
|
|
50
31
|
|
|
51
|
-
|
|
52
|
-
const { body, config } = input
|
|
53
|
-
const strategy = config.eval?.strategy
|
|
54
|
-
if (strategy !== "guide" && strategy !== "trim" && strategy !== "tail") return null
|
|
55
|
-
if (!isAgentThread(body)) return pass(body)
|
|
56
|
-
const counts = { turns: turns.length, messagesIn: body.messages.length }
|
|
57
|
-
if (strategy === "guide") {
|
|
58
|
-
return { body: { ...body, system: withSystemPrompt(body.system, guide ? searchOnlyInstructions(guide) : null) }, threadKey, note: { action: "guide", ...counts } }
|
|
59
|
-
}
|
|
60
|
-
if (strategy === "trim") {
|
|
61
|
-
const messages = trimMessages(body.messages, { evidence: config.eval?.evidence ?? true })
|
|
62
|
-
return { body: { ...body, system: withSystemPrompt(body.system, trimInstructions(guide)), messages }, threadKey, note: { action: "trim", ...counts, messagesOut: messages.length } }
|
|
63
|
-
}
|
|
64
|
-
const based = { ...body, system: withSystemPrompt(body.system) }
|
|
65
|
-
const size = estimator(based, ratioOf(threadKey))
|
|
66
|
-
let from = 1
|
|
67
|
-
if (input.window) {
|
|
68
|
-
const target = targetSize(size, input.window)
|
|
69
|
-
from = turns.length - 1
|
|
70
|
-
while (from > 1 && size.of(tailMessages(body.messages, from - 1, guide)) <= target) from -= 1
|
|
71
|
-
}
|
|
72
|
-
const messages = tailMessages(body.messages, from, guide)
|
|
73
|
-
return { body: { ...based, messages }, threadKey, note: { action: "tail", keptTurns: turns.length - from, ...counts, messagesOut: messages.length, estimate: size.of(messages) } }
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function strategize(input: RewriteInput): Rewrite {
|
|
77
|
-
const { body, sessionId, config, transcript } = input
|
|
78
|
-
if (config.mode === "off" || !sessionId || !Array.isArray(body.messages) || body.messages.length === 0) return pass(body)
|
|
79
|
-
const { turns } = splitTurns(body.messages)
|
|
80
|
-
if (turns.length === 0) return pass(body)
|
|
81
|
-
const threadKey = `${sessionId}-${turnFingerprint(turns[0])}`
|
|
82
|
-
const guide = transcript ? searchGuide(transcript) : null
|
|
83
|
-
const state = readState(threadKey)
|
|
84
|
-
if (attachHandoffs(body.messages, config, sessionId, state)) saveState(threadKey, state)
|
|
85
|
-
|
|
86
|
-
const forced = evalStrategy(input, turns, threadKey, guide)
|
|
87
|
-
if (forced) return forced
|
|
88
|
-
|
|
89
|
-
const based = isAgentThread(body) ? { ...body, system: withSystemPrompt(body.system) } : body
|
|
90
|
-
if (body.messages.length < 2) return { body: based, threadKey, note: { action: "full", turns: turns.length } }
|
|
91
|
-
const handoffs = activeHandoffs(state.handoffs)
|
|
92
|
-
const recalls = wantsRecall(input) ? activeRecalls(state.recalls) : {}
|
|
93
|
-
const result = compact({
|
|
94
|
-
body: based,
|
|
95
|
-
config,
|
|
96
|
-
state,
|
|
97
|
-
window: input.window,
|
|
98
|
-
guide,
|
|
99
|
-
transcriptDir: transcript && transcript.mode !== "jsonl" ? transcript.dir : null,
|
|
100
|
-
mayCompact: input.mayCompact,
|
|
101
|
-
omittedBefore: input.omittedBefore,
|
|
102
|
-
decorate: (messages) => withRecall(withHandoffs(messages, handoffs), messageFingerprint, recalls),
|
|
103
|
-
})
|
|
104
|
-
if (result.stateChanged) saveState(threadKey, state)
|
|
105
|
-
return { body: result.messages === body.messages ? based : { ...based, messages: result.messages }, threadKey, note: result.note }
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function withAttachedHandoffs(result: Rewrite): Rewrite {
|
|
109
|
-
if (!result.threadKey) return result
|
|
110
|
-
const handoffs = activeHandoffs(readState(result.threadKey).handoffs)
|
|
111
|
-
const messages = withHandoffs(result.body.messages, handoffs)
|
|
112
|
-
if (messages === result.body.messages) return result
|
|
113
|
-
const attached = Object.values(handoffs)
|
|
114
|
-
const note = { ...result.note, handoff: attached.reduce((sum, h) => sum + h.text.length, 0), handoffFrom: attached.map((h) => h.label).join(", ") }
|
|
115
|
-
return { ...result, body: { ...result.body, messages }, note }
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function once(input: RewriteInput): Rewrite {
|
|
119
|
-
const result = withAttachedHandoffs(strategize(input))
|
|
120
|
-
return wantsRecall(input) ? applyRecall(input, result) : result
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function sentEstimate(result: Rewrite): number {
|
|
124
|
-
return Math.round(sizeOf(result.body) * (result.threadKey ? ratioOf(result.threadKey) : DEFAULT_RATIO))
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function overLimit(input: RewriteInput, result: Rewrite): boolean {
|
|
128
|
-
if (!input.window || !result.threadKey || input.mayCompact === false || result.note.action === "pass") return false
|
|
129
|
-
return sentEstimate(result) > input.config.compactAt * input.window
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function carried(from: Rewrite, to: Rewrite): Rewrite {
|
|
133
|
-
const actionsOf = ({ note }: Rewrite) => (note.actions as string[] | undefined) ?? []
|
|
134
|
-
const actions = [...actionsOf(from), ...actionsOf(to)]
|
|
135
|
-
if (!from.note.compacted && actions.length === 0) return to
|
|
136
|
-
return { ...to, note: { ...to.note, compacted: Boolean(to.note.compacted || from.note.compacted), actions } }
|
|
137
|
-
}
|
|
32
|
+
const toCore = ({ sessionId, transcript, ...input }: RewriteInput): CoreInput => ({ ...input, threadId: sessionId, store: fileStore, sources: sessionSources(transcript, input.config, sessionId) })
|
|
138
33
|
|
|
139
|
-
export
|
|
140
|
-
const result = before ? carried(before, once(input)) : once(input)
|
|
141
|
-
if (!overLimit(input, result)) return result
|
|
142
|
-
const recut = carried(result, once(input))
|
|
143
|
-
if (!overLimit(input, recut)) return { ...recut, note: { ...recut.note, fit: "recut" } }
|
|
144
|
-
const bare = carried(recut, once({ ...input, config: { ...input.config, eval: { ...input.config.eval, recall: false } } }))
|
|
145
|
-
return { ...bare, note: { ...bare.note, fit: overLimit(input, bare) ? "over" : "bare", fitEstimate: sentEstimate(bare) } }
|
|
146
|
-
}
|
|
34
|
+
export const rewrite = (input: RewriteInput): Rewrite => rewriteCore(toCore(input))
|
|
147
35
|
|
|
148
|
-
export
|
|
149
|
-
if (!wantsRecall(input)) return null
|
|
150
|
-
const recalled = await prefetchFor(input, strategize(input), rerank)
|
|
151
|
-
return recalled === null ? null : { reranked: rerank !== identityReranker, rerankError: null, recalled }
|
|
152
|
-
}
|
|
36
|
+
export const prefetchRecall = (input: RewriteInput, rerank: Reranker) => prefetchCore(toCore(input), rerank)
|
|
153
37
|
|
|
154
|
-
export async function rewriteAsync(input: RewriteInput): Promise<Rewrite> {
|
|
155
|
-
const
|
|
38
|
+
export async function rewriteAsync(input: RewriteInput): Promise<Rewrite & { note: { reranked?: string; rerankError?: string } }> {
|
|
39
|
+
const core = toCore(input)
|
|
40
|
+
const model = core.config.mode !== "off" && (core.config.eval?.recall ?? true) ? rerankModel(input.config) : null
|
|
156
41
|
const key = model ? rerankKey() : null
|
|
157
|
-
if (!model || !key) return
|
|
42
|
+
if (!model || !key) return rewriteCore(core)
|
|
158
43
|
const failure = { reason: null as string | null }
|
|
159
44
|
const reranker = jevReranker(model, key, input.config.eval?.rerank?.endpoint, (reason) => {
|
|
160
45
|
failure.reason = reason
|
|
161
46
|
})
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
const final = rewrite(input, first)
|
|
165
|
-
if (recalled === null) return final
|
|
47
|
+
const result = await rewriteCoreAsync(core, reranker)
|
|
48
|
+
if (result.note.recalled === undefined) return result
|
|
166
49
|
const outcome = failure.reason ? { rerankError: `${model}: ${failure.reason}` } : { reranked: model }
|
|
167
|
-
return { ...
|
|
50
|
+
return { ...result, note: { ...result.note, ...outcome } }
|
|
168
51
|
}
|