Documentation
¶
Overview ¶
argsdelta.go — upgrade spec 3-3: live tool-argument previews. While the model streams a tool call's JSON arguments, the provider forwards each raw fragment (ChunkToolArgsDelta). For apply_patch the arguments are a single JSON string (patchText) whose value IS a patch — extract the partial value as it arrives (no need to wait for the closing quote) and let the frontend render the incomplete diff. Other tools' args don't preview cheaply (their JSON shape isn't line-oriented), so they stay opaque until dispatch.
crashrecovery.go — upgrade spec 5-2: crash-turn recovery markers. A marker file is created at turn start and deleted at turn end; if the app crashes mid-turn, the marker survives and the next startup can offer to resume.
interceptors.go — upgrade spec 3-11: the agent's built-in guardrails as a composable interceptor chain instead of inline code in Run. The main loop stays a pure state machine (stream → tools → loop); policy decisions live here where they can be tested, disabled, or extended independently.
Each interceptor sees the turn's state at its decision point and returns either "proceed" or a redirect (a user-role nudge message to re-enter the loop with). This is deliberately NOT the user-configureable hook system (ToolHooks) — these are the harness's own guardrails, always-on unless the corresponding feature is disabled.
search.go — upgrade spec 4-5: full-text search across a session directory. Sessions are small (single-digit MB at most), so a linear scan with a case-insensitive substring match is both simpler and faster than maintaining an index; results are aggregated per session with an excerpt around the first matches so the picker can show WHERE the query appeared, not just that it did. The meta sidecar (via ListSessions) supplies the title/preview columns the UI already renders.
Package agent wires a Provider, a tool Registry, and a Session into the harness loop that drives a coding task to completion.
worktree.go — gap analysis §5: sub-agent git worktree isolation. Each sub-agent that will write files gets its own git worktree, so parallel sub-agents never conflict on the same file. After the sub-agent finishes, its changes are merged back as a diff (not a git merge — the parent's session context drives whether to apply them).
Design:
- Only create a worktree when the sub-agent has writer tools (read-only sub-agents share the parent's workspace freely).
- The worktree lives under `.fairpeer/worktrees/<call-id>/` and is removed after the diff is extracted.
- The diff is returned to the model as a unified diff it can choose to apply via apply_patch, keeping the approval flow intact.
Index ¶
- Constants
- func BranchID(path string) string
- func BranchMetaPath(sessionPath string) string
- func ClearCrashMarker(sessionDir string)
- func CompactArgs(s string) string
- func ContinueSessionPath(prevPath, dir, model string) string
- func CrashMarkerPath(sessionDir 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 SetSessionIMSource(sessionPath, platform, remoteID, chatType, chatID string) 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
- func WriteCrashMarker(sessionDir, sessionPath, turnInput 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) DrainFollowUp() (string, bool)
- func (a *Agent) FollowUp(text string)
- func (a *Agent) FollowUps() []string
- func (a *Agent) IsPaused() bool
- func (a *Agent) LastUsage() *provider.Usage
- func (a *Agent) Pause()
- func (a *Agent) PreviewToolCall(name string, args json.RawMessage) ([]diff.Change, bool)
- 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) SetCacheKey(key string)
- 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) SetPostEditHook(fn func(string))
- 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) Steers() []string
- 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 CrashMarker
- 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 SearchHit
- type Session
- type SessionInfo
- type SharedPostEditHook
- type SharedPreEditHook
- 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) WithPostEditHook(fn func(string)) *TaskTool
- func (t *TaskTool) WithPreEditHook(fn func(diff.Change)) *TaskTool
- 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
- type WorktreeIsolation
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 ClearCrashMarker ¶ added in v0.2.0
func ClearCrashMarker(sessionDir string)
ClearCrashMarker removes the marker at turn end (normal or error).
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 CrashMarkerPath ¶ added in v0.2.0
CrashMarkerPath returns the marker file path for a session directory.
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 for a fresh session using the per-session folder layout (2026-08-21 redesign): <dir>/<yyyymmdd-hhmmss-xxxx>/<same>.jsonl — the transcript plus its .meta/.ckpt/.present siblings all live inside the folder, keeping every session self-contained and the parent dir navigable by date. The transcript FILE NAME equals the folder name so BranchID (filename stem) stays unique. model is no longer encoded in the name — the meta sidecar carries it. Falls back to the legacy flat layout only when the folder cannot be created.
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 SetSessionIMSource ¶ added in v0.2.0
SetSessionIMSource writes the IM origin (platform/remoteID/chatType/chatID) and mode="bot" into a session's .meta sidecar, so ListSessions can group bot sessions by IM contact. chatType+chatID separate the same user's conversations across different groups from their DM. Idempotent — re-writing the same values is harmless. The bot gateway calls this from OnTurnFinished once it has the session path.
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).
func WriteCrashMarker ¶ added in v0.2.0
func WriteCrashMarker(sessionDir, sessionPath, turnInput string)
WriteCrashMarker creates the marker at turn start.
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) DrainFollowUp ¶ added in v0.2.0
DrainFollowUp pops the oldest queued follow-up prompt, if any.
func (*Agent) FollowUp ¶ added in v0.2.0
FollowUp queues a prompt to run as an independent next turn once the current Run finishes (contrast Steer, which guides the in-flight task). The controller drains the queue between turns.
func (*Agent) FollowUps ¶ added in v0.2.0
FollowUps peeks the queued follow-up prompts (for the UI's queue strip).
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) PreviewToolCall ¶ added in v0.2.0
PreviewToolCall computes the per-file changes the named tool call would make, for the approval card a frontend renders before the permission decision. ok is false for read-only, unknown, or unpreviewable tools — callers render the plain subject in that case. It never writes.
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) SetCacheKey ¶ added in v0.2.0
SetCacheKey installs the provider cache-affinity key (hash of the session path); empty disables explicit routing.
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) SetPostEditHook ¶ added in v0.2.0
SetPostEditHook installs the post-edit hash hook (see onPostEdit). The controller wires it to its checkpoint store's NotePostEdit; nil disables the rewind safety classification.
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) Steers ¶ added in v0.2.0
Steers peeks the queued steer messages (for the UI's queue strip).
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"|"netdev") 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/netdev 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"`
// Platform/RemoteID/ChatType/ChatID/Mode mark a session as originating from an
// IM bot (the bot gateway creates the session in response to an IM message).
// Empty on desktop-tab sessions. Mode="bot" lets ListSessions separate bot
// sessions from desktop sessions and group them by IM contact for the sidebar
// detail. ChatType+ChatID distinguish the same user's conversations across
// different groups (group: per group+user) from their DM (per user).
Platform string `json:"platform,omitempty"`
RemoteID string `json:"remote_id,omitempty"`
ChatType string `json:"chat_type,omitempty"`
ChatID string `json:"chat_id,omitempty"`
Mode string `json:"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 CrashMarker ¶ added in v0.2.0
type CrashMarker struct {
SessionPath string `json:"session_path"`
TurnInput string `json:"turn_input"`
StartedAt time.Time `json:"started_at"`
}
CrashMarker records an interrupted turn for recovery (spec 5-2).
func FindCrashMarker ¶ added in v0.2.0
func FindCrashMarker(sessionDir string) (*CrashMarker, bool)
FindCrashMarker returns the marker if one survives (crash recovery).
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
// PreEditHook, when set, receives each writer tool's previewed change just
// before it runs — the seam the checkpoint store uses to snapshot pre-edit
// file state. The controller installs it on the main loop via
// SetPreEditHook; passing the same function here extends checkpoint capture
// to sub-agents (run_skill/task), whose file writes must rewind the same way.
PreEditHook func(diff.Change)
// PostEditHook, when set, receives each previewed path after the writer
// tool succeeded — paired with PreEditHook so the store can record the
// post-edit hash for the rewind safety classification.
PostEditHook func(string)
// 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 SearchHit ¶ added in v0.2.0
type SearchHit struct {
Path string
Excerpts []string
// Routing/title fields mirrored from SessionInfo so a frontend hit can be
// opened exactly like a regular session-list entry.
Title string
TopicID string
Scope string
WorkspaceRoot string
Profile string
}
SearchHit is one session that matched, with up to ExcerptCap excerpts of the matching lines.
func SearchSessions ¶ added in v0.2.0
SearchSessions returns the sessions under dir whose transcript contains query (case-insensitive), most recently active first, capped at 50 hits. A query shorter than 2 bytes returns nil — callers gate on typed input.
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. If a sibling .sig exists (written by a version with integrity checks), it's verified — a mismatch fails the load (tampering/corruption).
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
// Platform/RemoteID/ChatType/ChatID/Mode are non-empty only for IM bot
// sessions (see BranchMeta). ChatType+ChatID let callers tell the same user's
// group conversations apart (one per group) from their DM.
Platform string
RemoteID string
ChatType string
ChatID string
Mode 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 SharedPostEditHook ¶ added in v0.2.0
type SharedPostEditHook struct {
// contains filtered or unexported fields
}
SharedPostEditHook is the post-edit twin of SharedPreEditHook: it carries the checkpoint store's post-edit hash recorder to sub-agents constructed before the controller exists. Until Set installs the function, Fire is a no-op.
func NewSharedPostEditHook ¶ added in v0.2.0
func NewSharedPostEditHook() *SharedPostEditHook
func (*SharedPostEditHook) Fire ¶ added in v0.2.0
func (h *SharedPostEditHook) Fire(path string)
Fire is the hook handed to agent.Options.PostEditHook; safe for concurrent use.
func (*SharedPostEditHook) Set ¶ added in v0.2.0
func (h *SharedPostEditHook) Set(fn func(string))
type SharedPreEditHook ¶ added in v0.2.0
type SharedPreEditHook struct {
// contains filtered or unexported fields
}
SharedPreEditHook lets the controller attach its checkpoint snapshotter to agents that are constructed before the controller exists. boot creates one instance and threads it into every sub-agent spawn site (the task tool and the run_skill runner); once control.New has bound the checkpoint store, the concrete function is installed via Set. Until then Fire is a no-op, so headless builds (no controller checkpoint store) behave exactly as before.
func NewSharedPreEditHook ¶ added in v0.2.0
func NewSharedPreEditHook() *SharedPreEditHook
func (*SharedPreEditHook) Fire ¶ added in v0.2.0
func (h *SharedPreEditHook) Fire(ch diff.Change)
Fire is the hook handed to agent.Options.PreEditHook. It must be safe to call concurrently — writer tools dispatch in parallel goroutines.
func (*SharedPreEditHook) Set ¶ added in v0.2.0
func (h *SharedPreEditHook) Set(fn func(diff.Change))
Set installs (or replaces, on session rebind) the snapshot function.
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) WithPostEditHook ¶ added in v0.2.0
WithPostEditHook routes sub-agent writer post-edits into the caller's post-edit hash seam (typically a SharedPostEditHook owned by boot).
func (*TaskTool) WithPreEditHook ¶ added in v0.2.0
WithPreEditHook routes sub-agent writer pre-edits into the caller's checkpoint seam (typically a SharedPreEditHook owned by boot). nil (the default) keeps sub-agent edits un-checkpointed, as before.
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.
type WorktreeIsolation ¶ added in v0.2.0
type WorktreeIsolation struct {
// Root is the original workspace root.
Root string
// Path is the worktree mount point.
Path string
// Branch is the git branch created for this worktree.
Branch string
// contains filtered or unexported fields
}
WorktreeIsolation manages one sub-agent's isolated working copy.
func CreateWorktree ¶ added in v0.2.0
func CreateWorktree(root, callID string, isGit bool) *WorktreeIsolation
CreateWorktree sets up an isolated worktree for a sub-agent call. Returns a no-op isolation when root is not a git repo or isGit is false.
func (*WorktreeIsolation) Active ¶ added in v0.2.0
func (w *WorktreeIsolation) Active() bool
Active reports whether isolation is in effect.
func (*WorktreeIsolation) Cleanup ¶ added in v0.2.0
func (w *WorktreeIsolation) Cleanup()
Cleanup removes the worktree and its branch. Safe to call multiple times.
func (*WorktreeIsolation) Diff ¶ added in v0.2.0
func (w *WorktreeIsolation) Diff() string
Diff returns the sub-agent's changes as a unified diff against the base. Empty string when no changes.
func (*WorktreeIsolation) WorkDir ¶ added in v0.2.0
func (w *WorktreeIsolation) WorkDir() string
WorkDir returns the directory the sub-agent should treat as its workspace (the worktree when active, the original root otherwise).
Source Files
¶
- agent.go
- argsdelta.go
- ask.go
- branch.go
- cache_shape.go
- compact.go
- crashrecovery.go
- dream.go
- file_ops.go
- goal_display.go
- goal_judge.go
- interceptors.go
- max_mode.go
- migrate.go
- normalize.go
- op_gate.go
- parallel_tasks.go
- preedit.go
- prune.go
- reasoning_language.go
- repeat_text.go
- save.go
- search.go
- session.go
- subagent_store.go
- task.go
- textsink.go
- util.go
- width.go
- worktree.go