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:
- 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.
- Size gate: results that fit the budget pass as they are.
- 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. - 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. outputcan be text or anything JSON-serializable (Pydantic models and dataclasses are serialized by their fields).argscan be a dict or JSON text (OpenAI'sarguments).shrinkreturns aShrinkResult:text(what the model reads),mode(full,shapedortrimmed),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=toshrink(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
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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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.