tools

package
v1.0.1 Latest Latest
Warning

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

Go to latest
Published: Jun 20, 2026 License: MIT Imports: 33 Imported by: 0

Documentation

Overview

Package tools defines the tool interface the agent loop dispatches against, the deferred-tool flag that keeps big specs out of the default LLM manifest, and the built-in `tool_search` hook the model uses to fetch deferred specs.

Tool design rule: every tool with a long Description, examples, or a complex Parameters schema MUST set Deferred = true. The default LLM manifest then shows only Name + the first sentence of Description for those tools. The model loads the full spec on demand by calling `tool_search`, which protects the prompt cache (no per-turn manifest bloat) and lets the registry scale to N tools without context cost.

Index

Constants

View Source
const (
	KindClarification = "clarification"
	KindApproval      = "approval"
	KindChoice        = "choice"
)

Kind values constrain how the channel renders the pause (D-A3-02).

Variables

View Source
var ErrNoNonDeferredTool = errors.New("registry: at least one non-deferred tool is required (excluding tool_search)")

ErrNoNonDeferredTool is returned by Registry.Validate when no actionable (non-deferred) capability tool is registered, excluding the always-on tool_search hook. It mirrors Anthropic's hard 400 ("At least one tool must be non-deferred"): a registry where every capability is deferred would let the model only search, never act.

Functions

func ShellApprovalDigest

func ShellApprovalDigest(command, cwd string) string

ShellApprovalDigest normalizes cwd with filepath.Clean before hashing (AG-018) so cosmetic variants (/tmp vs /tmp/) yield one digest and an approved command is not re-prompted on retry. An empty cwd stays empty (Clean would yield ".").

func WithToolCallContext

func WithToolCallContext(ctx context.Context, sessionID, toolCallID, runDir string, previewCap int) context.Context

WithToolCallContext returns a ctx carrying the ids + run dir + preview cap the spillover helper reads. The agent calls this before each Tool.Execute (D-25).

Types

type ActionFunc

type ActionFunc func(ctx context.Context, args json.RawMessage) (ToolResult, error)

ActionFunc is one action handler: it receives the still-raw tool arguments (the whole object, including the `action` field) and returns a ToolResult or an error. Handlers re-unmarshal the fields they need from raw — the router does not pre-parse beyond the `action` discriminator.

type ActionRouter

type ActionRouter struct {
	// contains filtered or unexported fields
}

ActionRouter dispatches a single tool's `action` enum to a per-action handler (D-09). It is the first consumer of this pattern — the `task` tool routes schedule|list|cancel|run_now through one Execute — and is kept generic (no cron-specific types) so Slice 7 (skill_*) reuses it verbatim. The point of the router is to keep ONE manifest entry and ONE OpenAI-wire-safe schema per multi-action tool instead of N near-duplicate tool files (the pre-rewrite 587-LOC scheduler.go god-tool is the anti-pattern this replaces).

func NewActionRouter

func NewActionRouter(handlers map[string]ActionFunc) *ActionRouter

NewActionRouter builds a router from an action→handler map. The map is copied so a later mutation of the caller's map cannot change dispatch behavior.

func (*ActionRouter) Actions

func (r *ActionRouter) Actions() []string

Actions returns the registered action names, sorted — for building the schema's `action` enum and for the unknown-action error message (a stable, testable order).

func (*ActionRouter) Dispatch

func (r *ActionRouter) Dispatch(ctx context.Context, action string, args json.RawMessage) (ToolResult, error)

Dispatch routes action to its handler. An unknown action returns a structured error (never a panic) naming the valid actions, so a model that hallucinates an action gets an actionable correction rather than a crash.

type AskUser

type AskUser struct{}

AskUser is the non-deferred HITL pause primitive (PRD 1.5, SPEC Req#1). The model calls it to ask the user a structured question; instead of returning a ToolResult it returns the *ErrAwaitingUserInput sentinel, which the agent's dispatch loop intercepts (llm_agent_pause.go) to pause the turn — it NEVER produces a RoleTool result. Pure types only: this file imports no DB/sqlc package (D-A1-04). Durability lives in askuser.Store, wired by the Runner.

ask_user is a deliberate primitive, never auto-fired (D-A3-04): the model is made tool-aware without being encouraged to overuse it.

func (AskUser) Execute

func (AskUser) Execute(_ context.Context, raw json.RawMessage) (ToolResult, error)

Execute validates the arguments and returns the *ErrAwaitingUserInput sentinel on success (SPEC Req#1) — never a populated ToolResult. Validation rejects an empty question, an options count of exactly 1, non-distinct option labels, and a priority outside 0-100 (threat T-04-09). The returned ToolResult is always the zero value.

func (AskUser) Spec

func (AskUser) Spec() Spec

type BackgroundShells

type BackgroundShells struct {
	// contains filtered or unexported fields
}

BackgroundShells is the process-scoped registry of running/finished background shells. ONE instance is shared by shell_exec, shell_poll, and shell_kill at registration so a job started in one turn stays pollable/killable in a later turn (the tool instances outlive any single LlmAgent — the registry is built once at boot). Concurrency-safe.

func NewBackgroundShells

func NewBackgroundShells() *BackgroundShells

NewBackgroundShells builds an empty registry to share across the shell tools.

func (*BackgroundShells) Evict added in v1.0.0

func (b *BackgroundShells) Evict(string)

Evict reclaims finished background shells (SessionEvictor, AG-015). Background shells are process-scoped (not session-keyed), so any FINISHED shell is reclaimable regardless of the evicted session — a long-lived daemon must not accumulate finished-but-unpolled buffers (≤1 MiB each) until the next start. Running shells are left untouched. The sessionID arg satisfies the interface; pruning is by completion, not session.

func (*BackgroundShells) Shutdown

func (b *BackgroundShells) Shutdown(ctx context.Context) error

type CreateTaskInput

type CreateTaskInput struct {
	Kind         string
	ScheduleKind string
	CronExpr     string
	EveryMinutes int
	RunAt        time.Time
	TZ           string
	Payload      []byte
	StepBudget   int
	NotifyRoute  string
	Status       string
	NextRunAt    time.Time
	// OriginConversationID is forwarded from toolCallCtx(ctx).sessionID — the
	// conversation that scheduled the task; "" for a bare ctx (CLI / unit test).
	// The tool only forwards the raw id; the conv→identity snapshot happens in the
	// cmd/aura adapter (so tools never imports internal/conversations).
	OriginConversationID string
}

CreateTaskInput carries a resolved, validated task the tool asks the store to persist. Status is set by the tool (active, or pending_approval when scoring gates it). NextRunAt is the first fire the tool computed.

type CurrentTime

type CurrentTime struct{}

CurrentTime is a non-deferred builtin that returns the current instant as an RFC-3339 string. It is the ONLY path that reads the wall clock for the model (D-08): the live clock never enters the cached system prompt / messages[0], so the prompt prefix stays byte-stable and the KV cache is not poisoned.

func (CurrentTime) Execute

func (CurrentTime) Spec

func (CurrentTime) Spec() Spec

type DocumentSearch

type DocumentSearch struct {
	Searcher DocumentSearchBackend
}

func (*DocumentSearch) Execute

func (t *DocumentSearch) Execute(ctx context.Context, raw json.RawMessage) (ToolResult, error)

func (*DocumentSearch) Spec

func (t *DocumentSearch) Spec() Spec

type DocumentSearchBackend

type DocumentSearchBackend interface {
	Search(ctx context.Context, req documents.SearchRequest) ([]documents.SearchHit, error)
}

type ErrAwaitingUserInput

type ErrAwaitingUserInput struct {
	Question   string
	Options    []Option
	Kind       string
	Priority   int
	ToolCallID string
	// ResumeContext is optional machine-readable context for the caller that will
	// handle the human answer. It is persisted by the Runner but never rendered as
	// answer text; e.g. skill approvals carry {"type":"skill_approval","skill_name":"x"}.
	ResumeContext json.RawMessage
	// ProxiedFromChildID / ProxiedToolCallID are the optional swarm-relay ids the
	// model MAY fill when this ask_user relays a child's needs_user_input report
	// (D-05). Empty on a direct (non-proxied) pause; they stamp into
	// aura.paused_states so a future resume can map the answer back to the child.
	ProxiedFromChildID string
	ProxiedToolCallID  string
}

ErrAwaitingUserInput is the pause sentinel ask_user.Execute returns on a valid call (D-A1-04). It is a struct error (not a bare errors.New) so the dispatch loop can errors.As(err, &target) and carry the pause payload into the Actions.AwaitingInput Event. ToolCallID is stamped by the agent before the Event is emitted (the tool itself does not see the call id).

func (*ErrAwaitingUserInput) Error

func (e *ErrAwaitingUserInput) Error() string

Error makes *ErrAwaitingUserInput satisfy the error interface. The message is generic; the payload travels in the struct fields, not the string.

type FSEdit

type FSEdit struct{ WorkspaceRoot, SkillsDir string }

FSEdit replaces an exact string in a file. Like Claude Code's Edit, old_string must match exactly and be unique unless replace_all is set. SkillsDir, when set, fences edits out of the skills library (#54 / D-43); empty disables the fence.

func (*FSEdit) Execute

func (t *FSEdit) Execute(ctx context.Context, raw json.RawMessage) (ToolResult, error)

func (*FSEdit) Spec

func (t *FSEdit) Spec() Spec

type FSGlob

type FSGlob struct{ WorkspaceRoot string }

FSGlob finds files by name pattern (supporting **) across a directory tree.

func (*FSGlob) Execute

func (t *FSGlob) Execute(ctx context.Context, raw json.RawMessage) (ToolResult, error)

func (*FSGlob) Spec

func (t *FSGlob) Spec() Spec

type FSGrep

type FSGrep struct{ WorkspaceRoot string }

FSGrep searches file contents for a regexp across a directory tree, in-process.

func (*FSGrep) Execute

func (t *FSGrep) Execute(ctx context.Context, raw json.RawMessage) (ToolResult, error)

func (*FSGrep) Spec

func (t *FSGrep) Spec() Spec

type FSRead

type FSRead struct{ WorkspaceRoot string }

FSRead reads a file from the host filesystem in-process (no sandbox hop).

func (*FSRead) Execute

func (t *FSRead) Execute(ctx context.Context, raw json.RawMessage) (ToolResult, error)

func (*FSRead) Spec

func (t *FSRead) Spec() Spec

type FSWrite

type FSWrite struct{ WorkspaceRoot, SkillsDir string }

FSWrite writes (creating or overwriting) a file on the host filesystem. SkillsDir, when set, fences writes out of the skills library so the gated skill-authoring flow cannot be bypassed (#54 / D-43); empty disables the fence.

func (*FSWrite) Execute

func (t *FSWrite) Execute(ctx context.Context, raw json.RawMessage) (ToolResult, error)

func (*FSWrite) Spec

func (t *FSWrite) Spec() Spec

type ManifestEntry

type ManifestEntry struct {
	Name        string
	Summary     string
	Description string
	Parameters  []byte
	Deferred    bool
}

ManifestEntry is one row in the LLM-visible tool manifest. Deferred tools contribute only Name + Summary; non-deferred contribute the full Description + Parameters schema.

type Option

type Option struct {
	Label string `json:"label"`
	Value string `json:"value"`
}

Option is one selectable answer for kind=choice. The model may supply either a bare string (decoded as {Label, Value} both set to the string) or an explicit {label, value} object. Distinct labels are enforced in validation.

func (*Option) UnmarshalJSON

func (o *Option) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts either a JSON string ("foo" → {Label:"foo", Value:"foo"}) or a {label, value} object, so the SPEC Req#1 `options?:[2-4 string|{label,value}]` shape decodes uniformly.

type ReadToolOutput

type ReadToolOutput struct{}

ReadToolOutput pages byte ranges out of a sidecar file written by NewResult when a prior tool output exceeded the preview cap. Non-deferred builtin: the model calls it to fetch the full bytes a truncated preview pointed at (D-27).

func (ReadToolOutput) Execute

func (ReadToolOutput) Spec

func (ReadToolOutput) Spec() Spec

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry holds the set of tools available to one agent. It is built at startup and stays immutable for the lifetime of an agent run (swarm-spawned children may receive a filtered copy).

func NewRegistry

func NewRegistry() *Registry

func Without

func Without(parent *Registry, names ...string) *Registry

Without returns a FRESH registry holding every tool in parent EXCEPT those whose Spec().Name is listed in names. The parent registry is never mutated (it is immutable for the lifetime of a run — spec.go), so a derived registry that drops a tool (e.g. swarm_spawn for swarm workers per D-08/D-10, or swarm_spawn for an agent_job child per D-13) can be built per call without touching the parent.

Promoted out of internal/swarm so internal/cron can consume it without importing internal/swarm (D-24 forbids that import; OQ2 carve-out).

func (*Registry) All

func (r *Registry) All() []Tool

func (*Registry) Get

func (r *Registry) Get(name string) (Tool, bool)

func (*Registry) Register

func (r *Registry) Register(t Tool)

Register adds a tool to the registry, keyed by its Spec().Name. Registration is a boot-time operation (the registry is immutable for the lifetime of a run), so a duplicate name is a programming error — two tools fighting for one name, where a silent overwrite would shadow the first tool and leave the model dispatching against whichever happened to register last. It therefore FAILS LOUD with a panic at registration time, the same way net/http panics on a duplicate route. Register has no error return because no caller can sensibly recover from a static wiring collision: fix the wiring.

func (*Registry) Render

func (r *Registry) Render() []ManifestEntry

Render returns the manifest as a stable-ordered slice — alphabetical by Name for cache stability. Stable ordering matters: any reshuffle invalidates the provider-side prompt cache. See [[feedback_aura_cache_poisoning_sites_2026-05-27]].

func (*Registry) RenderText

func (r *Registry) RenderText() string

RenderText is a human-readable rendering of the manifest, useful for boot logs and `aura tools` CLI subcommand.

func (*Registry) RenderToolDefs

func (r *Registry) RenderToolDefs() []llm.ToolDef

RenderToolDefs maps the stable-ordered manifest to the []llm.ToolDef wire shape LlmAgent.Run puts in req.Tools. It REUSES the alphabetical ordering of Render() (cache-stability-load-bearing, manifest.go sort — feedback_aura_cache_poisoning_sites); it does NOT re-sort. Deferred tools contribute only Name + Summary (their full Description + Parameters schema stay hidden until tool_search loads them, per the deferred-tool pattern), so a deferred entry's wire Description falls back to its Summary and its Parameters are empty until promoted. Non-deferred tools carry their full Description + Parameters JSON schema.

func (*Registry) Validate

func (r *Registry) Validate() error

Validate fails closed when no actionable (non-deferred) capability tool is registered, mirroring Anthropic's hard 400. tool_search is excluded from the count because it is a non-deferrable discovery hook, never an actionable tool — dropping that exclusion would let a search-only registry pass (RESEARCH Pitfall 5). Call it once at boot after all Register calls (the registry is empty at construction, so a constructor-time check is impossible).

type ScheduledTask

type ScheduledTask struct {
	ID           string
	Kind         string
	ScheduleKind string
	Status       string // active | pending_approval | cancelled | ...
	NextRunAt    time.Time
	RiskTier     string
}

ScheduledTask is the tool-local projection of a scheduler row the task tool renders/operates on. It is deliberately decoupled from internal/cron's domain type so the tools package declares its own seam (interface segregation): the live cron store adapts its rows into this shape at registration (10-05).

type SendFile

type SendFile struct {
	// WorkspaceRoot, when set, fences delivery to files whose real path stays
	// inside this workspace. Empty preserves legacy unrestricted delivery in a CLI
	// context (the operator drives the agent directly).
	WorkspaceRoot string
	// RequireWorkspace makes the fence fail CLOSED when WorkspaceRoot is empty
	// (AG-019): in a non-CLI context (a channel-driven agent) an unset workspace
	// must NOT silently grant unrestricted host-file delivery. The composition root
	// sets it true for non-CLI runners; CLI keeps it false (legacy behavior).
	RequireWorkspace bool
}

SendFile is the channel-agnostic artifact delivery tool (D-05): the model calls it with an absolute path (and an optional caption) when IT decides to deliver a file to the user. There is NO renderer auto-detect — delivery is an explicit agent action. The tool reads the path, gates the size, and emits a generic artifact descriptor on the ToolResult's Meta; the agent loop lifts that onto Actions.ArtifactDelta (toolResultEvent), and the AG-UI translator maps it to a custom event each channel renders its own way (D-06). The substrate never names any channel — SendFile is delivery-mechanism unaware.

It is Deferred (path/caption schema + an inline example, the deferred-tool rule) and NON-Mutating (it reads a file and describes a delivery — no host state changes), so it never arms the completion-gate critic.

func (*SendFile) Execute

func (s *SendFile) Execute(_ context.Context, raw json.RawMessage) (ToolResult, error)

Execute reads the path, gates the size, and on success sets res.Meta with a channel-agnostic artifact descriptor {path, filename, caption}. The tool CANNOT stamp the Event itself (the inbound tool-call ctx is read-only); the agent loop's toolResultEvent lifts the Meta artifact onto Actions.ArtifactDelta. A too-large or unreadable path returns an inline error result (NOT a Go error) the model self-corrects on, with NO artifact Meta.

func (*SendFile) Spec

func (s *SendFile) Spec() Spec

type SessionEvictor

type SessionEvictor interface {
	Evict(sessionID string)
}

SessionEvictor is implemented by tools that hold per-session (per-conversation) state keyed by the WithToolCallContext session id. In a long-running `serve` daemon a finished conversation's tool state would otherwise accumulate forever (audit R-41 / AP-16) — a slow unbounded leak. The Runner calls Evict on every registry tool implementing this interface when a conversation's lifecycle ends (sessionID == conversationID per D-26). Evict MUST be idempotent (an unknown id is a no-op) and concurrency-safe (it holds the same lock the state uses).

type ShellApprovalChallenge

type ShellApprovalChallenge struct {
	Command  string
	Cwd      string
	Digest   string
	Question string
}

type ShellApprovals

type ShellApprovals struct {
	// contains filtered or unexported fields
}

ShellApprovals stores one-shot shell command approvals keyed by conversation session and command digest. Consume deletes the approval so a retry can run once.

func NewShellApprovals

func NewShellApprovals() *ShellApprovals

func (*ShellApprovals) Approve

func (a *ShellApprovals) Approve(sessionID, digest string)

func (*ShellApprovals) ApproveChallenge

func (a *ShellApprovals) ApproveChallenge(sessionID, digest, question string) error

func (*ShellApprovals) Consume

func (a *ShellApprovals) Consume(sessionID, digest string) bool

func (*ShellApprovals) CreateChallenge

func (a *ShellApprovals) CreateChallenge(sessionID, command, cwd string) ShellApprovalChallenge

func (*ShellApprovals) Evict

func (a *ShellApprovals) Evict(sessionID string)

Evict reclaims a finished session's approval ledger (SessionEvictor, R-41). The maps are keyed by sessionID+"\x00"+digest, so every entry under the session prefix is dropped. An unknown session id is a no-op; concurrency-safe under a.mu.

func (*ShellApprovals) PendingChallenge

func (a *ShellApprovals) PendingChallenge(sessionID, digest string) (ShellApprovalChallenge, bool)

type ShellExec

type ShellExec struct {
	// WorkspaceRoot is the default working directory when a call omits cwd and no
	// tracked cwd exists yet. Empty → the Aura process's current working directory.
	WorkspaceRoot string
	// DefaultTimeout caps a call that omits timeout_ms. Zero → defaultShellTimeout.
	DefaultTimeout time.Duration

	// Background, when set, is the shared registry that holds jobs started with
	// "background": true so shell_poll/shell_kill (wired to the same registry) can
	// read and stop them across turns. Nil → background mode is unavailable.
	Background *BackgroundShells

	// Approvals is the one-shot ledger for commands matching
	// AURA_SHELL_DESTRUCTIVE_PATTERNS. Nil fails closed for configured matches.
	Approvals *ShellApprovals
	// contains filtered or unexported fields
}

ShellExec is Aura's keystone host tool: a full terminal. It runs a command line through the host system shell, in-process, with the operator's own privileges — the same power Claude Code's Bash tool has. There is no sandbox hop and no path fence: for a single trusted operator on their own machine the host shell IS the capability (amendment #50 / D-15c). For a long job, "background": true returns a shell_id read via shell_poll and stopped via shell_kill.

func (*ShellExec) Evict

func (s *ShellExec) Evict(sessionID string)

Evict reclaims a finished session's tracked working directory (SessionEvictor, R-41). An unknown session id is a no-op; concurrency-safe under s.mu.

func (*ShellExec) Execute

func (s *ShellExec) Execute(ctx context.Context, raw json.RawMessage) (ToolResult, error)

func (*ShellExec) Spec

func (s *ShellExec) Spec() Spec

type ShellKill

type ShellKill struct {
	Shells *BackgroundShells
}

ShellKill terminates a background shell_exec job — Claude Code's KillBash.

func (*ShellKill) Execute

func (k *ShellKill) Execute(ctx context.Context, raw json.RawMessage) (ToolResult, error)

func (*ShellKill) Spec

func (k *ShellKill) Spec() Spec

type ShellPoll

type ShellPoll struct {
	Shells *BackgroundShells
}

ShellPoll reads new output from a background shell_exec job — Claude Code's BashOutput. Non-mutating: it only reads accumulated output + status.

func (*ShellPoll) Evict added in v1.0.0

func (p *ShellPoll) Evict(sessionID string)

Evict forwards a conversation-end signal to the shared background-shell registry, reclaiming finished shells (SessionEvictor, AG-015). ShellPoll is a registry tool, so the Runner's evict loop reaches the process-scoped registry through it; the prune is by completion, not by session.

func (*ShellPoll) Execute

func (p *ShellPoll) Execute(ctx context.Context, raw json.RawMessage) (ToolResult, error)

func (*ShellPoll) Spec

func (p *ShellPoll) Spec() Spec

type SkillMeta

type SkillMeta struct {
	Name        string
	Description string
}

SkillMeta is the tool-local projection of a loaded skill the manifest renders over. It is deliberately decoupled from internal/skills.Skill so the tools package declares its own seam (interface segregation): the live loader adapts its skills into this shape at registration.

type SkillTool

type SkillTool struct {
	Loader skillLoader
	// Writer is the consumer-declared write seam the create/update/delete actions
	// dispatch against (11-05). Nil on the pool-free manifest paths (`aura tools`)
	// and in unit tests that exercise only the read actions — a write action without
	// a writer returns a clear error, never a panic.
	Writer skillWriter
	// Alerter is the optional headless-alert seam (D-26): non-nil only in a context
	// with no interactive resume (a swarm worker / cron job) so a gated mutation
	// proposed there fires an immediate operator alert. Nil in the interactive REPL.
	Alerter skillAlerter
	// contains filtered or unexported fields
}

SkillTool is the ONE non-deferred skills verb the model sees (D-01/D-05). A single manifest entry — name "skill" — fronts the whole skills grammar via an `action` enum dispatched through an ActionRouter, mirroring the `task` tool (the pre-rewrite N-tool god-class is the anti-pattern this replaces). It is NON-deferred (D-05): the skill manifest the model needs to pick a skill rides in this tool's Description, so the spec must always be visible — a deferred skill tool would hide the very manifest the model searches.

This tool wires the READ actions list|info|use, the authoring actions create|update|delete (gated, pending→approve), and the snippet-lifecycle actions save_snippet|restore|archive (18-03): save_snippet stages a reusable snippet UNGATED (D-02 — normal result, never a pause), restore un-archives a snippet, archive de-materializes one (SAFE tier). Discovery+install of third-party skills is NOT a tool concern (amendment #51 / D-40): the always-on find-skills skill teaches the model to self-extend via `npx skills find/add` in the sandbox, so the catalog client + native installer + their tool actions were deleted.

The live loader is injected at registration via the consumer-declared skillLoader seam below (golang-structs-interfaces: the consumer owns the interface), so this package never imports internal/skills concretely — the live *skills.Loader is adapted at the composition root (cmd/aura). The Description is computed from the loader snapshot at build time (turn-stable, busts the prefix cache ONCE on add/remove — accepted, D-06).

func (*SkillTool) Execute

func (t *SkillTool) Execute(ctx context.Context, raw json.RawMessage) (ToolResult, error)

Execute parses the `action` discriminator and dispatches through the ActionRouter (lazily built once, bound to this tool's loader). It never panics on a bad action — the router returns a structured error.

func (*SkillTool) Spec

func (t *SkillTool) Spec() Spec

Spec returns the non-deferred manifest entry (D-05). The Description IS the turn-stable, alphabetical skill manifest (D-06) computed from the loader snapshot — the model reads it to choose a skill, then calls action=use.

type Spec

type Spec struct {
	Name        string
	Summary     string          // one line, always shown in the manifest
	Description string          // full description; only shown when not Deferred OR after a tool_search hit
	Parameters  json.RawMessage // JSON-schema for the tool arguments
	Deferred    bool            // true → full spec hidden until tool_search loads it
	// Mutating marks a tool that can change host state (write a file, run a
	// command, mutate the sandbox). The agent's completion gate (amendment #54 /
	// D-43) only runs its critic on a turn that dispatched at least one mutating
	// tool — a pure read/chat turn skips the gate at zero extra cost. It is NOT
	// LLM-visible (never wire-encoded); it is a runtime hint only. Conservative by
	// design: shell_exec is Mutating even though `ls` does not mutate, because the
	// agent cannot know statically whether a command writes.
	Mutating bool
}

Spec is the LLM-visible metadata for a tool.

type SwarmSpawn

type SwarmSpawn struct {
	Runner   swarmRunner
	MaxGoals int
}

SwarmSpawn is the Deferred:true fan-out tool (D-01). It validates the goals cap (D-13) and delegates to the injected swarmRunner; it constructs no worker and imports no engine package itself, so the tools package stays cycle-free. Runner is wired at the composition root (cmd/aura) to the internal/swarm adapter; MaxGoals comes from config (AURA_SWARM_MAX_GOALS).

func (*SwarmSpawn) Execute

func (e *SwarmSpawn) Execute(ctx context.Context, raw json.RawMessage) (ToolResult, error)

Execute unmarshals {goals}, rejects an over-cap call with a model-readable inline error (D-13, no runner call), and otherwise delegates to the injected runner. A missing runner is a real Go error (composition-root wiring bug); domain rejections ride in the NewResult string so the model self-corrects.

func (*SwarmSpawn) Spec

func (e *SwarmSpawn) Spec() Spec

type TaskTool

type TaskTool struct {
	Store taskStore
	// AlertThreshold is the risk tier at/above which a scheduled task fires an
	// immediate alert (config owns AURA_RISK_ALERT_THRESHOLD; scoring takes it as
	// an argument, never reads env). Empty defaults to Risky.
	AlertThreshold scoring.RiskTier
	// contains filtered or unexported fields
}

TaskTool is the ONE non-deferred scheduling verb the model sees (D-09/D-11). A single manifest entry — name "task" — fronts the whole scheduler grammar via an `action` enum (schedule|list|cancel|run_now) dispatched through an ActionRouter, replacing the pre-rewrite 587-LOC five-tool god-class. It is NON-deferred (D-11): scheduling/reminders are a core verb the model must always see, so its spec stays in the default manifest. The schema is OpenAI-wire-safe (D-10): top-level required=["action"] ONLY, with NO root oneOf/anyOf/enum (a root enum 400s DeepSeek, which is OpenAI-compat) — per-action requirements live in the field `description` strings.

The live cron store is injected at registration (10-05) via the consumer-declared taskStore seam below, so this package never imports internal/cron concretely.

func (*TaskTool) Execute

func (t *TaskTool) Execute(ctx context.Context, raw json.RawMessage) (ToolResult, error)

Execute parses the `action` discriminator and dispatches through the ActionRouter (lazily built once, bound to this tool's store). It never panics on a bad action — the router returns a structured error.

func (*TaskTool) Spec

func (t *TaskTool) Spec() Spec

Spec returns the non-deferred manifest entry (D-11). The Summary is one tight line; the Description is short and turn-stable (cache-load-bearing).

type TextResponse

type TextResponse struct{}

TextResponse is the canonical terminal tool — the LLM uses it to emit the final user-visible reply for the current turn. Non-deferred, single string argument. Calling it signals "I am done thinking, here is the answer."

func (TextResponse) Execute

func (TextResponse) Spec

func (TextResponse) Spec() Spec

type TodoTool

type TodoTool struct {
	// contains filtered or unexported fields
}

TodoTool is a lightweight working-memory scratchpad mirroring Claude Code's TodoWrite (parity P3): the model writes the FULL todo list each call (replacing the previous one) to plan and track multi-step work across a long turn — the coherence aid an autonomous swarm/cron run lacks today. The list is session- scoped and persists across turns. Non-deferred: like `task`, planning is a core verb the model should always see.

func (*TodoTool) Evict

func (t *TodoTool) Evict(sessionID string)

Evict reclaims a finished session's todo list (SessionEvictor, R-41). An unknown session id is a no-op; concurrency-safe under the same mutex that guards byID.

func (*TodoTool) Execute

func (t *TodoTool) Execute(ctx context.Context, raw json.RawMessage) (ToolResult, error)

func (*TodoTool) Spec

func (t *TodoTool) Spec() Spec

type Tool

type Tool interface {
	Spec() Spec
	Execute(ctx context.Context, args json.RawMessage) (ToolResult, error)
}

Tool is what the agent loop dispatches against. Execute returns a ToolResult (whose Preview is the content threaded back to the LLM) or an error. Implementations live in `internal/agent/tools/<name>.go`.

type ToolResult

type ToolResult struct {
	Preview    string // history-bound content: full content, or preview+footer when Truncated
	FullPath   string // sidecar path holding the full bytes; empty when not spilled
	Bytes      int    // full length of the original content in bytes
	Truncated  bool   // true when the output was spilled to a sidecar
	Meta       *ToolResultMeta
	Provenance *ToolResultProvenance
}

ToolResult is the value a tool's Execute returns. Preview is what the agent puts into the RoleTool history message (it is the full content for small outputs, or a truncated preview + a read_tool_output footer pointer for large ones). When an output exceeds the preview cap the full bytes are written to FullPath (the sidecar) and Truncated is true; Bytes is always the full (pre-truncation) length of the original content. See tools.NewResult (D-25).

func NewResult

func NewResult(ctx context.Context, content string) (ToolResult, error)

NewResult applies the shared cap → preview → (maybe) sidecar spillover (D-25). It reads the session_id, tool_call_id, run_dir, and preview cap from the ctx the agent injected via WithToolCallContext. Small outputs (≤cap) become a preview-only result with no disk write; large outputs are truncated on a rune boundary, get a read_tool_output footer pointer, and have their FULL bytes written to <run_dir>/conversations/<session_id>/<opaque-spill-id>.result. A sidecar write failure degrades clean: the preview carries a "full output unavailable" note and NO error is returned, so the turn continues (D-29).

func NewResultReservingTail

func NewResultReservingTail(ctx context.Context, body, footer string) (ToolResult, error)

NewResultReservingTail behaves like NewResult, but treats footer as always-visible content. It truncates body first, then appends footer, so shell status, stderr tails, and structured exit-code metadata cannot be sliced off by the preview cap.

type ToolResultMeta

type ToolResultMeta map[string]any

ToolResultMeta carries tool-specific structured fields for audit. It is behind a pointer so the zero ToolResult remains comparable in existing tests.

type ToolResultProvenance

type ToolResultProvenance struct {
	Source string
	Trust  TrustLevel
}

ToolResultProvenance is runtime-only metadata consumed by the agent loop before a tool result is threaded back into the next LLM prompt. It is not rendered in the LLM-visible tool schema.

type ToolSearch

type ToolSearch struct {
	Registry *Registry

	// Embed is the granite-embedding seam used to embed the per-tool search docs
	// and the query (semindex.Embedder; documents.EmbeddingClient satisfies it with
	// no adapter). Wired by the composition root (runner). nil => free-text ranking
	// returns an explicit error (Req-6); the select: path still works.
	Embed semindex.Embedder
	// contains filtered or unexported fields
}

ToolSearch is the built-in hook tool that lets the LLM fetch full specs of deferred tools. The pattern mirrors Claude Code's ToolSearch behavior: the model sees only Name+Summary by default, then calls `tool_search` with a `select:<name>,<name>` argument or a free-text query to load the full Description+Parameters into context.

Free-text queries are ranked SEMANTICALLY by granite-embedding cosine (semindex.Ranker, PerItem) over an expanded per-tool search document (the same flattened searchDocument BM25 builds — D-02, spike-056 "bm25doc input"), then capped to max_results. BM25 contributes only as a guarded, intersection-gated tiebreak (search_fusion.go). The `select:` path resolves any registered tool by exact name and stays uncapped — a different mechanism (load-by-name).

This is a NON-DEFERRED tool — always visible — so the model can find it without recursion. The embed sidecar is a HARD dependency for free-text ranking (Req-6): with it down, Execute returns an explicit model-visible error (never an empty/garbage ranking). The `select:` path needs no embedder.

func (*ToolSearch) EnableLearnedBoost

func (ts *ToolSearch) EnableLearnedBoost(embed semindex.Embedder)

EnableLearnedBoost attaches the stage-2 per-tool centroid boost over the given embedder (called once by the composition root). nil-safe; a nil embedder leaves the learned signal disabled (the stage-1 ranking stands alone).

func (*ToolSearch) Execute

func (ts *ToolSearch) Execute(ctx context.Context, raw json.RawMessage) (ToolResult, error)

func (*ToolSearch) FoldLearned

func (ts *ToolSearch) FoldLearned(examples []toolselectstore.LabeledVec)

FoldLearned replaces the per-tool centroid bank from the full confirmed-example set (the learner's Refresh hook re-folds the whole :ToolSelectionExample set after each save). nil-safe.

func (*ToolSearch) InvalidateIndex

func (ts *ToolSearch) InvalidateIndex()

InvalidateIndex signals that the deferred tool set may have changed (an MCP reconnect advertised new specs). It stays a ZERO-ARG signal (the bridge seam is unchanged): ToolSearch detects the delta itself on the next search by diffing the registry's deferred names against the embedded bank, and embeds ONLY the additions (D-03 incremental re-embed — exactly N new Embed calls per N-tool mount). The BM25 tiebreak index is dropped so it rebuilds over the current deferred set.

func (*ToolSearch) RankForLearner

func (ts *ToolSearch) RankForLearner(ctx context.Context, query string) (string, float64, bool)

RankForLearner is the toolselectlearn.Ranker seam: it embeds the request and ranks it against the STAGE-1 description bank (NOT the learned centroids — the loop must not self-confirm; guard #4 oracle-in-loop), returning the top-1 tool name and the top-2 cosine margin (the free-vs-escalate confidence signal). ok is false when the bank is empty or the embedder is down (then the detector uses only the embedding-free shell/fs heuristic and the oracle escalates). It builds the bank on demand so a post-turn Observe sees the same deferred corpus a query would.

ctx is the caller's (CR-01): the embed round-trips run off the turn goroutine (the learner's intake/activelearn workers) bounded by the learner's lifetime, so a hung sidecar can no longer wedge the user turn under the runner lock, and Close cancels an in-flight rank.

func (*ToolSearch) Spec

func (ts *ToolSearch) Spec() Spec

type TrustLevel

type TrustLevel string

TrustLevel classifies whether a tool result came from host/operator-trusted logic or from attacker-controllable external bytes. The zero value means the result did not explicitly declare provenance.

const (
	TrustUntrusted TrustLevel = "untrusted"
)

type WebFetch

type WebFetch struct {
	Engine fetchEngine
}

WebFetch is the thin Deferred:true adapter over the shared web.Client fetch engine (D-01). It marshals the model-supplied URL, delegates to the SSRF- hardened state machine (scheme allowlist → DNS pin → per-hop redirect revalidation → MIME/size gate → readability→markdown), and routes the successful page through NewResult so a large content_md spills to the sidecar automatically (D-21 — ZERO new spillover code). A sanitized *web.WebError (blocked_url / unsupported_scheme / unsupported_content_type / response_too_large / timeout / http_error / extraction_failed) becomes an inline {error,reason, message} object so the model self-corrects (D-41). The resolved IP / internal host / response headers / redirect chain NEVER reach the returned string (D-27).

func (*WebFetch) Execute

func (e *WebFetch) Execute(ctx context.Context, raw json.RawMessage) (ToolResult, error)

Execute unmarshals the URL, reads the conversation scope from the tool-call context (so the engine's DNS pin is per-conversation), delegates to the engine, and routes the outcome through NewResult. A *web.WebError is sanitized to an inline object (D-41); a genuine non-web infra fault propagates as a Go error.

func (*WebFetch) Spec

func (e *WebFetch) Spec() Spec

type WebSearch

type WebSearch struct {
	Engine searchEngine
}

WebSearch is the thin Deferred:true adapter over the shared web.Client search engine (D-01). It marshals the model-supplied args into web.SearchParams, delegates to the SSRF-hardened engine, maps a sanitized *web.WebError to an inline {error,reason,message} object via NewResult so the model self-corrects (D-41), and on success returns the ranked result list. It re-implements no security boundary — every dial/classify/pin lives in internal/web.

func (*WebSearch) Execute

func (e *WebSearch) Execute(ctx context.Context, raw json.RawMessage) (ToolResult, error)

Execute unmarshals the args, delegates to the hardened engine, and routes the outcome through NewResult. A *web.WebError (e.g. searxng unavailable / not configured) is sanitized to inline {error,reason,message} JSON so the model self-corrects (D-41) — it is NOT a Go error. Only a genuine non-web infra fault propagates as a Go error to the loop.

func (*WebSearch) Spec

func (e *WebSearch) Spec() Spec

Jump to

Keyboard shortcuts

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