npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ctxjev-core

v0.6.1

Published

Score AI agent context for relevance with Jev, and decide what to keep, drop, or summarize.

Readme

ctxjev-core

Score AI agent context for relevance with Jev, the engine behind ctxjev.

npm License: MIT Node TypeScript

Why · Install · How It Works · API · Related Packages


Why

Long-running agent loops, such as coding agents, browser agents, or anything with a growing tool-call history, accumulate context faster than it stays useful. Most of that history isn't hard to judge: "is this old tool result still relevant to the current task?" is exactly the kind of fast, cheap, structured decision Jev (TypeSafe AI's typed-decision model) is built for. It returns typed judgments (a yes/no probability, a choice, a score) instead of writing a sentence about it.

ctxjev-core can ask Jev that question about every entry, and decides what to keep, drop, or summarize. Whether Jev's answers beat simpler rankings is an open question, and so far the evidence says they don't on unseen tasks; see the main README. The default is plain truncation. It never asks Jev to see images, do arithmetic, or generate text. Token counting happens in code, and the keep/drop/summarize decision is a plain threshold applied to Jev's typed output.

Install

npm install ctxjev-core

No key needed by default: the default scorer is 'recency' (newest kept, i.e. plain truncation), which needs no network and sends nothing. On a preregistered holdout comparison, it tied Jev on whether the agent finished the job (+0 points, 95% CI [+0, +0]); see the main README for the full result, including where Jev's ranking did and didn't separate itself. Pass { scorer: 'jev' } to opt in, which needs TYPESAFE_API_KEY (get one at console.typesafe.ai/settings/keys, no waitlist), or { scorer: 'local' } for offline keyword overlap instead.

What the default does: with 'recency', the goal isn't used at all. Every entry's relevance is its position in the batch (oldest 0, newest 1), so with the default thresholds (dropBelow 0.3, summarizeBelow 0.6) pruneContext() drops roughly the oldest 30% of entries and marks the next 30% for summarizing, whatever they say, including the first request. pruneMessages() never touches the first message, the latest turn, or (by default) anything the user wrote; pruneContext() has no such protection, so exclude what must stay before calling it.

With 'local', keyword overlap is ranked within the batch before the thresholds apply: each decision's relevance is the entry's percentile rank of overlap (tied entries share their average rank), so the thresholds read as shares of the batch rather than as probabilities. Raw overlap rarely reaches 0.3, and the thresholds used to drop almost everything, relevant entries included. When most entries share no word with the goal, they tie in the middle and are marked for summarizing, not dropped; pass targetTokens to pruneMessages() if you need a fixed size. scoreEntries() still returns the raw overlap.

With scorer: 'jev', entry content and the goal are sent to TypeSafe AI's Jev API. Every request passes through redactSecrets() first, masking common secret formats to [REDACTED] (best-effort, not exhaustive). It's exported too, if you want to apply the same masking elsewhere.

How It Works

With scorer: 'jev', every entry becomes its own question, and the questions for up to 50 entries are evaluated in parallel against one shared state, as a single request. What Jev bills is input tokens, and those still grow with the entries in the request, since each entry's excerpt is part of the state; at Jev's published price that stays small, and every request's actual usage is reported through onUsage.

import { pruneContext } from 'ctxjev-core'

const decisions = await pruneContext(
  entries, // your agent's tool-call / message history
  'Fix a bug where checkout charges customers twice on a slow network retry.',
  undefined, // the default policy
  { scorer: 'jev' }, // without this, the default 'recency' ranks by position
)
[
  { "entryId": "a", "relevance": 0.96, "recency": 0, "combinedScore": 0.864, "action": "keep" },
  { "entryId": "b", "relevance": 0.04, "recency": 1, "combinedScore": 0.136, "action": "drop" }
]

This is a real response shape, captured against the live API. relevance is Jev's own judgment, recency is this entry's position in the batch (oldest=0, newest=1), and combinedScore blends the two per PruningPolicy.recencyWeight before action is decided.

API

  • scoreEntries(entries, goal, recencyWeight?, options?) scores every entry with no decision made, and returns { entryId, relevance, recency, combinedScore }[].

  • pruneContext(entries, goal, policy?, options?) runs scoreEntries() and applies PruningPolicy's dropBelow/summarizeBelow thresholds, returning the same shape plus action.

  • pruneMessages(messages, goal, options?) takes an Anthropic Messages conversation and returns { messages, decisions, removed }: the conversation with dropped entries removed, still a valid request. A tool_use and its tool_result are removed together, a message left empty is removed, and the first message and the latest turn are never touched. messagesToEntries(messages) exposes the entry mapping on its own. Options:

    • protectLastTurn (default true): the latest turn is the last user message with text of its own (an instruction, not only tool results) and everything after it, however many tool round-trips that is. In an agent loop whose only user text is the first message, that's the whole conversation, so nothing is pruned: pass protectLastTurn: false there.
    • protectLast (default 2): the last this-many messages are never touched either way, a floor for tool calls still waiting on a result.
    • targetTokens: after the drops, keep removing the lowest-scoring unprotected entries until the conversation fits. overBudget in the result says if only protected entries are left.
    • summarize: shorten entries marked summarize instead of leaving them as they are, either 'excerpt' (the head and tail of the text) or your own (entry, text) => Promise<string>. A tool call keeps its tool_use; only its result is replaced.
    • minSavedTokens: change nothing unless it saves at least this many tokens.
    • keepUserText (default true): never remove what the user wrote. It costs few tokens and holds the constraints; in the task eval, an agent that lost "keep the mark for 24 hours" chose its own TTL.
    • marker (default true): add a one-line note where history was removed, so the model knows to re-read rather than trust what it half-remembers.

    The result reports savedTokens, summarized, and cache (see below), and keptDrops: the entries marked drop that weren't removed, by reason. firstMessage, latestTurn, lastMessages, and userText are protected; noNetSaving (removing them wouldn't save any tokens once the removal note is counted) and belowMinSaved (held back by minSavedTokens) are not.

  • parseClaudeCodeTranscript(jsonl, { countTokens? }) / resolveClaudeCodeGoal(jsonl, entries) parse a real Claude Code session .jsonl transcript into Entry[] (what's still in context, without the text Claude Code writes into the user turn itself), and find the goal: the latest /ctxjev:set-goal, or else the session's first request plus its latest instruction. Pass countTokens: estimateTokens to fill in each entry's sourceTokens.

  • summarizeSavings(entries, decisions) / estimateTokens(text) provide token-based savings reporting, using a real tokenizer and never asking Jev to count.

  • options.onUsage (on scoreEntries/pruneContext) is an optional callback fired once per Jev request with that request's real { inputTokens, outputTokens }, for cost tracking.

  • options.cache takes any { get, set } score cache, checked before each Jev request.

  • options.scorer: 'recency' (default) ranks by position alone (plain truncation); 'local' scores by keyword overlap with localRelevance(). Both are offline. 'jev' opts in to Jev.

  • options.scorer also takes your own function (CustomScorer), to score with another model or a rule set. It gets the goal, a chunk of up to 50 entries (content already masked by redactSecrets()), and the batch's latest activity, and returns one relevance from 0 to 1 per entry, in order. cache and onUsage apply to Jev only.

    const decisions = await pruneContext(entries, goal, undefined, {
      scorer: async (goal, chunk) => chunk.map((entry) => (entry.content.includes('[error]') ? 0.9 : myModel.score(goal, entry.content))),
    })
  • redactSecrets(text) is the secret masking every Jev request already goes through.

  • summarizeSavings() counts the verdicts: droppedTokens (in entries marked drop: saved once they're all removed, which is what ctxjev-format pruning does; for an Anthropic Messages conversation, pruneMessages()' savedTokens is what's actually saved) separately from summarizableTokens (entries marked summarize). Jev doesn't generate text, so how much of the latter is saved depends on what you do with those entries: pruneMessages' summarize option can cut them to an excerpt or hand them to your own summarizer. Both counts use each entry's sourceTokens (the full payload's size) when it's set.

With prompt caching

Removing anything from a conversation changes every request after it, so a prompt cache starts over from the first changed message: that much has to be written to the cache again (at a premium) instead of being read from it (at a discount). Pruning on every turn can easily cost more than it saves. Prune in bulk instead, when the context crosses a threshold you choose, and check the result's cache.invalidatedTokens against savedTokens:

if (contextTokens > 120_000) {
  const result = await pruneMessages(messages, goal, { targetTokens: 60_000, summarize: 'excerpt', minSavedTokens: 20_000 })
  messages = result.messages // result.cache: { firstChangedMessage, invalidatedTokens }
}

A large saving pays for one rewrite quickly, because every later turn reads the smaller conversation from the cache again. minSavedTokens makes a small saving a no-op, so an unchanged conversation keeps its cache.

Full type definitions ship with the package. Design notes (why relevance and recency are separate fields, why recency is batch-relative not wall-clock, how recencyWeight's default was tuned against labeled fixtures) live in the main repo's README.

Related Packages

| Package | What it is | | --- | --- | | ctxjev-cli | ctxjev analyze <transcript>: a plain-text report, no UI. | | ctxjev-mcp | MCP server exposing this engine as tools for Claude Code, Codex, and other MCP hosts. | | ctxjev-claude | Claude Code plugin (not on npm; see the main repo). |

Full docs, design notes, and the Claude Code plugin live in the main repo: github.com/x96x64/ctxjev.

License

This package is released under the MIT license: free to use, modify, and distribute, including in a commercial product, as long as the license text and copyright notice ship with it. See the main repo for how this matches every dependency ctxjev currently uses.