@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.
Files changed (70) hide show
  1. package/README.md +143 -5
  2. package/dist/core/index.js +2235 -0
  3. package/dist/types/context/content.d.ts +31 -0
  4. package/dist/types/context/fold/evidence.d.ts +20 -0
  5. package/dist/types/context/fold/index.d.ts +11 -0
  6. package/dist/types/context/fold/strategies.d.ts +20 -0
  7. package/dist/types/context/fold/turns.d.ts +14 -0
  8. package/dist/types/context/prompts.d.ts +24 -0
  9. package/dist/types/context/recall/index.d.ts +44 -0
  10. package/dist/types/context/recall/rerank.d.ts +3 -0
  11. package/dist/types/core/compaction.d.ts +32 -0
  12. package/dist/types/core/config.d.ts +33 -0
  13. package/dist/types/core/estimate.d.ts +9 -0
  14. package/dist/types/core/fingerprint.d.ts +10 -0
  15. package/dist/types/core/fold.d.ts +18 -0
  16. package/dist/types/core/forward.d.ts +24 -0
  17. package/dist/types/core/handoff.d.ts +10 -0
  18. package/dist/types/core/index.d.ts +14 -0
  19. package/dist/types/core/recall.d.ts +8 -0
  20. package/dist/types/core/rewrite.d.ts +63 -0
  21. package/dist/types/core/state.d.ts +22 -0
  22. package/dist/types/core/subagents.d.ts +10 -0
  23. package/dist/types/core/transcript.d.ts +9 -0
  24. package/dist/types/core/usage.d.ts +7 -0
  25. package/dist/types/core/window.d.ts +5 -0
  26. package/dist/types/transcript/format.d.ts +16 -0
  27. package/dist/types/transcript/parse.d.ts +45 -0
  28. package/package.json +15 -2
  29. package/src/cli/commands/mode.ts +2 -1
  30. package/src/cli/commands/serve.ts +2 -0
  31. package/src/cli/commands/status.ts +1 -1
  32. package/src/cli/proxy/stats.ts +1 -1
  33. package/src/cli/replay/index.ts +3 -3
  34. package/src/cli/sessions/index.ts +2 -2
  35. package/src/config/index.ts +4 -27
  36. package/src/context/content.ts +64 -1
  37. package/src/context/fold/strategies.ts +55 -10
  38. package/src/context/fold/turns.ts +22 -2
  39. package/src/context/prompts.ts +4 -3
  40. package/src/context/recall/index.ts +259 -35
  41. package/src/context/recall/rerank.ts +0 -8
  42. package/src/{proxy → core}/compaction.ts +74 -24
  43. package/src/core/config.ts +31 -0
  44. package/src/core/fold.ts +69 -0
  45. package/src/core/forward.ts +192 -0
  46. package/src/{proxy → core}/handoff.ts +23 -23
  47. package/src/core/index.ts +14 -0
  48. package/src/{proxy → core}/recall.ts +18 -20
  49. package/src/core/rewrite.ts +189 -0
  50. package/src/core/state.ts +40 -0
  51. package/src/core/subagents.ts +28 -0
  52. package/src/core/transcript.ts +44 -0
  53. package/src/{proxy → core}/usage.ts +34 -1
  54. package/src/core/window.ts +39 -0
  55. package/src/proxy/rewrite.ts +22 -139
  56. package/src/proxy/server.ts +24 -137
  57. package/src/proxy/sessions.ts +1 -1
  58. package/src/proxy/sources.ts +35 -0
  59. package/src/proxy/state.ts +32 -21
  60. package/src/proxy/warm.ts +68 -0
  61. package/src/proxy/window.ts +3 -33
  62. package/src/transcript/entries.ts +114 -0
  63. package/src/transcript/format.ts +1 -2
  64. package/src/transcript/messages.ts +14 -11
  65. package/src/transcript/render-worker.ts +24 -0
  66. package/src/transcript/render.ts +12 -3
  67. package/src/transcript/search.ts +46 -5
  68. package/src/transcript/subagents.ts +55 -34
  69. /package/src/{proxy → core}/estimate.ts +0 -0
  70. /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 Usage = Record<string, number>
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
+ }
@@ -1,20 +1,15 @@
1
+ import { existsSync, readFileSync } from "node:fs"
1
2
  import type { Config } from "../config"
2
- import type { Body, Message } from "../context/content"
3
- import { appendSystem, tailMessages, trimMessages } from "../context/fold/strategies"
4
- import { splitTurns, turnFingerprint } from "../context/fold/turns"
5
- import { SYSTEM_PROMPT, searchGuide, searchOnlyInstructions, trimInstructions } from "../context/prompts"
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 { compact } from "./compaction"
10
- import { DEFAULT_RATIO, estimator, sizeOf, targetSize } from "./estimate"
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: 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 interface Rewrite {
27
- body: Body
28
- threadKey: string | null
29
- note: Record<string, unknown>
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
- function evalStrategy(input: RewriteInput, turns: Message[][], threadKey: string, guide: string | null): Rewrite | null {
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 function rewrite(input: RewriteInput, before?: Rewrite): Rewrite {
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 async function prefetchRecall(input: RewriteInput, rerank: Reranker): Promise<Prefetched | null> {
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 model = wantsRecall(input) ? rerankModel(input.config) : null
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 rewrite(input)
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 first = strategize(input)
163
- const recalled = await prefetchFor(input, first, reranker)
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 { ...final, note: { ...final.note, recalled, ...outcome } }
50
+ return { ...result, note: { ...result.note, ...outcome } }
168
51
  }