Problem Statement
Two related issues in the tool system limit its usefulness for agent builders:
1. Tools don't receive invocation context
Tool.Execute(ctx, args) receives only a context.Context and the JSON arguments. The ExecutionContext struct exists in tool/registry.go but is never passed to tools. Tools cannot access:
- Session data: Which session is this tool executing in? What's the conversation history?
- User identity: Who is the user? What are their permissions?
- Invocation metadata: Which turn is this? Which agent invoked me? What's the invocation ID?
- Other tool results: What did previous tools return in this turn?
- Memory/artifacts: Read or write shared state across tool invocations.
This means every tool that needs context must either:
- Receive it through the JSON args (requires the LLM to pass it, unreliable)
- Use
context.Value() (untyped, error-prone, no SDK support)
- Be implemented as a closure that captures state (limits reusability)
2. Registry.List() ordering is non-deterministic
Registry.List() at registry.go:151 iterates over a map[string]*registeredTool, which means tool ordering in the LLM prompt varies between calls. This causes:
- Non-reproducible behavior: The same agent with the same tools may behave differently between runs because tool ordering in the prompt affects LLM tool selection.
- Test flakiness: Tests that assert on tool definitions or request contents are order-dependent.
- Debugging difficulty: Comparing two requests is harder when tool order changes randomly.
Proposed Solution
Typed tool execution context
Extend Tool.Execute to receive a typed context that provides invocation data:
Option A: Extend the interface (breaking)
type Tool interface {
Definition() llm.ToolDefinition
Execute(ctx context.Context, execCtx *ExecutionContext, args json.RawMessage) (json.RawMessage, error)
}
type ExecutionContext struct {
InvocationID string
SessionID string
Turn int
UserID string
Metadata map[string]any // Interceptor-set metadata
Session *session.State // Read-only session access
}
Option B: Context key (non-breaking)
// SDK sets this before calling Execute
type executionContextKey struct{}
func ExecutionContextFromContext(ctx context.Context) (*ExecutionContext, bool) {
ec, ok := ctx.Value(executionContextKey{}).(*ExecutionContext)
return ec, ok
}
// Tools opt in to reading it:
func (t *MyTool) Execute(ctx context.Context, args json.RawMessage) (json.RawMessage, error) {
if ec, ok := tool.ExecutionContextFromContext(ctx); ok {
// Access ec.SessionID, ec.UserID, etc.
}
// ...
}
Option B is preferred as it maintains backward compatibility — existing tools continue to work unchanged, and new tools can opt in to reading the execution context.
Deterministic tool ordering
Change Registry to maintain insertion order:
type registry struct {
mu sync.RWMutex
tools map[string]*registeredTool
order []string // insertion order
config RegistryConfig
}
List() returns tools in insertion order. Unregister() removes from both the map and the order slice. This is a simple, predictable default.
Alternatively, sort alphabetically in List() — either way, the output must be deterministic.
Use Case Example
Authorization-aware tool:
func (t *DeleteResourceTool) Execute(ctx context.Context, args json.RawMessage) (json.RawMessage, error) {
ec, ok := tool.ExecutionContextFromContext(ctx)
if !ok {
return nil, errors.New("execution context required")
}
// Check user permissions
if !hasPermission(ec.UserID, "delete") {
return json.Marshal(map[string]string{"error": "permission denied"})
}
// Use session metadata for context
env := ec.Metadata["environment"] // Set by an interceptor
// ...
}
Audit logging tool:
func (t *AuditTool) Execute(ctx context.Context, args json.RawMessage) (json.RawMessage, error) {
ec, _ := tool.ExecutionContextFromContext(ctx)
log.Info("tool executed",
"tool", t.Definition().Name,
"user", ec.UserID,
"session", ec.SessionID,
"invocation", ec.InvocationID,
"turn", ec.Turn,
)
// ...
}
Why This Matters
- Authorization: Tools that perform sensitive operations need to know who is requesting the action. Without invocation context, tools can't enforce authorization, and the only alternative is passing user identity through LLM arguments (insecure and unreliable).
- Audit and compliance: Production tools need to log who did what, when, and in which context. Without structured execution context, audit logs lack the correlation IDs needed for traceability.
- Deterministic behavior: Non-deterministic tool ordering introduces a source of randomness that makes agent behavior harder to reproduce, test, and debug. Tool order should be a controlled variable, not a random one.
- Tool reusability: Tools that can read invocation context are more reusable across agents. Instead of building context-awareness into each agent's tool wiring, tools can self-serve the context they need.
Problem Statement
Two related issues in the tool system limit its usefulness for agent builders:
1. Tools don't receive invocation context
Tool.Execute(ctx, args)receives only acontext.Contextand the JSON arguments. TheExecutionContextstruct exists intool/registry.gobut is never passed to tools. Tools cannot access:This means every tool that needs context must either:
context.Value()(untyped, error-prone, no SDK support)2. Registry.List() ordering is non-deterministic
Registry.List()atregistry.go:151iterates over amap[string]*registeredTool, which means tool ordering in the LLM prompt varies between calls. This causes:Proposed Solution
Typed tool execution context
Extend
Tool.Executeto receive a typed context that provides invocation data:Option A: Extend the interface (breaking)
Option B: Context key (non-breaking)
Option B is preferred as it maintains backward compatibility — existing tools continue to work unchanged, and new tools can opt in to reading the execution context.
Deterministic tool ordering
Change
Registryto maintain insertion order:List()returns tools in insertion order.Unregister()removes from both the map and the order slice. This is a simple, predictable default.Alternatively, sort alphabetically in
List()— either way, the output must be deterministic.Use Case Example
Authorization-aware tool:
Audit logging tool:
Why This Matters