@chroxify/eunoe 1.2.2 → 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.
Files changed (64) hide show
  1. package/README.md +150 -6
  2. package/dist/core/index.js +2354 -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 +16 -0
  8. package/dist/types/context/prompts.d.ts +24 -0
  9. package/dist/types/context/recall/index.d.ts +47 -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 +34 -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 +9 -0
  20. package/dist/types/core/rewrite.d.ts +63 -0
  21. package/dist/types/core/state.d.ts +25 -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 +8 -0
  25. package/dist/types/core/window.d.ts +8 -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/status.ts +1 -1
  31. package/src/cli/proxy/stats.ts +1 -1
  32. package/src/cli/replay/index.ts +3 -3
  33. package/src/cli/sessions/index.ts +2 -2
  34. package/src/config/index.ts +4 -27
  35. package/src/context/content.ts +64 -1
  36. package/src/context/fold/strategies.ts +63 -10
  37. package/src/context/fold/turns.ts +34 -2
  38. package/src/context/prompts.ts +4 -3
  39. package/src/context/recall/index.ts +8 -4
  40. package/src/context/recall/rerank.ts +0 -8
  41. package/src/{proxy → core}/compaction.ts +85 -28
  42. package/src/core/config.ts +31 -0
  43. package/src/core/fold.ts +71 -0
  44. package/src/core/forward.ts +194 -0
  45. package/src/{proxy → core}/handoff.ts +23 -23
  46. package/src/core/index.ts +14 -0
  47. package/src/core/recall.ts +80 -0
  48. package/src/core/rewrite.ts +201 -0
  49. package/src/core/state.ts +50 -0
  50. package/src/core/subagents.ts +28 -0
  51. package/src/core/transcript.ts +44 -0
  52. package/src/{proxy → core}/usage.ts +43 -1
  53. package/src/core/window.ts +69 -0
  54. package/src/proxy/rewrite.ts +22 -139
  55. package/src/proxy/server.ts +21 -159
  56. package/src/proxy/sessions.ts +1 -1
  57. package/src/proxy/sources.ts +35 -0
  58. package/src/proxy/state.ts +2 -20
  59. package/src/proxy/window.ts +3 -33
  60. package/src/transcript/format.ts +1 -2
  61. package/src/transcript/subagents.ts +3 -3
  62. package/src/proxy/recall.ts +0 -58
  63. /package/src/{proxy → core}/estimate.ts +0 -0
  64. /package/src/{proxy → core}/fingerprint.ts +0 -0
package/README.md CHANGED
@@ -243,7 +243,10 @@ a small and very fast model, reorders the top 30 in about 300 ms, which
243
243
  lifted the right excerpt from 88% to 95% of questions and beat every other
244
244
  method and model that was tested. Without a key the BM25 order is used and
245
245
  nothing else changes. The recall is computed once per message and pinned to
246
- it, so it never disturbs the cache.
246
+ it, so it never disturbs the cache. Once that message is folded its recall is
247
+ dropped with it, and the recall on the messages still in context stays under
248
+ 30K characters in total and under a quarter of the context (with a 6K floor),
249
+ so it never crowds out the conversation itself.
247
250
 
248
251
  ## How it plugs in
249
252
 
@@ -430,6 +433,143 @@ reads, and every token a fold rewrote is charged back at the cache-write price.
430
433
  deletes sessions that have been idle for 30 days unless `cleanupPeriodDays` in
431
434
  its `settings.json` says otherwise.
432
435
 
436
+ ## Embedding
437
+
438
+ The proxy is one host of a small library. Anything that sends Anthropic
439
+ Messages requests can use the same folding, recall and estimates: a
440
+ forwarding server, an agent loop of your own, a Cloudflare Durable Object,
441
+ another proxy. The core reads no files, no environment and no global
442
+ config, and it runs in Node, Bun, Deno and Workers.
443
+
444
+ ```sh
445
+ npm install @chroxify/eunoe
446
+ ```
447
+
448
+ ```ts
449
+ import { CORE_DEFAULTS, memoryStore, rewrite } from "@chroxify/eunoe"
450
+
451
+ const store = memoryStore()
452
+ const { body, note } = rewrite({
453
+ body: request, // the Messages request you were about to send
454
+ threadId: conversationId, // anything stable per conversation
455
+ config: { ...CORE_DEFAULTS, mode: "compact" },
456
+ store,
457
+ window: 200_000, // the model's context window
458
+ mayCompact: true, // false for count_tokens and other side calls
459
+ })
460
+ // send `body` instead of `request`; `note` says what happened
461
+ ```
462
+
463
+ `rewrite` is a pure function of the request and the thread's saved state,
464
+ so the same history always produces the same bytes and the prompt cache
465
+ holds. Everything it needs from outside comes in through three small
466
+ pieces:
467
+
468
+ | | |
469
+ |---|---|
470
+ | `ThreadStore` | `read(key)` and `save(key, state)`, synchronous. `memoryStore()` keeps state in memory; give it your own to persist across restarts. |
471
+ | `ThreadSources` | optional: `readHistory()` for the whole conversation when you send a shortened one, `findHandoff` and `readHandoff` for `{ee:continue:…}`, `guide` and `transcriptDir` when the agent can search a transcript. |
472
+ | `Reranker` | optional, for `rewriteAsync`: reorders recall candidates. `jevReranker(model, key)` is the one the proxy uses. |
473
+
474
+ In a Durable Object, SQLite-backed storage is synchronous, so the store is
475
+ two lines:
476
+
477
+ ```ts
478
+ const store = {
479
+ read: (key) => this.ctx.storage.kv.get(key) ?? {},
480
+ save: (key, state) => this.ctx.storage.kv.put(key, state),
481
+ }
482
+ ```
483
+
484
+ ### Folding at a boundary you know
485
+
486
+ A host often knows better than any token count when a step is over. After a
487
+ planning step, fold every earlier turn and keep only the plan whole:
488
+
489
+ ```ts
490
+ import { foldThread, pinTurns } from "@chroxify/eunoe"
491
+
492
+ messages.push({ role: "user", content: "Build step one." })
493
+ foldThread({ store, threadId, messages }, { at: "latest", pin: [planTurn] })
494
+ ```
495
+
496
+ Turns are numbered as `note.cutTurn` numbers them, from 0 for the first
497
+ prompt, and the boundary must be a turn that already exists, so fold once
498
+ the next step's prompt is in the list. Every turn before the boundary is
499
+ folded to the user's message, the evidence and the reply. Pinned turns are
500
+ sent whole, with their tool calls, and stay whole even when a later
501
+ automatic cut drops the turns around them. `pinTurns(target, [n], false)`
502
+ releases one. A host fold applies in every mode, `off` included, and its
503
+ output stays byte-stable from one request to the next.
504
+
505
+ ### The note
506
+
507
+ Every call returns a note: `action` (`pass`, `full` or `compact`),
508
+ `cutTurn`, `folded`, `omitted`, `pinned`, `estimate` in tokens, `fit` when
509
+ the request had to be trimmed to fit (`recut`, `bare` or `over`), and
510
+ `stuck` when even the cut leaves it over the limit. `stuck` gives the
511
+ tokens in each part (`fixed`, `head`, `folded`, `live`) so the host can
512
+ act: start a fresh conversation with a handoff, or fold at its own
513
+ boundary. It clears as soon as a request fits. While a thread is stuck,
514
+ eunoe holds what it cleared still instead of clearing a little more on
515
+ every request, and clears again only when that makes the request fit or
516
+ brings it back under the window, so the cache keeps hitting.
517
+
518
+ ### The whole proxy as one call
519
+
520
+ `createForwarder` is the proxy's request loop as a fetch handler: it
521
+ rewrites each `/v1/messages` request, forwards everything to the API,
522
+ streams the answer back, learns the thread's real token ratio from the
523
+ answer's `usage`, retries once after a "prompt is too long" against the
524
+ limit the error names (and keeps that limit for the thread and model), retries a
525
+ stalled `count_tokens`, and looks the context window up from the Models API. `eunoe serve` is this call with a file-backed
526
+ store, Claude Code's session files as sources and a request log.
527
+
528
+ ```ts
529
+ import { CORE_DEFAULTS, createForwarder, memoryStore } from "@chroxify/eunoe"
530
+
531
+ const forward = createForwarder({ store: memoryStore(), config: CORE_DEFAULTS })
532
+ Bun.serve({ port: 8787, fetch: forward }) // or Deno.serve, a Worker's fetch, …
533
+ ```
534
+
535
+ Threads are told apart by an `x-thread-id` header, or Claude Code's own
536
+ session id; pass `threadIdOf`, `sources`, `reranker`, `window` or `record`
537
+ to change any part, or `prepare` to take the rewrite over entirely. A host
538
+ that calls the API itself gets the same calibration with
539
+ `recordUsage(store, threadKey, sentBody, usage)` and
540
+ `recordOverflow(store, threadKey, sentBody, errorText)`.
541
+
542
+ | export | |
543
+ |---|---|
544
+ | `rewrite`, `rewriteAsync` | rewrite one request for one thread; the async one takes a reranker |
545
+ | `foldThread`, `pinTurns` | fold at your own boundary, keep chosen turns whole |
546
+ | `createForwarder` | the full proxy loop as a `(Request) => Promise<Response>` |
547
+ | `recordUsage`, `recordOverflow` | feed what the API said back into the thread's estimates |
548
+ | `createWindowLookup` | a model's context window from the Models API, cached, with a per-family fallback |
549
+ | `memoryStore`, `threadKeyOf` | in-memory state, and the key a thread is saved under |
550
+ | `listSubagentRecall` | recall over what your own subagents printed |
551
+ | `renderTranscriptFiles`, `transcriptGuide` | the searchable per-turn transcript from your own messages, and the text that points the agent at it |
552
+ | `jevReranker`, `continueToken`, `watchUsage`, `claudeCodeSessionId` | the pieces the proxy uses, for hosts that want them |
553
+
554
+ What stays in the proxy is what only makes sense next to Claude Code on
555
+ your machine: reading its session files and subagent runs, finding earlier
556
+ sessions for `{ee:continue:…}`, rendering the markdown transcript, the
557
+ config file and the CLI.
558
+
559
+ Every export, with its signature, what it does and an example, is in
560
+ [`docs/api.md`](docs/api.md), which starts with where each kind of state and
561
+ data lives. [`examples/`](examples) runs as it is. [`tour.ts`](examples/tour.ts) walks
562
+ through every feature on real inputs and prints what each did: folding,
563
+ the full cut, evidence, recall, recall from host history, reranking,
564
+ handoffs, host folds and pins, `stuck`, calibration, custom storage, the
565
+ forwarder and transcripts. The others are hosts: a
566
+ [Durable Object](examples/durable-object.ts) per thread on its own storage,
567
+ tested in real `workerd`, a
568
+ [forwarding server](examples/forwarding-server.ts) on `node:http` with a
569
+ `/fold` endpoint, a [custom agent loop](examples/agent-loop.ts) that folds
570
+ at each step and keeps its plan pinned, and a
571
+ [Claude Code proxy](examples/proxy.ts) with a file-backed store.
572
+
433
573
  ## What it doesn't do
434
574
 
435
575
  - **It is a proxy, which makes it a single point of failure.** If it isn't
@@ -442,17 +582,21 @@ its `settings.json` says otherwise.
442
582
  502 that Claude Code retries, never a hang. Sessions that were already
443
583
  running keep the old address until restarted, so they wait for the proxy
444
584
  rather than falling back.
445
- - **It only understands Claude Code's shape of conversation.** Other
446
- Anthropic API clients pass through unchanged and get nothing out of it.
585
+ - **The proxy only understands Claude Code's shape of conversation.** Other
586
+ Anthropic API clients pass through it unchanged and get nothing out of
587
+ it; they can [embed the library](#embedding) instead.
447
588
  - **It can't recover what Claude Code already compacted,** so turn it on
448
589
  before a long session rather than after.
449
590
  - **Recall is keyword search.** Ask about "the deploy that failed" and it
450
591
  finds the deploy output, but ask about "that thing from before" and it
451
592
  finds nothing, leaving the agent to search the transcript itself. Jev
452
593
  narrows this gap without closing it.
453
- - **It never cuts against a limit it can't verify.** The context window size
454
- comes from the Models API for each model, and if that lookup fails the
455
- budget folds still happen but the full cut does not.
594
+ - **The window can be a guess.** The context window size comes from the
595
+ Models API for each model. When that lookup fails or takes over 1.5 s,
596
+ eunoe assumes the model family's window (1M for Claude 5 models, Opus
597
+ 4.8 and `[1m]` ids, 200K for other Claude models) and asks again ten
598
+ minutes later. A guess that is too big is caught by the "prompt is too
599
+ long" retry, which then keeps the real limit for that thread and model.
456
600
 
457
601
  ## Credits
458
602