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
- func BranchID(path string) string
- func BranchMetaPath(sessionPath string) string
- func CompactArgs(s string) string
- func ContinueSessionPath(prevPath, dir, model string) string
- func DeleteSubagentsByParent(sessionDir, parentSession string) error
- func DreamInFlight(kind DreamKind) bool
- func FilterReadOnlyRegistry(parent *tool.Registry, exclude ...string) *tool.Registry
- func FilterRegistry(parent *tool.Registry, names []string, exclude ...string) *tool.Registry
- func FormatSubagentResult(answer, ref string, failed bool) string
- func FormatUsageLine(u *provider.Usage, p *provider.Pricing, d *event.CacheDiagnostics) string
- func HandoffTask(s string) string
- func MigrateLegacySessions(srcDir, globalDest string, projectDir func(workspaceRoot string) string) (int, error)
- func MigrateLegacySessionsFromConfigDir(srcDir, globalDest string, projectDir func(workspaceRoot string) string) (int, error)
- func NestedSink(ctx context.Context, fallback event.Sink) event.Sink
- func NewSessionPath(dir, model string) string
- func NormalizeReasoningLanguage(lang string) string
- func NormalizeSession(msgs []provider.Message) []provider.Message
- func ParentSession(ctx context.Context) string
- func PlannerToolRegistry(parent *tool.Registry) *tool.Registry
- func ReasoningLanguageBlock(lang string) string
- func RegisterDistillComplete(fn func() string)
- func RenameSession(sessionPath string, title string) error
- func RunSubAgentWithSession(ctx context.Context, prov provider.Provider, reg *tool.Registry, sess *Session, ...) (string, error)
- func SaveBranchMeta(sessionPath string, m BranchMeta) error
- func SaveBranchMetaPreserveUpdated(sessionPath string, m BranchMeta) error
- func ShouldAutoDistill(sessionDir string) bool
- func ShouldAutoDream(sessionDir string) bool
- func SpawnDistill(ctx context.Context, sessionDir string, prov provider.Provider, ...) bool
- func SpawnDream(ctx context.Context, sessionDir, profile string, prov provider.Provider, ...) bool
- func StripGoalMarkers(text string) string
- func SubagentMetaTools() []string
- func TouchBranchMeta(sessionPath string) error
- func WithParentSession(ctx context.Context, parentSession string) context.Context
- func WithReasoningLanguage(content, lang string) string
- type Agent
- func (a *Agent) CompactNow(ctx context.Context, instructions string) error
- func (a *Agent) CompactRatio() float64
- func (a *Agent) ContextWindow() int
- func (a *Agent) IsPaused() bool
- func (a *Agent) LastUsage() *provider.Usage
- func (a *Agent) Pause()
- func (a *Agent) Provider() provider.Provider
- func (a *Agent) PruneStaleToolResults() (PruneStats, error)
- func (a *Agent) Resume()
- func (a *Agent) Run(ctx context.Context, input any) error
- func (a *Agent) Session() *Session
- func (a *Agent) SessionCache() (hit, miss int)
- func (a *Agent) SetAsker(as Asker)
- func (a *Agent) SetContextFilter(fn func([]provider.Message) []provider.Message)
- func (a *Agent) SetGate(g Gate)
- func (a *Agent) SetMemoryQueue(q memory.Queue)
- func (a *Agent) SetPlanMode(v bool)
- func (a *Agent) SetPreEditHook(fn func(diff.Change))
- func (a *Agent) SetSession(s *Session)
- func (a *Agent) SetSkipReadiness(v bool)
- func (a *Agent) SoftTrimLargeResults() (PruneStats, error)
- func (a *Agent) Steer(text string)
- func (a *Agent) SteerConsumed() bool
- func (a *Agent) SummarizeFrom(ctx context.Context, fromIdx int) error
- func (a *Agent) SummarizeUpTo(ctx context.Context, toIdx int) error
- type AskTool
- type Asker
- type BranchInfo
- type BranchMeta
- type CacheDiagnostics
- type DreamKind
- type DreamRun
- func DreamHistory(sessionDir string, kind DreamKind) []DreamRun
- func LastDreamRun(sessionDir string, kind DreamKind) (DreamRun, bool)
- func RunDistillOnce(ctx context.Context, sessionDir string, prov provider.Provider, ...) (DreamRun, bool)
- func RunDreamOnce(ctx context.Context, sessionDir, profile string, prov provider.Provider, ...) (DreamRun, bool)
- type DreamTrigger
- type FileOperations
- type Gate
- type GoalVerdict
- type MaxCandidate
- type MaxJudgeResult
- type Options
- type ParallelTasksTool
- type PrefixShape
- type PruneStats
- type Renderer
- type Runner
- type Session
- type SessionInfo
- type SubagentArtifact
- type SubagentMeta
- type SubagentRun
- type SubagentSpec
- type SubagentStatus
- type SubagentStore
- func (s *SubagentStore) LoadMeta(ref string) (SubagentMeta, error)
- func (s *SubagentStore) MarkRunning(run *SubagentRun) error
- func (s *SubagentStore) PrepareContinue(ref string, spec SubagentSpec) (*SubagentRun, error)
- func (s *SubagentStore) PrepareFork(ref string, spec SubagentSpec) (*SubagentRun, error)
- func (s *SubagentStore) PrepareFresh(spec SubagentSpec) (*SubagentRun, error)
- func (s *SubagentStore) SaveCompleted(run *SubagentRun) error
- func (s *SubagentStore) SaveFailed(run *SubagentRun) error
- type TaskTool
- func (t *TaskTool) Description() string
- func (t *TaskTool) Execute(ctx context.Context, args json.RawMessage) (string, error)
- func (t *TaskTool) Name() string
- func (t *TaskTool) ReadOnly() bool
- func (t *TaskTool) ResolveProfile(args json.RawMessage) *event.Profile
- func (t *TaskTool) Schema() json.RawMessage
- func (t *TaskTool) WithTranscriptIdentityResolver(resolve func(modelRef, effort string) (string, string)) *TaskTool
- func (t *TaskTool) WithTranscripts(store *SubagentStore, workspaceRoot, baseModel, baseEffort string) *TaskTool
- type TextSink
- type ToolHooks
- type ToolSchemaCost
Constants ¶
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.
const (
// DefaultMaxCandidates is the number of parallel propose-only candidates.
DefaultMaxCandidates = 5
)
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.
const DistillTask = `` /* 1132-byte string literal not displayed */
DistillTask is the prompt fed to a background agent for workflow extraction.
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.
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 BranchMetaPath ¶
func CompactArgs ¶
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 ¶
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 ¶
DeleteSubagentsByParent permanently removes sub-agent artifacts owned by a parent session. Missing counterpart files are ignored.
func DreamInFlight ¶
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 ¶
FilterReadOnlyRegistry builds a sub-registry containing only tools whose ReadOnly contract is true, minus explicit exclusions.
func FilterRegistry ¶
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 FormatUsageLine ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
ParentSession returns the active parent session ID carried by a turn context.
func PlannerToolRegistry ¶
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 ¶
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 ¶
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 ¶
ShouldAutoDistill reports whether the Distill agent should run this turn.
func ShouldAutoDream ¶
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 ¶
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 WithParentSession ¶
WithParentSession stamps the active parent session ID onto a turn context so persisted sub-agents can record and enforce their owning conversation.
func WithReasoningLanguage ¶
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 ¶
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 ¶
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 ¶
ContextWindow returns the configured context-window size in tokens. 0 means compaction is disabled for this agent.
func (*Agent) IsPaused ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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) SteerConsumed ¶
SteerConsumed returns true when the steer queue became empty after the last consume.
func (*Agent) SummarizeFrom ¶
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 ¶
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) ReadOnly ¶
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 ¶
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 ¶
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.
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 ¶
DreamHistory returns the recorded runs of the given kind, newest first.
func LastDreamRun ¶
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.
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 ¶
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 ¶
PruneStats reports one prune pass.
type Renderer ¶
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 ¶
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 ¶
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 ¶
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 ¶
NewSession initializes a session with an optional system prompt.
func (*Session) HasContent ¶
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 ¶
Replace swaps the whole message log — used by compaction, which rewrites the middle of the history.
func (*Session) RewriteVersion ¶
RewriteVersion returns the current rewrite version.
func (*Session) Save ¶
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.
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 (*TaskTool) ReadOnly ¶
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 (*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 ¶
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) SetShowReasoning ¶
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 ¶
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.