Skip to main content

contextwaise

Shrinks agent tool results before the model reads them: cache-safe, reversible, intent-aware.

In an agent loop, every tool result is resent on every later turn. A 20K-token test log read at turn 3 of a 20-turn task is paid ~17 more times. Contextwaise gives the model a short view of each result, once, at the moment the tool returns:

  1. Shapers (free, local, deterministic): strip color codes and progress bars, drop passing tests, collapse repeated log lines, remove empty JSON fields, turn HTML into text. Errors are never removed.
  2. Size gate: results that fit the budget pass as they are.
  3. Relevance: the parts that match what the agent was doing (the text it wrote before the call, plus the call's arguments) are kept within the budget; everything else becomes … N lines omitted.
  4. Nothing is lost: the model can call expand(ref, question) to get any part back.

Cache-safe by design: a result's view is decided once and replayed byte-for-byte by tool call id, so the provider's prompt cache keeps hitting and edited-history checks never trip.

pip install contextwaise

Python 3.10+. One dependency: httpx.

Usage

Shrink each tool result before it goes back to the model, and give the model the expand tool. With the Anthropic SDK, by hand (the adapters below do this for you):

import anthropic
from contextwaise import EXPAND_TOOL, Contextwaise

client = anthropic.Anthropic()
expand_tool = {
    "name": EXPAND_TOOL["name"],
    "description": EXPAND_TOOL["description"],
    "input_schema": EXPAND_TOOL["parameters"],
}
tools = [*my_tools, expand_tool]

# Reads CONTEXTWAISE_API_KEY and CONTEXTWAISE_URL from the environment.
with Contextwaise(agent="support-triage", budgets={"read_file": 1500}) as cw:
    messages = [{"role": "user", "content": question}]
    while True:
        response = client.messages.create(
            model="claude-opus-5-5", max_tokens=4096, tools=tools, messages=messages
        )
        messages.append({"role": "assistant", "content": response.content})
        if response.stop_reason != "tool_use":
            break
        # Why the agent calls each tool: the request, then what the model wrote before the call.
        said = question
        results = []
        for block in response.content:
            if block.type == "text":
                said += " " + block.text
            elif block.type == "tool_use":
                cw.note_intent(block.id, said)
                if block.name == "expand":
                    # The call's size: expanding costs one more call about as big,
                    # and the dashboard counts it against the savings.
                    u = response.usage
                    size = (
                        u.input_tokens
                        + (u.cache_read_input_tokens or 0)
                        + (u.cache_creation_input_tokens or 0)
                    )
                    content = cw.expand(**block.input, context_tokens=size)
                else:
                    output = run_tool(block.name, block.input)
                    view = cw.shrink(call_id=block.id, tool=block.name, args=block.input, output=output)
                    content = view.text
                results.append({"type": "tool_result", "tool_use_id": block.id, "content": content})
        messages.append({"role": "user", "content": results})
  • Your code keeps the raw output; only the model's view changes.
  • output can be text or anything JSON-serializable (Pydantic models and dataclasses are serialized by their fields). args can be a dict or JSON text (OpenAI's arguments).
  • shrink returns a ShrinkResult: text (what the model reads), mode (full, shaped or trimmed), reason (why: cut, fits_budget, small_saving, schema, marked_full, api_unavailable, …), token counts before and after, and what was kept and why.
  • Hosting many agents on one client? Pass agent= to shrink (and to the Anthropic/OpenAI helpers) to group each call under its own agent.
  • Tool schemas and lists of actions (an MCP server describing its tools, OpenAI or Anthropic tool definitions, JSON Schema) are detected and always reach the model whole: the next call must follow them exactly.

Vercel AI SDK for Python

ContextwaiseAgent is an ai.Agent whose model reads Contextwaise's view of each tool result. Your app (events, UI) keeps the raw output, the history carries the view (so later turns resend the same bytes), and an expand tool is added.

pip install "contextwaise[ai]"   # the ai package needs Python 3.12+
import ai
from contextwaise import AsyncContextwaise
from contextwaise.ai_sdk import ContextwaiseAgent

async with AsyncContextwaise(agent="support-triage") as cw:
    agent = ContextwaiseAgent(cw, tools=[get_orders, search_logs])  # your @ai.tool functions
    async with agent.run(ai.get_model("anthropic:claude-opus-5-5"), [ai.user_message(question)]) as stream:
        async for event in stream:
            ...

Pass full=lambda tool, args: ... to ContextwaiseAgent for calls whose output must reach the model whole (e.g. a file the agent will edit; schemas and lists of actions already are), and agent= when one client serves many agents. The adapter also reports the model id with each result, for your dashboard.

Anthropic and OpenAI SDKs

The adapters do the loop body for you: one call per model response runs your tools and returns what to send back, each output shrunk, expand answered, and a tool that raises reported to the model as an error. run(name, args) returns your tool's raw output.

from contextwaise.anthropic import EXPAND_TOOL, tool_results

response = client.messages.create(
    model=model, max_tokens=4096, messages=messages, tools=[*my_tools, EXPAND_TOOL]
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason == "tool_use":
    messages.append({"role": "user", "content": tool_results(cw, response, run_tool, request=question)})
from contextwaise.openai import EXPAND_TOOL, EXPAND_TOOL_RESPONSES, function_outputs, tool_messages

# Chat Completions
message = completion.choices[0].message
usage = completion.usage  # a message doesn't carry it: pass it so expands are counted
messages += [
    message,
    *tool_messages(cw, message, run_tool, request=question, prompt_tokens=usage.prompt_tokens),
]

# Responses API
items += [*response.output, *function_outputs(cw, response, run_tool, request=question)]

With AsyncContextwaise, use atool_results, atool_messages and afunction_outputs: the calls run concurrently and run may be async. The adapters read the SDKs' objects (or plain dicts); neither SDK is a dependency.

asyncio

AsyncContextwaise has the same interface, with await on shrink, expand and flush, and async with. The relevance API never blocks the event loop.

async with AsyncContextwaise(agent="support-triage") as cw:
    view = await cw.shrink(call_id=call.id, tool=call.name, args=call.input, output=output)

Options

Option Default What it does
api_key CONTEXTWAISE_API_KEY Your Contextwaise key (sw_live_…). Relevance is then judged by the API (Jev), and results are reported to your dashboard: sizes, tool names and the model id only, never tool output. Without a key, only the local cleanup runs: noise is removed, nothing relevant is ever cut.
base_url CONTEXTWAISE_URL Where your Contextwaise app runs. Required with a key.
agent — Groups this agent's results on the dashboard. Each call can override it (agent= in shrink, ContextwaiseAgent and the Anthropic/OpenAI helpers).
max_tokens 600 Budget for the model's view of one tool result.
budgets — Per-tool budgets, e.g. {"read_file": 1500}: files up to that size reach the model whole.
full — Tools whose output always reaches the model whole. Schemas and lists of actions already are.
ledger in memory, last 5,000 calls Where originals and views live. Implement get(key) / set(key, entry) over Redis or a database for agents spread over processes.

The API is never in the agent's way: scoring times out after 4 s and is then skipped for 30 s, or an hour once the free tokens are used up (a single result the API rejects as too big is just passed whole); meanwhile the model reads the cleaned output whole (nothing is cut without the API). Reports are batched and sent from a background thread, and dropped if they can't be sent. Leave the with block (or call close()) before a short-lived process exits, so pending reports are sent.

Decisions are logged on the contextwaise logger (DEBUG), API problems at WARNING.

Development

git clone https://github.com/Contextwaise/contextwaise-python.git && cd contextwaise-python
uv sync                 # virtualenv with the dev tools
uv run pytest           # tests (no network: the API is stubbed)
uv run ruff check . && uv run ruff format --check .
uv run mypy
uv build                # sdist + wheel in dist/

The TypeScript SDK lives in Contextwaise/contextwaise-typescript, with the rules it shares with this package in its core (shapers.ts, relevance.ts). The two SDKs make the same decisions: a change to the shapers, relevance or the SDK core should land in both.

License

MIT

Metadata

Release files for contextwaise 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for contextwaise 0.1.0
File Size Uploaded Provenance Actions
contextwaise-0.1.0.tar.gz 46.2 kB Without provenance Details for contextwaise-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for contextwaise 0.1.0
File Interpreter ABI Platform Actions
contextwaise-0.1.0-py3-none-any.whl Python 3 none any Details for contextwaise-0.1.0-py3-none-any.whl

Total release size: 87.5 kB

Release files / contextwaise-0.1.0.tar.gz

Download URL contextwaise-0.1.0.tar.gz
Size 46.2 kB
Tags Source
SHA-256 checksum
How to use checksums
8cd9aecd3a8778118199ae7010314aaaccb95c38fcff9b33cc7c544576b71455
BLAKE2b-256 checksum
How to use checksums
ad821e66e4a1bef27f2ce5573f750cc21acc6e6e2bcdaa929591eb0477ad38e8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.6.12

Release files / contextwaise-0.1.0-py3-none-any.whl

Download URL contextwaise-0.1.0-py3-none-any.whl
Size 41.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a30e29ab8cb7eadb446d3ad636869efce11e8471f62316cd1523503971beb091
BLAKE2b-256 checksum
How to use checksums
fbbcb40a2a139c4d1323e1b0c5f128434d19820d0b63de01a08bb7663507555c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.6.12

Security for contextwaise 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page