-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Expand file tree
/
Copy pathhandler.go
More file actions
477 lines (426 loc) · 23.8 KB
/
Copy pathhandler.go
File metadata and controls
477 lines (426 loc) · 23.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
/*
* Copyright 2025 CloudWeGo Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package adk
import (
"context"
"encoding/gob"
"fmt"
"io"
"reflect"
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/schema"
)
// InvokableToolCallEndpoint is the function signature for invoking a tool synchronously.
// Middleware authors implement wrappers around this endpoint to add custom behavior.
type InvokableToolCallEndpoint func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error)
// StreamableToolCallEndpoint is the function signature for invoking a tool with streaming output.
// Middleware authors implement wrappers around this endpoint to add custom behavior.
type StreamableToolCallEndpoint func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (*schema.StreamReader[string], error)
type EnhancedInvokableToolCallEndpoint func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error)
type EnhancedStreamableToolCallEndpoint func(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.StreamReader[*schema.ToolResult], error)
// ToolContext provides metadata about the tool being wrapped.
type ToolContext struct {
Name string
CallID string
}
// ToolCallsContext contains metadata about the tool calls that just completed.
type ToolCallsContext struct {
// ToolCalls contains the tool call metadata from the model's response.
ToolCalls []ToolContext
}
// TypedModelContext contains context information passed to WrapModel.
type TypedModelContext[M MessageType] struct {
// Tools contains the current tool list configured for the agent.
// This is populated at request time with the tools that will be sent to the model.
//
// Deprecated: Use TypedChatModelAgentState.ToolInfos in BeforeModelRewriteState instead.
// ModelContext.Tools remains populated for backward compatibility with existing WrapModel handlers,
// but new code should read and modify state.ToolInfos which is the source of truth for the model call.
Tools []*schema.ToolInfo
// ModelRetryConfig contains the retry configuration for the model.
// This is populated at request time from the agent's ModelRetryConfig.
// Used by EventSenderModelWrapper to wrap stream errors appropriately.
ModelRetryConfig *TypedModelRetryConfig[M]
// ModelFailoverConfig contains the failover configuration for the model.
// This is populated at request time from the agent's ModelFailoverConfig.
// Used by EventSenderModelWrapper to wrap stream errors so that failed failover
// attempts are skipped (not treated as fatal) by the flow event processor.
ModelFailoverConfig *ModelFailoverConfig[M]
cancelContext *cancelContext
}
// ModelContext is the default model context type using *schema.Message.
type ModelContext = TypedModelContext[*schema.Message]
// ChatModelAgentContext contains runtime information passed to handlers before each ChatModelAgent run.
// Handlers can modify Instruction, Tools, and ReturnDirectly to customize agent behavior.
//
// This type is specific to ChatModelAgent. Other agent types may define their own context types.
type ChatModelAgentContext struct {
// Instruction is the current instruction for the Agent execution.
// It includes the instruction configured for the agent, additional instructions appended by framework
// and AgentMiddleware, and modifications applied by previous BeforeAgent handlers.
// The finalized instruction after all BeforeAgent handlers are then passed to GenModelInput,
// to be (optionally) formatted with SessionValues and converted to system message.
Instruction string
// Tools are the raw tools (without any wrapper or tool middleware) currently configured for the Agent execution.
// They includes tools passed in AgentConfig, implicit tools added by framework such as transfer / exit tools,
// and other tools already added by middlewares.
Tools []tool.BaseTool
// ReturnDirectly is the set of tool names currently configured to cause the Agent to return directly.
// This is based on the return directly map configured for the agent, plus any modifications
// by previous BeforeAgent handlers.
ReturnDirectly map[string]bool
// ToolSearchTool is the tool info for the model's native tool search capability.
// When set by a BeforeAgent handler, the framework passes it to the model via model.WithToolSearchTool.
ToolSearchTool *schema.ToolInfo
}
// TypedChatModelAgentMiddleware defines the interface for customizing TypedChatModelAgent behavior.
//
// IMPORTANT: This interface is specifically designed for TypedChatModelAgent and agents built
// on top of it (e.g., DeepAgent).
//
// Why TypedChatModelAgentMiddleware instead of AgentMiddleware?
//
// AgentMiddleware is a struct type, which has inherent limitations:
// - Struct types are closed: users cannot add new methods to extend functionality
// - The framework only recognizes AgentMiddleware's fixed fields, so even if users
// embed AgentMiddleware in a custom struct and add methods, the framework cannot
// call those methods (config.Middlewares is []AgentMiddleware, not a user type)
// - Callbacks in AgentMiddleware only return error, cannot return modified context
//
// TypedChatModelAgentMiddleware is an interface type, which is open for extension:
// - Users can implement custom handlers with arbitrary internal state and methods
// - Hook methods return (context.Context, ..., error) for direct context propagation
// - Wrapper methods (WrapToolCall, WrapModel) enable context propagation through the
// wrapped endpoint chain: wrappers can pass modified context to the next wrapper
// - Configuration is centralized in struct fields rather than scattered in closures
//
// TypedChatModelAgentMiddleware vs AgentMiddleware:
// - Use AgentMiddleware for simple, static additions (extra instruction/tools)
// - Use TypedChatModelAgentMiddleware for dynamic behavior, context modification, or call wrapping
// - AgentMiddleware is kept for backward compatibility with existing users
// - Both can be used together; see AgentMiddleware documentation for execution order
//
// Use *TypedBaseChatModelAgentMiddleware as an embedded struct to provide default no-op
// implementations for all methods.
type TypedChatModelAgentMiddleware[M MessageType] interface {
// BeforeAgent is called before each agent run, allowing modification of
// the agent's instruction and tools configuration.
BeforeAgent(ctx context.Context, runCtx *ChatModelAgentContext) (context.Context, *ChatModelAgentContext, error)
// AfterAgent is called after the agent run reaches a successful terminal state.
// Successful terminal states are: final answer (model response with no tool calls),
// and return-directly tool result.
//
// AfterAgent is NOT called when the agent terminates with an error (e.g.,
// ErrExceedMaxIterations, context cancellation, model errors).
//
// The state parameter contains the final conversation state, including all messages
// from the completed run.
//
// AfterAgent handlers are called in the same order as BeforeAgent handlers
// (first registered = first called). Consistent with all other middleware hooks,
// if any handler returns an error, subsequent handlers are NOT called (fail-fast)
// and the error is sent to the event stream.
AfterAgent(ctx context.Context, state *TypedChatModelAgentState[M]) (context.Context, error)
// BeforeModelRewriteState is called before each model invocation.
// The returned state is persisted to the agent's internal state and passed to the model.
// The returned context is propagated to the model call and subsequent handlers.
//
// The ChatModelAgentState struct provides access to:
// - Messages: the conversation history
// - ToolInfos: the tool list that will be sent to the model (modifiable)
// - DeferredToolInfos: tools for server-side search (modifiable, nil if unused)
//
// This is the recommended place to modify messages and tools before a model call.
// Changes here are persisted in state and reflected in subsequent iterations.
BeforeModelRewriteState(ctx context.Context, state *TypedChatModelAgentState[M], mc *TypedModelContext[M]) (context.Context, *TypedChatModelAgentState[M], error)
// AfterModelRewriteState is called after each model invocation.
// The input state includes the model's response as the last message.
// The returned state is persisted to the agent's internal state.
//
// The ChatModelAgentState struct provides access to:
// - Messages: the conversation history including the model's response
// - ToolInfos: the tool list that was sent to the model
// - DeferredToolInfos: tools for server-side search (nil if unused)
AfterModelRewriteState(ctx context.Context, state *TypedChatModelAgentState[M], mc *TypedModelContext[M]) (context.Context, *TypedChatModelAgentState[M], error)
// WrapInvokableToolCall wraps a tool's synchronous execution with custom behavior.
// Return the input endpoint unchanged and nil error if no wrapping is needed.
//
// This method is only called for tools that implement InvokableTool.
// If a tool only implements StreamableTool, this method will not be called for that tool.
//
// This method is called at request time when the tool is about to be executed.
// The tCtx parameter provides metadata about the tool:
// - Name: The name of the tool being wrapped
// - CallID: The unique identifier for this specific tool call
WrapInvokableToolCall(ctx context.Context, endpoint InvokableToolCallEndpoint, tCtx *ToolContext) (InvokableToolCallEndpoint, error)
// WrapStreamableToolCall wraps a tool's streaming execution with custom behavior.
// Return the input endpoint unchanged and nil error if no wrapping is needed.
//
// This method is only called for tools that implement StreamableTool.
// If a tool only implements InvokableTool, this method will not be called for that tool.
//
// This method is called at request time when the tool is about to be executed.
// The tCtx parameter provides metadata about the tool:
// - Name: The name of the tool being wrapped
// - CallID: The unique identifier for this specific tool call
WrapStreamableToolCall(ctx context.Context, endpoint StreamableToolCallEndpoint, tCtx *ToolContext) (StreamableToolCallEndpoint, error)
// WrapEnhancedInvokableToolCall wraps an enhanced tool's synchronous execution with custom behavior.
// Return the input endpoint unchanged and nil error if no wrapping is needed.
//
// This method is only called for tools that implement EnhancedInvokableTool.
// If a tool only implements EnhancedStreamableTool, this method will not be called for that tool.
//
// This method is called at request time when the tool is about to be executed.
// The tCtx parameter provides metadata about the tool:
// - Name: The name of the tool being wrapped
// - CallID: The unique identifier for this specific tool call
WrapEnhancedInvokableToolCall(ctx context.Context, endpoint EnhancedInvokableToolCallEndpoint, tCtx *ToolContext) (EnhancedInvokableToolCallEndpoint, error)
// WrapEnhancedStreamableToolCall wraps an enhanced tool's streaming execution with custom behavior.
// Return the input endpoint unchanged and nil error if no wrapping is needed.
//
// This method is only called for tools that implement EnhancedStreamableTool.
// If a tool only implements EnhancedInvokableTool, this method will not be called for that tool.
//
// This method is called at request time when the tool is about to be executed.
// The tCtx parameter provides metadata about the tool:
// - Name: The name of the tool being wrapped
// - CallID: The unique identifier for this specific tool call
WrapEnhancedStreamableToolCall(ctx context.Context, endpoint EnhancedStreamableToolCallEndpoint, tCtx *ToolContext) (EnhancedStreamableToolCallEndpoint, error)
// WrapModel wraps a chat model with custom behavior around the actual model call.
// Return the input model unchanged and nil error if no wrapping is needed.
//
// This method is called at request time when the model is about to be invoked.
// Note: The parameter is model.BaseModel[M] (not ToolCallingChatModel) because wrappers
// only need to intercept Generate/Stream calls. Tool binding (WithTools) is handled
// separately by the framework and does not flow through user wrappers.
//
// Recommended use cases (behavior around the model call itself):
// - Model call retry logic
// - Model failover (switching to a backup model)
// - Sending events (e.g. streaming progress)
// - Processing or transforming the response stream
// - Changing call configurations (temperature, top_p, etc.)
//
// Discouraged use cases (use BeforeModelRewriteState instead):
// - Modifying input messages: changes here are NOT persisted in state, only
// affect a single model call, and break prompt cache across iterations.
// - Modifying the tool list: use state.ToolInfos / state.DeferredToolInfos in
// BeforeModelRewriteState, which is the source of truth for tool configuration.
//
// The mc parameter provides read-only context about the current model call:
// - Tools: The tool infos that will be sent to the model (Deprecated: read state.ToolInfos instead)
WrapModel(ctx context.Context, m model.BaseModel[M], mc *TypedModelContext[M]) (model.BaseModel[M], error)
}
// ChatModelAgentMiddleware is the default middleware type using *schema.Message.
// See TypedChatModelAgentMiddleware for full documentation.
type ChatModelAgentMiddleware = TypedChatModelAgentMiddleware[*schema.Message]
type TypedBaseChatModelAgentMiddleware[M MessageType] struct{}
// BaseChatModelAgentMiddleware provides default no-op implementations for ChatModelAgentMiddleware.
// Embed *BaseChatModelAgentMiddleware in custom handlers to only override the methods you need.
//
// Example:
//
// type MyHandler struct {
// *adk.BaseChatModelAgentMiddleware
// // custom fields
// }
//
// func (h *MyHandler) BeforeModelRewriteState(ctx context.Context, state *adk.ChatModelAgentState, mc *adk.ModelContext) (context.Context, *adk.ChatModelAgentState, error) {
// // custom logic
// return ctx, state, nil
// }
type BaseChatModelAgentMiddleware = TypedBaseChatModelAgentMiddleware[*schema.Message]
func (b *TypedBaseChatModelAgentMiddleware[M]) WrapInvokableToolCall(_ context.Context, endpoint InvokableToolCallEndpoint, _ *ToolContext) (InvokableToolCallEndpoint, error) {
return endpoint, nil
}
func (b *TypedBaseChatModelAgentMiddleware[M]) WrapStreamableToolCall(_ context.Context, endpoint StreamableToolCallEndpoint, _ *ToolContext) (StreamableToolCallEndpoint, error) {
return endpoint, nil
}
func (b *TypedBaseChatModelAgentMiddleware[M]) WrapEnhancedInvokableToolCall(_ context.Context, endpoint EnhancedInvokableToolCallEndpoint, _ *ToolContext) (EnhancedInvokableToolCallEndpoint, error) {
return endpoint, nil
}
func (b *TypedBaseChatModelAgentMiddleware[M]) WrapEnhancedStreamableToolCall(_ context.Context, endpoint EnhancedStreamableToolCallEndpoint, _ *ToolContext) (EnhancedStreamableToolCallEndpoint, error) {
return endpoint, nil
}
func (b *TypedBaseChatModelAgentMiddleware[M]) WrapModel(_ context.Context, m model.BaseModel[M], _ *TypedModelContext[M]) (model.BaseModel[M], error) {
return m, nil
}
func (b *TypedBaseChatModelAgentMiddleware[M]) BeforeAgent(ctx context.Context, runCtx *ChatModelAgentContext) (context.Context, *ChatModelAgentContext, error) {
return ctx, runCtx, nil
}
func (b *TypedBaseChatModelAgentMiddleware[M]) AfterAgent(ctx context.Context, state *TypedChatModelAgentState[M]) (context.Context, error) {
return ctx, nil
}
func (b *TypedBaseChatModelAgentMiddleware[M]) BeforeModelRewriteState(ctx context.Context, state *TypedChatModelAgentState[M], mc *TypedModelContext[M]) (context.Context, *TypedChatModelAgentState[M], error) {
return ctx, state, nil
}
func (b *TypedBaseChatModelAgentMiddleware[M]) AfterModelRewriteState(ctx context.Context, state *TypedChatModelAgentState[M], mc *TypedModelContext[M]) (context.Context, *TypedChatModelAgentState[M], error) {
return ctx, state, nil
}
func processTypedState(ctx context.Context, fn func(extra map[string]any) map[string]any) error {
runCtx := getRunCtx(ctx)
if runCtx != nil && runCtx.AgenticRootInput != nil {
return compose.ProcessState(ctx, func(_ context.Context, st *typedState[*schema.AgenticMessage]) error {
st.Extra = fn(st.Extra)
return nil
})
}
return compose.ProcessState(ctx, func(_ context.Context, st *typedState[*schema.Message]) error {
st.Extra = fn(st.Extra)
return nil
})
}
// TypedGetMessages returns the current ChatModelAgent message list.
//
// It can be called from middleware and tool hooks while a ChatModelAgent is
// running or resuming, including hooks that do not receive
// TypedChatModelAgentState directly. The returned slice is a snapshot: callers
// may append to or replace its entries without changing agent state. The
// messages themselves are not deep-copied and must be treated as read-only.
func TypedGetMessages[M MessageType](ctx context.Context) ([]M, error) {
var messages []M
err := compose.ProcessState(ctx, func(_ context.Context, st *typedState[M]) error {
messages = append([]M(nil), st.Messages...)
return nil
})
if err != nil {
return nil, fmt.Errorf("TypedGetMessages failed: must be called within a ChatModelAgent Run() or Resume() execution context: %w", err)
}
return messages, nil
}
// GetMessages returns the current ChatModelAgent message list.
//
// It is the *schema.Message specialization of TypedGetMessages.
func GetMessages(ctx context.Context) ([]Message, error) {
return TypedGetMessages[*schema.Message](ctx)
}
// SetRunLocalValue sets a key-value pair that persists for the duration of the current agent Run() invocation.
// The value is scoped to this specific execution and is not shared across different Run() calls or agent instances.
//
// Values stored here are compatible with interrupt/resume cycles - they will be serialized and restored
// when the agent is resumed. For custom types, you must register them using schema.RegisterName[T]()
// in an init() function to ensure proper serialization.
//
// This function can only be called from within a ChatModelAgentMiddleware during agent execution.
// Returns an error if called outside of an agent execution context.
func SetRunLocalValue(ctx context.Context, key string, value any) error {
if err := checkGobEncodability(key, value); err != nil {
return err
}
err := processTypedState(ctx, func(extra map[string]any) map[string]any {
if extra == nil {
extra = make(map[string]any)
}
extra[key] = value
return extra
})
if err != nil {
return fmt.Errorf("SetRunLocalValue failed: must be called within a ChatModelAgent Run() or Resume() execution context: %w", err)
}
return nil
}
// GetRunLocalValue retrieves a value that was set during the current agent Run() invocation.
// The value is scoped to this specific execution and is not shared across different Run() calls or agent instances.
//
// Values stored via SetRunLocalValue are compatible with interrupt/resume cycles - they will be serialized
// and restored when the agent is resumed. For custom types, you must register them using schema.RegisterName[T]()
// in an init() function to ensure proper serialization.
//
// This function can only be called from within a ChatModelAgentMiddleware during agent execution.
// Returns the value and true if found, or nil and false if not found or if called outside of an agent execution context.
func GetRunLocalValue(ctx context.Context, key string) (any, bool, error) {
var val any
var found bool
err := processTypedState(ctx, func(extra map[string]any) map[string]any {
if extra != nil {
val, found = extra[key]
}
return extra
})
if err != nil {
return nil, false, fmt.Errorf("GetRunLocalValue failed: must be called within a ChatModelAgent Run() or Resume() execution context: %w", err)
}
return val, found, nil
}
// DeleteRunLocalValue removes a value that was set during the current agent Run() invocation.
//
// This function can only be called from within a ChatModelAgentMiddleware during agent execution.
// Returns an error if called outside of an agent execution context.
func DeleteRunLocalValue(ctx context.Context, key string) error {
err := processTypedState(ctx, func(extra map[string]any) map[string]any {
if extra != nil {
delete(extra, key)
}
return extra
})
if err != nil {
return fmt.Errorf("DeleteRunLocalValue failed: must be called within a ChatModelAgent Run() or Resume() execution context: %w", err)
}
return nil
}
// TypedSendEvent sends a custom TypedAgentEvent to the event stream during agent execution.
// This allows TypedChatModelAgentMiddleware implementations to emit custom events that will be
// received by the caller iterating over the agent's event stream.
//
// Note: TypedSendEvent is a pure transport — it does NOT auto-assign message IDs.
// Framework-created messages (model output, tool results) receive IDs automatically
// via internal wrapper layers. If your middleware constructs its own messages, call
// EnsureMessageID before sending to assign an ID.
//
// This function can only be called from within a TypedChatModelAgentMiddleware during agent execution.
// Returns an error if called outside of an agent execution context.
func TypedSendEvent[M MessageType](ctx context.Context, event *TypedAgentEvent[M]) error {
execCtx := getTypedChatModelAgentExecCtx[M](ctx)
if execCtx == nil || execCtx.generator == nil {
return fmt.Errorf("TypedSendEvent failed: must be called within a ChatModelAgent Run() or Resume() execution context")
}
execCtx.send(event)
return nil
}
// SendEvent sends a custom AgentEvent to the event stream during agent execution.
// This allows ChatModelAgentMiddleware implementations to emit custom events that will be
// received by the caller iterating over the agent's event stream.
//
// This function can only be called from within a ChatModelAgentMiddleware during agent execution.
// Returns an error if called outside of an agent execution context.
func SendEvent(ctx context.Context, event *AgentEvent) error {
return TypedSendEvent(ctx, event)
}
// checkGobEncodability probes whether the value can be gob-encoded as part of
// a map[string]any, which is exactly how State.Extra is serialized during
// checkpoint. This catches unregistered types early at Set time, rather than
// letting them fail at checkpoint/resume time with a confusing error.
func checkGobEncodability(key string, value any) error {
probe := map[string]any{key: value}
if err := gob.NewEncoder(io.Discard).Encode(probe); err != nil {
typeName := reflect.TypeOf(value).String()
return fmt.Errorf("SetRunLocalValue: the value (type %s) for key %q is not gob-serializable, "+
"which means it will fail when the agent checkpoint is saved or resumed.\n\n"+
"To fix this, register the type in an init() function in your package:\n\n"+
" func init() {\n"+
" schema.RegisterName[%s](\"a_unique_name_for_this_type\")\n"+
" }\n\n"+
"This is required because agent state (including values set via SetRunLocalValue) is "+
"persisted using gob encoding for interrupt/resume support. All concrete types stored "+
"in interface-typed fields (like map[string]any) must be registered with gob.\n\n"+
"If this value does not need to survive interrupt/resume, store it on the context instead, "+
"for example via context.WithValue, so you don't need gob registration.\n\n"+
"Underlying error: %w", typeName, key, typeName, err)
}
return nil
}