Documentation
¶
Overview ¶
Package toolexec hosts utilities used by the runtime to execute tool calls. It is intentionally free of runtime-private state so its primitives can be reused, tested in isolation, and incrementally grown into a fully fledged tool dispatcher.
The package currently provides:
- LoopDetector: detects consecutive identical tool-call batches so the runtime can break degenerate loops where the model is not making progress.
- ResolveModelOverride: extracts the per-toolset model override that should apply to the next LLM turn from a batch of tool calls.
Future extractions (approval flow, hooks dispatch, tool handler registry) belong here so that the runtime keeps shrinking toward pure orchestration.
Index ¶
- Constants
- func NewHooksInput(sess *session.Session, toolCall tools.ToolCall) *hooks.Input
- func NewPostToolHooksInput(sess *session.Session, toolCall tools.ToolCall, res *tools.ToolCallResult) *hooks.Input
- func ParseToolInput(arguments string) map[string]any
- func ResolveModelOverride(calls []tools.ToolCall, agentTools []tools.Tool) string
- type CallOutcome
- type Dispatcher
- type Emitter
- type HookDispatcher
- type LoopDetector
- type NamedChecker
- type PermissionDecision
- type PermissionOutcome
- type PermissionReason
- type ResumeRequest
- type ResumeType
- type ToolHandler
Constants ¶
const ( ApprovalDecisionAllow = "allow" ApprovalDecisionDeny = "deny" ApprovalDecisionCanceled = "canceled" ApprovalSourceYolo = "yolo" ApprovalSourceSessionPermissionsAllow = "session_permissions_allow" ApprovalSourceSessionPermissionsDeny = "session_permissions_deny" ApprovalSourceTeamPermissionsAllow = "team_permissions_allow" ApprovalSourceTeamPermissionsDeny = "team_permissions_deny" ApprovalSourcePreToolUseHookAllow = "pre_tool_use_hook_allow" ApprovalSourcePreToolUseHookDeny = "pre_tool_use_hook_deny" ApprovalSourceReadOnlyHint = "readonly_hint" ApprovalSourceUserApproved = "user_approved" ApprovalSourceUserApprovedSession = "user_approved_session" ApprovalSourceUserApprovedTool = "user_approved_tool" ApprovalSourceUserRejected = "user_rejected" ApprovalSourceContextCanceled = "context_canceled" )
Verdicts and sources surfaced via [HookDispatcher.NotifyApprovalDecision]. The strings are part of the on_tool_approval_decision hook contract and must stay stable.
Variables ¶
This section is empty.
Functions ¶
func NewHooksInput ¶
NewHooksInput builds a hooks.Input from the common tool-call fields. hooks.Executor.Dispatch auto-fills Cwd from the executor's working directory, so callers don't set it here.
func NewPostToolHooksInput ¶
func NewPostToolHooksInput(sess *session.Session, toolCall tools.ToolCall, res *tools.ToolCallResult) *hooks.Input
NewPostToolHooksInput builds a hooks.Input for the post-tool-use event. It enriches the common fields built by NewHooksInput with the tool result so post-tool-use hooks can inspect the response and error flag.
func ParseToolInput ¶
ParseToolInput parses a tool-call arguments JSON string into a map. Invalid or empty input yields a nil map; callers that need to distinguish "no arguments" from "invalid arguments" must inspect the input themselves.
Types ¶
type CallOutcome ¶
CallOutcome captures the verdicts of a single tool invocation as observed by the dispatcher.
Canceled and StopRun are mutually exclusive in practice but signal different things to the caller: cancellation halts the current batch silently (the run loop continues so the synthesised tool error responses can be sent back to the model on the next turn); StopRun also terminates the agent's run loop with a user-visible reason produced by a post_tool_use hook deny verdict.
type Dispatcher ¶
type Dispatcher struct {
// Tracer records per-call spans. May be nil (no-op tracing).
Tracer trace.Tracer
// Hooks dispatches pre/post tool-use hooks. May be nil for runtimes
// without hook support; in that case every call runs unchanged.
Hooks HookDispatcher
// Resume receives user-confirmation responses. Must be set; the
// dispatcher blocks on it whenever a tool requires confirmation.
Resume <-chan ResumeRequest
// AgentFor returns the active agent for a session. Required.
AgentFor func(*session.Session) *agent.Agent
// Permissions returns the ordered list of permission checkers for a
// session (typically session-level first, then team-level). May be
// nil; treated the same as returning an empty slice.
Permissions func(*session.Session) []NamedChecker
// Handlers maps tool names to runtime-managed handlers (transfer_task,
// handoff, change_model, ...). Tools not in this map are routed to
// their toolset Handler.
Handlers map[string]ToolHandler
}
Dispatcher executes batches of tool calls. Construct one per runtime (or per RunStream) and call Dispatcher.Process for each LLM response. The dispatcher is goroutine-safe only insofar as its dependencies are.
func (*Dispatcher) Process ¶
func (d *Dispatcher) Process(ctx context.Context, sess *session.Session, calls []tools.ToolCall, agentTools []tools.Tool, em Emitter) (stopRun bool, stopMessage string)
Process runs every tool call in calls in order, emitting events through em.
Returns (stopRun, message) when a post_tool_use hook signalled a terminating verdict during this batch; the run loop then fans out the standard Error / notification / on_error stanzas before exiting. (false, "") in every other path — including user cancellation, which halts the *batch* but keeps the loop alive so the synthesised tool error responses can be sent back to the model on the next turn.
type Emitter ¶
type Emitter interface {
EmitToolCall(toolCall tools.ToolCall, tool tools.Tool, agentName string)
EmitToolCallResponse(toolCallID string, tool tools.Tool, result *tools.ToolCallResult, output, agentName string)
EmitToolCallConfirmation(toolCall tools.ToolCall, tool tools.Tool, agentName string)
EmitHookBlocked(toolCall tools.ToolCall, tool tools.Tool, message, agentName string)
EmitMessageAdded(sessionID string, msg *session.Message, agentName string)
}
Emitter receives the events the Dispatcher emits while processing a batch of tool calls. Runtimes typically implement this by sending typed events to their event channel.
The dispatcher only emits the five events below. Runtime-managed handlers (registered via [Dispatcher.Handlers]) emit any additional runtime-specific events directly via the channel they captured at registration time.
type HookDispatcher ¶
type HookDispatcher interface {
// Dispatch fires a tool-related hook (typically [hooks.EventPreToolUse]
// or [hooks.EventPostToolUse]). Returning nil is the "carry on with the
// original call" signal — used uniformly when no hook is configured,
// the agent is missing, or dispatch failed.
Dispatch(ctx context.Context, a *agent.Agent, event hooks.EventType, in *hooks.Input) *hooks.Result
// NotifyUserInput is invoked just before the dispatcher blocks waiting
// for the user (tool confirmation). Implementations typically fire
// [hooks.EventOnUserInput].
NotifyUserInput(ctx context.Context, sessionID, label string)
// NotifyApprovalDecision is invoked once per tool call after the
// approval pipeline (auto-allow, deny, user confirmation, ...) has
// resolved a verdict. Implementations typically fire
// [hooks.EventOnToolApprovalDecision] with decision and source set
// to the supplied strings (see ApprovalDecision* / ApprovalSource*
// constants).
NotifyApprovalDecision(ctx context.Context, sess *session.Session, a *agent.Agent, tc tools.ToolCall, decision, source string)
}
HookDispatcher abstracts pre/post tool-use hook dispatch and the "user is being prompted" notification.
type LoopDetector ¶
type LoopDetector struct {
// contains filtered or unexported fields
}
LoopDetector detects consecutive identical tool call batches. When the model issues the same tool call(s) N times in a row without making progress, the detector signals that the agent should be terminated.
The zero value is not usable; callers must construct a detector with NewLoopDetector.
func NewLoopDetector ¶
func NewLoopDetector(threshold int, exemptTools ...string) *LoopDetector
NewLoopDetector creates a detector that triggers after threshold consecutive identical call batches. Tool names passed in exemptTools are polling-safe: batches composed entirely of exempt tools (e.g. view_background_agent, view_background_job) never count toward the consecutive-duplicate limit.
func (*LoopDetector) Consecutive ¶
func (d *LoopDetector) Consecutive() int
Consecutive returns the number of consecutive identical batches recorded since the last reset (or since a non-matching batch was seen).
func (*LoopDetector) Record ¶
func (d *LoopDetector) Record(calls []tools.ToolCall) bool
Record updates the detector with the latest tool call batch and returns true if the consecutive-duplicate threshold has been reached. Batches composed entirely of exempt (polling) tools are silently skipped so that expected polling patterns are not flagged.
func (*LoopDetector) Reset ¶
func (d *LoopDetector) Reset()
Reset clears the detector state so it can be reused after recovery.
type NamedChecker ¶
type NamedChecker struct {
Checker *permissions.Checker
Source string
}
NamedChecker pairs a permissions.Checker with a human-readable source label (e.g. "session permissions", "permissions configuration") used to construct denial messages and debug logs.
type PermissionDecision ¶
type PermissionDecision struct {
Outcome PermissionOutcome
Reason PermissionReason
Source string
}
PermissionDecision is the result of Decide: an outcome plus the reason and (when the reason is ReasonChecker) the source label of the checker that produced it.
func Decide ¶
func Decide( yoloApproved bool, checkers []NamedChecker, toolName string, toolArgs map[string]any, readOnlyHint bool, ) PermissionDecision
Decide resolves the final permission outcome for a tool call by walking the configured pipeline in priority order:
- yoloApproved (--yolo) — auto-allow everything.
- checkers (in order; typically session-level first, then team-level) — the first checker that returns Allow / Deny / ForceAsk wins. ForceAsk produces OutcomeAsk: an explicit ask pattern always overrides the read-only fast path below.
- readOnlyHint — auto-allow.
- default — Ask.
Decide is pure (no I/O, no side effects) so the entire approval matrix can be exhaustively unit-tested without a runtime.
type PermissionOutcome ¶
type PermissionOutcome int
PermissionOutcome is the resolved decision after evaluating the full approval pipeline.
const ( // OutcomeAllow means the tool can run without asking the user. OutcomeAllow PermissionOutcome = iota // OutcomeDeny means the tool must be rejected; the caller should // surface a tool-error response that mentions Source. OutcomeDeny // OutcomeAsk means the user must be asked for explicit confirmation. OutcomeAsk )
type PermissionReason ¶
type PermissionReason int
PermissionReason explains *why* a PermissionDecision was reached. Callers use it to produce accurate log messages and to know which auto-approval path was taken (yolo, checker rule, read-only hint, or default).
const ( // ReasonYolo: --yolo (sess.ToolsApproved) auto-approved the tool. ReasonYolo PermissionReason = iota // ReasonChecker: a configured permission checker (session-level or // team-level) produced a definitive Allow/Deny/ForceAsk verdict. // PermissionDecision.Source identifies which checker. ReasonChecker // ReasonReadOnlyHint: no checker matched and the tool's ReadOnlyHint // annotation auto-approved it. ReasonReadOnlyHint // ReasonDefault: nothing matched; the user must confirm. ReasonDefault )
type ResumeRequest ¶
type ResumeRequest struct {
Type ResumeType
Reason string // Optional; primarily used with [ResumeTypeReject]
ToolName string // Optional; used with [ResumeTypeApproveTool]
}
ResumeRequest carries the user's response to a tool-confirmation prompt. The runtime aliases this type publicly via runtime.ResumeRequest so the dispatcher and the runtime share one definition.
type ResumeType ¶
type ResumeType string
ResumeType identifies the kind of confirmation a user responded with.
const ( ResumeTypeApprove ResumeType = "approve" ResumeTypeApproveSession ResumeType = "approve-session" ResumeTypeApproveTool ResumeType = "approve-tool" ResumeTypeReject ResumeType = "reject" )
type ToolHandler ¶
type ToolHandler func(ctx context.Context, sess *session.Session, tc tools.ToolCall) (*tools.ToolCallResult, error)
ToolHandler is the signature for runtime-managed tool handlers (e.g. transfer_task, handoff, change_model). The dispatcher wraps every handler in tracing/telemetry/event-emission, so handlers MUST NOT emit ToolCall/ToolCallResponse themselves. Handlers that need to emit other event types should be wired by the caller to capture the relevant channel via closure when registering the handler.