Files
Arnaud Héritier 39edf8b77d fix: isolate think scratchpads by conversation
Persist caller- and instance-scoped thoughts in the session store, bind tool execution to the durable conversation root, and cover reload, concurrent isolation, and cleanup. Preserve Origin in same-ID session clones so retained thoughts remain addressable.

Signed-off-by: Arnaud Héritier <aheritier@users.noreply.github.com>
2026-10-14 08:27:50 +00:00

123 lines
5.3 KiB
Go

package tools
import (
"context"
"errors"
)
// Capability identifies an optional runtime facility a tool can probe for
// via [Runtime.Supports] before relying on it.
type Capability string
const (
// CapabilityOutput indicates [Runtime.EmitOutput] streams incremental
// output to a live consumer (UI, event feed).
CapabilityOutput Capability = "output"
// CapabilityRecall indicates [Runtime.Recall] can inject messages into
// a running agent loop.
CapabilityRecall Capability = "recall"
)
// ConfirmedRun describes an action a tool has to perform on the user's machine
// outside of its own tool call — today a shell command embedded in the body of
// a skill the tool just loaded. The host presents it as a tool call so the
// user learns about it and can reject it.
type ConfirmedRun struct {
// ToolName attributes the action to a tool: the host classifies,
// permission-matches and renders it as a call to that tool, so an
// embedded shell command uses the shell tool's name.
ToolName string
// Args are the tool-call arguments describing the action, e.g.
// {"cmd": "git status"}.
Args map[string]any
// Metadata carries extra key/value context for the approval prompt,
// such as the skill the action originates from.
Metadata map[string]string
}
// SkillContent is the exact skill text submitted for approval before use.
type SkillContent struct {
Name string `json:"name"`
Source string `json:"source"` // local, remote, or inline
Path string `json:"path,omitempty"`
Content string `json:"content"`
}
// Runtime is the tool-side handle to the hosting agent runtime, passed
// explicitly to every [ToolHandler]. Implementations must remain valid after
// the handler returns so background work can hold the handle; every method
// takes ctx from the caller instead of capturing one.
//
// Hosts without an agent loop pass [NopRuntime].
type Runtime interface {
// CheckSkillContent applies the host's mandatory skill policy before use.
CheckSkillContent(ctx context.Context, content SkillContent) error
// EmitOutput streams incremental output for the current tool call.
EmitOutput(ctx context.Context, output string)
// Recall injects a message into the agent loop, waking it if idle.
// Typically called when background work completes after the tool call
// that started it has already returned.
Recall(ctx context.Context, message string) error
// ConfirmAndRun obtains the user's approval for run and, once granted,
// calls exec and returns its output. Denied and unsupported actions
// return [ErrConfirmationDenied] / [ErrConfirmationUnsupported] without
// calling exec.
ConfirmAndRun(ctx context.Context, run ConfirmedRun, exec func(context.Context, ConfirmedRun) (string, error)) (string, error)
// Supports reports whether the host provides the named capability,
// letting tools fail fast before starting background work.
Supports(capability Capability) bool
}
// ToolRunner is an optional runtime facility for tools that invoke other tools.
// RunTool emits the nested call's lifecycle events without adding model-facing
// conversation messages. It preserves the handler's result and error semantics.
// Implementations must be safe for concurrent calls.
type ToolRunner interface {
RunTool(ctx context.Context, toolCall ToolCall, tool Tool) (*ToolCallResult, error)
}
// ScratchpadAppender is an optional host-owned, conversation-scoped think store.
// owner identifies the calling toolset instance, not a caller-supplied namespace.
type ScratchpadAppender interface {
AppendThought(ctx context.Context, owner any, thought string) ([]string, error)
}
// ErrRecallNotSupported is returned by [Runtime.Recall] implementations that
// cannot steer an agent loop.
var ErrRecallNotSupported = errors.New("recall is not supported by this host")
// ErrConfirmationDenied is returned by [Runtime.ConfirmAndRun] when the user,
// a permission rule or the session's safety mode refused the action.
var ErrConfirmationDenied = errors.New("action was not approved")
// ErrConfirmationUnsupported is returned by [Runtime.ConfirmAndRun] when the
// host has no way to ask the user. It is a denial too: an unattended host must
// not silently run what it cannot get approved.
var ErrConfirmationUnsupported = errors.New("cannot ask for approval here, so the action was skipped")
// ErrCallTimeout is returned when a tool call exceeds its configured
// call_timeout. Wrapped with the elapsed duration via fmt.Errorf("%w after
// %s", ErrCallTimeout, ...) — use errors.Is to detect it.
var ErrCallTimeout = errors.New("tool call timed out")
// NopRuntime is the [Runtime] for hosts without an agent loop (prompt
// templates, tests). EmitOutput is a no-op, Recall fails with
// [ErrRecallNotSupported], ConfirmAndRun refuses with
// [ErrConfirmationUnsupported], and no capability is supported.
type NopRuntime struct{}
var _ Runtime = NopRuntime{}
func (NopRuntime) EmitOutput(context.Context, string) {}
func (NopRuntime) Recall(context.Context, string) error { return ErrRecallNotSupported }
func (NopRuntime) ConfirmAndRun(context.Context, ConfirmedRun, func(context.Context, ConfirmedRun) (string, error)) (string, error) {
return "", ErrConfirmationUnsupported
}
func (NopRuntime) Supports(Capability) bool { return false }
// CheckSkillContent allows reads in hosts without an agent configuration.
func (NopRuntime) CheckSkillContent(context.Context, SkillContent) error { return nil }