Documentation
¶
Overview ¶
Package tools provides ADK-side helpers for wiring tools into the agent. Today it holds only the GateToolset wrapper that bridges permissions.Gate to ADK's tool.Toolset interface; consumer projects add their own tool implementations on top.
Package tools provides ADK-side helpers and built-in tool implementations for the agent loop:
- GateToolset: bridges permissions.Gate to ADK Toolsets (used by mcp + skills)
- Built-in tool suite: file I/O, shell, todo tracking, output truncation
All built-in tools share a common shape via google.golang.org/adk/tool/functiontool and the helpers in this package: output truncation (Truncate) and gate consultation (via permissions.Gate). Tool authors define a typed Args + Result pair and a handler closing over any dependencies; builtins.go assembles them into the slice consumed by agent.WithTools.
Index ¶
- Variables
- func BuiltinToolNames() []string
- func ContextWithWake(ctx context.Context, wake <-chan struct{}) context.Context
- func GateToolset(ts adktool.Toolset, gate *permissions.Gate, namespace string) adktool.Toolset
- func LatestActivePlan(agentsDir string) string
- func NewAskUserTool(opts AskUserOptions) (tool.Tool, error)
- func NewFetchURLTool(gate *permissions.Gate, cfg *config.Config) tool.Tool
- func NewJSONQueryTool(gate *permissions.Gate, cfg *config.Config) tool.Tool
- func NewLifecycleTool(opts LifecycleOptions) (tool.Tool, error)
- func NewRetrieveRawTool(opts RetrieveRawOptions) (tool.Tool, error)
- func NewScheduleTool(opts ScheduleOptions) (tool.Tool, <-chan ScheduleEvent, error)
- func NewSciontoolStatusTool() (tool.Tool, error)
- func PackTool(req *model.LLMRequest, t Packable) error
- func RecordPlan(gate *permissions.Gate, agentsDir string) (tool.Tool, error)
- func RevokeLatestPlan(gate *permissions.Gate, agentsDir string) (string, error)
- func Truncate(s string, maxBytes, maxLines int) string
- type AskUserOptions
- type BuiltinTools
- type LifecycleEvent
- type LifecycleHandler
- type LifecycleOptions
- type Packable
- type Prompter
- type PrompterFunc
- type Registry
- type RetrieveRawOptions
- type ScheduleEvent
- type ScheduleOptions
- type Scheduler
- type SchedulerFunc
- type TodoItem
- type TodoStore
Constants ¶
This section is empty.
Variables ¶
var ErrSchedulerDefer = errors.New("scheduler: defer to next process")
ErrSchedulerDefer is the sentinel a Scheduler returns to exit the autonomous loop with StopReasonDeferred — handing the wake-time off to whatever orchestrator restarts the process or invokes ResumeAutonomous.
Functions ¶
func BuiltinToolNames ¶
func BuiltinToolNames() []string
BuiltinToolNames returns a fresh copy of the canonical built-in tool names. Order matches the field order in BuiltinTools so callers can iterate deterministically.
func ContextWithWake ¶
ContextWithWake attaches a wake channel to ctx. SleepScheduler selects on it alongside its sleep timer and ctx.Done. Nil channels are safe (select on nil blocks forever, so the case never fires).
Exported so the autonomous driver (in package agent) can plumb the agent's wake channel through to SleepScheduler without scheduler callers learning about the agent type.
func GateToolset ¶
GateToolset wraps ts so every tool inside it goes through the permission gate before running. namespace is the policy bucket used for allow/deny matching ("mcp", "skill", etc.); a nil gate returns ts unchanged.
Reuses the existing permission UX: in `ask` mode the same prompt surface is used; in `allow` mode the same allowlist patterns apply (now with `mcp:<tool>` / `skill:<tool>` keys); in `yolo` mode the call proceeds. The bash denylist does NOT apply to non-bash tools.
func LatestActivePlan ¶
LatestActivePlan returns the path of the highest-sequence non- revoked plan in <agentsDir>/plans/, or empty string if none. Used by /replan to find what to archive.
func NewAskUserTool ¶
func NewAskUserTool(opts AskUserOptions) (tool.Tool, error)
NewAskUserTool wraps a Prompter as an ADK tool the agent can call during a turn. The tool's handler blocks until the prompter responds, then returns the answer as the tool result — so the agent's reasoning continues in the same turn (no end-of-turn break).
Use the in-turn pattern when the wait is short (interactive CLI, quick approval). For long waits where the model should release the turn (Scion-style: emit a status, end the turn, driver loop reads the next stdin message and starts a fresh agent.Run), use a status-emitting tool instead and let your driver loop handle the hand-off.
Returns an error only if opts.Prompter is nil.
func NewFetchURLTool ¶
NewFetchURLTool returns the fetch_url built-in. Only meaningful when cfg.URLScope.Allow is non-empty — with no allowlist, every fetch will be denied. The caller (builtins.go) should skip registering this tool when no allowlist is configured rather than register-then-refuse, but the tool itself is safe either way.
func NewJSONQueryTool ¶
NewJSONQueryTool returns the json_query tool. Uses gojq (pure Go, no CGO — safe for distroless) to evaluate a jq expression against JSON loaded from either a file path or an inline json string. Designed primarily to slice the large structured outputs that remote MCP servers (kubectl get pods -o json, etc.) return, so the model gets just the field it asked for rather than burning prompt-cache space on a hundreds-of-KB JSON blob.
Returns an error result (not a Go error) for malformed JSON or bad jq expressions so the model can adapt rather than aborting the turn. Real errors (gate denials, file I/O) propagate as Go errors.
func NewLifecycleTool ¶
func NewLifecycleTool(opts LifecycleOptions) (tool.Tool, error)
NewLifecycleTool wraps a LifecycleHandler as an ADK tool the agent can call to signal state ("thinking", "blocked", "done", custom). The consumer's handler decides where the events go: stdout, a status file, an orchestrator's event log, a websocket, etc.
Returns an error only if opts.Handler is nil or AllowedStates contains an empty entry.
func NewRetrieveRawTool ¶
func NewRetrieveRawTool(opts RetrieveRawOptions) (tool.Tool, error)
NewRetrieveRawTool exposes digest.Store.Get as an ADK tool the agent can call during a turn. The tool returns the raw payload verbatim; the caller / model is responsible for deciding what to slice or extract before feeding it to the next step.
Design intent: aggressive digesting is only safe when the model has an escape hatch to fetch back the raw payload. This tool is that hatch. Register it alongside any consumer wiring digest.Process with a Store.
Error handling: the tool NEVER returns a Go error from its handler — unknown call_ids and store failures surface as a tool-response with Raw carrying a "(error: ...)" prefix. That keeps the model in the loop (it can retry, ask the user, or pick a different call_id) rather than aborting the whole turn.
Returns an error only if opts.Store is nil at construction time. Consumers who don't have a Store should not call this — see the Store field docstring on RetrieveRawOptions.
func NewScheduleTool ¶
func NewScheduleTool(opts ScheduleOptions) (tool.Tool, <-chan ScheduleEvent, error)
NewScheduleTool returns the schedule_next_turn tool plus a channel the autonomous driver consumes after each turn. The tool emits one event per successful call; rejected calls (validation failures, cap violations) return a tool-result error to the model and emit nothing on the channel.
The channel is buffered (default 1); a non-blocking send is used so a single misbehaving turn that calls the tool multiple times won't deadlock the goroutine. The autonomous driver drains the channel non-blockingly after each turn and consults the configured Scheduler only if an event was received.
Returns an error only when opts is invalid (currently: never; all options have sensible zero-value defaults).
func NewSciontoolStatusTool ¶
NewSciontoolStatusTool returns an ADK tool the agent can call to signal sticky lifecycle transitions to Scion. Wired into the built-in registry by tools.Build when `sciontool` is on PATH; can also be constructed directly by out-of-registry consumers.
func PackTool ¶
func PackTool(req *model.LLMRequest, t Packable) error
PackTool registers t with req so ADK can dispatch tool calls back to it (via req.Tools[name]) and so the model sees its declaration (via req.Config.Tools[*].FunctionDeclarations).
This is a re-implementation of google.golang.org/adk/internal/toolinternal/toolutils.PackTool — that package is internal to ADK and we can't import it. The algorithm is identical: register the tool in req.Tools, then either append the declaration onto an existing genai.Tool that already carries FunctionDeclarations, or create a new one.
Wrappers that forward methods to an inner tool (e.g. an MCP namespace prefixer or a permission-gate wrapper) MUST call this with themselves rather than delegating to inner.ProcessRequest. Packing the wrapper ensures (a) the model sees the wrapper's renamed Declaration and (b) ADK's call-back dispatch hits the wrapper's Run, preserving namespacing and gating.
func RecordPlan ¶
RecordPlan returns the built-in record_plan tool. Calling it with a non-empty plan writes the plan to `<agentsDir>/plans/plan-<seq>.md` and flips the gate's `planRecorded` flag, which unblocks mutating tool calls when RequirePlanArtifact is set.
The tool is ALWAYS allowed regardless of gate mode or planRecorded state — it's the escape valve from plan-first gating. It does not call the gate; it writes directly to agentsDir/plans/ via atomic-rename.
Per docs/plan-first-design.md Q2 ("any non-empty string"), no schema validation beyond non-empty-after-trim. Plan quality is the operator's judgment, enforced via /replan when needed.
func RevokeLatestPlan ¶
func RevokeLatestPlan(gate *permissions.Gate, agentsDir string) (string, error)
RevokeLatestPlan renames `<agentsDir>/plans/plan-<seq>.md` to `plan-<seq>-revoked.md` (preserving the audit trail) and clears the gate's planRecorded flag. Called by /replan.
Returns the path of the file that was archived (empty if no active plan existed). An empty path with no error means there was nothing to revoke; the gate flag is still cleared in case it was set out of band.
func Truncate ¶
Truncate caps s at the lower of maxBytes / maxLines. When truncation occurs, a marker line is appended so the model knows it received an abridged output and can ask for more (e.g. via offset/limit on a re-read or a narrower grep).
maxBytes <= 0 means "no byte limit"; same for maxLines.
Types ¶
type AskUserOptions ¶
type AskUserOptions struct {
// Prompter is the consumer's delivery mechanism. Required.
Prompter Prompter
// Name overrides the tool's function name. Defaults to "ask_user"
// when empty.
Name string
// Description overrides the tool's description (the prose the
// model sees in its function-decl list). Empty falls back to a
// sensible default.
Description string
}
AskUserOptions configures NewAskUserTool.
type BuiltinTools ¶
type BuiltinTools struct {
Bash bool // /bin/sh -c with timeout + denylist + gate
ReadFile bool // Read a file with offset/limit
ReadManyFiles bool // Read a batch of files (paths + pattern) in one call
WriteFile bool // Atomic write/create
EditFile bool // Single-occurrence string replacement
DeleteFile bool // Remove a regular file (refuses directories)
Stat bool // Metadata (size / mtime / mode / is_dir) for a single path
ListDir bool // Sorted directory listing
Glob bool // Walk + filepath.Match by basename
Grep bool // Walk + RE2 regex per line
JSONQuery bool // jq expression over JSON loaded from file or inline string
FetchURL bool // HTTP GET against url_scope.allow; URL-allowlist enforced
Todo bool // In-process plan tracker
RecordPlan bool // Plan-first artifact + gate-flag flip (record_plan)
// SciontoolStatus is enabled in the Default struct but Build only
// registers it when `sciontool` is on PATH — inside a Scion
// container. Outside Scion the tool would be inert (subprocess
// no-op) and pointlessly visible in the model's schema.
SciontoolStatus bool
}
BuiltinTools toggles core-agent's built-in tool suite. Each enabled flag becomes one entry in the returned Registry's Tools slice.
All defaults are on — every consumer that's writing an agent will almost certainly want read/write/edit/list/bash. The Todo store is always created; the toggle just controls whether the model can drive it via the `todo` tool.
To turn one off:
reg, _ := tools.Build(cfg, gate, tools.BuiltinTools{
Bash: false, // disable shell
ReadFile: true,
WriteFile: true,
EditFile: true,
ListDir: true,
Todo: true,
})
Or use Default() and override fields directly or via Disable:
b := tools.Default()
b.Disable("bash") // by canonical name; errors on typos
b.WriteFile = false // or set the field directly
reg, _ := tools.Build(cfg, gate, b)
func Default ¶
func Default() BuiltinTools
Default returns a BuiltinTools with every tool enabled. This is the recommended starting set for any agent that needs to act on its workspace.
func (*BuiltinTools) Disable ¶
func (b *BuiltinTools) Disable(name string) error
Disable turns off the named tool. Returns an error for unknown names so typos in --disable-tools or .agents/config.json fail loudly at startup rather than silently leaving the tool on. Calling Disable twice with the same name is a no-op.
type LifecycleEvent ¶
LifecycleEvent is the payload delivered to a LifecycleHandler each time the model calls a tool built by NewLifecycleTool. State is the value the model passed (e.g. "thinking", "blocked", "done"); Detail is the optional human-readable context. Time is the moment the handler received the call.
type LifecycleHandler ¶
type LifecycleHandler func(ctx context.Context, ev LifecycleEvent) error
LifecycleHandler receives each lifecycle emit. Returning a non-nil error surfaces the error string back to the model as the tool's response — useful for "this state isn't valid right now" feedback. A nil return acks the call with a generic "ok" so the model can keep working.
type LifecycleOptions ¶
type LifecycleOptions struct {
// Handler is invoked once per emit. Required.
Handler LifecycleHandler
// Name overrides the tool's function name. Defaults to
// "set_status" when empty.
Name string
// Description overrides the tool's description (the prose the
// model sees in its function-decl list). Empty falls back to a
// sensible default.
Description string
// AllowedStates, when non-empty, restricts the State values the
// tool will accept. A call with a State outside the set is
// rejected with an error result returned to the model — the
// handler is not invoked for the rejected call.
AllowedStates []string
}
LifecycleOptions configures NewLifecycleTool.
type Packable ¶
type Packable interface {
Name() string
Declaration() *genai.FunctionDeclaration
}
Packable is the minimum surface PackTool needs from a tool. Both the ADK-internal toolinternal.RequestProcessor contract and our own wrapper types satisfy it.
type Prompter ¶
Prompter delivers a question from the agent to a human (or any external authority) and returns their answer. Implementations decide the channel — stdin, websocket, queue, file watch, etc.
Returning a non-nil error signals "no user available" or transport failure; the agent sees the error string as the tool's response and can adapt (typically by proceeding with its best assumption or reporting the problem) instead of blocking forever.
Prompt should respect ctx cancellation so SIGTERM, deadlines, and agent-driven aborts unblock the tool cleanly.
func RefusePrompter ¶
RefusePrompter returns a Prompter that always errors with reason. Use in non-interactive runs (batch jobs, CI, daemon deployments without a wired-up channel) so the model gets a clear "no user available" signal as the ask_user tool result and adapts, rather than blocking forever on a missing reader.
Recommended reason text guides the model toward useful behavior:
tools.RefusePrompter("running unattended; proceed with reasonable defaults and explain in your final response")
func StaticPrompter ¶
StaticPrompter returns a Prompter that always returns answer with no delay. Test fixture; production code should not use this.
func StdinPrompter ¶
StdinPrompter reads the user's answer from in (typically os.Stdin). The question is written to out (typically os.Stderr) so it doesn't pollute stdout. Each call reads one newline-terminated line and returns it with surrounding whitespace trimmed.
Suitable for interactive CLI use and for stdin-driven adapters (e.g. Scion's tmux-fed message loop) where one line of input maps to one user answer.
Cancellation note: the underlying os.Stdin read isn't ctx-aware on every platform, so a hung read may not unblock until a newline arrives. For long-blocking deployments, wrap with a goroutine + channel pattern in your own Prompter.
type PrompterFunc ¶
PrompterFunc adapts a function to the Prompter interface for one-off uses.
type Registry ¶
Registry is the assembled built-in tool set returned by Build.
Tools is the slice you pass to agent.WithTools(...). Todo is the underlying store, exposed so hosts can render plan progress (e.g. for a /todo slash command in a TUI).
func Build ¶
func Build(cfg *config.Config, gate *permissions.Gate, agentsDir string, b BuiltinTools) (*Registry, error)
Build constructs the registry. cfg supplies output-truncation caps; gate gates every tool call. agentsDir is the resolved .agents/ directory (may be empty when none was found) and is required only by tools that persist artifacts to it (today: record_plan). Both cfg and gate are required.
We deliberately do NOT set ADK's functiontool.Config.RequireConfirmation even when the gate is in "ask" mode. core-agent's gate handles approval itself by calling its Prompter from inside each tool handler — going through ADK's HITL flow would be a second approval round-trip on top of ours.
type RetrieveRawOptions ¶
type RetrieveRawOptions struct {
// Store is the CCR backing populated by digest.Process. Required.
// When the operator hasn't wired a store (e.g. --session-db is
// off and no explicit FilesystemStore was configured), consumers
// should skip registering the tool rather than passing nil here
// — the tool would refuse every call and only confuse the model.
Store digest.Store
// Name overrides the tool's function name. Defaults to
// "retrieve_raw" when empty.
Name string
// Description overrides the tool's description (the prose the
// model sees in its function-decl list). Empty falls back to a
// sensible default that explains when NOT to use it.
Description string
}
RetrieveRawOptions configures NewRetrieveRawTool.
type ScheduleEvent ¶
type ScheduleEvent struct {
// WakeAt is the absolute time the next turn should run, resolved
// from either an absolute time or a relative duration in the tool
// args before this event is emitted.
WakeAt time.Time
// NextPrompt is the continuation message the next turn should
// receive. Empty string means the driver should fall back to its
// configured continuation prompt (typically "continue").
NextPrompt string
// Detail is the freeform one-liner the model passed for telemetry
// / operator visibility (e.g. "polling cluster-A on 10m cadence").
// Surfaces on the chat-style streaming UI and in the eventlog.
Detail string
// Time is the moment the tool handler received the call.
Time time.Time
}
ScheduleEvent is what the schedule_next_turn tool emits when the model calls it. The autonomous driver consumes one event per turn from the channel returned by NewScheduleTool and feeds it to the configured Scheduler.
type ScheduleOptions ¶
type ScheduleOptions struct {
// Name overrides the tool's function name. Defaults to
// "schedule_next_turn" when empty.
Name string
// Description overrides the model-facing tool description. Defaults
// to a baseline that includes the report_done distinction, the
// cadence ladder (30s fast-changing state, 5-15m steady-state,
// 1h+ slow-changing infra), good-vs-bad next_prompt examples, and
// the state-persistence reminder.
Description string
// MaxDefer caps how far in the future a single call may schedule.
// Zero means no cap. A call with wake_in_sec or wake_at past the
// cap returns a tool-result error to the model so it can adapt;
// the channel sees no event for that rejected call.
MaxDefer time.Duration
// BufferSize sets the schedule channel buffer. Default 1 — exactly
// one schedule emission is consumed per turn by the autonomous
// driver, so anything past 1 is overflow that would indicate a
// bug. Exposed for tests that want to inspect multiple emissions
// without a driver in the loop.
BufferSize int
}
ScheduleOptions configures NewScheduleTool.
type Scheduler ¶
type Scheduler interface {
// BeforeNextTurn is consulted by RunAutonomous after a turn that
// emitted a schedule intent, after the per-turn checkpoint is
// written. Return nil to let the loop continue at ev.WakeAt with
// prompt=ev.NextPrompt. Return ErrSchedulerDefer to exit the loop
// with StopReasonDeferred. Any other error aborts the run.
BeforeNextTurn(ctx context.Context, ev ScheduleEvent) error
}
Scheduler decides what RunAutonomous should do between turns when the prior turn emitted a schedule intent via the schedule_next_turn tool. Loops whose model never emits a schedule intent never consult the scheduler — zero overhead by default.
Bundled implementations:
- SleepScheduler — long-lived daemon: sleeps the goroutine until WakeAt, then returns nil so the loop continues.
- ExitOnDeferScheduler — orchestrator-managed (k8s CronJob, AX, external queue): returns ErrSchedulerDefer so the loop exits cleanly with StopReasonDeferred + RunResult.NextWakeAt populated. A subsequent ResumeAutonomous picks up at the persisted wake-time.
Consumers ship their own Scheduler for distributed shapes (NATS queue, custom orchestrator). The schedulerFunc adapter is exported for one-off function-shaped implementations.
func ExitOnDeferScheduler ¶
func ExitOnDeferScheduler() Scheduler
ExitOnDeferScheduler returns a Scheduler that exits the autonomous loop on every schedule emission via ErrSchedulerDefer. The driver reports StopReasonDeferred with RunResult.NextWakeAt populated from the schedule event; whatever orchestrator wraps the process (k8s CronJob, supervisord, AX) restarts it at or after that wake-time and a fresh ResumeAutonomous call picks up where this run left off (the deferred checkpoint persists the wake-time to the eventlog).
Use when external scheduling infrastructure already exists and the operator prefers short-lived processes over a long-lived daemon. For most monitoring shapes SleepScheduler is the cleaner default; see docs/scheduled-monitoring-design.md.
func SleepScheduler ¶
func SleepScheduler() Scheduler
SleepScheduler returns a Scheduler that sleeps the calling goroutine until ev.WakeAt, respecting context cancellation. Returns nil on wake; returns ctx.Err() when the context is cancelled mid-sleep.
Also returns nil immediately when an external wake signal fires on the channel attached via ContextWithWake — this is how a child alert arrival or an operator Inject pierces an active sleep so the agent reacts without waiting out the full scheduled interval.
Use when the agent runs as a long-lived daemon that should retain in-memory state between scan cycles (warm prompt cache, supervisor subagent tree, etc.). For deployments where the process exits between cycles, use ExitOnDeferScheduler instead.
A WakeAt already in the past returns immediately.
type SchedulerFunc ¶
type SchedulerFunc func(ctx context.Context, ev ScheduleEvent) error
SchedulerFunc adapts a plain function to the Scheduler interface for callers that don't want to declare a type just to wire one behavior.
func (SchedulerFunc) BeforeNextTurn ¶
func (f SchedulerFunc) BeforeNextTurn(ctx context.Context, ev ScheduleEvent) error
BeforeNextTurn implements Scheduler.
type TodoItem ¶
type TodoItem struct {
ID int `json:"id"`
Status string `json:"status"` // "pending" | "in_progress" | "completed"
Text string `json:"text"`
}
TodoItem is one entry in the agent's plan.