tools

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jun 15, 2026 License: MIT Imports: 32 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

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) 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.
	WorkspaceRoot string
}

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) 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