Documentation
¶
Overview ¶
Package toolwrap provides composable middleware for tool execution.
Each Middleware wraps a Handler, returning a new Handler with added behaviour (audit logging, HITL approval, caching, loop detection, etc.). The Service assembles the default middleware chain via DefaultMiddlewares
Package toolwrap – context-mode middleware for local tool-output compression.
Package toolwrap – PII rehydration of tool-call arguments before execution.
Package toolwrap – auto-summarization middleware for large tool results.
Package toolwrap provides composable middleware for tool execution.
Each Middleware wraps a Handler, returning a new Handler with added behaviour (audit logging, HITL approval, caching, loop detection, etc.). The Service assembles the default middleware chain via DefaultMiddlewares and callers can customise or extend the chain.
The Service holds session-stable dependencies (Auditor, ApprovalStore) so callers only need to supply per-request fields (WorkingMemory, ThreadID, RunID) via WrapRequest.
Index ¶
- Constants
- func IsRetrievalTool(name string) bool
- func OriginalQuestionFrom(ctx context.Context) string
- func TruncateForAudit(s string, maxLen int) string
- func WithCancelCause(ctx context.Context, fn context.CancelCauseFunc) context.Context
- func WithOriginalQuestion(ctx context.Context, question string) context.Context
- type ApproveList
- type CircuitBreakerConfig
- type CircuitBreakerMW
- type CompositeMiddleware
- type ConcurrencyConfig
- type ContextModeConfig
- type ContextModeToolOverride
- type DirectCaller
- type HITLOption
- type Handler
- type MetricsConfig
- type Middleware
- func AuditMiddleware(auditor audit.Auditor) Middleware
- func AutoSummarizeMiddleware(summarize SummarizeFunc, threshold int) Middleware
- func ConcurrencyMiddleware(cfg ConcurrencyConfig) Middleware
- func ContextModeMiddleware(cfg ContextModeConfig) Middleware
- func FailureLimitMiddleware() Middleware
- func HITLApprovalMiddleware(store hitl.ApprovalStore, wm *rtmemory.WorkingMemory, opts ...HITLOption) Middleware
- func LoopDetectionMiddleware() Middleware
- func PanicRecoveryMiddleware() Middleware
- func RateLimitMiddleware(cfg RateLimitConfig) Middleware
- func SemanticCacheMiddleware(keyFields map[string][]string) Middleware
- type MiddlewareConfig
- type MiddlewareDeps
- type MiddlewareFunc
- func InputValidationMiddleware(toolDeclarations func(name string) *tool.Declaration) MiddlewareFunc
- func LoggerMiddleware() MiddlewareFunc
- func OutputSanitizationMiddleware(perTool map[string][]string, replacement string) MiddlewareFunc
- func PIIRehydrateMiddleware() MiddlewareFunc
- func PerToolTimeoutMiddleware(perTool map[string]time.Duration, fallback time.Duration) MiddlewareFunc
- func RetryMiddleware(cfg RetryConfig) MiddlewareFunc
- func TimeoutMiddleware(d time.Duration) MiddlewareFunc
- func TracingMiddleware() MiddlewareFunc
- type RateLimitConfig
- type RetryConfig
- type SanitizeMiddlewareConfig
- type Service
- type ServiceOption
- type SummarizeConfig
- type SummarizeFunc
- type TimeoutConfig
- type ToolCallContext
- type ToolCaller
- type TracingConfig
- type ValidationConfig
- type WrapRequest
- type Wrapper
Constants ¶
const AuditEventHITLDecision audit.EventType = "hitl_decision"
AuditEventHITLDecision is the audit event type for HITL approval/rejection.
Variables ¶
This section is empty.
Functions ¶
func IsRetrievalTool ¶ added in v0.1.6
IsRetrievalTool reports whether the given tool name is classified as a retrieval-only tool (memory_search, graph_query, etc.).
func OriginalQuestionFrom ¶
OriginalQuestionFrom extracts the original question from the context.
func TruncateForAudit ¶
TruncateForAudit truncates a string to maxLen runes for audit log metadata.
func WithCancelCause ¶ added in v0.1.6
WithCancelCause stores a context.CancelCauseFunc in the context so that middleware (loop detection) can terminate the sub-agent run. Only sub-agents set this; the parent agent's context has no cancel function, preserving backward compatibility.
func WithOriginalQuestion ¶
WithOriginalQuestion stashes the user's original question in the context so that requireApproval can persist it alongside the approval row. Without this, recovered approvals after a restart would have no way to replay the original request through the chat handler.
Types ¶
type ApproveList ¶
type ApproveList struct {
// contains filtered or unexported fields
}
ApproveList is an in-memory, thread-safe list of tool approvals that bypass the HITL gate for a limited duration. Blind entries are stored in a TTL+LRU cache so expired entries are evicted on access and size is bounded. Filter entries are pruned opportunistically when adding new entries.
func NewApproveList ¶
func NewApproveList() *ApproveList
NewApproveList creates an empty in-memory approve list.
func (*ApproveList) AddBlind ¶
func (l *ApproveList) AddBlind(toolName string, duration time.Duration)
AddBlind adds a tool to the approve list for the given duration. Any call to that tool within the duration is auto-approved regardless of args.
func (*ApproveList) AddWithArgsFilter ¶
func (l *ApproveList) AddWithArgsFilter(toolName string, substrings []string, duration time.Duration)
AddWithArgsFilter adds a tool to the approve list when args contain any of the given substrings, for the given duration. If substrings is empty, it behaves like AddBlind for that tool.
func (*ApproveList) IsApproved ¶
func (l *ApproveList) IsApproved(toolName string, args string) bool
IsApproved returns true if the tool is on the approve list and not expired. For blind entries, any args match. For filter entries, args must contain at least one of the stored substrings. Blind entries use a TTL+LRU cache so expired entries are evicted on access.
type CircuitBreakerConfig ¶
type CircuitBreakerConfig struct {
// Enabled activates the circuit breaker middleware. Defaults to false.
Enabled bool `yaml:"enabled,omitempty" toml:"enabled,omitempty"`
// FailureThreshold is the number of consecutive failures before the
// circuit opens. Defaults to 5 if zero.
FailureThreshold int `yaml:"failure_threshold,omitempty" toml:"failure_threshold,omitempty,omitzero"`
// OpenDuration is how long the circuit stays open before transitioning
// to half-open for a probe. Defaults to 30s if zero.
OpenDuration time.Duration `yaml:"open_duration,omitempty" toml:"open_duration,omitempty"`
// HalfOpenMaxCalls is the number of test calls allowed in half-open
// state before deciding to close or re-open. Defaults to 1 if zero.
HalfOpenMaxCalls int `yaml:"half_open_max_calls,omitempty" toml:"half_open_max_calls,omitempty,omitzero"`
}
CircuitBreakerConfig configures the CircuitBreakerMiddleware.
type CircuitBreakerMW ¶
type CircuitBreakerMW struct {
// contains filtered or unexported fields
}
CircuitBreakerMW implements the standard three-state circuit breaker (closed → open → half-open → closed) using sony/gobreaker.
The TwoStepCircuitBreaker separates the "allow" check from the "report outcome" callback, which makes it straightforward to swap the in-memory state to a remote backend (Redis, DynamoDB) — you only need to replace the inner breaker with one that reads/writes counts externally. See gobreaker.Counts for available counters.
The optional scope field prefixes breaker keys so that the same tool name in different agents gets independent circuit state. This prevents policy-denied failures in one agent from opening the circuit globally.
func CircuitBreakerMiddleware ¶
func CircuitBreakerMiddleware(cfg CircuitBreakerConfig) *CircuitBreakerMW
CircuitBreakerMiddleware creates a per-tool circuit breaker middleware backed by sony/gobreaker. Each tool gets its own breaker instance.
func (*CircuitBreakerMW) OpenTools ¶
func (m *CircuitBreakerMW) OpenTools() []string
OpenTools returns the names of tools whose circuit breaker is currently in the Open state. The adaptive loop uses this to remove broken tools from the LLM's tool set — preventing wasted calls where the LLM tries a broken tool only to get rejected by the middleware.
When a scope is set, only breakers matching this scope are considered and the scope prefix is stripped from the returned tool names.
func (*CircuitBreakerMW) WithScope ¶
func (m *CircuitBreakerMW) WithScope(scope string) *CircuitBreakerMW
WithScope returns a new CircuitBreakerMW that shares the same breaker map and config but uses the given scope as a key prefix. This scopes circuit state per-agent so that failures (e.g. policy denials) in one agent don't open the circuit for other agents using the same tool.
If scope is empty, 'this' is returned unchanged. Without per-agent scoping, a policy that denies tool X in agent A would trip the circuit globally, preventing agent B from using tool X even though agent B has no such policy.
func (*CircuitBreakerMW) Wrap ¶
func (m *CircuitBreakerMW) Wrap(next Handler) Handler
type CompositeMiddleware ¶
type CompositeMiddleware []Middleware
func (CompositeMiddleware) Wrap ¶
func (c CompositeMiddleware) Wrap(next Handler) Handler
type ConcurrencyConfig ¶
type ConcurrencyConfig struct {
// Enabled activates the concurrency middleware. Defaults to false.
Enabled bool `yaml:"enabled,omitempty" toml:"enabled,omitempty"`
// GlobalLimit is the maximum number of concurrent tool calls across
// all tools. Zero means no global limit.
GlobalLimit int `yaml:"global_limit,omitempty" toml:"global_limit,omitempty,omitzero"`
// PerToolLimits maps tool names to per-tool concurrency caps.
// Tools not in the map are only subject to the global limit.
PerToolLimits map[string]int `yaml:"per_tool_limits,omitempty" toml:"per_tool_limits,omitempty"`
}
ConcurrencyConfig configures the ConcurrencyMiddleware.
type ContextModeConfig ¶
type ContextModeConfig struct {
// Enabled activates the context-mode middleware. Default: false (disabled).
// Set to true to opt in.
Enabled bool `yaml:"enabled,omitempty" toml:"enabled,omitempty"`
// Threshold is the character count above which a response is compressed.
// When 0, defaultContextModeThreshold (20 000) is used.
Threshold int `yaml:"threshold,omitempty" toml:"threshold,omitempty"`
// MaxChunks is the maximum number of scored chunks to return.
// When 0, defaultMaxChunks (10) is used.
MaxChunks int `yaml:"max_chunks,omitempty" toml:"max_chunks,omitempty"`
// ChunkSize is the target character count per chunk.
// When 0, defaultChunkSize (800) is used.
ChunkSize int `yaml:"chunk_size,omitempty" toml:"chunk_size,omitempty"`
// MinTermLen is the minimum character length for a query term to be
// considered meaningful. When 0, the package default (3) is used.
// Lowering this value retains shorter tokens such as 2-character IDs.
MinTermLen int `yaml:"min_term_len,omitempty" toml:"min_term_len,omitempty"`
// PerTool maps tool names to per-tool overrides. Non-zero fields in the
// override replace the global defaults for that tool. This lets operators
// tune compression aggressiveness per tool — e.g. relax thresholds for
// run_shell output.
PerTool map[string]ContextModeToolOverride `yaml:"per_tool,omitempty" toml:"per_tool,omitempty"`
}
ContextModeConfig controls the context-mode middleware.
type ContextModeToolOverride ¶ added in v0.1.6
type ContextModeToolOverride struct {
Threshold int `yaml:"threshold,omitempty" toml:"threshold,omitempty"`
MaxChunks int `yaml:"max_chunks,omitempty" toml:"max_chunks,omitempty"`
ChunkSize int `yaml:"chunk_size,omitempty" toml:"chunk_size,omitempty"`
MinTermLen int `yaml:"min_term_len,omitempty" toml:"min_term_len,omitempty"`
}
ContextModeToolOverride holds per-tool overrides for the context-mode middleware. Only non-zero fields take effect; zero values fall back to the global defaults.
type DirectCaller ¶
type DirectCaller struct{}
DirectCaller is the default ToolCaller that invokes tools in-process via the tool.CallableTool interface. This is the legacy execution path and is used when no alternative caller is configured.
type HITLOption ¶
type HITLOption func(*hitlApprovalMiddleware)
HITLOption configures the behaviour of the HITL approval middleware.
func WithApproveListOption ¶
func WithApproveListOption(list *ApproveList) HITLOption
WithApproveListOption injects an in-memory approve list so the middleware can auto-approve when IsApproved returns true. Used by Service and by tests.
func WithHITLAuditor ¶ added in v0.1.7
func WithHITLAuditor(a audit.Auditor) HITLOption
WithHITLAuditor injects an auditor so HITL approval/rejection decisions are written to the durable audit trail (not just the AG-UI event bus).
func WithNonBlockingHITL ¶
func WithNonBlockingHITL() HITLOption
WithNonBlockingHITL configures the middleware to return an interrupt.Error instead of blocking in hitl.ApprovalStore.WaitForResolution.
Use this when tool calls run inside an executor that models human interaction as an interrupt/resume cycle (e.g. Temporal workflows). The returned interrupt.Error carries the approval ID and the hitl.ApprovalRequest as its Payload.
Default: blocking (the current in-process behaviour).
func WithSharedApprovalCache ¶
func WithSharedApprovalCache(cache *approvalCache) HITLOption
WithSharedApprovalCache injects a session-scoped approval cache shared across all sub-agents. When a tool+args combination is approved in one sub-agent, it is auto-approved in subsequent sub-agents within the same session, avoiding redundant HITL prompts.
type Handler ¶
type Handler func(ctx context.Context, tc *ToolCallContext) (any, error)
Handler is a function that processes a tool call and returns its output. The terminal handler executes the real tool; middlewares wrap this with cross-cutting concerns.
func Chain ¶
func Chain(terminal Handler, middlewares ...Middleware) Handler
Chain composes a slice of middlewares into a single Handler by wrapping the terminal handler from outermost (index 0) to innermost (last index). If middlewares is empty, the terminal handler is returned unchanged.
type MetricsConfig ¶
type MetricsConfig struct {
Enabled bool `yaml:"enabled,omitempty" toml:"enabled,omitempty"`
Prefix string `yaml:"prefix,omitempty" toml:"prefix,omitempty"`
}
MetricsConfig controls the MetricsMiddleware.
func (MetricsConfig) MetricsMiddleware ¶
func (cfg MetricsConfig) MetricsMiddleware() MiddlewareFunc
MetricsMiddleware returns a Middleware that records OpenTelemetry metrics for every tool call: a call counter, a latency histogram, and an error counter. Without this middleware, operators have no quantitative signal for tool usage patterns, p99 latencies, or failure rates.
type Middleware ¶
Middleware wraps a Handler, returning a new Handler with added behaviour. Middlewares are composed in order: the first middleware in the slice is the outermost wrapper and executes first.
The open-source default chain (from DefaultMiddlewares) is:
LoopDetection → FailureLimit → SemanticCache → FileCache → HITLApproval → ContextEnrich → Audit → Execute
func AuditMiddleware ¶
func AuditMiddleware(auditor audit.Auditor) Middleware
AuditMiddleware returns a Middleware that writes tool call results to the audit trail for compliance and debugging. Without this middleware, tool calls would produce no durable trace.
func AutoSummarizeMiddleware ¶
func AutoSummarizeMiddleware(summarize SummarizeFunc, threshold int) Middleware
AutoSummarizeMiddleware returns a Middleware that detects tool responses exceeding the configured character threshold and summarises them via the provided SummarizeFunc. If summarize is nil the middleware is a no-op. Identical content is served from a short-lived cache to avoid redundant LLM calls when multiple sub-agent steps produce the same tool output.
func ConcurrencyMiddleware ¶
func ConcurrencyMiddleware(cfg ConcurrencyConfig) Middleware
ConcurrencyMiddleware returns a Middleware that limits concurrent tool executions using golang.org/x/sync/semaphore.
func ContextModeMiddleware ¶
func ContextModeMiddleware(cfg ContextModeConfig) Middleware
ContextModeMiddleware returns a Middleware that detects tool responses exceeding the configured character threshold and compresses them by chunking the text and returning only the top-K most relevant chunks, scored against the tool's input arguments using BM25-lite.
This middleware is complementary to AutoSummarizeMiddleware: if context mode reduces a 200K response to 15K (below the summarise threshold), the LLM summariser is never called — saving both time and API cost. Without this middleware, every large tool response would require an LLM call for compression.
func FailureLimitMiddleware ¶
func FailureLimitMiddleware() Middleware
FailureLimitMiddleware returns a Middleware that blocks a tool after maxConsecutiveToolFailures consecutive errors (of any kind). Unlike CircuitBreakerMiddleware, this has no recovery timer — the tool stays blocked until a success. Use CircuitBreakerMiddleware for automatic recovery after a cooldown.
func HITLApprovalMiddleware ¶
func HITLApprovalMiddleware( store hitl.ApprovalStore, wm *rtmemory.WorkingMemory, opts ...HITLOption, ) Middleware
HITLApprovalMiddleware creates a new HITL approval middleware. Approval request events are emitted via the agui event bus (keyed by MessageOrigin in context), so no explicit event channel is needed.
When called without WithSharedApprovalCache, a per-middleware cache is used (suitable for tests). The Service always passes a shared cache so that approvals carry across sub-agents within the same session.
func LoopDetectionMiddleware ¶
func LoopDetectionMiddleware() Middleware
LoopDetectionMiddleware returns a Middleware that blocks a tool after maxConsecutiveRepeatCalls identical (same name + args) consecutive calls, and cancels the sub-agent after maxConsecutiveEmptyResults consecutive empty results from retrieval tools. This prevents infinite agent loops where the LLM keeps issuing the same call or rephrasing queries against an empty store.
func PanicRecoveryMiddleware ¶
func PanicRecoveryMiddleware() Middleware
PanicRecoveryMiddleware returns a Middleware that recovers from panics in downstream handlers to prevent server crashes.
func RateLimitMiddleware ¶
func RateLimitMiddleware(cfg RateLimitConfig) Middleware
RateLimitMiddleware returns a Middleware that throttles tool calls using golang.org/x/time/rate token-bucket limiters. It supports both a global rate limit and per-tool overrides (bulkhead pattern).
func SemanticCacheMiddleware ¶
func SemanticCacheMiddleware(keyFields map[string][]string) Middleware
SemanticCacheMiddleware creates a new semantic dedup middleware. keyFields maps tool names to the JSON argument fields that form the semantic identity of a call. Only tools in this map are eligible for caching; all others pass through.
Example:
SemanticCacheMiddleware(map[string][]string{
"create_recurring_task": {"name"},
})
type MiddlewareConfig ¶
type MiddlewareConfig struct {
ContextModeConfig ContextModeConfig `yaml:"context_mode,omitempty" toml:"context_mode,omitempty"`
Timeout TimeoutConfig `yaml:"timeout,omitempty" toml:"timeout,omitempty"`
RateLimit RateLimitConfig `yaml:"rate_limit,omitempty" toml:"rate_limit,omitempty"`
Retry RetryConfig `yaml:"retry,omitempty" toml:"retry,omitempty"`
CircuitBreaker CircuitBreakerConfig `yaml:"circuit_breaker,omitempty" toml:"circuit_breaker,omitempty"`
Concurrency ConcurrencyConfig `yaml:"concurrency,omitempty" toml:"concurrency,omitempty"`
Metrics MetricsConfig `yaml:"metrics,omitempty" toml:"metrics,omitempty"`
Tracing TracingConfig `yaml:"tracing,omitempty" toml:"tracing,omitempty"`
Validation ValidationConfig `yaml:"validation,omitempty" toml:"validation,omitempty"`
Sanitize SanitizeMiddlewareConfig `yaml:"sanitize,omitempty" toml:"sanitize,omitempty"`
}
MiddlewareConfig is the central configuration for all toolwrap middlewares. It is embedded in GenieConfig and populated from the config file (YAML/TOML). Each middleware has its own sub-struct (defined alongside its middleware in the respective mw_*.go file) with an Enabled or Disabled flag. Where applicable, per-tool overrides are supported via map[string] fields on the individual configs.
func DefaultMiddlewareConfig ¶
func DefaultMiddlewareConfig() MiddlewareConfig
DefaultMiddlewareConfig returns sensible defaults. Circuit breaker is enabled by default to prevent agents from burning LLM calls retrying tools that are consistently failing (e.g. DuckDuckGo rate-limited). All other optional middlewares start disabled.
type MiddlewareDeps ¶
type MiddlewareDeps struct {
Auditor audit.Auditor
// SemanticKeyFields maps tool names to the JSON argument fields that
// form the semantic identity of a call for deduplication.
SemanticKeyFields map[string][]string
// WorkingMemory is used for HITL feedback storage.
WorkingMemory *rtmemory.WorkingMemory
// Summarize is an optional function that condenses large tool results.
// When non-nil, tool responses exceeding the threshold are automatically
// summarized before being returned to the LLM. Typically backed by
// agentutils.Summarizer.
Summarize SummarizeFunc
// SummarizeThreshold is the character count above which a tool result
// triggers auto-summarization. When 0, defaultSummarizeThreshold is used.
SummarizeThreshold int
// Config holds opt-in middleware settings (metrics, tracing, retry, etc).
// Zero value disables all optional middlewares.
Config MiddlewareConfig
// CircuitBreaker is an optional shared circuit breaker singleton.
// When set, it is reused across all Wrap() calls so that tool
// failures in one sub-agent trip the circuit for ALL agents.
CircuitBreaker *CircuitBreakerMW
}
MiddlewareDeps carries the dependencies needed by the default middleware chain. These are supplied per-request via WrapRequest and per-service via the Service constructor.
func (MiddlewareDeps) DefaultMiddlewares ¶
func (deps MiddlewareDeps) DefaultMiddlewares( others ...Middleware, ) Middleware
DefaultMiddlewares returns the standard middleware chain. The chain is ordered from outermost (runs first) to innermost (runs just before the terminal handler):
PanicRecovery → PIIRehydrate → [Tracing] → [Metrics] → Emitter → Logger → Audit → [Timeout] → [RateLimit] → [CircuitBreaker] → [Concurrency] → [Retry] → SemanticCache → LoopDetection → FailureLimit → [Validation] → [Sanitize] → HITLApproval → ContextEnrich
Bracketed items are opt-in and only included when their Enabled flag is true in MiddlewareConfig.
type MiddlewareFunc ¶
func InputValidationMiddleware ¶
func InputValidationMiddleware(toolDeclarations func(name string) *tool.Declaration) MiddlewareFunc
InputValidationMiddleware returns a Middleware that validates tool call arguments against the tool's declared InputSchema before execution. It checks that:
- The arguments are valid JSON
- Required top-level fields (from the schema) are present
Without this middleware, malformed arguments pass through to the tool implementation, leading to cryptic runtime errors instead of clear validation messages that help the LLM self-correct.
The validation is intentionally lightweight (required-field check) rather than a full JSON Schema validator to keep the dependency footprint small.
func LoggerMiddleware ¶
func LoggerMiddleware() MiddlewareFunc
LoggerMiddleware returns a Middleware that logs the outcome of every tool call — debug for success, error for failure. It wraps the downstream handler, measures the response, and emits a structured log line. Without this middleware, tool call outcomes would be invisible in application logs.
func OutputSanitizationMiddleware ¶
func OutputSanitizationMiddleware(perTool map[string][]string, replacement string) MiddlewareFunc
OutputSanitizationMiddleware returns a Middleware that scrubs sensitive data from tool outputs before they propagate back up the chain and into the LLM context window. Patterns are configured per tool via the perTool map (tool name → list of case-insensitive substrings to redact). Tools not in the map pass through unmodified. Without this middleware, secrets returned by tools (e.g. environment variables, config files) would be stored in the conversation history and potentially leaked.
func PIIRehydrateMiddleware ¶
func PIIRehydrateMiddleware() MiddlewareFunc
PIIRehydrateMiddleware returns a Middleware that rehydrates [HIDDEN:hash] placeholders in tool-call arguments back to original values before the tool runs. The LLM sees redacted content (e.g. [HIDDEN:53233f] instead of an email) and may echo that in tool arguments; without rehydration, tools like email_send would receive invalid values and fail (e.g. "recipient address <[HIDDEN:53233f]> is not a valid RFC 5321 address"). The replacer is stored in context by the model BeforeModel callback (pii.WithReplacer). When present, this middleware replaces placeholders in tc.Args so the tool receives real addresses, tokens, etc.
func PerToolTimeoutMiddleware ¶
func PerToolTimeoutMiddleware(perTool map[string]time.Duration, fallback time.Duration) MiddlewareFunc
PerToolTimeoutMiddleware returns a Middleware that enforces per-tool timeout overrides. Tools not present in the map use the fallback duration. This allows expensive tools (e.g. code execution) to have a longer deadline than fast tools (e.g. read_file).
func RetryMiddleware ¶
func RetryMiddleware(cfg RetryConfig) MiddlewareFunc
RetryMiddleware returns a Middleware that automatically retries failed tool calls with exponential backoff and jitter, powered by cenkalti/backoff/v4. Only errors for which cfg.Retryable returns true are retried; non-retryable errors propagate immediately via backoff.Permanent. Without this middleware, transient failures (network timeouts, 429 responses) require the LLM to manually re-issue the call, wasting a turn and tokens.
func TimeoutMiddleware ¶
func TimeoutMiddleware(d time.Duration) MiddlewareFunc
TimeoutMiddleware returns a Middleware that enforces a maximum execution time per tool call. If the tool does not complete within the deadline, the context is cancelled and the call returns context.DeadlineExceeded. Without this middleware, a misbehaving tool (e.g. a shell command or external API call) could hang indefinitely, blocking the agent loop.
func TracingMiddleware ¶
func TracingMiddleware() MiddlewareFunc
TracingMiddleware returns a Middleware that creates an OpenTelemetry span for every tool call. The span records the tool name, argument size, and any error that occurs. It also injects the span context into the downstream context so nested tool calls (e.g. sub-agents) are correlated as children. Without this middleware, tool calls are invisible in distributed traces, making it impossible to diagnose latency or failure chains.
func (MiddlewareFunc) Wrap ¶
func (f MiddlewareFunc) Wrap(next Handler) Handler
type RateLimitConfig ¶
type RateLimitConfig struct {
// Enabled activates the rate limit middleware. Defaults to false.
Enabled bool `yaml:"enabled,omitempty" toml:"enabled,omitempty"`
// GlobalRatePerMinute is the maximum number of tool calls per minute
// across all tools. Zero means no global limit.
GlobalRatePerMinute float64 `yaml:"global_rate_per_minute,omitempty" toml:"global_rate_per_minute,omitempty,omitzero"`
// PerToolRatePerMinute maps tool names to per-tool rate limits.
// Tools not in the map use the global limit. Zero entries are ignored.
PerToolRatePerMinute map[string]float64 `yaml:"per_tool_rate_per_minute,omitempty" toml:"per_tool_rate_per_minute,omitempty"`
}
RateLimitConfig specifies rate limits for the RateLimitMiddleware.
type RetryConfig ¶
type RetryConfig struct {
// Enabled activates the retry middleware. Defaults to false.
Enabled bool `yaml:"enabled,omitempty" toml:"enabled,omitempty"`
// MaxAttempts is the total number of attempts (including the first call).
// Defaults to 3 if zero.
MaxAttempts int `yaml:"max_attempts,omitempty" toml:"max_attempts,omitempty,omitzero"`
// InitialBackoff is the delay before the first retry. Subsequent retries
// double this value (exponential backoff). Defaults to 500ms if zero.
InitialBackoff time.Duration `yaml:"initial_backoff,omitempty" toml:"initial_backoff,omitempty"`
// MaxBackoff caps the backoff duration. Defaults to 10s if zero.
MaxBackoff time.Duration `yaml:"max_backoff,omitempty" toml:"max_backoff,omitempty"`
// Retryable decides whether an error is transient. When nil, all errors
// are considered retryable. Return false to stop retrying immediately.
Retryable func(err error) bool `yaml:"-" toml:"-"`
}
RetryConfig configures the RetryMiddleware behaviour.
type SanitizeMiddlewareConfig ¶
type SanitizeMiddlewareConfig struct {
// Enabled activates the output sanitization middleware.
Enabled bool `yaml:"enabled,omitempty" toml:"enabled,omitempty"`
// Replacement is the string that replaces each redacted occurrence.
// Defaults to "[REDACTED]".
Replacement string `yaml:"replacement,omitempty" toml:"replacement,omitempty"`
// PerTool maps tool names to per-tool redaction patterns (case-insensitive).
// Tools not in the map pass through unmodified.
PerTool map[string][]string `yaml:"per_tool,omitempty" toml:"per_tool,omitempty"`
}
SanitizeMiddlewareConfig controls the OutputSanitizationMiddleware.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service holds the session-stable dependencies for tool wrapping. Create one at startup and reuse it for every request.
func NewService ¶
func NewService( auditor audit.Auditor, approvalStore hitl.ApprovalStore, fn SummarizeFunc, opts ...ServiceOption, ) *Service
func (*Service) CircuitBreaker ¶
func (s *Service) CircuitBreaker() *CircuitBreakerMW
CircuitBreaker returns the shared circuit breaker instance, or nil if circuit breaking is disabled. Callers can use OpenTools() to query which tools are currently tripped.
type ServiceOption ¶
type ServiceOption func(*Service)
ServiceOption configures optional behaviour on a toolwrap Service.
func WithApprovalCacheTTL ¶
func WithApprovalCacheTTL(ttl time.Duration) ServiceOption
WithApprovalCacheTTL sets the time-to-live for the shared HITL approval cache. After this duration a previously approved tool+args combination requires fresh human approval. Default is 10 minutes.
func WithApproveList ¶
func WithApproveList(list *ApproveList) ServiceOption
WithApproveList injects the in-memory approve list so the HITL middleware can auto-approve tools that the user added via "approve for X mins" in the UI.
func WithExtraMiddleware ¶
func WithExtraMiddleware(mw ...Middleware) ServiceOption
WithExtraMiddleware appends caller-supplied middleware to the default chain. These run after the built-in middlewares (audit, cache, HITL, etc.) and just before the terminal tool-execution handler.
Use this to inject domain-specific middleware (e.g. policy enforcement) from the host application without modifying the genie core.
func WithMiddlewareConfig ¶
func WithMiddlewareConfig(cfg MiddlewareConfig) ServiceOption
WithMiddlewareConfig overrides the default middleware configuration. Pass a per-agent config to enable rate limiting, tracing, retries, etc. on a per-agent basis. When omitted, DefaultMiddlewareConfig() is used.
type SummarizeConfig ¶
type SummarizeConfig struct {
// Enabled activates the summarization middleware. Default: false (opt-in).
Enabled bool
// Threshold is the character count above which a result is summarized.
// When 0, defaultSummarizeThreshold (100 000) is used.
Threshold int
}
SummarizeConfig controls the auto-summarization behaviour for large tool results.
type SummarizeFunc ¶
SummarizeFunc is called with the tool response content and returns a condensed version. The concrete implementation is typically backed by agentutils.Summarizer, wired at the Service layer to avoid importing agentutils (and creating a circular dependency) from this package.
type TimeoutConfig ¶
type TimeoutConfig struct {
// Enabled activates the timeout middleware. Defaults to false.
Enabled bool `yaml:"enabled,omitempty" toml:"enabled,omitempty"`
// Default is the fallback timeout for all tools.
Default time.Duration `yaml:"default,omitempty" toml:"default,omitempty"`
// PerTool maps tool names to per-tool timeout overrides.
PerTool map[string]time.Duration `yaml:"per_tool,omitempty" toml:"per_tool,omitempty"`
}
TimeoutConfig controls the TimeoutMiddleware.
type ToolCallContext ¶
type ToolCallContext struct {
// ToolName is the declared name of the tool being called.
ToolName string
// OriginalArgs is the raw JSON arguments before any middleware processing.
OriginalArgs []byte
// Args is the JSON arguments after middleware processing (e.g. _justification stripped).
Args []byte
// Justification is extracted from the _justification field by the HITL middleware.
Justification string
// ResumeValue is set by the executor when resuming a previously-interrupted
// tool call. The concrete type depends on the InterruptKind:
// - InterruptClarify: string (the user's answer)
// - InterruptApproval: *hitl.ApprovalRequest (with resolved status)
//
// When nil, this is a fresh (non-resumed) call.
ResumeValue any
}
ToolCallContext carries per-call state through the middleware chain. Middlewares read and write fields to communicate with each other (e.g. JustificationExtract sets Justification, HITLApproval reads it).
type ToolCaller ¶
type ToolCaller interface {
// CallTool executes the named tool with the given JSON arguments.
// Implementations must be safe for concurrent use.
CallTool(ctx context.Context, toolName string, args []byte) (any, error)
}
ToolCaller is the pluggable contract for invoking a tool.
The default implementation casts to tool.CallableTool and calls Call() directly. Alternative implementations (e.g. toolexec) may route through Temporal activities, gRPC, or any other execution backend.
This interface lives in toolwrap — not in toolexec — so the middleware package has zero knowledge of Temporal, keeping the dependency graph clean.
type TracingConfig ¶
type TracingConfig struct {
Enabled bool `yaml:"enabled,omitempty" toml:"enabled,omitempty"`
}
TracingConfig controls the TracingMiddleware.
type ValidationConfig ¶
type ValidationConfig struct {
Enabled bool `yaml:"enabled,omitempty" toml:"enabled,omitempty"`
}
ValidationConfig controls the InputValidationMiddleware.
type WrapRequest ¶
type WrapRequest struct {
WorkingMemory *rtmemory.WorkingMemory
// AgentName, when set, scopes the circuit breaker to this agent.
// Policy-denied failures in agent A will only open the circuit for
// agent A, not for agent B using the same tool without the policy.
// When empty, the global (unscoped) circuit breaker is used.
AgentName string
}
WrapRequest contains the per-request fields needed when wrapping tools.
type Wrapper ¶
Wrapper wraps a tool with a composable middleware chain. After decoupling, the Wrapper is a thin shell: just the underlying tool and a pre-built middleware chain. All mutable state lives inside the middleware closures.
When a ToolCaller is set, the terminal handler delegates to it instead of calling tool.CallableTool.Call() directly. This allows plugging in alternative execution strategies (e.g. Temporal activities) without changing any middleware.
func NewWrapper ¶
func NewWrapper(t tool.Tool, deps MiddlewareDeps) *Wrapper
NewWrapper creates a Wrapper with an eagerly-built middleware chain. This is the primary constructor for tests and callers that don't go through Service.Wrap.
Source Files
¶
- approve_list.go
- config.go
- context.go
- justification.go
- middleware.go
- mw_audit.go
- mw_cache.go
- mw_circuitbreaker.go
- mw_concurrency.go
- mw_contextmode.go
- mw_hitl.go
- mw_logger.go
- mw_loop.go
- mw_metrics.go
- mw_pii_rehydrate.go
- mw_ratelimit.go
- mw_retry.go
- mw_sanitize.go
- mw_summarize.go
- mw_timeout.go
- mw_tracing.go
- mw_validate.go
- service.go
- wrapper.go