agent

package
v0.1.5 Latest Latest
Warning

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

Go to latest
Published: Aug 9, 2026 License: MIT Imports: 30 Imported by: 0

Documentation

Overview

Package agent wires a Provider, a tool Registry, and a Session into the harness loop that drives a coding task to completion.

Index

Constants

View Source
const (

	// SoftTrim constants: used by SoftTrimLargeResults for graduated pruning.
	// Outputs larger than SoftTrimThreshold in the prune zone are partially
	// trimmed (keep head+tail) before being candidates for full elision.
	SoftTrimThreshold = 4096
	SoftTrimKeepHead  = 1536
	SoftTrimKeepTail  = 1536
)

Pruning is the free half of context maintenance: stale tool results are re-derivable (files can be re-read, commands re-run), so eliding them needs no summarizer call and never drops a message — tool_call/result pairing and assistant content (including signed reasoning) are untouched by construction.

View Source
const (
	// DefaultMaxCandidates is the number of parallel propose-only candidates.
	DefaultMaxCandidates = 5
)
View Source
const DefaultTaskSystemPrompt = `` /* 357-byte string literal not displayed */

DefaultTaskSystemPrompt steers a sub-agent toward focused, terse delivery — it doesn't see the parent's conversation so it must self-contain.

View Source
const DistillTask = `` /* 1132-byte string literal not displayed */

DistillTask is the prompt fed to a background agent for workflow extraction.

View Source
const DreamCompactTask = `` /* 1512-byte string literal not displayed */

DreamCompactTask is the prompt fed to dream when one or more portrait files have grown past their compact threshold. This run's PRIMARY job is to SHRINK the bloated files back under target — by merging redundant lines, dropping stale facts, and rewriting verbose prose tighter.

View Source
const DreamTask = `` /* 3516-byte string literal not displayed */

DreamTask is the prompt fed to a background agent for portrait consolidation. The dream agent MAINTAINS THE PORTRAIT FILES directly — profile/user.md, profile/memory.md, profile/<mode>.md — because those are the only memories injected into every turn. Its output must be concise, human-prose, and merged, not a list of scattered facts. It writes with write_file, NOT remember.

Variables

This section is empty.

Functions

func BranchID

func BranchID(path string) string

func BranchMetaPath

func BranchMetaPath(sessionPath string) string

func CompactArgs

func CompactArgs(s string) string

CompactArgs trims and caps a tool's raw JSON arguments for the dispatch line. Exported so the CLI can reuse the same rendering without duplicating the logic.

func ContinueSessionPath

func ContinueSessionPath(prevPath, dir, model string) string

ContinueSessionPath returns where a conversation carried into a rebuilt controller (model switch, config change) should keep auto-saving: its existing file when it has one, so the continued session stays a single file instead of the old one being orphaned as an identical duplicate (#2807). A session with no file yet gets a fresh path; "" when persistence is disabled.

func DeleteSubagentsByParent

func DeleteSubagentsByParent(sessionDir, parentSession string) error

DeleteSubagentsByParent permanently removes sub-agent artifacts owned by a parent session. Missing counterpart files are ignored.

func DreamInFlight

func DreamInFlight(kind DreamKind) bool

DreamInFlight reports whether a run of the given kind is currently executing. Used by the desktop UI to show a "running" state and disable the trigger button.

func FilterReadOnlyRegistry

func FilterReadOnlyRegistry(parent *tool.Registry, exclude ...string) *tool.Registry

FilterReadOnlyRegistry builds a sub-registry containing only tools whose ReadOnly contract is true, minus explicit exclusions.

func FilterRegistry

func FilterRegistry(parent *tool.Registry, names []string, exclude ...string) *tool.Registry

FilterRegistry builds a sub-registry from parent: the named whitelist (empty = every parent tool), minus any excluded names. Used to scope what a spawned sub-agent — a `task` sub-agent or a subagent skill — may call, e.g. excluding `task` to bar recursive nesting, or restricting to a skill's allowed-tools.

func FormatSubagentResult

func FormatSubagentResult(answer, ref string, failed bool) string

func FormatUsageLine

func FormatUsageLine(u *provider.Usage, p *provider.Pricing, d *event.CacheDiagnostics) string

FormatUsageLine renders the per-turn token/cache summary — the key signal for the cache-first design — as a single line (no trailing newline), or "" when usage is unset or empty. Cache is reported as absolute "(N cached / M new)" so a turn that adds a lot of fresh content doesn't read as "cache broke" the way a falling percentage would; the cached prefix is still hitting, the denominator just grew. Reasoning tokens (a subset of completion) show the chain-of-thought cost. Shared by TextSink and the chat TUI so both frontends render the line identically.

func HandoffTask

func HandoffTask(s string) string

HandoffTask returns the original user task embedded in an executor handoff message, or s unchanged when it is not one. Session previews and auto-titles use it so legacy dual-model sessions surface the user's words, not the handoff boilerplate (#3860).

func MigrateLegacySessions

func MigrateLegacySessions(srcDir, globalDest string, projectDir func(workspaceRoot string) string) (int, error)

MigrateLegacySessions imports v0.x event-log sessions (<name>.events.jsonl under srcDir) into the v1+ message-log format, routing each session into the per-workspace dir its sidecar meta names (via projectDir) so the desktop sidebar can see it; sessions without a live workspace land in globalDest. It also re-homes sessions a previous flat import left in globalDest. Runs once — guarded by a marker in globalDest — and never modifies the legacy files. Returns the count imported (including re-homed).

func MigrateLegacySessionsFromConfigDir

func MigrateLegacySessionsFromConfigDir(srcDir, globalDest string, projectDir func(workspaceRoot string) string) (int, error)

MigrateLegacySessionsFromConfigDir imports v0.x event-log sessions found in the current user config session directory. It uses an independent marker so a previous ~/.fairpeer import marker cannot hide sessions from a redirected config root on Windows/macOS.

func NestedSink

func NestedSink(ctx context.Context, fallback event.Sink) event.Sink

NestedSink returns a sink that forwards a sub-agent's tool activity to the parent stream, nested under the tool call carried by ctx, so a frontend shows it beneath that call (the same nesting `task` uses). Falls back to the given sink when ctx carries no call context. Used by subagent skills.

func NewSessionPath

func NewSessionPath(dir, model string) string

NewSessionPath returns the path to use for a fresh session, namespaced by the model so the filename hints at what the conversation was with. dir is typically config.SessionDir().

func NormalizeReasoningLanguage

func NormalizeReasoningLanguage(lang string) string

NormalizeReasoningLanguage returns one of auto|zh|en for runtime-only visible reasoning preferences. Keep this local to the agent package so sub-agents can inherit the preference without depending on config.

"auto" (the default) leaves the reasoning text language up to the provider — it deliberately does NOT force a language, since some models reason better in their training-dominant language and forcing it can hurt quality.

func NormalizeSession

func NormalizeSession(msgs []provider.Message) []provider.Message

NormalizeSession runs the persisted-history-safe repairs on a loaded conversation and is the agent-side entry point for making old, partially saved, or interrupted sessions replayable. It is a thin wrapper over provider.NormalizeSessionMessages, which shares assistant-turn repairs with the provider send path without applying wire-only cleanup such as dropping standalone tool messages.

LoadSession calls this right after decoding so a session that was written by an older code version, or that was cut short mid-turn, is corrected in memory before anything reads it. The corrected messages are persisted lazily: the next Session.Save (naturally triggered by the following turn) rewrites the whole file with the repairs baked in, so the same stale-data bug is not re-repaired on every turn forever. A session that is only ever read (never appended to) stays unmodified on disk and is simply re-normalized on the next load — cheap, because the fast path returns the input slice unchanged.

Well-formed histories are returned without allocating (see provider.NormalizeSessionMessages), so this is a no-op in both time and memory for the common case and cannot perturb a provider's prefix-cache key.

Ported from DeepSeek-Reasonix (PR #4811 unifying history normalization).

func ParentSession

func ParentSession(ctx context.Context) string

ParentSession returns the active parent session ID carried by a turn context.

func PlannerToolRegistry

func PlannerToolRegistry(parent *tool.Registry) *tool.Registry

PlannerToolRegistry returns the tool set exposed to the two-model planner: read-only research tools only. It deliberately excludes workflow/meta tools that are technically read-only but can prompt the user, update visible task state, wait on jobs, or expand commands instead of inspecting context.

func ReasoningLanguageBlock

func ReasoningLanguageBlock(lang string) string

ReasoningLanguageBlock is transient user-turn context. It deliberately does not belong in the stable system prompt or tool schemas — those must stay byte-stable across turns so the provider's prefix cache stays warm. Injecting it as a per-turn prefix on the user message keeps the cache-stable prefix untouched while still steering the visible reasoning text language.

The block only steers the THINKING/reasoning text. It explicitly does NOT override the user's choice for the final answer language, and it keeps code, identifiers, file paths, shell commands, and untranslated technical terms in their original form.

func RegisterDistillComplete

func RegisterDistillComplete(fn func() string)

RegisterDistillComplete installs the callback fired after a successful Distill run. boot.go passes a closure that retires cold skills; passing nil disables it (e.g. when [dream] is off). Safe to call at any time; the next Distill run picks up the new hook.

func RenameSession

func RenameSession(sessionPath string, title string) error

RenameSession updates the topic title of the session at sessionPath.

func RunSubAgentWithSession

func RunSubAgentWithSession(ctx context.Context, prov provider.Provider, reg *tool.Registry, sess *Session, prompt string, opts Options, sink event.Sink) (string, error)

RunSubAgentWithSession continues an existing sub-agent session with prompt and returns the latest final assistant answer. Fresh sub-agents pass a newly-created session; continued sub-agents pass a loaded transcript session.

func SaveBranchMeta

func SaveBranchMeta(sessionPath string, m BranchMeta) error

func SaveBranchMetaPreserveUpdated

func SaveBranchMetaPreserveUpdated(sessionPath string, m BranchMeta) error

func ShouldAutoDistill

func ShouldAutoDistill(sessionDir string) bool

ShouldAutoDistill reports whether the Distill agent should run this turn.

func ShouldAutoDream

func ShouldAutoDream(sessionDir string) bool

ShouldAutoDream reports whether the Dream agent should run this turn, based on the live config (master switch + cadence). It is the entry point called from the controller turn loop.

func SpawnDistill

func SpawnDistill(ctx context.Context, sessionDir string, prov provider.Provider, reg *tool.Registry, sess *Session, sink event.Sink, wg *sync.WaitGroup) bool

SpawnDistill kicks off a background distill agent if an automatic run is due. See SpawnDream for the wg draining contract.

func SpawnDream

func SpawnDream(ctx context.Context, sessionDir, profile string, prov provider.Provider, reg *tool.Registry, sess *Session, sink event.Sink, wg *sync.WaitGroup) bool

SpawnDream kicks off a background dream agent if an automatic run is due. It runs asynchronously — the caller does not block on completion. profile selects the portrait sizing check (which determines compress vs merge mode). wg, when non-nil, is Add(1)/Done()'d so a caller can drain it on shutdown.

func StripGoalMarkers

func StripGoalMarkers(text string) string

StripGoalMarkers removes goal status markers like [goal:complete], [goal:continue], and [goal:blocked:...] from display text so users see natural language instead of protocol markers. Exported for use by frontends (desktop wire, CLI TUI, HTTP/SSE serve).

The markers are still kept in the session history — the controller's parseGoalStatusMarker relies on them to drive the goal loop, and the HTTP history endpoint returns them verbatim so a replayed conversation still advances. This function is only for the live, user-facing display path.

Ported from DeepSeek-Reasonix. Behavior:

  • [goal:complete] and [goal:continue] lines are dropped entirely.
  • [goal:blocked:<reason>] is rewritten as "⚠️ Blocked: <reason>" so the user still sees that the turn hit a blocker, just without the raw tag.
  • A line with other content keeps that content; only bare marker lines are removed.

func SubagentMetaTools

func SubagentMetaTools() []string

SubagentMetaTools returns the tool names that spawned agents should not inherit from the parent registry unless a future call site deliberately opts into a different boundary. They can spawn or author more agent work, so excluding them preserves one layer of delegation without adding a spawn-count cap.

func TouchBranchMeta

func TouchBranchMeta(sessionPath string) error

func WithParentSession

func WithParentSession(ctx context.Context, parentSession string) context.Context

WithParentSession stamps the active parent session ID onto a turn context so persisted sub-agents can record and enforce their owning conversation.

func WithReasoningLanguage

func WithReasoningLanguage(content, lang string) string

WithReasoningLanguage prefixes content with the transient reasoning-language block unless the turn already starts with an injected reasoning-language block. User-authored mentions of the tag later in the prompt must not suppress the configured preference, so only a LEADING block counts as "already injected". Returns content unchanged when lang is "auto" (no preference).

Types

type Agent

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

Agent drives a single task: a Provider, a tool Registry, and a Session wired into the main loop.

func New

func New(prov provider.Provider, tools *tool.Registry, session *Session, opts Options, sink event.Sink) *Agent

New constructs an Agent. MaxSteps <= 0 means no cap — the run loop continues until the model gives a final answer, the context is cancelled, or the provider errors (compaction keeps the context bounded). A nil sink is replaced with event.Discard so the agent can always emit unconditionally.

func (*Agent) CompactNow

func (a *Agent) CompactNow(ctx context.Context, instructions string) error

CompactNow runs one compaction pass immediately, regardless of the usage-ratio threshold maybeCompact normally honours. Used by the chat TUI's `/compact` command so the user can reset the prefix before it naturally fills up.

func (*Agent) CompactRatio

func (a *Agent) CompactRatio() float64

CompactRatio returns the fraction of the window at which auto-compaction fires (e.g. 0.8). The status line uses it to show headroom to the next compact.

func (*Agent) ContextWindow

func (a *Agent) ContextWindow() int

ContextWindow returns the configured context-window size in tokens. 0 means compaction is disabled for this agent.

func (*Agent) IsPaused

func (a *Agent) IsPaused() bool

IsPaused reports whether the agent is currently blocked on a pause (between steps, awaiting Resume). False when running normally or not running at all.

func (*Agent) LastUsage

func (a *Agent) LastUsage() *provider.Usage

LastUsage returns the most recent per-turn token telemetry the provider reported (nil if no turn has run yet). The TUI uses it to show a context gauge alongside the prompt; the actual cache decisions still live inside maybeCompact.

func (*Agent) Pause

func (a *Agent) Pause()

Pause requests a graceful pause. The run loop finishes the current step (it does NOT interrupt an in-flight LLM call), then blocks at the top of the next iteration until Resume is called. State — session, todos, history — is fully preserved, so Resume continues from exactly where the agent stopped. Calling Pause when no run is active, or when already paused, is a no-op. Idempotent and safe to call from any goroutine.

func (*Agent) Provider

func (a *Agent) Provider() provider.Provider

Provider returns the agent's LLM provider, available for auxiliary calls such as the goal judge.

func (*Agent) PruneStaleToolResults

func (a *Agent) PruneStaleToolResults() (PruneStats, error)

PruneStaleToolResults elides tool-result content older than the protected recent tail, archiving the originals first. Idempotent; a no-op when compaction is disabled (no context window).

func (*Agent) Resume

func (a *Agent) Resume()

Resume unblocks a paused run. If the run isn't paused (or none is active), it's a no-op. Safe from any goroutine.

func (*Agent) Run

func (a *Agent) Run(ctx context.Context, input any) error

Run appends the user input and drives the tool loop until the model returns a final answer (no tool calls), the context is cancelled, or the provider errors. With maxSteps <= 0 the loop is unbounded — the natural termination is the model finishing, and the real safety bounds are user cancellation and compaction, not a round count. A positive maxSteps imposes an optional hard guard, surfaced as a resumable notice when hit.

func (*Agent) Session

func (a *Agent) Session() *Session

Session returns the agent's current conversation, useful for persistence hooks that need to read the message log between turns. sessMu serialises this pointer read against SetSession, so a frontend (serve's concurrent /history and /new handlers) can't race the swap. The run loop touches a.session directly and only swaps it via SetSession while idle, so its reads need no lock.

func (*Agent) SessionCache

func (a *Agent) SessionCache() (hit, miss int)

SessionCache returns the cumulative cache hit/miss prompt tokens across every API call this session — the basis for the status line's aggregate hit-rate.

func (*Agent) SetAsker

func (a *Agent) SetAsker(as Asker)

SetAsker installs the asker the `ask` tool uses to question the user. Interactive frontends wire one in; headless runs leave it nil.

func (*Agent) SetContextFilter

func (a *Agent) SetContextFilter(fn func([]provider.Message) []provider.Message)

SetContextFilter installs a read-side transform applied to session messages before they're sent to the model. It lets a caller (e.g. the experts engine) keep a full-fidelity message in the transcript while showing the model a compact projection of it, so the context window isn't bloated. nil (the default) passes messages through unchanged.

func (*Agent) SetGate

func (a *Agent) SetGate(g Gate)

SetGate installs the per-call permission gate. Used by `fairpeer chat` to swap the headless gate built in setup for an interactive one that prompts the user; nil disables gating. Safe to call before the run loop starts.

func (*Agent) SetMemoryQueue

func (a *Agent) SetMemoryQueue(q memory.Queue)

SetMemoryQueue installs the sink the remember/forget tools use to apply a memory change in the current session. The controller wires itself in.

func (*Agent) SetPlanMode

func (a *Agent) SetPlanMode(v bool)

SetPlanMode flips the read-only gate. While true, executeOne refuses any non-ReadOnly tool the model calls and returns a "blocked" result instead of running it. The cache-friendly bits — system prompt, tools schema, message history — are left untouched, so the toggle costs nothing in cache hits.

func (*Agent) SetPreEditHook

func (a *Agent) SetPreEditHook(fn func(diff.Change))

SetPreEditHook installs the pre-edit snapshot hook (see onPreEdit). The controller wires it to its per-session checkpoint store; nil disables capture.

func (*Agent) SetSession

func (a *Agent) SetSession(s *Session)

SetSession replaces the agent's conversation wholesale. Used by `fairpeer chat --resume` to load a saved JSONL transcript before the first turn, so the model picks up exactly where it left off. Callers serialise it against a running turn (it only fires while idle); sessMu guards the pointer swap itself.

func (*Agent) SetSkipReadiness

func (a *Agent) SetSkipReadiness(v bool)

SetSkipReadiness toggles the final-answer readiness gate. compose uses this to suppress the gate during its phased implement/verify/review runs, where the gate would otherwise force the model to complete all todos in a single turn or hard-error after maxFinalReadinessBlocks. See audit finding C3.

func (*Agent) SoftTrimLargeResults

func (a *Agent) SoftTrimLargeResults() (PruneStats, error)

SoftTrimLargeResults partially trims tool results in the prune zone that are larger than SoftTrimThreshold, keeping head and tail. This is a graduated step between "keep everything" and "full elision" — it preserves the most useful parts (commands/setup at top, results/errors at bottom) while saving context. Call this BEFORE PruneStaleToolResults for a two-pass approach: soft trim first, then hard prune whatever is still too large.

func (*Agent) Steer

func (a *Agent) Steer(text string)

Steer queues a message for mid-turn injection.

func (*Agent) SteerConsumed

func (a *Agent) SteerConsumed() bool

SteerConsumed returns true when the steer queue became empty after the last consume.

func (*Agent) SummarizeFrom

func (a *Agent) SummarizeFrom(ctx context.Context, fromIdx int) error

SummarizeFrom replaces the messages from fromIdx onward with a single summary, keeping everything before it verbatim ("summarize from here"). fromIdx is a turn boundary (a user message), so the split never severs a tool_call/result pair — those live within one turn. A no-op when the region is empty.

func (*Agent) SummarizeUpTo

func (a *Agent) SummarizeUpTo(ctx context.Context, toIdx int) error

SummarizeUpTo replaces the messages before toIdx (after the system prompt) with a single summary, keeping toIdx onward verbatim ("summarize up to here"). toIdx is a turn boundary, so no tool pair is split. A no-op when the region is empty.

type AskTool

type AskTool struct{}

AskTool lets the model put a structured multiple-choice question (or a few) to the user mid-task and get the answer back — for genuine forks the model can't resolve from the request or the code (which library, which approach, …) rather than guessing or asking in prose. The frontend renders selectable options, the user picks, and the choices come back as the tool result. It reaches the user through the Asker carried on the call context (CallContext); with no asker (headless runs) it returns an explicit model-assumption fallback so an autonomous run never blocks or pretends a user answered.

func NewAskTool

func NewAskTool() *AskTool

func (*AskTool) Description

func (*AskTool) Description() string

func (*AskTool) Execute

func (*AskTool) Execute(ctx context.Context, args json.RawMessage) (string, error)

func (*AskTool) Name

func (*AskTool) Name() string

func (*AskTool) ReadOnly

func (*AskTool) ReadOnly() bool

ReadOnly is true: asking has no host side effects, so it never needs approval and stays available in plan mode (clarifying scope while planning is fine).

func (*AskTool) Schema

func (*AskTool) Schema() json.RawMessage

type Asker

type Asker interface {
	Ask(ctx context.Context, questions []event.AskQuestion) ([]event.AskAnswer, error)
}

Asker puts structured multiple-choice questions to the user and blocks for the answers. The agent consults it for the `ask` tool. It is interface-shaped so the agent stays independent of the frontend; a nil asker means no interactive user (headless runs), where `ask` returns a "decide for yourself" result. The interactive frontends wire the controller in as the Asker.

func CallContext

func CallContext(ctx context.Context) (parentID string, sink event.Sink, asker Asker, ok bool)

CallContext returns the executing call's ID, the agent's sink, and the asker, if the context was set by an agent's executeOne. ok is false for a plain context (headless tool tests, calls made outside the run loop).

type BranchInfo

type BranchInfo struct {
	BranchMeta
	Path    string
	ModTime time.Time
	Preview string
	Turns   int
}

BranchInfo combines sidecar metadata with the session file details needed for pickers and tree rendering.

func ListBranches

func ListBranches(dir string) ([]BranchInfo, error)

type BranchMeta

type BranchMeta struct {
	ID               string    `json:"id"`
	Name             string    `json:"name,omitempty"`
	ParentID         string    `json:"parent_id,omitempty"`
	ForkTurn         int       `json:"fork_turn,omitempty"`
	ForkMessageIndex int       `json:"fork_message_index,omitempty"`
	CreatedAt        time.Time `json:"created_at"`
	UpdatedAt        time.Time `json:"updated_at"`
	Scope            string    `json:"scope,omitempty"`
	WorkspaceRoot    string    `json:"workspace_root,omitempty"`
	TopicID          string    `json:"topic_id,omitempty"`
	TopicTitle       string    `json:"topic_title,omitempty"`
	// ExpertTeamID identifies the expert team a scope="expert" session belongs
	// to. Empty for normal (global/project) sessions. Lets ListSessions tag the
	// session as an expert-team collaboration so the frontend can group it.
	ExpertTeamID string `json:"expert_team_id,omitempty"`
	// Profile records the product mode ("dev"|"cowork") this session was created
	// under. Empty (on legacy sidecars) is treated as "dev". It lets findTopic*
	// scope its scan so a dev session is never matched as a cowork one and vice
	// versa, even before topic storage is fully partitioned.
	Profile string `json:"profile,omitempty"`
	// CachedTurns/CachedPreview mirror what previewSession computes by decoding
	// the .jsonl. Session.Save refreshes them so ListSessions reads the sidecar
	// instead of re-decoding every session file on each render. Older readers
	// ignore these fields; older sidecars without them fall back to the decode.
	// Ported from DeepSeek-Reasonix perf(sessions) work (#4882/#4886).
	CachedTurns   int    `json:"cached_turns,omitempty"`
	CachedPreview string `json:"cached_preview,omitempty"`
	// PlanMode records whether plan mode (read-only gate) was active when the
	// session was last saved, so Resume can restore it. Restoring plan mode is
	// safe (it only restricts writes, never starts background work). See C8.
	PlanMode bool `json:"plan_mode,omitempty"`
	// ToolApprovalMode records the writer-tool approval stance ("ask"/"auto"/
	// "yolo") so Resume can restore it. Restoring YOLO is intentional — if the
	// user had auto-approve on, they expect it to stay on across a restart.
	ToolApprovalMode string `json:"tool_approval_mode,omitempty"`
}

BranchMeta is the small sidecar record that turns flat session files into a navigable conversation tree. The conversation itself remains in the .jsonl file; metadata lives beside it at <session>.meta.

func EnsureBranchMeta

func EnsureBranchMeta(sessionPath string) (BranchMeta, error)

func LoadBranchMeta

func LoadBranchMeta(sessionPath string) (BranchMeta, bool, error)

func (BranchMeta) DefaultScope

func (m BranchMeta) DefaultScope() string

type CacheDiagnostics

type CacheDiagnostics = event.CacheDiagnostics

CacheDiagnostics is a type alias for event.CacheDiagnostics so the agent can construct and compare diagnostics without importing event itself in every call site, while still assigning to event.Event.CacheDiagnostics.

func CompareShape

func CompareShape(prev, cur PrefixShape, usage *provider.Usage) CacheDiagnostics

CompareShape returns diagnostics describing what changed between two shapes.

type DreamKind

type DreamKind string

DreamKind identifies which self-evolution agent a record describes.

const (
	KindDream   DreamKind = "dream"
	KindDistill DreamKind = "distill"
)

type DreamRun

type DreamRun struct {
	Kind      DreamKind    `json:"kind"`
	Trigger   DreamTrigger `json:"trigger"`
	StartedAt time.Time    `json:"started_at"`
	Duration  string       `json:"duration,omitempty"`
	Status    string       `json:"status"`             // "ok" | "error" | "timeout"
	Error     string       `json:"error,omitempty"`    // set when status != ok
	Memories  int          `json:"memories,omitempty"` // best-effort count when discoverable
}

DreamRun is one completed (or failed) Dream/Distill invocation.

func DreamHistory

func DreamHistory(sessionDir string, kind DreamKind) []DreamRun

DreamHistory returns the recorded runs of the given kind, newest first.

func LastDreamRun

func LastDreamRun(sessionDir string, kind DreamKind) (DreamRun, bool)

LastDreamRun returns the most recent recorded run of the given kind (zero DreamRun if none). It reads only from disk — the cadence gate uses this so a manual run is visible to the next automatic decision.

func RunDistillOnce

func RunDistillOnce(ctx context.Context, sessionDir string, prov provider.Provider, reg *tool.Registry, sess *Session, sink event.Sink) (DreamRun, bool)

RunDistillOnce triggers a manual Distill run. See RunDreamOnce.

func RunDreamOnce

func RunDreamOnce(ctx context.Context, sessionDir, profile string, prov provider.Provider, reg *tool.Registry, sess *Session, sink event.Sink) (DreamRun, bool)

RunDreamOnce triggers a manual Dream run. It blocks until the run completes (or times out) and returns the resulting record + whether a run actually executed. The caller (controller → desktop) surfaces the status to the user.

type DreamTrigger

type DreamTrigger string

DreamTrigger records how a run was initiated.

const (
	TriggerAuto   DreamTrigger = "auto"
	TriggerManual DreamTrigger = "manual"
)

type FileOperations

type FileOperations struct {
	Read    map[string]struct{}
	Written map[string]struct{}
	Edited  map[string]struct{}
}

FileOperations tallies file paths touched by the path-taking built-in tools over a slice of messages. It is the deterministic source of the <read-files> and <modified-files> blocks appended to a compaction summary: rather than trust the summarizer to lift exact paths out of free-text transcripts, we read them straight from the tool-call arguments (mirroring pi's compaction/utils.ts extractFileOpsFromMessage).

func ExtractFileOps

func ExtractFileOps(messages []provider.Message) FileOperations

ExtractFileOps scans messages for assistant tool calls against the path-taking built-ins and collects their paths. It is best-effort: any tool call whose arguments don't parse as JSON (truncated mid-stream, malformed) is silently skipped — a missing path never aborts compaction. We deliberately do not invoke provider's internal argument-repair path here: a tool call whose "path" we can't read straight from the wire JSON is not trustworthy enough to attribute to a file anyway.

func (FileOperations) Empty

func (ops FileOperations) Empty() bool

Empty reports whether no file operation was recorded.

func (FileOperations) Format

func (ops FileOperations) Format() string

Format renders the operations as <read-files> and <modified-files> blocks suitable for appending to a compaction summary. modified = written ∪ edited; read-only = read − modified (a file both read and later edited is listed only under modified, which is what the agent needs when resuming). Paths are sorted for deterministic output. Returns "" when nothing was touched.

type Gate

type Gate interface {
	Check(ctx context.Context, toolName string, args json.RawMessage, readOnly bool) (allow bool, reason string, err error)
}

Gate decides, per tool call, whether it may run. The agent consults it at execute time (after the plan-mode gate). It is interface-shaped so the agent stays independent of the permission package and of how "ask" is resolved (silently in headless runs, interactively in the chat TUI). A nil gate means no gating — every call runs, preserving behaviour for callers that don't wire one in. reason is fed back to the model when allow is false; a non-nil err (e.g. ctx cancelled awaiting approval) is treated as a block for that call.

type GoalVerdict

type GoalVerdict struct {
	OK         bool   `json:"ok"`
	Impossible bool   `json:"impossible,omitempty"`
	Reason     string `json:"reason"`
}

GoalVerdict is the structured response from the independent goal judge.

func GoalJudge

func GoalJudge(ctx context.Context, prov provider.Provider, transcript []provider.Message, condition string, temperature float64) GoalVerdict

GoalJudge calls an independent model to evaluate whether a goal condition has been met based on the conversation transcript. The judge is "cold" — it only reads the transcript and never does the work itself, preventing optimism bias.

func GoalJudgeWithRetry

func GoalJudgeWithRetry(ctx context.Context, prov provider.Provider, transcript []provider.Message, condition string, temperature float64) GoalVerdict

GoalJudgeWithRetry calls the goal judge, retrying once on transient errors.

type MaxCandidate

type MaxCandidate struct {
	Index     int
	Text      string
	Reasoning string
	ToolCalls []provider.ToolCall
	Usage     provider.Usage
	Err       error
}

MaxCandidate holds one candidate's streamed response.

type MaxJudgeResult

type MaxJudgeResult struct {
	BestIndex int    `json:"best"`
	Reason    string `json:"reason"`
}

MaxJudgeResult is the judge's selection.

func RunMaxStep

func RunMaxStep(ctx context.Context, prov provider.Provider, sysPrompt string, messages []provider.Message, tools []provider.ToolSchema, n int, temperature float64) *MaxJudgeResult

RunMaxStep runs N parallel propose-only candidates, then a judge selects the best one. The winner's tool calls are returned for actual execution. If all candidates fail, returns nil (caller should fall back to normal single-step).

type Options

type Options struct {
	MaxSteps int
	// MaxStepsKey names the configuration knob shown when the MaxSteps guard is
	// hit. Empty defaults to agent.max_steps.
	MaxStepsKey string
	Temperature float64
	Pricing     *provider.Pricing // optional, for per-turn cost display

	// Gate is the per-call permission gate. nil disables gating.
	Gate Gate

	// Context management. ContextWindow <= 0 disables compaction. Ratios and
	// RecentKeep fall back to defaults when unset.
	ContextWindow     int
	SoftCompactRatio  float64
	CompactRatio      float64
	CompactForceRatio float64
	RecentKeep        int
	ArchiveDir        string
	// ContextBudgetPercent caps the effective context window the agent treats as
	// available, triggering compaction earlier (SPEC v2 §3.6). 0 or 100 = use
	// the full window (the default, zero user config). 80 = compact as if the
	// window were 80% of its real size — useful for cost tiers (input past a
	// provider's pricing breakpoint doubles) and for models whose quality
	// degrades near the window edge. Clamped to (0,100].
	ContextBudgetPercent int

	// Hooks fires PreToolUse / PostToolUse shell hooks around tool calls. nil
	// disables hook firing.
	Hooks ToolHooks

	// Jobs is the session's background-job manager (nil disables background tools).
	Jobs *jobs.Manager

	// ProjectChecks are host-observable structured checks extracted during boot.
	ProjectChecks []instruction.VerifyCheck
}

Options configures an Agent.

type ParallelTasksTool

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

ParallelTasksTool dispatches multiple sub-agent tasks concurrently and collects all results. Each sub-task runs as a foreground sub-agent in its own goroutine, emitting nested events so the frontend renders independent cards per sub-task. It reuses TaskTool's sub-agent infrastructure (provider resolution, tool filtering, transcript runs) so every sub-task inherits the same sandbox, gate, and hooks — only the dispatch is parallel.

Multi-model fit: each sub-task accepts an optional model/effort override, so a caller can route independent pieces of work to different models on the same platform — e.g. planning to a large reasoning model, code generation to a code-tuned model, search/routing to a small fast model. The TaskTool.resolveProvider callback resolves each per-sub-task model just like a single task call would.

Ported from DeepSeek-Reasonix (parallel_tasks tool), adapted to fairpeer's TaskTool shape. Read-only classification matches the upstream tool so the agent's parallel-batch optimizer runs these concurrently without write races; a sub-agent that needs to write should use the sequential `task` tool.

func NewParallelTasksTool

func NewParallelTasksTool(taskTool *TaskTool, reg *tool.Registry) *ParallelTasksTool

NewParallelTasksTool creates a parallel dispatch tool that reuses the given TaskTool's sub-agent infrastructure. reg is the parent registry the per-task tool whitelists are filtered from.

func (*ParallelTasksTool) Description

func (p *ParallelTasksTool) Description() string

func (*ParallelTasksTool) Execute

func (p *ParallelTasksTool) Execute(ctx context.Context, args json.RawMessage) (string, error)

func (*ParallelTasksTool) Name

func (p *ParallelTasksTool) Name() string

func (*ParallelTasksTool) ReadOnly

func (p *ParallelTasksTool) ReadOnly() bool

ReadOnly is true: the parallel-dispatch path only collects results and must not let concurrent writes race. Sub-agents that need to mutate state should go through the sequential `task` tool, where ordering is preserved. This also lets the agent's parallel-batch optimizer run parallel_tasks alongside other read-only calls.

func (*ParallelTasksTool) Schema

func (p *ParallelTasksTool) Schema() json.RawMessage

type PrefixShape

type PrefixShape struct {
	SystemHash        string
	ToolsHash         string
	PrefixHash        string
	LogRewriteVersion int
	ToolSchemaTokens  int
}

PrefixShape hashes the portions of the request prefix that influence provider-side prompt-cache reuse. Comparing snapshots across turns lets us explain *why* a cache miss happened. (some providers do not report cache tokens; the prefix stability still reduces token transmission and prepares for future cache support.)

func CaptureShape

func CaptureShape(systemPrompt string, schemas []provider.ToolSchema, rewriteVersion int) PrefixShape

CaptureShape takes a snapshot of the current prefix state.

type PruneStats

type PruneStats struct {
	Results    int
	SavedChars int
	Archive    string
}

PruneStats reports one prune pass.

type Renderer

type Renderer interface {
	Render(text string) string
}

Renderer redraws the assistant's final-answer text as styled output. It is applied only after a turn's text stream completes, so the user sees raw markdown stream live, then a single redraw replaces it with formatted output. The renderer is intentionally interface-shaped so the agent stays independent of the cli's markdown library choice. Consumed by TextSink.

type Runner

type Runner interface {
	Run(ctx context.Context, input any) error
}

Runner executes one task turn. *Agent satisfies it; the controller and compose hold a Runner so they're agnostic to the concrete executor. (The two-model Coordinator that previously also satisfied Runner has been removed — fairpeer uses a single-model planner-executor path exclusively. The interface stays so callers don't depend on the concrete *Agent type.)

type Session

type Session struct {
	Messages []provider.Message
	// contains filtered or unexported fields
}

Session holds the conversation history for one task. The run loop (one turn at a time) is the only writer, but a frontend can read History/Save from another goroutine while a turn appends, so mu guards Messages. Direct Messages reads on the run-loop goroutine stay lock-free (serial with its own writes); cross- goroutine access goes through Snapshot.

func LoadSession

func LoadSession(path string) (*Session, error)

LoadSession reads a JSONL file written by Save into a fresh Session value. Missing files surface as os.IsNotExist so callers can fall through to a new session.

func NewSession

func NewSession(system string) *Session

NewSession initializes a session with an optional system prompt.

func (*Session) Add

func (s *Session) Add(m provider.Message)

Add appends a message.

func (*Session) HasContent

func (s *Session) HasContent() bool

HasContent returns true when the session carries at least one user, assistant, or tool message — i.e. more than just a system prompt. An "empty" conversation that has never been used should not be persisted.

func (*Session) IncrementRewrite

func (s *Session) IncrementRewrite()

IncrementRewrite bumps the rewrite version by 1.

func (*Session) Replace

func (s *Session) Replace(msgs []provider.Message)

Replace swaps the whole message log — used by compaction, which rewrites the middle of the history.

func (*Session) RewriteVersion

func (s *Session) RewriteVersion() int

RewriteVersion returns the current rewrite version.

func (*Session) Save

func (s *Session) Save(path string) error

Save writes the session's messages to path in JSONL — one provider.Message per line — so a user can resume the conversation later. The file is rewritten in full on every save: chat sessions are small (kilobytes), and append-only would have to be reconciled with the compaction pass that mutates the middle of session.Messages.

func (*Session) Snapshot

func (s *Session) Snapshot() []provider.Message

Snapshot returns a copy of the messages, safe to read from another goroutine while a turn appends. Frontends (History, Save) use it instead of touching the live slice.

type SessionInfo

type SessionInfo struct {
	Path           string
	CreatedAt      time.Time
	LastActivityAt time.Time
	ModTime        time.Time // compatibility alias for LastActivityAt
	Preview        string
	Turns          int
	Scope          string
	WorkspaceRoot  string
	TopicID        string
	TopicTitle     string
	Profile        string
	ExpertTeamID   string
}

SessionInfo summarises a saved session for the --resume picker: where it is on disk, when it was created/last active, the first user message as a preview, and a rough turn count.

func ListSessions

func ListSessions(dir string) ([]SessionInfo, error)

ListSessions returns every *.jsonl session under dir, most-recently-active first, each with a preview line so the picker can show something the user recognises. A missing directory is not an error — it just means there's nothing to resume yet.

type SubagentArtifact

type SubagentArtifact struct {
	Ref         string
	SessionPath string
	MetaPath    string
	Meta        SubagentMeta
}

SubagentArtifact is a persisted sub-agent transcript and metadata pair owned by a parent session. One file may be missing after a crash; lifecycle cleanup should operate on the paths that exist.

func ListSubagentsByParent

func ListSubagentsByParent(sessionDir, parentSession string) ([]SubagentArtifact, error)

ListSubagentsByParent returns persisted sub-agent artifacts whose metadata declares the given parent session owner.

type SubagentMeta

type SubagentMeta struct {
	Ref              string         `json:"ref"`
	CreatedAt        time.Time      `json:"createdAt"`
	UpdatedAt        time.Time      `json:"updatedAt"`
	Status           SubagentStatus `json:"status"`
	Kind             string         `json:"kind"` // task | skill
	Name             string         `json:"name"`
	WorkspaceRoot    string         `json:"workspaceRoot"`
	ParentSession    string         `json:"parentSession,omitempty"`
	ParentToolCallID string         `json:"parentToolCallId,omitempty"`
	SystemPromptHash string         `json:"systemPromptHash"`
	ToolScope        []string       `json:"toolScope"`
	ToolSchemaHash   string         `json:"toolSchemaHash"`
	Model            string         `json:"model"`
	Effort           string         `json:"effort"`
}

SubagentMeta is the sidecar for a persisted sub-agent transcript. It captures the execution identity that must stay stable for continuation/fork.

type SubagentRun

type SubagentRun struct {
	Ref     string
	Session *Session
	Meta    SubagentMeta
	// contains filtered or unexported fields
}

SubagentRun is a prepared transcript run. Call Release exactly once.

func EphemeralSubagentRun

func EphemeralSubagentRun(systemPrompt string) *SubagentRun

EphemeralSubagentRun is a non-persisted run for callers without an owning parent session — e.g. headless `fairpeer run`, which never mints a session path. Its empty Ref makes the store's MarkRunning/SaveCompleted/SaveFailed methods no-op and keeps FormatSubagentResult from emitting a transcript reference, so the sub-agent behaves exactly as it did before persisted transcripts existed. It holds no lock, so Release is a no-op.

func (*SubagentRun) Release

func (r *SubagentRun) Release()

type SubagentSpec

type SubagentSpec struct {
	Kind             string
	Name             string
	WorkspaceRoot    string
	ParentSession    string
	ParentToolCallID string
	SystemPrompt     string
	Registry         *tool.Registry
	Model            string
	Effort           string
}

SubagentSpec describes the current invocation identity.

type SubagentStatus

type SubagentStatus string
const (
	SubagentRunning   SubagentStatus = "running"
	SubagentCompleted SubagentStatus = "completed"
	SubagentFailed    SubagentStatus = "failed"
)

type SubagentStore

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

SubagentStore persists sub-agent transcripts under config.SessionDir()/subagents. Its locks are process-local; cross-process mutation is intentionally out of v1.

func NewSubagentStore

func NewSubagentStore(dir string) *SubagentStore

func (*SubagentStore) LoadMeta

func (s *SubagentStore) LoadMeta(ref string) (SubagentMeta, error)

func (*SubagentStore) MarkRunning

func (s *SubagentStore) MarkRunning(run *SubagentRun) error

func (*SubagentStore) PrepareContinue

func (s *SubagentStore) PrepareContinue(ref string, spec SubagentSpec) (*SubagentRun, error)

func (*SubagentStore) PrepareFork

func (s *SubagentStore) PrepareFork(ref string, spec SubagentSpec) (*SubagentRun, error)

func (*SubagentStore) PrepareFresh

func (s *SubagentStore) PrepareFresh(spec SubagentSpec) (*SubagentRun, error)

func (*SubagentStore) SaveCompleted

func (s *SubagentStore) SaveCompleted(run *SubagentRun) error

func (*SubagentStore) SaveFailed

func (s *SubagentStore) SaveFailed(run *SubagentRun) error

type TaskTool

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

TaskTool spawns a sub-agent in its own session for a focused sub-task. The sub-agent runs with a filtered tool whitelist and the same step budget shape as the parent (see Execute); its tool calls are forwarded to the parent's event stream nested under this call, while only its final assistant message is returned to the parent model. Use cases: keep noisy tool sequences (multi-file exploration, repeated grep / read_file) out of the parent's context budget, or parallel research across independent areas (the parallel-dispatch path picks these up only when readOnly, which task is not).

func NewTaskTool

func NewTaskTool(prov provider.Provider, pricing *provider.Pricing, parentReg *tool.Registry,
	maxSteps, contextWindow int, softCompactRatio, compactRatio, compactForceRatio, temperature float64, archiveDir, sysPrompt string, gate Gate,
	subagentModel, subagentEffort string, resolveProvider func(string, string) (provider.Provider, *provider.Pricing, int, error)) *TaskTool

NewTaskTool wires a task tool to the parent agent's environment so its sub-agents can use the same provider and tools. sysPrompt is the system prompt every sub-agent starts with; pass "" for DefaultTaskSystemPrompt. gate is the permission gate sub-agents inherit — pass the headless variant so deny rules still bite while autonomous sub-agents are never blocked on an interactive prompt (there is no UI to answer one).

func (*TaskTool) Description

func (t *TaskTool) Description() string

func (*TaskTool) Execute

func (t *TaskTool) Execute(ctx context.Context, args json.RawMessage) (string, error)

func (*TaskTool) Name

func (t *TaskTool) Name() string

func (*TaskTool) ReadOnly

func (t *TaskTool) ReadOnly() bool

ReadOnly is false: a sub-agent can invoke any whitelisted tool, including writers. Conservative classification keeps the parallel-dispatch path from running two sub-agents at once and letting their writes race.

func (*TaskTool) ResolveProfile

func (t *TaskTool) ResolveProfile(args json.RawMessage) *event.Profile

ResolveProfile extracts model/effort from task args and applies config defaults.

func (*TaskTool) Schema

func (t *TaskTool) Schema() json.RawMessage

func (*TaskTool) WithTranscriptIdentityResolver

func (t *TaskTool) WithTranscriptIdentityResolver(resolve func(modelRef, effort string) (string, string)) *TaskTool

func (*TaskTool) WithTranscripts

func (t *TaskTool) WithTranscripts(store *SubagentStore, workspaceRoot, baseModel, baseEffort string) *TaskTool

WithTranscripts enables persisted sub-agent transcript continuation for this task tool. The base model/effort are the parent provider identity used when no subagent override is configured.

type TextSink

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

TextSink renders a turn's event stream to ANSI text on an io.Writer. It is the reference terminal frontend: a headless `fairpeer run` writes to stdout, and during the cache-first migration the chat TUI is fed through it too. The output is byte-for-byte what the agent used to print directly, now driven by typed events instead of inline Fprint calls.

renderer, when non-nil, replaces the streamed raw answer text with styled markdown once the text stream completes (a Message event). termWidth is the column count used to count how many rows the raw stream occupied before the redraw moves the cursor back. A nil renderer keeps the raw stream — correct for piped output and for the chat TUI, which renders markdown itself.

func NewTextSink

func NewTextSink(out io.Writer, renderer Renderer, termWidth int) *TextSink

NewTextSink builds a TextSink writing to out. renderer/termWidth drive the post-stream markdown redraw; pass a nil renderer to keep the raw stream.

func (*TextSink) Emit

func (s *TextSink) Emit(e event.Event)

Emit renders one event. Called serially by the run loop.

func (*TextSink) SetShowReasoning

func (s *TextSink) SetShowReasoning(show bool)

SetShowReasoning toggles Claude Code-style verbose display for thinking-mode reasoning. Reasoning is still kept in session state by the agent; this only controls terminal rendering.

type ToolHooks

type ToolHooks interface {
	PreToolUse(ctx context.Context, name string, args json.RawMessage) (block bool, message string)
	PostToolUse(ctx context.Context, name string, args json.RawMessage, result string)
	// PostLLMCall fires after each model turn completes (streaming finishes)
	// but before reasoning_content is stored. It returns the (possibly
	// translated) reasoning string — the original when no hook is configured.
	// HasPostLLMCall reports whether such a hook exists, so the agent keeps
	// streaming reasoning live when none is wired up.
	PostLLMCall(ctx context.Context, reasoning string, turn int) string
	HasPostLLMCall() bool
	// SubagentStop fires when a `task` sub-agent finishes (foreground). PreCompact
	// fires just before a compaction pass and returns extra summary guidance (its
	// hooks' stdout) to fold into the summary prompt; "" when no hook contributes.
	SubagentStop(ctx context.Context, last string)
	PreCompact(ctx context.Context, trigger string) string
}

ToolHooks fires user-configured shell hooks around each tool call. PreToolUse runs before the call and may block it (block=true; message is the reason fed back to the model); PostToolUse runs after and only surfaces output to the user (it can't block). It is interface-shaped so the agent stays independent of the hook package — a nil hooks field disables hook firing entirely.

type ToolSchemaCost

type ToolSchemaCost struct {
	Name   string
	Tokens int
}

ToolSchemaCost is a per-tool token cost estimate for diagnostic display.

func SchemaTokenCosts

func SchemaTokenCosts(schemas []provider.ToolSchema) []ToolSchemaCost

SchemaTokenCosts returns per-tool token cost estimates for display.

Directories

Path Synopsis
Package testutil provides reusable test helpers for the agent package.
Package testutil provides reusable test helpers for the agent package.

Jump to

Keyboard shortcuts

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