Skip to content

Conversation

@shentongmartin
Copy link
Contributor

Summary

This PR introduces a new AgentHandler interface to replace the struct-based AgentMiddleware approach, enabling runtime customization of agent behavior.

Motivation

The existing AgentMiddleware has fundamental limitations:

  1. Static at creation time: Configuration is locked when the agent is created, preventing per-request customization
  2. Append-only: Can only add instructions/tools, cannot remove or conditionally filter them
  3. No context flow: Cannot propagate context values between middleware stages

These limitations block important use cases like:

  • Per-user tool filtering based on permissions
  • Dynamic instruction injection based on request context
  • Conditional tool availability based on conversation state

Solution

New AgentHandler Interface

type AgentHandler interface {
    Name() string
    BeforeAgent(ctx context.Context, config *AgentConfig) (context.Context, error)
    BeforeModelRewriteHistory(ctx context.Context, messages []Message) (context.Context, []Message, error)
    AfterModelRewriteHistory(ctx context.Context, messages []Message) (context.Context, []Message, error)
    WrapInvokableToolCall(ctx, input, next) (*ToolCallResult, error)
    WrapStreamableToolCall(ctx, input, next) (*StreamToolCallResult, error)
}

Key design decisions:

  • BeforeAgent runs at Run time, not creation time, enabling per-request customization
  • AgentConfig is mutable, allowing handlers to modify instruction and tools
  • Context propagation throughout the handler chain
  • BaseAgentHandler provides default no-op implementations for easy composition

Example: Runtime Tool Filtering

agent, _ := NewChatModelAgent(ctx, &ChatModelAgentConfig{
    Handlers: []AgentHandler{
        WithToolsFunc(func(ctx context.Context, tools []ToolMeta) (context.Context, []ToolMeta, error) {
            if GetUserRole(ctx) != "admin" {
                return ctx, filterOutAdminTools(tools), nil
            }
            return ctx, tools, nil
        }),
    },
})

Convenience Helpers

  • WithInstruction(text) / WithInstructionFunc(fn) - Modify instructions
  • WithTools(tools...) / WithToolsFunc(fn) - Add/remove/filter tools
  • WithBeforeModelRewriteHistory(fn) / WithAfterModelRewriteHistory(fn) - Rewrite message history
  • WithInvokableToolCallWrapper(fn) / WithStreamableToolCallWrapper(fn) - Wrap tool calls

Backward Compatibility

AgentMiddleware now implements AgentHandler, so existing code continues to work unchanged.

Change-Id: I9a84f2a3e13721d06a86f5f2739772831067f10b
- Rename BeforeChatModelHandler to MessageStatePreProcessor
- Rename AfterChatModelHandler to MessageStatePostProcessor
- Rename BeforeChatModel/AfterChatModel methods to PreProcessMessageState/PostProcessMessageState
- Remove type aliases InvokableToolCallNext/StreamableToolCallNext (inline function types)
- Update helper functions: WithBeforeChatModel -> WithMessageStatePreProcessor, WithAfterChatModel -> WithMessageStatePostProcessor
- Update all references in chatmodel.go and react.go
- Fix state.Messages assignment in no-tools case to maintain backward compatibility

The new naming better conveys that message changes are persisted to agent state
and visible to subsequent iterations of the agent loop.

Change-Id: I0d7be02aeae82a9412a5615685ce7e6fb88d517c
…modification

Key changes:
- Move BeforeAgent callback from NewChatModelAgent to Run/Resume time
- Add support for runtime tools modification with lazy graph rebuilding
- Simplify AgentHandler interface with unified method signatures
- Add runtime ReturnDirectly configuration via context
- Support graph config compatibility checking to minimize rebuilds

Implementation details:
- Add graphConfig struct to track graph configuration (hasTools, hasReturnDirectly)
- Add preAppliedBeforeAgentData to avoid duplicate BeforeAgent calls
- Implement getRunFunc with lazy rebuild logic for incompatible configs
- Support runtime ReturnDirectly via context in react.go
- Update handler helpers to use new AgentConfig-based API

Breaking changes:
- AgentHandler interface methods now take AgentConfig instead of separate params
- BeforeAgent errors now returned at Run time instead of NewChatModelAgent time

Change-Id: Ic392eacba8f2c5acdc9df32f1d0e64e54711a900
@codecov
Copy link

codecov bot commented Jan 4, 2026

Codecov Report

❌ Patch coverage is 80.86124% with 80 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.28%. Comparing base (d11713a) to head (6b7acb1).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
adk/chatmodel.go 80.00% 46 Missing and 21 partials ⚠️
adk/handler_helpers.go 84.78% 3 Missing and 4 partials ⚠️
adk/handler.go 66.66% 4 Missing ⚠️
adk/react.go 92.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #660      +/-   ##
==========================================
- Coverage   80.31%   80.28%   -0.03%     
==========================================
  Files         124      126       +2     
  Lines       11904    12181     +277     
==========================================
+ Hits         9561     9780     +219     
- Misses       1613     1653      +40     
- Partials      730      748      +18     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…s in HandlerToolsConfig

ToToolMetas and ToolMetasToHandlerToolsConfig now accept context.Context
and return error when tool.Info() fails, instead of silently skipping
the ReturnDirectly configuration.

Change-Id: I850c29a4588c18a11b66b9171403314c420f3dcf
@shentongmartin shentongmartin force-pushed the refactor/agent_middleware branch from 5298ecd to a5a144c Compare January 4, 2026 06:50
…tions

Change-Id: Icc84f849f03ca5bacbcdf7c6841d927c726eccd9
- Remove Name() method from AgentHandler interface (unused)
- Replace WrapInvokableToolCall/WrapStreamableToolCall with GetToolMiddleware()
- Use compose.ToolMiddleware directly for tool wrapping
- Simplify BaseAgentHandler (remove name field)
- Replace WithInvokableToolWrapper/WithStreamableToolWrapper with WithToolMiddleware
- Add documentation for ToolInput field mutability

This change:
1. Reduces interface complexity by using existing compose.ToolMiddleware
2. Makes it easier to wrap both invoke and stream tool calls together
3. Provides forward compatibility for future tool types

Change-Id: I0a5247795abe954326f1eb633e079c54a0126560
- Add type aliases: ToolCall, ToolResult, StreamToolResult (from compose package)
- Add ToolCallHandler interface with HandleInvoke/HandleStream methods
- Add BaseToolCallHandler with pass-through implementations
- Replace GetToolMiddleware() with GetToolCallHandler() in AgentHandler
- Replace WithToolMiddleware with WithToolCallHandler helper
- Add legacyToolMiddlewareAdapter for AgentMiddleware backward compatibility
- Update collectToolMiddlewares to convert ToolCallHandler to compose.ToolMiddleware

This change:
1. Combines invoke/stream handling into one interface (must implement both)
2. Avoids Middleware/Endpoint terminology
3. Uses clearer type names (ToolCall vs ToolInput)
4. Explicit next() signature: next(ctx, call) makes data flow clear

Change-Id: I9b3918d9f491b611e31606f83cef8c48f5233f4a
- Rename AgentConfig to AgentRunContext with only Instruction and Tools fields
- Remove unused AgentRunOptions struct and AgentConfig.Input field
- Remove unused HandlerToolsConfig and related functions
- Move ToolMeta and AgentRunContext to handler.go
- Delete tools_config.go
- Remove AgentRunOptions parameter from applyBeforeAgent

This simplifies the handler interface by focusing only on instruction and tools
configuration, which are the primary customization points for handlers.

Change-Id: I4c4182e4f2708ecd9c2d6cda7ff71a04020f4676
…reAgent signature

- Rename ToolCallHandler to ToolCallWrapper
- Rename HandleInvoke/HandleStream to WrapInvoke/WrapStream
- Rename BaseToolCallHandler to BaseToolCallWrapper
- Rename GetToolCallHandler() to GetToolCallWrapper()
- Rename WithToolCallHandler() to WithToolCallWrapper()
- Change BeforeAgent signature to return *AgentRunContext for functional style

The new signature: BeforeAgent(ctx, runCtx) -> (ctx, runCtx, error)
This is consistent with BeforeModelRewriteHistory and AfterModelRewriteHistory,
making the data flow explicit and avoiding in-place mutation.

Change-Id: Ice503ed130c3bbd07ef93cc7baa92ac6812dc568
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants