Documentation
¶
Overview ¶
Package tool defines the tool execution framework for Hygge and its built-in tools.
IsError vs ToolError — the most important distinction ¶
A Tool.Execute call has two failure axes that must be kept separate:
A Result with IsError: true is a NORMAL outcome the model is expected to handle. "File not found", "no matches", "command exited non-zero", and even "user denied permission" are all IsError results. They flow back to the model as ordinary tool output so the model can adapt its plan.
A returned error of type *ToolError is an INFRASTRUCTURE failure that prevents the tool from producing a meaningful Result at all. Bad JSON in the arguments, an offline permission engine, or a recovered panic all bubble up as ToolErrors and are surfaced to the user as a system fault — not as a tool message the model should reason about.
Tools never return both a Result and an error. Either the Result is usable (IsError: true or false) and err is nil, or the Result is the zero value and err is a *ToolError.
Permission gating ¶
Every tool that touches the filesystem, runs a process, or hits the network MUST call permission.Engine.Ask before performing the side effect. On permission.ActionDeny the tool returns an IsError Result (the user-deny case described above). On engine/bus failure during the ask, the tool returns a *ToolError with Code = CodePermissionDenied.
JSON Schema is the source of truth ¶
Tool.InputSchema returns a JSON Schema object that is passed verbatim to the provider so the model knows how to construct arguments. Tools decode the raw JSON into their own private args struct and validate the shape themselves; the schema is documentation for the model, not the runtime gate.
Streaming is opt-in ¶
Most tools return a single Result when they complete. Tools that produce incremental output (currently just "bash") publish bus.ToolCallProgress events for each chunk and still return a complete Result at the end. See the bash tool's documentation for details.
Parallel execution ¶
Tools that return true from Tool.Parallelizable may be invoked concurrently with other parallelizable tools within the same turn. Tools that return false are always executed serially after the parallel batch completes.
The contract for Parallelizable: return true only when the tool's effects are commutative with any sibling parallelizable tool that could run in the same turn. Read-only tools qualify; tools that mutate the filesystem, run shell commands, or hold shared mutable state must return false.
Built-in mapping:
- read, grep, glob, skill, task → Parallelizable() == true
- bash, write, edit, remember, forget, todo, question → Parallelizable() == false
Plugin tools default to false; opt in via the Lua registration table:
hygge.register_tool { ..., parallelizable = true, ... }
Index ¶
Constants ¶
const ( // CodeInvalidArgs is returned when the JSON arguments fail schema // validation or cannot be decoded into the tool's args struct. CodeInvalidArgs = "invalid_args" // CodePermissionDenied is returned when an unrecoverable permission // engine failure occurs (engine closed, bus closed, context cancelled // while waiting for a reply). An ordinary user "deny" decision is NOT // a ToolError — it is a Result with IsError: true. CodePermissionDenied = "permission_denied" // CodeExecutionFailed is returned for internal failures that prevent // the tool from running at all: panic recovery, missing dependencies, // or unrecoverable I/O setup errors. An ordinary "file not found" or // "command exited non-zero" is NOT a ToolError — it is a Result with // IsError: true. CodeExecutionFailed = "execution_failed" )
Error codes for ToolError.Code. These are intentionally small in number so callers can switch on them; new codes should be added sparingly.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type CompactTool ¶ added in v0.12.0
type CompactTool struct {
// contains filtered or unexported fields
}
CompactTool allows the model to trigger history compaction on the current session. Invoking the tool summarises older messages, writes a compaction marker, and resets the context window so subsequent turns start with a concise summary instead of the raw history.
The tool accepts no input parameters; all context is sourced from ExecContext (session ID).
func NewCompactTool ¶ added in v0.12.0
func NewCompactTool(compactor Compactor) *CompactTool
NewCompactTool builds a CompactTool backed by compactor. compactor must not be nil; callers are responsible for omitting the tool when no agent is wired.
func (*CompactTool) Description ¶ added in v0.12.0
func (t *CompactTool) Description() string
Description implements Tool.
func (*CompactTool) Execute ¶ added in v0.12.0
func (t *CompactTool) Execute(ctx context.Context, raw json.RawMessage, ec ExecContext) (Result, error)
Execute implements Tool.
func (*CompactTool) InputSchema ¶ added in v0.12.0
func (t *CompactTool) InputSchema() map[string]any
InputSchema implements Tool. The tool accepts no parameters.
func (*CompactTool) Name ¶ added in v0.12.0
func (t *CompactTool) Name() string
Name implements Tool.
func (*CompactTool) Parallelizable ¶ added in v0.12.0
func (t *CompactTool) Parallelizable() bool
Parallelizable implements Tool. Compaction mutates session state and is therefore not safe to run concurrently with other operations.
type Compactor ¶ added in v0.12.0
type Compactor interface {
// Compact summarises the session's pre-marker history and writes a new
// compaction marker. Returns [agent.ErrNothingToCompact] when the
// session contains too few messages to justify summarising.
//
// The concrete implementation is agent.Agent.Compact; see its
// documentation for full semantics (bus events, marker persistence,
// error behaviour).
Compact(ctx context.Context, sessionID string) error
}
Compactor is the interface the `compact` tool needs from the agent. Defining it here (rather than importing internal/agent) avoids a dependency cycle: internal/agent imports internal/tool, so the reverse direction must be cut.
*agent.Agent satisfies this interface; cmd/hygge/cli wires the concrete agent into NewCompactTool at bootstrap.
type DefaultOptions ¶
type DefaultOptions struct {
// SkillRegistry, when non-nil, causes the returned tool registry to
// include the "skill" tool wired to it. When nil the skill tool is
// omitted; the model never sees it in the tool list.
SkillRegistry *skill.Registry
TodoStore interface {
GetSessionTodos(ctx context.Context, sessionID string) ([]session.TodoItem, session.TodoSummary, error)
ReplaceSessionTodos(ctx context.Context, sessionID string, items []session.TodoItem) (session.TodoSummary, error)
}
SessionMemoryStore interface {
RememberSessionMemory(ctx context.Context, sessionID string, in session.NewMemory) (*session.Memory, error)
ForgetSessionMemory(ctx context.Context, sessionID, memoryID string) (*session.Memory, error)
}
FileMemoryStore interface {
Remember(ctx context.Context, scope session.MemoryScope, content string) (*session.Memory, error)
Forget(ctx context.Context, scope session.MemoryScope, memoryID string) (*session.Memory, error)
MemoryDir(scope session.MemoryScope) (string, error)
}
}
DefaultOptions configures DefaultWith. Add fields here when a new built-in needs caller-supplied dependencies.
type ExecContext ¶
type ExecContext struct {
// SessionID is the session that issued the tool call. Used to scope
// the read-tracker (anti-clobber) and to tag bus events.
SessionID string
// Pwd is the session's working directory; always an absolute path.
// Tools resolve relative arguments against this.
Pwd string
// Bus is the in-process event bus. Tools publish progress events
// here; the agent loop is responsible for forwarding them onward.
Bus *bus.Bus
// Permission is the permission engine. Tools call its Ask method
// before any side effect.
Permission *permission.Engine
// ToolUseID is the provider-assigned identifier for this tool call.
// Forwarded to bus events so subscribers can correlate progress with
// the originating call.
ToolUseID string
// MessageID is the conversation message the tool call belongs to.
// Forwarded to bus events.
MessageID string
// ModelName is the upstream model name the parent agent is using
// for the current turn. Tools that delegate to a fresh agent
// (currently just `task`) read this so the sub-agent inherits
// the parent's model. Other tools may ignore it.
ModelName string
// Now is an injectable time source. Defaults to time.Now via
// ensureNow when zero.
Now func() time.Time
}
ExecContext is the per-call runtime context handed to a tool.
The struct is intentionally narrow: tools should reach for nothing else in their environment. Add fields here when (and only when) a new tool genuinely needs them.
type QuestionTool ¶ added in v0.4.0
type QuestionTool struct{}
QuestionTool asks the user to choose between bounded options and returns the selected answer to the model.
func NewQuestionTool ¶ added in v0.4.0
func NewQuestionTool() *QuestionTool
NewQuestionTool constructs the interactive question tool.
func (*QuestionTool) Description ¶ added in v0.4.0
func (t *QuestionTool) Description() string
Description implements Tool.
func (*QuestionTool) Execute ¶ added in v0.4.0
func (t *QuestionTool) Execute(ctx context.Context, raw json.RawMessage, ec ExecContext) (Result, error)
Execute implements Tool.
func (*QuestionTool) InputSchema ¶ added in v0.4.0
func (t *QuestionTool) InputSchema() map[string]any
InputSchema implements Tool.
func (*QuestionTool) Name ¶ added in v0.4.0
func (t *QuestionTool) Name() string
Name implements Tool.
func (*QuestionTool) Parallelizable ¶ added in v0.4.0
func (t *QuestionTool) Parallelizable() bool
Parallelizable implements Tool.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry holds a named set of tools and the cross-tool state they share (currently just the read-tracker used for anti-clobber on write/edit).
Registries are safe for concurrent use; Register may be called from any goroutine and Get/All/AsProviderTools never block writers.
func Default ¶
func Default() *Registry
Default returns a Registry preloaded with the built-in tools. The returned registry owns its own read-tracker and todo store; the built-ins are wired to use them.
Equivalent to DefaultWith(DefaultOptions{}).
func DefaultWith ¶
func DefaultWith(opts DefaultOptions) *Registry
DefaultWith returns a Registry preloaded with the built-in tools, plus any optional tools enabled by opts. Callers that need the skill tool pass DefaultOptions{SkillRegistry: reg}.
func NewRegistry ¶
func NewRegistry() *Registry
NewRegistry returns an empty Registry with a fresh read-tracker.
func (*Registry) All ¶
All returns every registered tool, sorted by name. The returned slice is a fresh copy; mutating it does not affect the registry.
func (*Registry) AsProviderTools ¶
AsProviderTools converts the registry into the slice the provider layer consumes when constructing a provider.Request. Output is sorted by name so the model sees a deterministic tool list.
func (*Registry) Get ¶
Get returns the tool registered under name and true, or (nil, false) if no such tool exists.
func (*Registry) ReadTracker ¶
func (r *Registry) ReadTracker() *readTracker
ReadTracker returns the registry's anti-clobber read tracker. Exposed for tests and for callers that build their own tool set sharing the same tracker as the built-ins.
func (*Registry) Register ¶
Register adds t to the registry under t.Name(). Returns an error when the name is empty or already in use; the registry is unchanged on error.
func (*Registry) Unregister ¶ added in v0.4.0
Unregister removes the tool registered under name. It is a no-op when the name is not present.
type Result ¶
type Result struct {
// Content is the text representation of the result that flows back to
// the model. For binary or structured payloads, render a compact
// human-readable summary here and attach the raw form to Metadata.
Content string
// IsError is true when the tool surfaced a logical error the model
// must handle (file not found, command failed, permission denied,
// ...). See package doc for the IsError-vs-ToolError distinction.
IsError bool
// Metadata is structured information about the call: bytes written,
// lines returned, exit codes, durations. The agent loop persists
// this alongside the tool message for audit and replay.
Metadata map[string]any
}
Result is the successful (or logical-error) outcome of a tool call.
type SubagentResult ¶
type SubagentResult struct {
SessionID string
FinalText string
Usage provider.Usage
CostUSD float64
Duration time.Duration
}
SubagentResult mirrors internal/subagent.Result with the subset of fields the subagent tool surfaces in its Metadata.
type SubagentRunInput ¶
type SubagentRunInput struct {
ParentSessionID string
ParentToolUseID string
Type string
Description string
Prompt string
ModelName string
}
SubagentRunInput is the tool-facing view of internal/subagent.RunInput.
type SubagentRunner ¶
type SubagentRunner interface {
// Run executes one sub-agent invocation synchronously and
// returns the result. See internal/subagent.Runner.Run for full
// semantics.
Run(ctx context.Context, in SubagentRunInput) (SubagentResult, error)
// Types returns the registered sub-agent types (name +
// description) so the tool's input-schema enum can be built
// lazily. The runner returns a fresh slice on each call.
Types() []SubagentType
}
SubagentRunner is the interface the `subagent` tool needs from the sub-agent runtime. Defining it here (rather than importing internal/subagent) keeps the tool package free of an agent / store dependency loop: internal/subagent imports internal/tool to build a per-call tool registry, so the reverse direction must be cut.
internal/subagent.Runner satisfies this interface; cmd/hygge/cli wires the concrete runner into NewSubagentTool at bootstrap.
type SubagentTool ¶
type SubagentTool struct {
// contains filtered or unexported fields
}
SubagentTool dispatches a mission to a registered sub-agent type. The tool blocks until the sub-agent finishes (success or hard error) and returns the sub-agent's final assistant text as the tool result.
The tool is registered ONLY in the orchestrator's tool registry. Sub-agents NEVER see it -- the subagent runtime strips it from every sub-agent's tool set regardless of TOML config. This is the recursion guard that prevents a `subagent` tool from launching another `subagent` tool.
Permission category: permission.CategoryAgent. One ask covers the entire sub-agent run; individual tools the sub-agent invokes still go through their own permission gate (same engine).
func NewSubagentTool ¶
func NewSubagentTool(runner SubagentRunner) *SubagentTool
NewSubagentTool builds a SubagentTool backed by runner. runner must not be nil; callers building the orchestrator's tool set are responsible for omitting the tool entirely when no runner is configured.
func (*SubagentTool) Description ¶
func (t *SubagentTool) Description() string
Description implements Tool.
func (*SubagentTool) Execute ¶
func (t *SubagentTool) Execute(ctx context.Context, raw json.RawMessage, ec ExecContext) (Result, error)
Execute implements Tool. See the Stage A design doc for the failure mode summary; in short:
- Unknown subagent_type -> IsError result, no sub-session created.
- Permission denied -> IsError result from the shared askPermission helper, no sub-session created.
- Sub-agent run failure -> IsError result with the sub-session id in Metadata so the user can inspect the audit trail.
- Success -> the sub-agent's final assistant text as Content.
func (*SubagentTool) InputSchema ¶
func (t *SubagentTool) InputSchema() map[string]any
InputSchema implements Tool. The schema is built lazily so newly loaded TOML types (if any) become visible to the model on the next request.
func (*SubagentTool) Parallelizable ¶
func (t *SubagentTool) Parallelizable() bool
Parallelizable implements Tool. Each sub-agent runs in an isolated session with its own message history, so concurrent subagent calls are safe: they do not share per-turn state within the parent session.
Note: tools invoked INSIDE a sub-agent still go through their own permission checks, and sub-agents share the parent's permission engine. The engine's session cache is mutex-guarded, so concurrent sub-agent dispatches are safe.
type SubagentType ¶
SubagentType is the tool-facing view of an internal/subagent.Type. We mirror only the fields the subagent tool needs for its input schema.
type Tool ¶
type Tool interface {
// Name is the stable identifier the model uses to invoke this tool.
// Names must be unique within a registry and match the regular
// expression [a-z][a-z0-9_]*.
Name() string
// Description is the human-language summary surfaced to the model in
// the provider's tool-list payload. Keep it terse: one or two
// sentences explaining what the tool does and when to use it.
Description() string
// InputSchema returns a JSON Schema object describing the tool's
// arguments. The schema is shipped verbatim to the provider.
// Implementations should return a fresh map per call so callers can
// mutate it without affecting other tools.
InputSchema() map[string]any
// Execute runs the tool with the supplied raw JSON arguments and
// returns either a [Result] (with err == nil) or a *ToolError (with
// Result as the zero value). See the package doc for the
// IsError-vs-ToolError distinction.
Execute(ctx context.Context, args json.RawMessage, ec ExecContext) (Result, error)
// Parallelizable reports whether this tool is safe to invoke
// concurrently with other parallelizable tools in the same turn.
//
// Return true only when the tool's effects are commutative with any
// sibling parallelizable call in the same turn. Read-only tools
// (read, grep, glob, skill, task) return true. Tools that mutate
// the filesystem or run shell commands (bash, write, edit) return
// false.
//
// The agent loop runs all parallelizable calls in a single concurrent
// batch, then runs the sequential calls serially after the batch
// completes. Bus events from siblings within the parallel batch
// arrive in undefined order; subscribers must not rely on
// intra-batch ordering.
//
// Plugin tools default to false; they opt in via the registration
// struct or the Lua `parallelizable = true` key.
Parallelizable() bool
}
Tool is the interface every tool implements.
Implementations must be safe for concurrent Execute calls from many goroutines. The framework does not serialise tool calls.
func NewSkillTool ¶
NewSkillTool builds a skillTool backed by reg. reg may be nil; in that case the tool always returns an IsError result with a "no skills configured" message.
type ToolError ¶
type ToolError struct {
// Code is one of the Code* constants.
Code string
// Message is a short human-readable description.
Message string
// Wrapped is the underlying error if any. Unwrap returns it so
// errors.Is/As work across the boundary.
Wrapped error
}
ToolError is a transport-level failure that prevents the tool from producing a Result at all. See the package documentation for the IsError-vs-ToolError distinction; in short:
- A Result with IsError: true is a normal outcome the model handles (file not found, command failed, permission denied).
- A ToolError is an infrastructure problem (bad JSON arguments, internal panic, engine offline) that the agent loop must surface as a system-level failure.