tools

package
v0.19.0 Latest Latest
Warning

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

Go to latest
Published: Sep 2, 2026 License: MIT Imports: 24 Imported by: 0

Documentation

Overview

Package tools implements what turns dcode into a coding agent rather than a chat: reading, searching, editing and running.

This is the package with the most consequence per line. A wrong edit here corrupts a user's work silently, so several rules are enforced structurally rather than trusted to the model:

  • edit only operates on a file read in this session and unchanged since;
  • an ambiguous match fails instead of guessing;
  • Declare reports what a call would touch without doing any of it, so policy decides before there is anything to undo.

Spec: docs/specs/architecture/tool-suite/202608072337-*.

Index

Constants

View Source
const (
	// EchoDiffNever returns the count alone.
	EchoDiffNever = "never"
	// EchoDiffMulti returns the diff only where the model cannot derive the
	// result: a replace_all that hit more than one occurrence.
	EchoDiffMulti = "multi"
	// EchoDiffAlways returns it on every edit. Expensive in an append-only
	// history, and useful for debugging a model that is editing wrongly.
	EchoDiffAlways = "always"
)

The three modes of DCODE_EDIT_ECHO_DIFF.

View Source
const (
	KindDef = "def"
	KindRef = "ref"
	KindAny = "any"
)

The three kinds.

View Source
const (
	CodeFileNotRead     = "file_not_read"
	CodeFileChanged     = "file_changed"
	CodeNoMatch         = "no_match"
	CodeAmbiguousMatch  = "ambiguous_match"
	CodeNoOpEdit        = "no_op_edit"
	CodeNotFound        = "not_found"
	CodeInvalidPattern  = "invalid_pattern"
	CodeTimeout         = "timeout"
	CodePlanMultiActive = "plan_multiple_active"
	CodePlanNoReason    = "plan_blocked_no_reason"
	CodeDenied          = "denied"
	CodeBadInput        = "bad_input"
)

Error codes. Everything but denied is recoverable: the model reads the error and corrects itself, which is the whole reason the messages are written for a reader rather than for a log.

View Source
const DiffContext = 3

DiffContext is how many unchanged lines surround each change. Three is the convention every reviewer already reads.

View Source
const DiffMaxLines = 400

DiffMaxLines caps the diff carried on an event.

A whole-file rewrite would otherwise produce a payload larger than the file, and the client can only show a screenful anyway. The cap is generous enough that an ordinary edit is never touched.

View Source
const DoneProposeFile = "done.toml"

DoneProposeFile is what a proposal is written to, inside the spec folder.

View Source
const GrepMaxContextLines = 10

GrepMaxContextLines caps how much surrounding text one match may carry.

A ceiling rather than a configuration key, for the same reason the match limit is one: the number that matters is "enough to understand the match", and past a handful more lines stop informing and start costing. Asking for five thousand is asking for the file, which is what grep exists not to be.

View Source
const ProgressStep = 25

ProgressStep is how often a scan reports.

Every file would put one event per file into the log and the record — a scan of ten thousand files writing ten thousand lines nobody reads. Every twenty-five keeps a hundred-file scan honest at four updates and a large one from flooding, and the final count arrives with the result either way.

View Source
const SymbolLimit = "textual match on symbol boundary; does not resolve interface or dynamic dispatch"

SymbolLimit is appended to every result, always.

A textual search cannot see a call made through an interface, a function pointer, reflection, or a name assembled at run time. That is a false negative, and a false negative is the worst failure mode there is: an empty result looks exactly like a complete answer. An agent that concludes "there are no other callers" from this and renames half of them does so with confidence.

Same principle as RN-5 on truncation, and for the same reason: output must not look complete when it is not.

Variables

This section is empty.

Functions

func Reporter added in v0.2.0

func Reporter(ctx context.Context, kind string, total int) func(done int)

Reporter counts and reports on the step, so a caller writes one line in its loop instead of the same arithmetic in each tool.

func UnifiedDiff

func UnifiedDiff(before, after, path string) string

UnifiedDiff renders the change between two texts.

Empty when nothing changed: a diff with no hunks is noise, and the caller uses the empty string to mean "nothing to show".

func WithProgress added in v0.2.0

func WithProgress(ctx context.Context, r Report) context.Context

WithProgress attaches a reporter to a call's context.

Types

type BackgroundRunner

type BackgroundRunner interface {
	Start(ctx context.Context, workdir, command string) (Handle, error)
}

BackgroundRunner starts a command that outlives the call that started it.

Separate from Runner rather than a method on it: a foreground command is waited on and a background one is not, and the two return different things. Folding them together would give one method two meanings decided by a flag, which is the shape Declare exists to keep out of this package.

type Bash

type Bash struct {
	Runner  Runner
	Workdir string
	Timeout time.Duration

	// Background starts a command that is not waited on. Nil means this build
	// cannot start one, and asking for it is refused rather than silently run
	// in the foreground — which would freeze the session, the exact failure
	// the flag exists to prevent.
	Background BackgroundRunner
	// Settle is how long a background start watches before reporting. It is
	// not a readiness check: it answers "did it die immediately", which is the
	// common failure and the one a model cannot otherwise see.
	Settle time.Duration
}

Bash runs a shell command.

func (Bash) Declare

func (b Bash) Declare(input json.RawMessage) (policy.Request, error)

func (Bash) Description

func (Bash) Description() string

func (Bash) Execute

func (b Bash) Execute(ctx context.Context, input json.RawMessage, s *State) (Result, error)

func (Bash) Name

func (Bash) Name() string

func (Bash) Schema

func (Bash) Schema() json.RawMessage

type BashInput

type BashInput struct {
	Command    string `json:"command"`
	Timeout    int    `json:"timeout_seconds,omitempty"`
	Background bool   `json:"background,omitempty"`
}

BashInput is the argument shape.

type Delegator

type Delegator interface {
	// Explore answers one question, reading and — when owns is non-empty —
	// writing the paths it names.
	//
	// The implementation excludes this tool from the child's registry, so
	// nesting stays impossible by absence. It also decides the mode: read-only
	// when owns is empty, and otherwise the PARENT's mode, intersected with
	// the owned paths. The caller never chooses either, which is why owns is a
	// request rather than a grant.
	Explore(ctx context.Context, task, path string, owns []string) (conclusion string, read, wrote, unread []string, truncated bool, err error)
}

Delegator runs a read-only sub-turn and reports what it found.

An interface for the same reason Bash takes a Runner: the loop owns turns and this package owns tools, and having either import the other would close a cycle. The tool declares and validates; the loop decides what a child turn is allowed to be.

type DonePropose added in v0.13.0

type DonePropose struct {
	// Spec is the folder the proposal is for, absolute. Set by whoever built
	// the qualifying turn: the model does not get to choose which folder its
	// proposal lands in.
	Spec string
	// Submit records the proposal. It does not measure it and does not write
	// anything: the loop does both, after the turn.
	//
	// Injected because it keeps this package from importing the loop, which
	// imports this one — and because what happens to a proposal is not a
	// tool's business. A tool is the boundary the model reaches through.
	Submit func(ctx context.Context, in DoneProposeInput) (string, error)
}

DonePropose is how the model hands over a definition of done it derived.

A tool, and not prose, for the reason everything else in this harness is a tool: the call IS how the thing gets done, and there is no other way to do it. A model that describes criteria in a sentence has described them.

It is available only in a qualifying turn. A tool that can redefine done, within reach of a working turn, is the shortest way out of a loop — the agent rewrites the ruler instead of meeting it.

func (DonePropose) Declare added in v0.13.0

Declare touches nothing, and that is the design rather than a shortcut.

The first version declared a write to the spec folder, which was honest about the consequence and wrong about the actor. A qualifying turn runs in plan mode — read-only, because working out what you will be measured by is reading — and read-only denies every write with no exception. So a tool that declared one was denied, and the model correctly reported that it could not propose.

Nothing here writes. The proposal is RECORDED, the turn ends, and the LOOP measures it and writes it down afterwards, under the boundary the work will actually run under. Keeping the write out of the turn is what lets plan mode stay a guarantee with no hole in it — and it measures the criteria somewhere they can actually run, which read-only is not.

func (DonePropose) Description added in v0.13.0

func (DonePropose) Description() string

func (DonePropose) Execute added in v0.13.0

func (d DonePropose) Execute(ctx context.Context, input json.RawMessage, _ *State) (Result, error)

func (DonePropose) Name added in v0.13.0

func (DonePropose) Name() string

func (DonePropose) Schema added in v0.13.0

func (DonePropose) Schema() json.RawMessage

type DoneProposeCriterion added in v0.13.0

type DoneProposeCriterion struct {
	Name     string `json:"name"`
	Command  string `json:"command"`
	ExitCode int    `json:"exit_code,omitempty"`
	Expects  string `json:"expects"`
	Why      string `json:"why,omitempty"`
}

DoneProposeCriterion is one candidate.

type DoneProposeInput added in v0.13.0

type DoneProposeInput struct {
	Criteria  []DoneProposeCriterion `json:"criteria"`
	Protected []string               `json:"protected,omitempty"`
}

DoneProposeInput is the argument shape.

type Edit

type Edit struct{}

Edit replaces an exact string. The most consequential tool in the set.

func (Edit) Declare

func (e Edit) Declare(input json.RawMessage) (policy.Request, error)

func (Edit) Description

func (Edit) Description() string

func (Edit) Execute

func (e Edit) Execute(_ context.Context, input json.RawMessage, s *State) (Result, error)

func (Edit) Name

func (Edit) Name() string

func (Edit) Schema

func (Edit) Schema() json.RawMessage

type EditInput

type EditInput struct {
	Path       string `json:"path"`
	OldString  string `json:"old_string"`
	NewString  string `json:"new_string"`
	ReplaceAll bool   `json:"replace_all,omitempty"`
	// Edits is the batch form. When present it wins, and the four fields above
	// are ignored: two ways of saying the same call in one call is ambiguity
	// nobody can resolve from the outside.
	Edits []editOp `json:"edits,omitempty"`
}

EditInput is the argument shape.

type Explore

type Explore struct {
	Delegator Delegator
}

Explore delegates reading, so the cost of it does not come back.

dcode has one head. A task crossing twenty files reads all twenty in the same window, and each one pushes the last out; by file fifteen compaction hits and what was learned at file three becomes two lines of summary, if it survives at all.

Delegating inverts that. A child reads the twenty files in ITS window and returns half a page. The gain is not speed — it is that the cost of the reading does not come back.

func (Explore) Declare

func (e Explore) Declare(input json.RawMessage) (policy.Request, error)

func (Explore) Description

func (Explore) Description() string

func (Explore) Execute

func (e Explore) Execute(ctx context.Context, input json.RawMessage, s *State) (Result, error)

func (Explore) Name

func (Explore) Name() string

func (Explore) Schema

func (Explore) Schema() json.RawMessage

type ExploreInput

type ExploreInput struct {
	Task string `json:"task"`
	Path string `json:"path,omitempty"`
	// Owns are the paths the child may write, and it is the only way a child
	// writes at all.
	//
	// Absent is the read-only child that already existed, so nothing changes
	// for anyone already delegating. Present is a request, never a grant: the
	// loop intersects it with what the parent may already write, and a child
	// can only ever end up narrower than its parent.
	//
	// There is still no mode field, and the absence is still the guarantee.
	// What the model passes is the task and the paths, and both may only
	// narrow.
	Owns []string `json:"owns,omitempty"`
}

ExploreInput is the argument shape.

The child receives the TASK, not the parent's history. That is the whole point: a copied history would return exactly the cost delegation exists to avoid.

There is no mode field, and that absence is the guarantee. Read-only is fixed where the sub-turn is constructed, so it is not something the model passes and not something a caller forgets.

type Fetch

type Fetch struct {
	// Client is injected so a test never reaches the network and so a
	// deployment can supply its own timeouts and proxy.
	Client *http.Client
	// Limit caps the body. A document that does not fit is truncated and says
	// so, like every other output here.
	Limit int
}

Fetch reads a document off the web.

The tool suite put network access out of scope from the beginning, and the reason was sound at the time: a network tool without a permission model is a hole. That premise is gone. Consent is asked once per project and kept in grants, the policy already treats network as an axis of its own, and the evaluator already refuses what was not granted.

A fetch, not a browser. It returns the text at a URL, which is what reading a changelog or a library's documentation needs. Search is a different capability with a different failure mode — a model given a search box will answer from whatever came back first — and it waits for evidence that fetching alone is not enough.

Unlike bash, this runs in this process rather than behind the sandbox, so the gate is the policy verdict and not the operating system. That is the same guarantee every other tool here relies on: `read` is kept inside the workspace by the resolver, not by seatbelt. bash is the exception because a shell command is opaque, and this one is not.

func (Fetch) Declare

func (f Fetch) Declare(input json.RawMessage) (policy.Request, error)

Declare reports the crossing and no path.

Always the network, whatever the URL looks like. Reading the string to decide is the reasoning bash already rejected: there is no reading of it that answers whether this one reaches out, because all of them do.

func (Fetch) Description

func (Fetch) Description() string

func (Fetch) Execute

func (f Fetch) Execute(ctx context.Context, input json.RawMessage, s *State) (Result, error)

func (Fetch) Name

func (Fetch) Name() string

func (Fetch) Schema

func (Fetch) Schema() json.RawMessage

type FetchInput

type FetchInput struct {
	URL string `json:"url"`
}

FetchInput is the argument shape.

type Glob

type Glob struct{}

Glob finds files by pattern.

func (Glob) Declare

func (g Glob) Declare(input json.RawMessage) (policy.Request, error)

func (Glob) Description

func (Glob) Description() string

func (Glob) Execute

func (g Glob) Execute(ctx context.Context, input json.RawMessage, s *State) (Result, error)

func (Glob) Name

func (Glob) Name() string

func (Glob) Schema

func (Glob) Schema() json.RawMessage

type GlobInput

type GlobInput struct {
	Pattern string `json:"pattern"`
	Path    string `json:"path,omitempty"`
}

GlobInput is the argument shape.

type Grep

type Grep struct{}

Grep searches file contents.

func (Grep) Declare

func (g Grep) Declare(input json.RawMessage) (policy.Request, error)

func (Grep) Description

func (Grep) Description() string

func (Grep) Execute

func (g Grep) Execute(ctx context.Context, input json.RawMessage, s *State) (Result, error)

func (Grep) Name

func (Grep) Name() string

func (Grep) Schema

func (Grep) Schema() json.RawMessage

type GrepInput

type GrepInput struct {
	Pattern      string `json:"pattern"`
	Path         string `json:"path,omitempty"`
	Glob         string `json:"glob,omitempty"`
	ContextLines int    `json:"context_lines,omitempty"`
}

GrepInput is the argument shape.

type Handle

type Handle interface {
	// Output is everything written so far, stdout and stderr together.
	Output() string
	// Exited reports the exit code, and whether it has exited at all.
	Exited() (code int, done bool)
	// Stop ends it. Calling it on a command that already exited does nothing.
	Stop()
}

Handle is a command that is still running, or one that has finished and whose output nobody has collected yet.

type Limits

type Limits struct {
	ReadMaxLines      int
	ReadMaxLineLength int
	GlobMaxResults    int
	GrepMaxMatches    int
	SymbolMaxMatches  int
	MaxToolOutput     int
	RespectGitignore  bool
	RequireReadBefore bool
	AtomicWrite       bool
	// EditEchoDiff decides when the diff of an edit goes back to the MODEL.
	// It always goes to the client, in every mode.
	EditEchoDiff string
}

Limits are the per-tool caps.

func DefaultLimits

func DefaultLimits() Limits

DefaultLimits mirrors the documented defaults.

type LocalRunner

type LocalRunner struct{}

LocalRunner executes through a plain shell. It exists for the development entry point only; the sandbox package supplies the real one.

func (LocalRunner) Run

func (LocalRunner) Run(ctx context.Context, workdir, command string) (string, int, error)

Run executes command with sh.

type Meta

type Meta struct {
	// Lines read, or matched, or written.
	Lines int
	// Files touched or matched.
	Files int
	// Added and Removed are line counts for an edit or a write.
	Added   int
	Removed int
	// ExitCode of a command. Meaningful only when HasExit is set, because a
	// successful command exits zero and zero is also the empty value.
	ExitCode int
	HasExit  bool
	// Diff is the unified diff of a change, for a client to render.
	//
	// It never reaches the model: the model wrote the edit and already knows
	// what it changed, so putting the diff in the history would pay tokens for
	// something nobody reads. It rides on the event instead.
	Diff string
}

Meta is the structured account of what a tool call did. Every field is optional: a tool reports what applies to it and leaves the rest zero.

type Plan

type Plan struct{}

Plan maintains the session plan. It touches no file and runs nothing: it changes session state only, which is why its policy verdict is always allow.

func (Plan) Declare

func (p Plan) Declare(json.RawMessage) (policy.Request, error)

func (Plan) Description

func (Plan) Description() string

func (Plan) Execute

func (p Plan) Execute(_ context.Context, input json.RawMessage, s *State) (Result, error)

func (Plan) Name

func (Plan) Name() string

func (Plan) Schema

func (Plan) Schema() json.RawMessage

type PlanInput

type PlanInput struct {
	Items []protocol.PlanItem `json:"items"`
}

PlanInput is the argument shape.

type Process

type Process struct{}

Process reads and stops the commands bash started in the background.

A separate tool from bash, and the reason is the policy verdict rather than the tool count. bash declares the network and a write to the workspace, because a shell command is opaque and the worst case is what gets declared. Reading a buffer this process already owns crosses nothing at all. Folded together, every read of a log would queue an approval for a boundary it does not touch, and a question asked for no reason is how people learn to answer without reading.

func (Process) Declare

func (p Process) Declare(json.RawMessage) (policy.Request, error)

Declare reports nothing, because nothing is touched: no path, no network, no command run. The verdict is always allow.

func (Process) Description

func (Process) Description() string

func (Process) Execute

func (p Process) Execute(_ context.Context, input json.RawMessage, s *State) (Result, error)

func (Process) Name

func (Process) Name() string

func (Process) Schema

func (Process) Schema() json.RawMessage

type ProcessInput

type ProcessInput struct {
	ID   string `json:"id,omitempty"`
	Stop bool   `json:"stop,omitempty"`
}

ProcessInput is the argument shape.

type Read

type Read struct{}

Read returns file contents with line numbers, so the model and edit share one frame of reference.

func (Read) Declare

func (r Read) Declare(input json.RawMessage) (policy.Request, error)

func (Read) Description

func (Read) Description() string

func (Read) Execute

func (r Read) Execute(_ context.Context, input json.RawMessage, s *State) (Result, error)

func (Read) Name

func (Read) Name() string

func (Read) Schema

func (Read) Schema() json.RawMessage

type ReadInput

type ReadInput struct {
	Path   string `json:"path"`
	Offset int    `json:"offset,omitempty"`
	Limit  int    `json:"limit,omitempty"`
}

ReadInput is the argument shape.

type Registry

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

Registry holds the enabled tools.

func NewRegistry

func NewRegistry(ts ...Tool) *Registry

NewRegistry builds a registry from tools.

func (*Registry) Defs

func (r *Registry) Defs() []ce.ToolDef

Defs renders the declarations that go into the context prefix.

Sorted by name so the output is byte-identical between runs: this lands in the cached prefix, and an unstable order would invalidate the cache of every session on every restart.

func (*Registry) Get

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

Get returns a tool by name.

func (*Registry) Names

func (r *Registry) Names() []string

Names lists registered tool names, sorted.

type Remember

type Remember struct {
	// Commit and Today are handed in rather than read here, so the provenance a
	// memory carries agrees with the repository state the prefix already
	// described. Two readings of git in one session can disagree, and
	// disagreeing about where the session is is worse than not knowing.
	Commit string
	Today  string
}

Remember writes something learned where the next session will read it.

A tool rather than something that happens on its own, and the distinction is the design: reading what was learned is a fact the model needs every turn, so it lives in the prefix; writing one is an act with consequences, and an act that happens without being asked is an act nobody authorised.

func (Remember) Declare

func (r Remember) Declare(input json.RawMessage) (policy.Request, error)

Declare reports the one path it writes and nothing else.

func (Remember) Description

func (Remember) Description() string

func (Remember) Execute

func (r Remember) Execute(_ context.Context, input json.RawMessage, s *State) (Result, error)

func (Remember) Name

func (Remember) Name() string

func (Remember) Schema

func (Remember) Schema() json.RawMessage

type RememberInput

type RememberInput struct {
	Kind    string `json:"kind"`
	Subject string `json:"subject"`
	Body    string `json:"body"`
}

RememberInput is the argument shape.

type Report added in v0.2.0

type Report func(kind string, done, total int)

Report says how far a call has got. Total 0 means the total is not known yet, which is honest for a walk that has not finished enumerating.

func Progress added in v0.2.0

func Progress(ctx context.Context) Report

Progress returns the reporter, and never nil.

A no-op rather than a nil check at every call site: a tool should say how far it has got without first asking whether anybody is listening, and a nil dereference in a scan is a crash in the middle of somebody's work.

type Result

type Result struct {
	Output    string
	IsError   bool
	Code      string
	Truncated bool
	Remaining int

	// Meta is what a client needs to render the call in one line without
	// parsing Output.
	//
	// Parsing prose to rebuild a number the tool already knew is how a UI
	// silently breaks when the wording changes. The tool states it once.
	Meta Meta
}

Result is what a tool hands back to the loop.

type Runner

type Runner interface {
	Run(ctx context.Context, workdir, command string) (stdout string, exitCode int, err error)
}

Runner executes a command inside the sandbox. The tool never reaches for os/exec directly: every execution goes through the boundary, and an interface is what makes that structural instead of a rule to remember.

type State

type State struct {
	Resolver *policy.Resolver
	Limits   Limits
	// contains filtered or unexported fields
}

State is the per-session state tools share.

func NewState

func NewState(r *policy.Resolver, l Limits, tools []string) *State

NewState builds session state for a session offering these tools.

The names are a parameter rather than a setter with a default, because every default here is a guess about what the model can reach, and a guess that is wrong is an error message sending it somewhere that does not exist.

func (*State) AddProcess

func (s *State) AddProcess(command string, h Handle) string

AddProcess records a started command and returns the identifier the model will use to reach it again.

The identifier is a sequence, not a clock. A timestamp would ride into tool output and make two otherwise identical sessions differ byte-for-byte, which is the same reason MarkRead takes a message index instead of a time.

func (*State) Adopt

func (s *State) Adopt(child *State)

Adopt takes over what a delegated child recorded, so the turn that asked for the work is the turn that can undo it.

Undo is per turn and delegation happens inside one. Without this the parent's undo would reach everything except the part it delegated, and undoing half of a division of work leaves a tree nobody designed.

Three things move, and each for its own reason:

  • the snapshots, so there is something to put back. The parent's own snapshot wins where both have one: the first of the turn is what the turn started from, and the child's later one records a state the turn itself produced.
  • the file records, because undo refuses a file that moved since the turn left it, and that judgement needs the hash of what was actually written. Here the child's wins, because the child wrote last and the disk agrees with it.
  • the written set, because "did this session change anything" is the fact the definition of done reads, and work done by a child is still work done.

func (*State) BeginCycle added in v0.16.0

func (s *State) BeginCycle()

BeginCycle marks a point inside a turn that UndoCycle can come back to.

A turn holds many verification cycles, and BeginTurn's snapshot set spans all of them. Undoing at turn scope after one bad cycle would throw away every good cycle that came before it, which is why the loop could not be given the undo it already had.

It takes a SECOND layer of snapshots — how each already-touched path stands right now — and leaves the turn's own layer alone. One layer was tried and is wrong: a path written in cycle one and again in cycle two would keep cycle two's content, so the bad cycle's write survived the undo of the bad cycle. Two layers because they answer different questions: the turn's is "how did this look before the model started", the cycle's is "how did it look before this attempt".

func (*State) BeginTurn

func (s *State) BeginTurn()

BeginTurn starts a new set of undoable changes.

A new turn replaces the old set rather than adding to it. Keeping them would let one undo reach back through work the person already read and accepted, and "undo" that goes further than the last thing is not undo.

func (*State) ChangedSinceRead

func (s *State) ChangedSinceRead(read func(path string) (string, error)) []string

ChangedSinceRead reports files whose content on disk no longer matches what the model was shown.

The reader is injected so the check is testable without a filesystem, and so the caller decides what "on disk" means. A file that has become unreadable is not reported as changed: it is a different failure, and the tool that touches it next will say so precisely.

func (*State) CheckEditable

func (s *State) CheckEditable(path, current string) *ToolError

CheckEditable enforces the read-before-edit invariant.

This is the rule that prevents the most expensive failure the product can produce: blind-editing a file whose contents the model assumed. Without it the agent silently overwrites work the user did in parallel.

func (*State) Close

func (s *State) Close()

Close stops every background process this session started.

This is where "a process dies with the session" stops being a rule someone has to remember and becomes ownership: the table lives in session state, and state that goes away takes the processes with it. It is also what makes approving a long command need no separate question about duration — the authorisation and the process have the same lifetime, so there is no window in which someone consented to an instant and granted an era.

func (*State) MarkRead

func (s *State) MarkRead(path, content string, msgIdx int)

MarkRead records that path was read with this content.

msgIdx is the position in history, deliberately not a timestamp: a clock value here would leak into tool output and invalidate the prompt cache on every call.

func (*State) MarkWritten

func (s *State) MarkWritten(path string)

MarkWritten records that a tool changed path.

Separate from MarkRead on purpose. MarkRead is also called right after a write, to keep the read-before-edit invariant satisfied for the next edit — so it cannot answer "did this session change anything", which is the fact the definition of done needs.

func (*State) Plan

func (s *State) Plan() []protocol.PlanItem

Plan returns a copy of the current plan.

func (*State) ReadPaths

func (s *State) ReadPaths() []string

WasRead reports whether path has been read this session. ReadPaths returns the workspace-relative paths this session opened, sorted.

It is what a delegated turn hands back beside its conclusion: it does not prove the child understood, but it proves it looked, and it turns "trust me" into something a person can spot-check.

func (*State) Snapshot

func (s *State) Snapshot(path string)

Snapshot records how a file stood before this turn changes it.

Called by the tools that write, before they write. Only the first call per path per turn is kept: the first is what the turn started from, and a later one would record a state the turn itself produced.

A file that is not there is recorded as not there, which is what makes undoing a creation mean removing it.

func (*State) Undo

func (s *State) Undo() (restored, refused []string, err error)

Undo puts back what the last turn changed, and reports what it would not touch.

A file that changed on disk after the turn left it is refused, never overwritten. Undoing over somebody's own edit would throw away their work to restore something older, which is the opposite of what undo is for — and it is the same invariant `edit` already enforces when it refuses a file that moved under it.

Refusing is per file rather than all-or-nothing. Seven files changed and one edited by hand should still give six back, and saying which one did not go is more useful than refusing everything on account of it.

func (*State) UndoCycle added in v0.16.0

func (s *State) UndoCycle() (restored, refused []string, err error)

UndoCycle puts back what changed since the last BeginCycle.

Only what this cycle wrote. A path the turn touched earlier and this cycle left alone is not restored, because there is nothing about it to undo.

Without a BeginCycle it undoes nothing and says so by returning nothing. A cycle boundary nobody marked is not a boundary, and guessing one would undo the whole turn under a name that promises less.

func (*State) WasRead

func (s *State) WasRead(path string) bool

func (*State) WriteSeq

func (s *State) WriteSeq() uint64

WriteSeq is how many writes this session has made, ever-increasing.

The set above answers "what changed"; this answers "did anything change since a moment I recorded". They are different questions, and the set cannot stand in for the counter: rewriting a file already in it leaves it identical, which is the ordinary shape of fixing what a failing check just reported.

A count rather than a clock, deliberately. The verification seal is compared between turns and shown to a person, and a timestamp there would vary per run for a fact that is purely ordinal.

func (*State) Written

func (s *State) Written() []string

Written returns the workspace-relative paths this session changed, sorted.

type Symbol

type Symbol struct{}

Symbol finds where an identifier is declared and where it is used.

The eighth tool, and what justifies it is NOT the word boundary — `\bParse\b` is a regular expression the model could already write, since grep takes Go regexps. What it could not write without knowing every language is the distinction between a definition and a use: `func Parse(` in Go, `def parse` in Python, `fn parse` in Rust, `function` or `const … =` in TypeScript.

That knowledge is data this package carries, rather than something the model reconstructs per language on every call and gets wrong in half of them.

func (Symbol) Declare

func (sy Symbol) Declare(input json.RawMessage) (policy.Request, error)

func (Symbol) Description

func (Symbol) Description() string

func (Symbol) Execute

func (sy Symbol) Execute(_ context.Context, input json.RawMessage, s *State) (Result, error)

func (Symbol) Name

func (Symbol) Name() string

func (Symbol) Schema

func (Symbol) Schema() json.RawMessage

type SymbolInput

type SymbolInput struct {
	// Name is the symbol. NOT a regular expression: it is escaped before it
	// becomes a pattern. Accepting a regexp here would reintroduce the problem
	// through the back door and make symbol into grep with another name.
	Name string `json:"name"`
	Kind string `json:"kind,omitempty"` // "def" | "ref" | "any"; default "any"
	Path string `json:"path,omitempty"`
	Glob string `json:"glob,omitempty"`
}

SymbolInput is the argument shape.

type Tool

type Tool interface {
	Name() string
	Description() string
	Schema() json.RawMessage
	// Declare reports what this call would touch, with no side effect at all.
	// Splitting it from Execute is what makes "policy decides first" a
	// structural property rather than a rule someone has to remember.
	Declare(input json.RawMessage) (policy.Request, error)
	Execute(ctx context.Context, input json.RawMessage, s *State) (Result, error)
}

Tool is one capability.

type ToolError

type ToolError struct {
	Tool   string
	Code   string
	Reason string
	Detail string
	Hint   string
}

ToolError is a failure the model is expected to recover from.

Reason says what failed; Hint suggests a way forward without prescribing one. A generic message forces the model to guess, and guessing is how an agent corrupts a file.

func (*ToolError) Error

func (e *ToolError) Error() string

func (*ToolError) Result

func (e *ToolError) Result() Result

Result renders the error as the model will read it.

type Write

type Write struct{}

Write creates a file or replaces one wholesale.

func (Write) Declare

func (w Write) Declare(input json.RawMessage) (policy.Request, error)

func (Write) Description

func (Write) Description() string

func (Write) Execute

func (w Write) Execute(_ context.Context, input json.RawMessage, s *State) (Result, error)

func (Write) Name

func (Write) Name() string

func (Write) Schema

func (Write) Schema() json.RawMessage

type WriteInput

type WriteInput struct {
	Path    string `json:"path"`
	Content string `json:"content"`
}

WriteInput is the argument shape.

Jump to

Keyboard shortcuts

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