Commit Graph

76 Commits

Author SHA1 Message Date
Eugene Yurtsev
c04676f93e x 2025-10-13 15:18:57 -04:00
Eugene Yurtsev
6c2f0eb67a update langchain_v1 2025-10-13 10:02:19 -04:00
Sydney Runkle
760fc3bc12 chore(langchain_v1): use args for HITL (#33442) 2025-10-11 07:12:46 -04:00
Eugene Yurtsev
6dc34beb71 chore(langchain_v1): stricter handling of sync vs. async for wrap_model_call and wrap_tool_call (#33429)
Wrap model call and wrap tool call
2025-10-10 16:54:42 -04:00
Eugene Yurtsev
c2205f88e6 chore(langchain_v1): further namespace clean up (#33428)
Reduce exposed namespace for now
2025-10-10 20:48:24 +00:00
Eugene Yurtsev
0559558715 feat(langchain_v1): add async implementation for wrap_tool_call (#33420)
Add async implementation. No automatic delegation to sync at the moment.
2025-10-10 15:07:19 -04:00
Eugene Yurtsev
75965474fc chore(langchain_v1): tool error exceptions (#33424)
Tool error exceptions
2025-10-10 15:06:40 -04:00
Eugene Yurtsev
74983f8a96 chore(langchain_v1): update on_tool_call to wrap_tool (#33410)
Improve naming on ToolNode for on_tool_call interceptor
2025-10-10 03:19:45 +00:00
Eugene Yurtsev
11c5b86981 chore(langchain_v1): update wrap_on_model return (#33408)
Update wrap on model return to capture the full return type of the model
so we can accommodate dynamic structured outputs.
2025-10-09 23:01:21 -04:00
Eugene Yurtsev
045e7ad4a1 feat(langchain_v1): tool emulator (#33357)
This is tool emulation middleware. The idea is to help test out an agent
that may have some tools that either take a long time to run or are
expensive to set up. This could allow simulating the behavior a bit.
2025-10-10 01:39:40 +00:00
Sydney Runkle
c99773b652 feat(langchain_v1): refactoring HITL API (#33397)
Easiest to review side by side (not inline)

* Adding `dict` type requests + responses so that we can ship config w/
interrupts. Also more extensible.
* Keeping things generic in terms of `interrupt_on` rather than
`tool_config`
* Renaming allowed decisions -- approve, edit, reject
* Draws differentiation between actions (requested + performed by the
agent), in this case tool calls, though we generalize beyond that and
decisions - human feedback for said actions

New request structure

```py
class Action(TypedDict):
    """Represents an action with a name and arguments."""

    name: str
    """The type or name of action being requested (e.g., "add_numbers")."""

    arguments: dict[str, Any]
    """Key-value pairs of arguments needed for the action (e.g., {"a": 1, "b": 2})."""


DecisionType = Literal["approve", "edit", "reject"]


class ReviewConfig(TypedDict):
    """Policy for reviewing a HITL request."""

    action_name: str
    """Name of the action associated with this review configuration."""

    allowed_decisions: list[DecisionType]
    """The decisions that are allowed for this request."""

    description: NotRequired[str]
    """The description of the action to be reviewed."""

    arguments_schema: NotRequired[dict[str, Any]]
    """JSON schema for the arguments associated with the action, if edits are allowed."""

class HITLRequest(TypedDict):
    """Request for human feedback on a sequence of actions requested by a model."""

    action_requests: list[Action]
    """A list of agent actions for human review."""

    review_configs: list[ReviewConfig]
    """Review configuration for all possible actions."""
```

New response structure

```py
class ApproveDecision(TypedDict):
    """Response when a human approves the action."""

    type: Literal["approve"]
    """The type of response when a human approves the action."""


class EditDecision(TypedDict):
    """Response when a human edits the action."""

    type: Literal["edit"]
    """The type of response when a human edits the action."""

    edited_action: Action
    """Edited action for the agent to perform.

    Ex: for a tool call, a human reviewer can edit the tool name and args.
    """


class RejectDecision(TypedDict):
    """Response when a human rejects the action."""

    type: Literal["reject"]
    """The type of response when a human rejects the action."""

    message: NotRequired[str]
    """The message sent to the model explaining why the action was rejected."""


Decision = ApproveDecision | EditDecision | RejectDecision


class HITLResponse(TypedDict):
    """Response payload for a HITLRequest."""

    decisions: list[Decision]
    """The decisions made by the human."""
```

User facing API:

NEW

```py
HumanInTheLoopMiddleware(interrupt_on={
    'send_email': True,
    # can also use a callable for description that takes tool call, state, and runtime
    'execute_sql': {
        'allowed_decisions': ['approve', 'edit', 'reject'], 
        'description': 'please review sensitive tool execution'},
    }
})

Command(resume={"decisions": [{"type": "approve"}, {"type": "reject": "message": "db down"}]})
```

OLD

```py
HumanInTheLoopMiddleware(interrupt_on={
    'send_email': True,
    'execute_sql': {
        'allow_accept': True, 
        'allow_edit': True, 
        'allow_respond': True, 
        description='please review sensitive tool execution'
    },
})

Command(resume=[{"type": "approve"}, {"type": "reject": "message": "db down"}])
```
2025-10-09 17:51:28 -04:00
Mason Daugherty
6fc21afbc9 style: .. code-block:: admonition translations (#33400)
biiiiiiiiiiiiiiiigggggggg pass
2025-10-09 16:52:58 -04:00
Mason Daugherty
d8a680ee57 style: address Sphinx double-backtick snippet syntax (#33389) 2025-10-09 13:35:51 -04:00
Mason Daugherty
b6132fc23e style: remove more Optional syntax (#33371) 2025-10-08 23:28:43 -04:00
Eugene Yurtsev
f33b1b3d77 chore(langchain_v1): rename on_model_call to wrap_model_call (#33370)
rename on_model_call to wrap_model_call
2025-10-08 23:28:14 -04:00
Eugene Yurtsev
e193a1f273 chore(langchain_v1): replace modify model request with on model call (#33368)
* Replace modify model request with on model call
* Remove modify model request
2025-10-09 02:46:48 +00:00
Eugene Yurtsev
eb70672f4a chore(langchain): add unit tests for wrap_tool_call decorator (#33367)
Add unit tests for wrap_tool_call decorator
2025-10-09 02:30:07 +00:00
Eugene Yurtsev
87df179ca9 chore(langchain_v1): rename on_tool_call to wrap_tool_call (#33366)
Replace on tool call with wrap tool call
2025-10-08 22:10:36 -04:00
Eugene Yurtsev
982a950ccf chore(langchain_v1): add runtime and context to model request (#33365)
Add runtime and context to ModelRequest to make the API more convenient
2025-10-08 21:59:56 -04:00
Eugene Yurtsev
c2435eeca5 chore(langchain_v1): update on_tool_call to regular callbacks (#33364)
Refactor tool call middleware from generator-based to handler-based
pattern

Simplifies on_tool_call middleware by replacing the complex generator
protocol with a straightforward handler pattern. Instead of yielding
requests and receiving results via .send(),
handlers now receive an execute callable that can be invoked multiple
times for retry logic.


Before vs. After

Before (Generator):
```python
class RetryMiddleware(AgentMiddleware):
    def on_tool_call(self, request, state, runtime):
        for attempt in range(3):
            response = yield request  # Yield request, receive result via .send()
            if is_valid(response) or attempt == 2:
                return  # Final result is last value sent to generator
```

After (Handler):

```python
class RetryMiddleware(AgentMiddleware):
    def on_tool_call(self, request, handler):
        for attempt in range(3):
            result = handler(request)  # Direct function call
            if is_valid(result):
                return result
        return result
```


Follow up after this PR:

* Rename the interceptor to wrap_tool_call
* Fix the async path for the ToolNode
2025-10-08 21:46:03 -04:00
Mason Daugherty
d13823043d style: monorepo pass for refs (#33359)
* Delete some double backticks previously used by Sphinx (not done
everywhere yet)
* Fix some code blocks / dropdowns

Ignoring CLI CI for now
2025-10-08 18:41:39 -04:00
Eugene Yurtsev
b665b81a0e chore(langchain_v1): simplify on model call logic (#33358)
Moving from the generator pattern to the slightly less verbose (but explicit) handler pattern.

This will be more familiar to users

**Before (Generator Pattern):**
```python
def on_model_call(self, request, state, runtime):
    try:
        result = yield request
    except Exception:
        result = yield request  # Retry
```

**After (Handler Pattern):**
```python
def on_model_call(self, request, state, runtime, handler):
    try:
        return handler(request)
    except Exception:
        return handler(request)  # Retry
```
2025-10-08 17:23:11 -04:00
Eugene Yurtsev
1bf29da0d6 feat(langchain_v1): add on_tool_call middleware hook (#33329)
Adds generator-based middleware for intercepting tool execution in
agents. Middleware can retry on errors, cache results, modify requests,
or short-circuit execution.

### Implementation

**Middleware Protocol**
```python
class AgentMiddleware:
    def on_tool_call(
        self,
        request: ToolCallRequest,
        state: StateT,
        runtime: Runtime[ContextT],
    ) -> Generator[ToolCallRequest | ToolMessage | Command, ToolMessage | Command, None]:
        """
        Yields: ToolCallRequest (execute), ToolMessage (cached result), or Command (control flow)
        Receives: ToolMessage or Command via .send()
        Returns: None (final result is last value sent to handler)
        """
        yield request  # passthrough
```

**Composition**
Multiple middleware compose automatically (first = outermost), with
`_chain_tool_call_handlers()` stacking them like nested function calls.

### Examples

**Retry on error:**
```python
class RetryMiddleware(AgentMiddleware):
    def on_tool_call(self, request, state, runtime):
        for attempt in range(3):
            response = yield request
            if not isinstance(response, ToolMessage) or response.status != "error":
                return
            if attempt == 2:
                return  # Give up
```

**Cache results:**
```python
class CacheMiddleware(AgentMiddleware):
    def on_tool_call(self, request, state, runtime):
        cache_key = (request.tool_call["name"], tuple(request.tool_call["args"].items()))
        if cached := self.cache.get(cache_key):
            yield ToolMessage(content=cached, tool_call_id=request.tool_call["id"])
        else:
            response = yield request
            self.cache[cache_key] = response.content
```

**Emulate tools with LLM**
```python
class ToolEmulator(AgentMiddleware):
    def on_tool_call(self, request, state, runtime):
        prompt = f"""Emulate: {request.tool_call["name"]}
Description: {request.tool.description}
Args: {request.tool_call["args"]}
Return ONLY the tool's output."""

        response = emulator_model.invoke([HumanMessage(prompt)])
        yield ToolMessage(
            content=response.content,
            tool_call_id=request.tool_call["id"],
            name=request.tool_call["name"],
        )
```

**Modify requests:**
```python
class ScalingMiddleware(AgentMiddleware):
    def on_tool_call(self, request, state, runtime):
        if "value" in request.tool_call["args"]:
            request.tool_call["args"]["value"] *= 2
        yield request
```
2025-10-08 16:43:32 +00:00
Eugene Yurtsev
2c3fec014f feat(langchain_v1): on_model_call middleware (#33328)
Introduces a generator-based `on_model_call` hook that allows middleware
to intercept model calls with support for retry logic, error handling,
response transformation, and request modification.

## Overview

Middleware can now implement `on_model_call()` using a generator
protocol that:
- **Yields** `ModelRequest` to execute the model
- **Receives** `AIMessage` via `.send()` on success, or exception via
`.throw()` on error
- **Yields again** to retry or transform responses
- Uses **implicit last-yield semantics** (no return values from
generators)

## Usage Examples

### Basic Retry on Error

```python
from langchain.agents.middleware.types import AgentMiddleware

class RetryMiddleware(AgentMiddleware):
    def on_model_call(self, request, state, runtime):
        for attempt in range(3):
            try:
                yield request  # Execute model
                break  # Success
            except Exception:
                if attempt == 2:
                    raise  # Max retries exceeded
```

### Response Transformation

```python
class UppercaseMiddleware(AgentMiddleware):
    def on_model_call(self, request, state, runtime):
        result = yield request
        modified = AIMessage(content=result.content.upper())
        yield modified  # Return transformed response
```

### Error Recovery

```python
class FallbackMiddleware(AgentMiddleware):
    def on_model_call(self, request, state, runtime):
        try:
            yield request
        except Exception:
            fallback = AIMessage(content="Service unavailable")
            yield fallback  # Convert error to fallback response
```

### Caching / Short-Circuit

```python
class CacheMiddleware(AgentMiddleware):
    def on_model_call(self, request, state, runtime):
        if cached := get_cache(request):
            yield cached  # Skip model execution
        else:
            result = yield request
            save_cache(request, result)
```

### Request Modification

```python
class SystemPromptMiddleware(AgentMiddleware):
    def on_model_call(self, request, state, runtime):
        modified_request = ModelRequest(
            model=request.model,
            system_prompt="You are a helpful assistant.",
            messages=request.messages,
            tools=request.tools,
        )
        yield modified_request
```

### Function Decorator

```python
from langchain.agents.middleware.types import on_model_call

@on_model_call
def retry_three_times(request, state, runtime):
    for attempt in range(3):
        try:
            yield request
            break
        except Exception:
            if attempt == 2:
                raise

agent = create_agent(model="openai:gpt-4o", middleware=[retry_three_times])
```

## Middleware Composition

Middleware compose with first in list as outermost layer:

```python
agent = create_agent(
    model="openai:gpt-4o",
    middleware=[
        RetryMiddleware(),      # Outer - wraps others
        LoggingMiddleware(),    # Middle
        UppercaseMiddleware(),  # Inner - closest to model
    ]
)
```
2025-10-08 12:34:04 -04:00
Sydney Runkle
c8205ff511 fix(langchain_v1): fix edges when there's no middleware (#33321)
1. Main fix: when we don't have a response format or middleware, don't
draw a conditional edge back to the loop entrypoint (self loop on model)
2. Supplementary fix: when we jump to `end` and there is an
`after_agent` hook, jump there instead of `__end__`

Other improvements -- I can remove these if they're more harmful than
helpful
1. Use keyword only arguments for edge generator functions for clarity
2. Rename args to `model_destination` and `end_destination` for clarity
2025-10-06 18:08:08 -04:00
Sydney Runkle
2fa9741f99 chore(langchain_v1): rename model_request node -> model (#33310) 2025-10-06 16:18:18 -04:00
Sydney Runkle
00f4db54c4 chore(langchain_v1): remove support for ToolNode in create_agent (#33306)
Let's add a note to help w/ migration once we add the tool call retry
middleware.
2025-10-06 15:06:20 -04:00
Sydney Runkle
62ccf7e8a4 feat(langchain_v1): simplify to use ONE agent (#33302)
This reduces confusion w/ types like `AgentState`, different arg names,
etc.

Second attempt, following
https://github.com/langchain-ai/langchain/pull/33249

* Ability to pass through `cache` and name in `create_agent` as
compilation args for the agent
* Right now, removing `test_react_agent.py` but we should add these
tests back as implemented w/ the new agent
* Add conditional edge when structured output tools are present to allow
for retries
* Rename `tracking` to `model_call_limit` to be consistent w/ tool call
limits

We need in the future (I'm happy to own):
* Significant test refactor
* Significant test overhaul where we emphasize and enforce coverage
2025-10-06 14:46:29 -04:00
Eugene Yurtsev
0ff2bc890b chore(langchain_v1): remove text splitters from langchain v1 namespace (#33297)
Removing text splitters for now for a lighter dependency. We may re-introduce
2025-10-06 14:42:23 -04:00
Sydney Runkle
0ccc0cbdae feat(langchain_v1): before_agent and after_agent hooks (#33279)
We're adding enough new nodes that I think a refactor in terms of graph
building is warranted here, but not necessarily required for merging.
2025-10-06 11:31:52 -04:00
Nuno Campos
f308139283 feat(langchain_v1): Implement Context Editing Middleware (#33267)
Brings functionality similar to Anthropic's context editing to all chat
models
https://docs.claude.com/en/docs/build-with-claude/context-editing

---------

Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
2025-10-06 10:34:04 -04:00
ccurme
4e50ec4b98 feat(openai): enable stream_usage when using default base URL and client (#33205) 2025-10-06 08:56:38 -04:00
Nuno Campos
a9aa3f232d feat(langchain_v1): Add retry_model_request middleware hook, add ModelFallbackMiddleware (#33275)
- retry_model_request hook lets a middleware decide to retry a failed
model request, with full ability to modify as much or as little of the
request before doing so
- ModelFallbackMiddleware tries each fallback model in order, until one
is successful, or fallback list is exhausted

Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
2025-10-05 20:32:45 +00:00
Eugene Yurtsev
df2ecd9448 feat(langchain_v1): add llm selection middleware (#33272)
* Add llm based tool selection middleware.
* Note that we might want some form of caching for when the agent is
inside an active tool calling loop as the tool selection isn't expected
to change during that time.

API:

```python
class LLMToolSelectorMiddleware(AgentMiddleware):
    """Uses an LLM to select relevant tools before calling the main model.

    When an agent has many tools available, this middleware filters them down
    to only the most relevant ones for the user's query. This reduces token usage
    and helps the main model focus on the right tools.

    Examples:
        Limit to 3 tools:
        ```python
        from langchain.agents.middleware import LLMToolSelectorMiddleware

        middleware = LLMToolSelectorMiddleware(max_tools=3)

        agent = create_agent(
            model="openai:gpt-4o",
            tools=[tool1, tool2, tool3, tool4, tool5],
            middleware=[middleware],
        )
        ```

        Use a smaller model for selection:
        ```python
        middleware = LLMToolSelectorMiddleware(model="openai:gpt-4o-mini", max_tools=2)
        ```
    """

    def __init__(
        self,
        *,
        model: str | BaseChatModel | None = None,
        system_prompt: str = DEFAULT_SYSTEM_PROMPT,
        max_tools: int | None = None,
        always_include: list[str] | None = None,
    ) -> None:
        """Initialize the tool selector.

        Args:
            model: Model to use for selection. If not provided, uses the agent's main model.
                Can be a model identifier string or BaseChatModel instance.
            system_prompt: Instructions for the selection model.
            max_tools: Maximum number of tools to select. If the model selects more,
                only the first max_tools will be used. No limit if not specified.
            always_include: Tool names to always include regardless of selection.
                These do not count against the max_tools limit.
        """
```



```python
"""Test script for LLM tool selection middleware."""

from langchain.agents import create_agent
from langchain.agents.middleware import LLMToolSelectorMiddleware
from langchain_core.tools import tool


@tool
def get_weather(location: str) -> str:
    """Get current weather for a location."""
    return f"Weather in {location}: 72°F, sunny"


@tool
def search_web(query: str) -> str:
    """Search the web for information."""
    return f"Search results for: {query}"


@tool
def calculate(expression: str) -> str:
    """Perform mathematical calculations."""
    return f"Result of {expression}: 42"


@tool
def send_email(to: str, subject: str) -> str:
    """Send an email to someone."""
    return f"Email sent to {to} with subject: {subject}"


@tool
def get_stock_price(symbol: str) -> str:
    """Get current stock price for a symbol."""
    return f"Stock price for {symbol}: $150.25"


@tool
def translate_text(text: str, target_language: str) -> str:
    """Translate text to another language."""
    return f"Translated '{text}' to {target_language}"


@tool
def set_reminder(task: str, time: str) -> str:
    """Set a reminder for a task."""
    return f"Reminder set: {task} at {time}"


@tool
def get_news(topic: str) -> str:
    """Get latest news about a topic."""
    return f"Latest news about {topic}"


@tool
def book_flight(destination: str, date: str) -> str:
    """Book a flight to a destination."""
    return f"Flight booked to {destination} on {date}"


@tool
def get_restaurant_recommendations(city: str, cuisine: str) -> str:
    """Get restaurant recommendations."""
    return f"Top {cuisine} restaurants in {city}"


# Create agent with tool selection middleware
middleware = LLMToolSelectorMiddleware(
    model="openai:gpt-4o-mini",
    max_tools=3,
)

agent = create_agent(
    model="openai:gpt-4o",
    tools=[
        get_weather,
        search_web,
        calculate,
        send_email,
        get_stock_price,
        translate_text,
        set_reminder,
        get_news,
        book_flight,
        get_restaurant_recommendations,
    ],
    middleware=[middleware],
)

# Test with a query that should select specific tools
response = agent.invoke(
    {"messages": [{"role": "user", "content": "I need to find restaurants"}]}
)

print(response)
```
2025-10-05 15:55:55 -04:00
Nuno Campos
30f7c87b6f feat(langchain_v1): Implement PIIMiddleware (#33271)
- supports 6 well-known PII types (email, credit_card, ip, mac_address,
url)
- 4 handling strategies (block, redact, mask, hash)
- supports custom PII types with detector functions or regex
- the built-in types were chosen because they are common, and detection
can be reliably implemented with stdlib
2025-10-04 22:27:51 -04:00
Nuno Campos
2286d0d993 feat(langchain_v1): Add ToolCallLimitMiddleware (#33269)
which implements a tool call budget for either all tools, or a specific tool

---------

Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
2025-10-04 15:03:45 -04:00
Eugene Yurtsev
46b87e435c chore(langchain_v1): change modifyModelRequest back to tools (#33270)
Seems like a much better devx with fairly little downside (we'll
document that you can't register new tools)
2025-10-04 12:33:54 -04:00
Eugene Yurtsev
905c6d7bad fix(langchain_v1): handle switching resposne format strategy based on model identity (#33259)
Change response format strategy dynamically based on model.

After this PR there are two remaining issues:

- [ ] Review binding of tools used for output to ToolNode (shouldn't be
required)
- [ ] Update ModelRequest to also support the original schema provided
by the user (to correctly support auto mode)
2025-10-04 11:56:56 -04:00
Sydney Runkle
acd1aa813c feat(langchain_v1): implement nicer devx for dynamic prompt (#33264)
Adding a `dynamic_prompt` decorator to support smoother devx for dynamic
system prompts

```py
from langchain.agents.middleware.types import dynamic_prompt, ModelRequest, AgentState
from langchain.agents.middleware_agent import create_agent
from langgraph.runtime import Runtime
from dataclasses import dataclass
from langchain_core.messages import HumanMessage


@dataclass
class Context:
    user_name: str


@dynamic_prompt
def my_prompt(request: ModelRequest, state: AgentState, runtime: Runtime[Context]) -> str:
    user_name = runtime.context.user_name
    return (
        f"You are a helpful assistant helping {user_name}. Please refer to the user as {user_name}."
    )


agent = create_agent(model="openai:gpt-4o", middleware=[my_prompt]).compile()

result = agent.invoke({"messages": [HumanMessage("Hello")]}, context=Context(user_name="Sydney"))
for msg in result["messages"]:
    msg.pretty_print()

"""
================================ Human Message =================================

Hello
================================== Ai Message ==================================

Hello Sydney! How can I assist you today?
"""

```
2025-10-03 21:06:23 -04:00
Sydney Runkle
2671fee2c6 feat(langchain_v1): description generator for HITL middleware (#33195)
Need to decide - what information should we feed to this description
factory? Right now, feeding:
* state
* runtime
* tool call (so the developer doesn't have to search through the state's
messages for the corresponding tool call)

I can see a case for just passing tool call. But again, this abstraction
is semi-bound to interrupts for tools... though we pretend it's more
abstract than that.

Right now:

```py
def custom_description(state: AgentState, runtime: Runtime, tool_call: ToolCall) -> str:
        """Generate a custom description."""
        return f"Custom: {tool_call['name']} with args {tool_call['args']}"

middleware = HumanInTheLoopMiddleware(
    interrupt_on={
        "tool_with_callable": {"allow_accept": True, "description": custom_description},
        "tool_with_string": {"allow_accept": True, "description": "Static description"},
    }
)
```
2025-10-04 01:01:44 +00:00
Eugene Yurtsev
1074ce5fe5 chore(langchain_v1)!: Remove ToolNode from agents (#33250)
Remove ToolNode from agents namespace. It should only be present in tools
2025-10-03 10:57:54 -04:00
Sydney Runkle
3d2f13a2f1 feat(langchain): model call limits (#33178)
This PR adds a model call limit middleware that helps to manage:

* number of model calls during a run (helps w/ avoiding tool calling
loops) - implemented w/ `UntrackedValue`
* number of model calls on a thread (helps w/ avoiding lengthy convos) -
standard state

Concern here is w/ other middlewares overwriting the model call count...
we could use a `_` prefixed field?
2025-10-03 08:28:56 -04:00
Mason Daugherty
eaa6dcce9e release: v1.0.0 (#32567)
Co-authored-by: Mohammad Mohtashim <45242107+keenborder786@users.noreply.github.com>
Co-authored-by: Caspar Broekhuizen <caspar@langchain.dev>
Co-authored-by: ccurme <chester.curme@gmail.com>
Co-authored-by: Christophe Bornet <cbornet@hotmail.com>
Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
Co-authored-by: Sadra Barikbin <sadraqazvin1@yahoo.com>
Co-authored-by: Vadym Barda <vadim.barda@gmail.com>
2025-10-02 10:49:42 -04:00
Sydney Runkle
a336afaecd feat(langchain): use decorators for jumps instead (#33179)
The old `before_model_jump_to` classvar approach was quite clunky, this
is nicer imo and easier to document. Also moving from `jump_to` to
`can_jump_to` which is more idiomatic.

Before:

```py
class MyMiddleware(AgentMiddleware):
    before_model_jump_to: ClassVar[list[JumpTo]] = ["end"]

    def before_model(state, runtime) -> dict[str, Any]:
        return {"jump_to": "end"}
```

After

```py
class MyMiddleware(AgentMiddleware):

    @hook_config(can_jump_to=["end"])
    def before_model(state, runtime) -> dict[str, Any]:
        return {"jump_to": "end"}
```
2025-10-01 16:49:27 -07:00
Sydney Runkle
a10e880c00 feat(langchain_v1): add async support for create_agent (#33175)
This makes branching **much** more simple internally and helps greatly
w/ type safety for users. It just allows for one signature on hooks
instead of multiple.

Opened after https://github.com/langchain-ai/langchain/pull/33164
ballooned more than expected, w/ branching for:
* sync vs async
* runtime vs no runtime (this is self imposed)

**This also removes support for nodes w/o `runtime` in the signature.**
We can always go back and add support for nodes w/o `runtime`.

I think @christian-bromann's idea to re-export `runtime` from
langchain's agents might make sense due to the abundance of imports
here.

Check out the value of the change based on this diff:
https://github.com/langchain-ai/langchain/pull/33176
2025-10-01 19:15:39 +00:00
Eugene Yurtsev
7b5e839be3 chore(langchain_v1): use list[str] for modifyModelRequest (#33166)
Update model request to return tools by name. This will decrease the
odds of misusing the API.

We'll need to extend the type for built-in tools later.
2025-10-01 14:46:19 -04:00
Mohammad Mohtashim
34f8031bd9 feat(langchain): Using Structured Response as Key in Output Schema for Middleware Agent (#33159)
- **Description:** Changing the key from `response` to
`structured_response` for middleware agent to keep it sync with agent
without middleware. This a breaking change.
 - **Issue:** #33154
2025-10-01 03:24:59 +00:00
Sydney Runkle
eed0f6c289 feat(langchain): todo middleware (#33152)
Porting the [planning
middleware](39c0138d0f/src/deepagents/middleware.py (L21))
over from deepagents.

Also adding the ability to configure:
* System prompt
* Tool description

```py
from langchain.agents.middleware.planning import PlanningMiddleware
from langchain.agents import create_agent

agent = create_agent("openai:gpt-4o", middleware=[PlanningMiddleware()])

result = await agent.invoke({"messages": [HumanMessage("Help me refactor my codebase")]})

print(result["todos"])  # Array of todo items with status tracking
```
2025-09-30 02:23:26 +00:00
Mason Daugherty
f402fdcea3 fix(langchain): add context_management to Anthropic chat model init (#33150) 2025-09-29 16:13:47 -04:00
nhuang-lc
c456c8ae51 fix(langchain): fix response action for HITL (#33131)
Multiple improvements to HITL flow:

* On a `response` type resume, we should still append the tool call to
the last AIMessage (otherwise we have a ToolResult without a
corresponding ToolCall)
* When all interrupts have `response` types (so there's no pending tool
calls), we should jump back to the first node (instead of end) as we
enforced in the previous `post_model_hook_router`
* Added comments to `model_to_tools` router so clarify all of the
potential exit conditions

Additionally:
* Lockfile update to use latest LG alpha release
* Added test for `jump_to` behaving ephemerally, this was fixed in LG
but surfaced as a bug w/ `jump_to`.
* Bump version to v1.0.0a10 to prep for alpha release

---------

Co-authored-by: Sydney Runkle <sydneymarierunkle@gmail.com>
Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
2025-09-29 13:08:18 +00:00