Metadata-Version: 2.5
Name: langchain-turn-window
Version: 0.1.0
Summary: Model-agnostic LangChain middleware that keeps only the last N conversational turns, without splitting tool_call/tool_result pairs.
Project-URL: Homepage, https://github.com/bharatkhanna-dev/langchain-turn-window
Project-URL: Repository, https://github.com/bharatkhanna-dev/langchain-turn-window
Project-URL: Issues, https://github.com/bharatkhanna-dev/langchain-turn-window/issues
Author: Bharat Khanna
License-Expression: MIT
License-File: LICENSE
Keywords: agents,context-window,conversation-history,langchain,langgraph,middleware
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: langchain-core>=1.0.0
Requires-Dist: langchain>=1.0.0
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Description-Content-Type: text/markdown

# langchain-turn-window

A small, model-agnostic [LangChain](https://github.com/langchain-ai/langchain) agent middleware that keeps only the last **N conversational turns** before each model call — without ever splitting an `AIMessage` tool call from its `ToolMessage` response.

## Why

Every provider (OpenAI, Anthropic, Gemini, ...) rejects a request where a `tool_calls` entry has no matching tool result message. Naive history trimming — slicing by raw message count or token count — can easily cut the window in the middle of a tool-call/tool-result pair and break the request.

`TurnWindowMiddleware` anchors the window on `HumanMessage` boundaries instead: it always starts the kept slice at the Nth-most-recent `HumanMessage`, which guarantees every `AIMessage` tool call inside the window still has its `ToolMessage` response attached.

This is a lightweight alternative to LangChain's built-in `SummarizationMiddleware` (which needs an extra LLM call) or `context_editing` (which selectively clears old tool outputs) for the common case of "just bound the context to the last few turns."

## Install

```bash
pip install langchain-turn-window
```

## Usage

```python
from langchain.agents import create_agent

from langchain_turn_window import TurnWindowMiddleware

agent = create_agent(
    model="openai:gpt-4o",
    tools=[...],
    middleware=[
        TurnWindowMiddleware(keep_last_turns=3),
    ],
)
```

`keep_last_turns` is the number of most-recent `HumanMessage` turns to retain. If the conversation has fewer turns than that, the full history passes through unchanged.

## How it works

`TurnWindowMiddleware` implements `wrap_model_call` / `awrap_model_call`. On every model call it:

1. Finds the indexes of all `HumanMessage`s in `request.messages`.
2. If there are fewer than `keep_last_turns` human turns, passes the messages through unchanged.
3. Otherwise, slices the message list starting at the `keep_last_turns`-th-most-recent `HumanMessage` and forwards only that suffix to the model.

The underlying pure function is also exported directly if you want to use it outside of middleware (e.g. in a custom node):

```python
from langchain_turn_window import window_by_human_turns

windowed = window_by_human_turns(messages, n=3)
```

`window_by_human_turns` never mutates its input and returns the original list object unchanged when no trimming is needed (positive-`n` guard, empty list, or fewer human turns than `n`).

## Configuration

| Constructor arg | Default | Description |
|---|---|---|
| `keep_last_turns` | `3` | Number of most-recent human turns to keep. Must be `>= 1`. |

## Limitations

- Assumes a single-user conversational shape (one `HumanMessage` per turn). For multi-agent handoff patterns where turns aren't anchored on `HumanMessage`, this middleware is not the right fit as-is.
- This is a hard trim, not a summary — content outside the window is dropped, not condensed. Combine with `SummarizationMiddleware` if you need older context preserved in compressed form.

## Development

```bash
pip install -e ".[dev]"
pytest
```

## License

MIT
