Skip to main content

needlepath-strands

Needlepath context selection for Strands Agents (AWS's agent framework), as a ConversationManager.

pip install needlepath-strands

One line

from strands import Agent
from needlepath_strands import NeedlepathConversationManager

agent = Agent(
    model=model,
    tools=tools,
    conversation_manager=NeedlepathConversationManager(
        operating_point="np-2026-08-r4",
    ),
)

The key comes from NEEDLEPATH_API_KEY, the operating point is pinned explicitly, and if the service is slow, down, or stands down, agent.messages is left exactly as it was and the reason is recorded on .stats and on each rewritten message's metadata.custom.needlepath.

This measures and reports; it does not rewrite anything yet. shadow defaults to True — pass shadow=False once .engine_stats shows what it would have saved (.stats, the actual-applied ledger, is always 0 under shadow — see Two stats objects, two truths below). See Shadow-first.

Not the same shape as the LangChain / LlamaIndex adapters

Strands' ConversationManager interface is not AgentMiddleware. There is no per-tool-call hook: the interface gives an implementation exactly two places to run —

  • apply_management(agent) — called once after every invocation completes. This is the seam this package uses: the accumulated tool results in agent.messages are selected against the current task and rewritten in place. Engages only when the prunable history exceeds max_context_tokens (or trigger_tokens, if set separately).
  • reduce_context(agent, e=...) — called reactively on a context-window overflow, and proactively (if proactive_compression= is configured) before a model call projected to exceed a threshold. Tries a Needlepath selection first, without the trigger gate. See Reactive overflow below for what happens when that selection does not resolve the overflow.

Both work by mutating agent.messages — a Bedrock-shaped list[{"role": ..., "content": [ContentBlock, ...]}]in place, which is the contract every shipped ConversationManager (SlidingWindowConversationManager, SummarizingConversationManager) also follows.

Two invariants, ported from the LangChain adapter

A message or content block is never removed, only rewritten. SlidingWindowConversationManager goes to real lengths (find_valid_trim_point, _find_tool_pair_trim_point) to avoid ever leaving a toolUse block without its paired toolResult — every model provider rejects that shape. This package never deletes a message or a content block, so it cannot produce that shape at all: a toolResult keeps its toolUseId and its position; only its content may shrink.

A rewrite never grows the context and never empties a message. An excerpt longer than what it replaces, or an empty excerpt applied to non-empty content, is refused here rather than trusted upstream.

By default only toolResult content blocks are rewritten. A toolUse block is never rewritten, on any setting: it is the call, not prose.

No preserve_recent — a deliberate difference from SlidingWindowConversationManager

Strands' own conversation managers let you pin a trailing window of messages (SlidingWindowConversationManager(pin_first=...), or simply never trimming recent turns). This package does not offer an equivalent, and an earlier version that did was removed (NEXPE-517 review).

The reason is not that tail protection is a bad idea — it is that a client-side version of it cannot be both honest and free of a selection decision made locally. The only way to protect a trailing window without telling the engine is to silently narrow which records a rewrite is allowed to touch, while .stats goes on reporting the engine's un-narrowed tokens_after/tokens_saved — which makes the receipt lie about what the model actually received whenever the engine's own selection happens to favor the protected window. What this package applies is the engine's result, exactly as returned, over every record it sent. No exceptions, no local overrides.

If you need recent-turn protection, it is either a request to make of the engine (a position/recency signal it can act on like any other input to its own decision), or a genuinely separate, engine-blind concern you compose yourself — fallback=SlidingWindowConversationManager() covers the one place in this package's own lifecycle where that composition already has a hook (reactive overflow; see below).

Configuration

Parameter Default What it does
operating_point Required. Immutable label. Also NEEDLEPATH_OPERATING_POINT.
shadow True Measure and report; never apply. Shadow-first — see below. Pass False for live selection.
enabled True Kill switch. Needs no credentials when False.
max_context_tokens 8000 Trigger and budget for the prunable history.
trigger_tokens 0 Separate trigger from the budget; 0 means "use max_context_tokens".
include_ai_messages / include_human_messages False Widen beyond tool results.
placeholder see source Replaces an unselected tool result.
per_turn False Also run before every (or every Nth) model call within a turn. See below.
fallback None A real ConversationManager to delegate to on an unresolved reactive overflow. See below.
proactive_compression None Forwarded unchanged to the base class.

Any other keyword is forwarded to the core client (base_url, api_key, timeout, max_retries, …).

Reactive overflow: what reduce_context does when selection is not enough

Needlepath's selection is itself fail-open: it may decline (shadow, escalation, empty selection, a transport failure, an unrecognised outcome) and produce no reduction. reduce_context's reactive contract (e set) requires the implementation to still reduce the history, or re-raise.

This package does not invent a trimming heuristic of its own to satisfy that. Deciding which messages survive a plain truncation is exactly the sort of decision the thin-client rule keeps out of a client package — a selection decision belongs server-side, and "just drop the oldest messages" is not Needlepath's call to make unilaterally from inside a package that also carries the selection contract.

Instead: pass fallback= a real, host-native ConversationManagerSlidingWindowConversationManager() is the natural choice — and this class delegates to it, in full, exactly as if it had been composed by the caller. Without one, a reactive overflow that Needlepath's own selection did not resolve re-raises the original exception — the same thing NullConversationManager does, and the honest answer when there is nothing safe left to try:

from strands.agent import SlidingWindowConversationManager
from needlepath_strands import NeedlepathConversationManager

conversation_manager = NeedlepathConversationManager(
    operating_point="np-2026-08-r4",
    fallback=SlidingWindowConversationManager(),
)

Sync only — a real difference from the other two adapters

Strands' ConversationManager.apply_management / reduce_context are synchronous methods with no async counterpart; the event loop calls them directly even under agent.invoke_async(). This adapter therefore wraps needlepath.NeedlepathClient only — there is no async seam to put an AsyncNeedlepathClient into, unlike needlepath_langchain and llama-index-postprocessor-needlepath, whose host frameworks do expose one. A slow selection call blocks whatever called apply_management, the same as any other synchronous hook would; size timeout accordingly.

Fail open

Every error path leaves agent.messages untouched, with the reason recorded on conversation_manager.stats and, for any message that was rewritten, on message["metadata"]["custom"]["needlepath"]:

{
    "rewritten": True,
    "request_id": "np-…",
    "blocks": [
        {
            "block_index": 0,
            "record_id": "m2b0",
            "rewrite_reason": "excerpt",
            "original_tokens": 3001,
            "new_tokens": 12,
        },
    ],
}

conversation_manager.stats.as_dict() aggregates the same counters (calls, applied, passthrough, failures, tokens_saved, reasons), in the same shape the other adapters expose — but reporting what was actually applied, not the engine's raw claim. For a shadow report (what a live run would have saved), read conversation_manager.engine_stats.as_dict() instead; see Two stats objects, two truths below.

Two stats objects, two truths

conversation_manager.stats reports what this package actually did to agent.messages — real before/after token counts, measured the same way the rewrite itself is measured. conversation_manager.engine_stats reports what the engine's raw response claimed, unmodified.

These are not the same number, on purpose. The engine cannot see the placeholder text (DEFAULT_PLACEHOLDER, or your own) this package substitutes for a record it declined to select, so the engine's own tokens_after describes an idealized outcome — "if you kept only what I selected" — not the message this package actually installs, which is a little larger because every unselected record still costs a placeholder's worth of tokens instead of costing nothing. In shadow mode the gap is total: stats.tokens_saved is always 0 (nothing is ever applied), while engine_stats.tokens_saved carries the engine's prediction of what a live run would have saved.

Use .stats for anything downstream that has to be true of agent.messages. Use .engine_stats to reconcile against what the service itself measured or billed.

Shadow-first

shadow defaults to True. Wiring in NeedlepathConversationManager with nothing but an operating_point measures, don't apply:

NeedlepathConversationManager(operating_point="np-2026-08-r4")  # shadow=True, implicitly

A shadow run makes the same call, at the same rate, as a live run would — and never touches agent.messages. Check conversation_manager.engine_stats for what the engine predicts a live run would save (conversation_manager.stats correctly shows 0, since nothing was applied). Turn on live selection explicitly, once you trust the numbers:

NeedlepathConversationManager(operating_point="np-2026-08-r4", shadow=False)

This is the one constructor default that intentionally does not match needlepath_langchain or llama-index-postprocessor-needlepath, both of which default to live selection (shadow=False). Those two adapters shipped and were verified together; this one is newer and has not run against real Strands traffic yet, so the safer posture is the default rather than something a caller has to remember to opt into.

Operating point

Pinned explicitly, always — np-2026-08-r4 today (see API_VERSIONING.md for why an operating point is never left to a service default).

Tested against

strands-agents==1.52.0 — installed and introspected directly (strands.agent.ConversationManager, strands.types.content, strands.types.tools, strands.types.exceptions.ContextWindowOverflowException), the newest published release at the time this package was written (NEXPE-517). Floored at >=1.52.0 for that reason — the interface was verified against exactly this version, not assumed from documentation — and capped at <2.0.0, same as every other adapter in this tree.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

needlepath_strands-0.1.1.tar.gz (39.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

needlepath_strands-0.1.1-py3-none-any.whl (28.0 kB view details)

Uploaded Python 3

File details

Details for the file needlepath_strands-0.1.1.tar.gz.

File metadata

  • Download URL: needlepath_strands-0.1.1.tar.gz
  • Upload date:
  • Size: 39.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.3

File hashes

Hashes for needlepath_strands-0.1.1.tar.gz
Algorithm Hash digest
SHA256 1d2f535c3a92dc6cf393d1a27f6e787b2c8c1e6d16f3f68662bcd39abde692d7
MD5 32d7a2119cb4850bc0c6ec2edeeac4e6
BLAKE2b-256 8eddb267bfa78e9247294b7bdeab79f87288febaaff06a017f717db72adb50a9

See more details on using hashes here.

File details

Details for the file needlepath_strands-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for needlepath_strands-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 770f3227a01c553fb76a46713ec2e25887e171b611d660f3d2d534982f809d4b
MD5 8356e18b019bc05dad8af4041b4a91fa
BLAKE2b-256 8a005d00f8615274b811af1146f10085b26fbd2faea31d5fe64373dcd0db3ddd

See more details on using hashes here.

Release history Release notifications | RSS feed

0.1.2

2 files

This release

0.1.1 This release

2 files

0.1.0

2 files

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