toolexec

package
v1.53.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 28, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

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

View Source
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

func NewHooksInput(sess *session.Session, toolCall tools.ToolCall) *hooks.Input

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

func ParseToolInput(arguments string) map[string]any

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.

func ResolveModelOverride

func ResolveModelOverride(calls []tools.ToolCall, agentTools []tools.Tool) string

ResolveModelOverride returns the per-toolset model override from the given tool calls, or "" if none. When multiple tools specify different overrides, the first one wins.

Types

type CallOutcome

type CallOutcome struct {
	Canceled    bool
	StopRun     bool
	StopMessage string
}

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:

  1. yoloApproved (--yolo) — auto-allow everything.
  2. 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.
  3. readOnlyHint — auto-allow.
  4. 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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL