control

package
v0.1.5 Latest Latest
Warning

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

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

Documentation

Overview

Package control is the transport-agnostic session driver. A Controller owns the agent run loop and session lifecycle, takes commands (Send/Cancel/Approve/ SetPlanMode/Compact/NewSession/…), and emits everything that happens — reasoning, tool calls, approvals, turn completion — as a typed event stream to a single event.Sink.

The point is one orchestration layer behind every frontend: a terminal TUI, a desktop webview, or an HTTP/SSE server each drive the Controller identically (issue commands, render events) and none of them re-implement turn lifecycle, cancellation, or approval. The Controller depends on no frontend.

Index

Constants

View Source
const (
	AutoPlanOff = "off"
	AutoPlanOn  = "on"
)
View Source
const (
	ToolApprovalAsk  = "ask"
	ToolApprovalAuto = "auto"
	ToolApprovalYolo = "yolo"
)
View Source
const (
	GoalStatusRunning  = "running"
	GoalStatusComplete = "complete"
	GoalStatusBlocked  = "blocked"
	GoalStatusStopped  = "stopped"
)
View Source
const PlanApprovalTool = "exit_plan_mode"

PlanApprovalTool is the Tool name on the ApprovalRequest the controller emits to gate a proposed plan. Frontends key their plan-approval UI on it (the desktop renders a plan card; the chat TUI a plan banner). This is the single source of truth — frontends import this constant instead of re-declaring it.

View Source
const PlanApprovedMessage = "" /* 600-byte string literal not displayed */

PlanApprovedMessage is the follow-up turn sent once the user approves a plan — the in-context nudge to execute and keep the (already-seeded) task list honest.

View Source
const PlanModeMarker = "" /* 785-byte string literal not displayed */

PlanModeMarker is prepended to every user turn while plan mode is on. It rides in the user message (not the system prompt or tools), so the cache-stable prompt prefix is left untouched and the toggle costs nothing in cache hits. (some providers do not report cache tokens; the prefix stability still helps.)

Variables

View Source
var ErrTurnRunning = errors.New("turn already running")

ErrTurnRunning reports that a caller tried to start a second foreground turn while one is already active in the same Controller.

Functions

func FileRefLine

func FileRefLine(line string) (string, bool)

FileRefLine reports whether a submitted line is nothing but a path to an existing file — a dragged or pasted file lands as its bare path, which on POSIX starts with '/' and would otherwise be misread as a slash command. The returned string is that path turned into an @reference so it attaches.

func FormatBranchTree

func FormatBranchTree(branches []agent.BranchInfo, currentID string) string

func ImageDataURL

func ImageDataURL(path string) (string, error)

func IsSyntheticUserMessage

func IsSyntheticUserMessage(content string) bool

IsSyntheticUserMessage returns true if the content matches one of the known synthetic user messages injected by the controller or agent loop (plan approval, stream recovery, readiness retry, etc.). These should not be shown in the chat UI.

func MemoryQuickAddNote

func MemoryQuickAddNote(input string) (note string, ok bool)

MemoryQuickAddNote parses the legacy "# <note>" memory shortcut. The space after "#" is intentional: "#7", "#issue", and "#标题" are ordinary user prompts, not memory writes.

func NormalizeAutoPlan

func NormalizeAutoPlan(mode string) string

NormalizeAutoPlan is the single source of truth for auto_plan normalization. Exported so boot.go and config consumers share the same semantics.

func ParseBranchTarget

func ParseBranchTarget(args string) (turn int, name string, fromTurn bool, err error)

ParseBranchTarget parses the arguments after "/branch". A leading positive integer means "branch from displayed turn N"; otherwise the whole argument is the optional branch name for a tip branch.

func RememberCommandNote

func RememberCommandNote(input string) (note string, ok bool)

RememberCommandNote parses the explicit "/remember <note>" memory command.

func SaveAttachmentDataURL

func SaveAttachmentDataURL(origName, dataURL string) (string, error)

SaveAttachmentDataURL stores a non-image file (dropped/pasted in the desktop app, where the browser exposes bytes but not a real path) under .fairpeer/attachments and returns its repo-relative path for @referencing. origName supplies only the extension; the stored name is generated.

func SaveAttachmentFile

func SaveAttachmentFile(path string) (string, error)

func SaveClipboardImage

func SaveClipboardImage() (string, error)

func SaveImageBytes

func SaveImageBytes(declaredMime string, raw []byte) (string, error)

func SaveImageDataURL

func SaveImageDataURL(dataURL string) (string, error)

func SaveImageFile

func SaveImageFile(path string) (string, error)

func ShortGoalForNotice

func ShortGoalForNotice(goal string) string

func SlashPathLikeLine

func SlashPathLikeLine(line string) bool

SlashPathLikeLine reports whether a slash-prefixed line looks like a POSIX absolute path rather than a slash command. It intentionally stays conservative: unknown "/foo" remains an unknown command, while "/foo/bar..." is sent as ordinary prompt text even if the path no longer exists.

func SlashPathLineRef

func SlashPathLineRef(line, baseDir string) (string, bool)

SlashPathLineRef reports whether a slash-prefixed line starts with a local file path, including common compiler-location suffixes like ":12" or ":12:34". It returns an @reference for the file so diagnostics that begin with an absolute path can keep their original text while also attaching file context.

func StripComposePrefixes

func StripComposePrefixes(content string) string

StripComposePrefixes removes controller-injected prefixes from a composed user message so that the display text matches what the user actually typed. It strips the PlanModeMarker, <memory-update>…</memory-update>, and <background-jobs>…</background-jobs> blocks that Compose prepends to user turns. This is used as a fallback when no .display.json sidecar recording exists (e.g. sessions created before the display-recording feature, or synthetic user messages injected by the controller).

func StripReferencedContextPrefix

func StripReferencedContextPrefix(content string) string

StripReferencedContextPrefix removes a leading "Referenced context:\n\n<block>\n\n" wrapper that the controller prepends when resolving @-references, leaving only the user's actual message. The wrapper has no terminator, so this matches the prefix, skips the block up to the user-text separator, and keeps the rest. Used by exporters (e.g. /export) that want to reconstruct the original prompt.

Types

type ArgData

type ArgData struct {
	Skills          []skill.Skill
	DisabledSkills  []skill.Skill
	ServerNames     []string
	ConfiguredMCP   []string
	DisconnectedMCP []string
	ModelRefs       []string
	CurrentModel    string
	ProviderNames   []string
	CurrentProvider string
}

ArgData supplies the dynamic data SlashArgItems needs, so the completion logic is one shared function both frontends call with their own session data — the chat TUI (controller-free, from its cached lists) and the desktop (from the controller). This keeps the CLI and desktop sub-command hints identical.

type Controller

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

Controller drives one chat session. Construct with New; drive with the command methods; observe through the Sink passed in Options.

func New

func New(opts Options) *Controller

New builds a Controller. A nil Sink is replaced with event.Discard.

func (*Controller) AddMCPServer

func (c *Controller) AddMCPServer(e config.PluginEntry) (int, error)

AddMCPServer connects an MCP server live and persists it to the config file. Its tools are registered immediately and become available on the next turn (the agent reads the registry per turn). The raw entry — ${VARS} intact — is what's written to disk; the live connection uses the expanded form. Returns the number of tools the server exposed. A save failure after a successful connect is reported but non-fatal: the server still works this session.

func (*Controller) AllSkills

func (c *Controller) AllSkills() []skill.Skill

AllSkills returns every discoverable skill, including disabled ones, for management surfaces that need to re-enable a hidden skill.

func (*Controller) AnswerQuestion

func (c *Controller) AnswerQuestion(id string, answers []event.AskAnswer)

AnswerQuestion resolves a pending AskRequest by ID with the user's selections. Unknown/expired IDs are ignored.

func (*Controller) AppendExpertCollab

func (c *Controller) AppendExpertCollab(content string) error

AppendExpertCollab persists a finished expert-team collaboration into the active session as a folded-block message (a tool message whose Content is the full collab record JSON, with the context layer projecting it down to a synthesis-only summary for the model). It then emits an ExpertCollab event so the frontend renders an expandable card, and snapshots the session so the record survives restarts.

It refuses while a turn is running (the agent's run loop reads session messages lock-free, so mid-turn mutation would race). The collab is then surfaced on the next render rather than interleaved with a live turn.

func (*Controller) Approve

func (c *Controller) Approve(id string, allow, session, persist bool)

Approve answers a pending ApprovalRequest by ID: allow runs the call, session also remembers a grant for the rest of the session so the same approval scope is not re-prompted. Unknown/expired IDs are ignored.

func (*Controller) Ask

func (c *Controller) Ask(ctx context.Context, questions []event.AskQuestion) ([]event.AskAnswer, error)

Ask implements agent.Asker: it emits an AskRequest and blocks until AnswerQuestion(ID, …) answers or ctx is cancelled. promptMu serialises it against tool-approval prompts so at most one user prompt is outstanding. Unlike tool-approval gates, Ask is NOT bypassed in YOLO mode — the `ask` tool exists to get a genuine user decision, and YOLO only auto-approves tool calls; it must not answer the user's questions for them.

func (*Controller) AutoApproveTools

func (c *Controller) AutoApproveTools() bool

AutoApproveTools reports whether YOLO/full-access tool auto-approval is on, for status indicators and mode persistence.

func (*Controller) BeginDestroySession

func (c *Controller) BeginDestroySession(sessionPath string) *SessionDestroyHandle

BeginDestroySession starts a two-phase session destroy. Call Wait() to cancel and drain background jobs, then Finish() to release resources. If the session has no background job manager, Wait is a no-op.

func (*Controller) Branch

func (c *Controller) Branch(name string) (string, error)

Branch copies the current conversation into a child branch and switches to it. Unlike Fork, it branches at the current tip and does not require a checkpoint.

func (*Controller) BranchTreeText

func (c *Controller) BranchTreeText() string

func (*Controller) Branches

func (c *Controller) Branches() ([]agent.BranchInfo, error)

Branches lists saved conversation branches in this controller's session dir.

func (*Controller) Bypass

func (c *Controller) Bypass() bool

Bypass is the legacy name for AutoApproveTools.

func (*Controller) Cancel

func (c *Controller) Cancel()

Cancel aborts the in-flight turn. A goroutine blocked awaiting approval unblocks via the cancelled context.

func (*Controller) CheckpointHasBoundary

func (c *Controller) CheckpointHasBoundary(turn int) bool

func (*Controller) Checkpoints

func (c *Controller) Checkpoints() []checkpoint.Meta

Checkpoints lists the session's rewind points (one per user turn), oldest first.

func (*Controller) ClearGoal

func (c *Controller) ClearGoal()

func (*Controller) ClearSession

func (c *Controller) ClearSession() error

ClearSession discards the current conversation without preserving it in resume/history, then rotates to a clean session carrying the same system prompt.

func (*Controller) Close

func (c *Controller) Close()

Close stops plugin subprocesses and releases resources. A session that ever started fires SessionEnd so a teardown hook runs.

func (*Controller) Commands

func (c *Controller) Commands() []command.Command

Commands returns the loaded custom slash commands.

func (*Controller) Compact

func (c *Controller) Compact(ctx context.Context, instructions string) error

Compact runs one compaction pass on the executor's session on demand. instructions is optional `/compact <focus>` guidance steering what to keep.

func (*Controller) CompactRatio

func (c *Controller) CompactRatio() float64

CompactRatio returns the auto-compaction threshold as a fraction of the window (0 when the executor is unset). The status line shows headroom against it.

func (*Controller) Compose

func (c *Controller) Compose(text string) string

Compose applies the plan-mode marker to a turn's text when plan mode is on, returning the message to actually send to the model. The frontend keeps showing the raw text as the user bubble.

func (*Controller) ComposeSynthetic

func (c *Controller) ComposeSynthetic(text string) string

ComposeSynthetic is a lighter compose path for controller-injected messages (e.g. planApprovedMessage after plan approval). Unlike Compose, it does not re-inject plan mode markers, goals, or memory — those are already part of the session context. It applies only transformations that synthetic messages need (currently a no-op; will apply reasoning language when that feature lands).

func (*Controller) ConfiguredMCPNames

func (c *Controller) ConfiguredMCPNames() []string

func (*Controller) ConnectCodegraphMCPServer

func (c *Controller) ConnectCodegraphMCPServer(cfg *config.Config) (int, error)

ConnectCodegraphMCPServer connects the built-in CodeGraph server using an already-resolved config. Desktop uses this after saving user-level settings so a stale project config cannot override the just-applied choice.

func (*Controller) ConnectConfiguredMCPServer

func (c *Controller) ConnectConfiguredMCPServer(name string) (int, error)

func (*Controller) ConnectMCPServer

func (c *Controller) ConnectMCPServer(e config.PluginEntry) (int, error)

ConnectMCPServer connects an MCP server entry for this session without writing it to config. Desktop owns config placement so it can keep user-level settings out of project fairpeer.toml while preserving the CLI AddMCPServer semantics.

func (*Controller) ContextSnapshot

func (c *Controller) ContextSnapshot() (int, int)

ContextSnapshot returns (promptTokens, contextWindow) from the most recent turn. Both zero means no data yet — a gauge hides itself.

func (*Controller) CustomCommand

func (c *Controller) CustomCommand(input string) (sent string, found bool)

CustomCommand resolves a "/name args…" line against the loaded custom slash commands, returning the rendered prompt to send (found=false when no command matches). It does not apply the plan-mode marker — call Compose for that.

func (*Controller) DeleteExpertCollab

func (c *Controller) DeleteExpertCollab(ordinal int) error

DeleteExpertCollab removes the Nth expert-collab message (0-based among expert_team_collab tool messages) from the session and re-persists. This is the "不采纳" affordance: a user discards a collaboration from both the stored transcript and the model's context. The ordinal is stable — deleting one doesn't shift the others' ordinals — because it's recomputed over the remaining set each call. Refused while a turn is running, like the append.

func (*Controller) DisabledSkills

func (c *Controller) DisabledSkills() []skill.Skill

DisabledSkills returns all discoverable skills that are disabled in config.

func (*Controller) DisconnectMCPServer

func (c *Controller) DisconnectMCPServer(name string) bool

DisconnectMCPServer disconnects a live server for this session without touching config — the connector toggle's "off". Its tools vanish next turn; it reconnects on the next session start, or now via ConnectConfiguredMCPServer (the "on"). Reports whether a live server was actually disconnected.

func (*Controller) DisconnectedMCPNames

func (c *Controller) DisconnectedMCPNames() []string

func (*Controller) DreamInFlight

func (c *Controller) DreamInFlight(kind agent.DreamKind) bool

DreamInFlight reports whether a run of the given kind is currently executing.

func (*Controller) EmitExpertCollab

func (c *Controller) EmitExpertCollab(collab event.Collab)

EmitExpertCollab fires an ExpertCollab event on this controller's sink so the frontend renders the folded card. Kept separate from AppendExpertCollab so a caller can append-then-emit (or emit for a panel-initiated run whose data it already has) without coupling the two.

func (*Controller) EnableInteractiveApproval

func (c *Controller) EnableInteractiveApproval()

EnableInteractiveApproval swaps the executor's gate for one that routes approval decisions to the frontend via ApprovalRequest events, and wires the controller in as the executor's Asker so the `ask` tool can question the user. Interactive frontends (chat, desktop) call this; the headless run keeps the silent gate and a nil asker from setup.

func (*Controller) ForgetMemory

func (c *Controller) ForgetMemory(name string) error

ForgetMemory deletes a saved auto-memory by name — the panel/TUI delete action, the manual counterpart to the model's `forget` tool. It queues a turn-tail note so the deletion applies this session (the cached prefix still lists the fact until the next session re-folds the index).

func (*Controller) Fork

func (c *Controller) Fork(turn int) (string, error)

Fork branches the conversation at the start of turn into a NEW session file, preserving the current one as the branch point, and switches to the branch. Code is untouched (it's a conversation operation). Like a conversation rewind it needs the live boundary, so it is unavailable for resumed-session turns and refused while a turn runs. Returns the new session path.

func (*Controller) ForkNamed

func (c *Controller) ForkNamed(turn int, name string) (string, error)

func (*Controller) ForkSession

func (c *Controller) ForkSession(turn int, name string) (string, error)

ForkSession copies the conversation at the start of turn into a new session file without switching this controller to it. Desktop uses this to open the branch in a new tab while the source tab keeps its current transcript.

func (*Controller) Goal

func (c *Controller) Goal() string

func (*Controller) GoalStatus

func (c *Controller) GoalStatus() string

func (*Controller) GoalStrict

func (c *Controller) GoalStrict() bool

GoalStrict reports whether the active goal is under strict enforcement.

func (*Controller) HasRefs

func (c *Controller) HasRefs(line string) bool

HasRefs reports whether a line contains any resolvable @references, so a frontend can decide to resolve off its event loop only when needed.

func (*Controller) History

func (c *Controller) History() []provider.Message

History returns the executor's current message log (for repopulating a resumed frontend's view).

func (*Controller) HookRunner

func (c *Controller) HookRunner() *hook.Runner

HookRunner returns the session's hook runner (nil-safe; may hold zero hooks), so a frontend can list the active hooks via `/hooks`.

func (*Controller) Host

func (c *Controller) Host() *plugin.Host

Host returns the running MCP host (nil when no plugins), for frontends that list servers / resolve MCP prompts.

func (*Controller) ImportMCPEntries

func (c *Controller) ImportMCPEntries(entries []config.PluginEntry) (total, added, updated, connected, failed, skipped int, err error)

ImportMCPEntries persists selected MCP entries and attempts to connect them live. A connection failure does not roll back the config import: the user can fix local dependencies and reconnect in a later session.

func (*Controller) InheritLifecycleFrom

func (c *Controller) InheritLifecycleFrom(prev *Controller)

InheritLifecycleFrom carries lifecycle state from a previous controller into this one, preventing duplicate SessionStart hooks and preserving the turn counter across controller rebuilds (e.g. model switch that preserves the conversation). Call this before the first turn on the new controller.

func (*Controller) IsDestroyingSession

func (c *Controller) IsDestroyingSession(sessionPath string) bool

IsDestroyingSession reports whether the given session path is currently in the destroy window (between BeginDestroySession and artifact removal).

func (*Controller) Jobs

func (c *Controller) Jobs() []jobs.View

Jobs returns the still-running background jobs for the status bar (nil when background jobs are disabled).

func (*Controller) Label

func (c *Controller) Label() string

Label returns the human-readable model label, e.g. "openai/gpt-4o".

func (*Controller) LastDreamRun

func (c *Controller) LastDreamRun(kind agent.DreamKind) (agent.DreamRun, bool)

LastDreamRun exposes the most recent recorded run of a kind for this session, for the desktop "last run" status display.

func (*Controller) LastUsage

func (c *Controller) LastUsage() *provider.Usage

LastUsage returns the most recent turn's token telemetry (nil before the first turn), so frontends can derive the prompt cache-hit rate for the status line.

func (*Controller) MCPPrompt

func (c *Controller) MCPPrompt(ctx context.Context, input string) (sent string, found bool, err error)

MCPPrompt resolves a "/mcp__server__prompt args…" line: it maps the positional args onto the prompt's declared arguments and fetches the rendered prompt from the MCP server (an async prompts/get). found is false when no such prompt exists; err carries a fetch failure. Honours ctx.

func (*Controller) Memory

func (c *Controller) Memory() *memory.Set

Memory returns the loaded memory snapshot (nil when memory is disabled), for frontends that surface a memory panel or the /memory command. The returned *Set is immutable — mutations go through QuickAdd / SaveDoc.

func (*Controller) NewSession

func (c *Controller) NewSession() error

NewSession snapshots the current conversation, rotates to a fresh file, and resets the executor to a clean session carrying the same system prompt. It ends the old session and starts the new one for lifecycle hooks.

func (*Controller) Pause

func (c *Controller) Pause()

Pause requests a graceful pause of the in-flight turn. Unlike Cancel (which aborts and discards partial work), Pause lets the current step finish, then freezes the agent with full state preserved so ResumeTurn continues from the break point. No-op when no turn is running or when already paused. Safe from any goroutine; the run loop checks the pause request at the top of each step.

func (*Controller) Paused

func (c *Controller) Paused() bool

Paused reports whether the in-flight turn is currently frozen on a pause (between steps, awaiting ResumeTurn). False when running normally, not running, or no executor is bound.

func (*Controller) PlanMode

func (c *Controller) PlanMode() bool

PlanMode reports whether outgoing turns currently receive the plan-mode marker. Frontends use it after Compose because auto-plan may flip the mode.

func (*Controller) Profile

func (c *Controller) Profile() ProfileView

Profile returns the active mode's portrait for the preference panel. Read-only — saves go through SaveDoc (the profile path is whitelisted). The panel calls this on open to populate its editor.

func (*Controller) QueueMemory

func (c *Controller) QueueMemory(note string)

QueueMemory implements memory.Queue: when the model runs the remember/forget tool, the tool calls this with a note that rides the next turn so the change applies this session without touching the cache-stable prefix. It also refreshes the snapshot a memory panel reads.

func (*Controller) QuickAdd

func (c *Controller) QuickAdd(scope memory.Scope, note string) (string, error)

QuickAdd appends a one-line note to the doc-memory file for scope (project fairpeer.md by default) — the write side of "#<note>". Returns the file written.

func (*Controller) RemoveMCPServer

func (c *Controller) RemoveMCPServer(name string) (disconnected bool, err error)

RemoveMCPServer disconnects a live MCP server — its tools vanish from the next turn — and removes it from the config file. It reports whether a live server was disconnected; an error only when the name is neither connected nor in config (or the config save fails). A server declared in .mcp.json disconnects for this session but returns on the next start, since that file isn't ours to edit.

func (*Controller) ReplayPendingPrompts

func (c *Controller) ReplayPendingPrompts()

ReplayPendingPrompts re-emits the ApprovalRequest / AskRequest event for every prompt currently blocking the run loop. A frontend that reconnected or reloaded after the original event has no way to rebuild its approval/ask modal otherwise, so the blocked gate goroutine stays stuck forever while the session shows a "waiting" status with no actionable prompt. promptMu serialises Ask and requestApproval, so in practice at most one prompt is outstanding; the loops stay general so a future concurrent prompt would still replay correctly.

func (*Controller) ResolveRefs

func (c *Controller) ResolveRefs(ctx context.Context, line string) (block any, errs []string)

ResolveRefs resolves the @references in a line into a context block. Returns either a plain string (text-only refs) or []provider.ContentPart (when image refs are present, for multimodal vision support). Per-reference error strings are returned for any that failed. Safe to call off a frontend's event loop; honours ctx for the resource reads.

func (*Controller) Resume

func (c *Controller) Resume(s *agent.Session, path string)

Resume seeds the session from a loaded transcript and pins the active file to its path so auto-save keeps appending there.

func (*Controller) ResumeTurn

func (c *Controller) ResumeTurn()

ResumeTurn unblocks a paused turn. Named ResumeTurn (not Resume) to avoid a clash with the session-lifecycle Resume(session, path). No-op when not paused. Safe from any goroutine.

func (*Controller) Rewind

func (c *Controller) Rewind(turn int, scope RewindScope) error

Rewind restores the session to the start of `turn`: Code reverts every file that turn (or a later one) changed to its pre-turn content; Conversation truncates the message log back to that turn; Both does both. Refused while a turn is running. Conversation rewind relies on the live boundary recorded at turn start, so it is unavailable for turns inherited from a resumed session (code rewind still works). Frontends re-render their transcript from History after the call.

func (*Controller) Run

func (c *Controller) Run(ctx context.Context, input string) error

Run executes a turn synchronously, returning the agent's error. Used by the headless `fairpeer run` path, where the Sink renders to stdout and the caller just needs the exit status — no TurnDone event, no cancel bookkeeping.

func (*Controller) RunShell

func (c *Controller) RunShell(command string)

RunShell executes a shell command directly (bypassing the model) and streams the output as ToolDispatch/ToolProgress/ToolResult events. It uses the same bash-tool infrastructure (shell resolution, timeout) and shares the runGuarded lock with model turns — only one can run at a time. User-invoked "!" commands run without the OS sandbox (the user typed the command explicitly).

func (*Controller) RunSkill

func (c *Controller) RunSkill(input string) (sent string, found bool)

RunSkill resolves a "/<name> args…" line against the loaded skills, returning the skill's rendered body to send as a turn (found=false when no skill matches). Invoking a skill by slash always inlines its body — the model reads and follows the playbook in the main loop; a subagent skill's isolation is only engaged when the model calls it via run_skill / the dedicated tool. The caller applies Compose for plan-mode/memory framing.

func (*Controller) RunTurn

func (c *Controller) RunTurn(ctx context.Context, input string) error

RunTurn executes one foreground turn synchronously through the same lifecycle used by interactive frontends: auto-plan, transient memory/background-job composition, checkpoints, hooks, and plan approval. It is for transports that need a blocking request/response boundary, such as ACP session/prompt.

func (*Controller) Running

func (c *Controller) Running() bool

Running reports whether a turn is currently in flight.

func (*Controller) SaveDoc

func (c *Controller) SaveDoc(path, body string) (string, error)

SaveDoc overwrites a recognized memory doc with body — the save side of the desktop panel's in-place editor. Returns the file written.

func (*Controller) Send

func (c *Controller) Send(input string)

Send starts a turn with an uncomposed message. The controller applies auto-plan, plan-mode, memory, and background-job framing inside the async turn path so frontends do not block on classifier I/O.

func (*Controller) SendWithRaw

func (c *Controller) SendWithRaw(input, raw string)

SendWithRaw starts a turn with separate model input and raw prompt text. The raw prompt is used only for auto-plan scoring; it deliberately excludes resolved @-reference payloads so referenced file contents cannot inflate the complexity score.

func (*Controller) SessionCache

func (c *Controller) SessionCache() (hit, miss int)

SessionCache returns cumulative cache hit/miss prompt tokens for the session, so a frontend can render the aggregate (session-wide) cache-hit rate — steadier than the single-turn rate and unaffected by compaction. some providers do not report cache tokens, so this returns zeros for providers that omit cache stats.

func (*Controller) SessionDir

func (c *Controller) SessionDir() string

SessionDir reports the directory new session files land in ("" disables persistence), so the caller can decide whether to mint a path.

func (*Controller) SessionPath

func (c *Controller) SessionPath() string

SessionPath reports the file the current conversation auto-saves to ("" when persistence is disabled), so a history view can mark the active session.

func (*Controller) SetAutoApproveTools

func (c *Controller) SetAutoApproveTools(on bool)

SetAutoApproveTools turns YOLO/full-access mode on or off for the session: while on, every tool approval request is auto-allowed (writers and bash run without asking). Ask requests and plan approval still reach the user. Deny rules still block. Runtime-only — never written to config.

func (*Controller) SetAutoPlan

func (c *Controller) SetAutoPlan(mode string)

SetAutoPlan updates the interactive auto-plan gate for subsequent turns.

func (*Controller) SetBypass

func (c *Controller) SetBypass(on bool)

SetBypass is the legacy name for SetAutoApproveTools. Keep it for existing desktop/serve bindings and CLI code that still uses the bypass wording.

func (*Controller) SetContextFilter

func (c *Controller) SetContextFilter(fn func([]provider.Message) []provider.Message)

SetContextFilter installs a read-side transform applied to session messages before they reach the model. It is the seam the experts engine uses to keep a full-fidelity expert-collab message in the transcript while showing the model a synthesis-only projection of it. Pass nil to restore the identity filter. Safe to call before or after the executor is built; applied (re-applied) on the next executor wiring.

func (*Controller) SetDisplayRecorder

func (c *Controller) SetDisplayRecorder(fn func(content, display string))

SetDisplayRecorder installs an optional hook used by frontends that persist a shorter user-facing transcript than the fully composed model prompt.

func (*Controller) SetGoal

func (c *Controller) SetGoal(goal string)

SetGoal stores a session-scoped active goal. Compose injects it into outgoing user turns, not the system prompt or tool schema, so it does not disturb the cache-stable prefix.

func (*Controller) SetGoalStrict

func (c *Controller) SetGoalStrict(strict bool)

SetGoalStrict toggles strict enforcement on the active goal: when on, the goal may not be declared complete unless the session produced tool-backed work, so a bare "I'm done" cannot satisfy it. Setting strict on a stopped/no goal is remembered but has no effect until a goal runs. (PR #4827 goal enforcement.)

func (*Controller) SetMode

func (c *Controller) SetMode(plan, autoApproveTools bool)

SetMode applies plan (read-only) and tool auto-approval together so a turn submitted right after a composer mode switch can't observe a half-applied gate. Turning tool auto-approval on drains any pending tool approval.

func (*Controller) SetPlanMode

func (c *Controller) SetPlanMode(v bool)

SetPlanMode flips the executor's read-only gate without touching the cache-stable prompt prefix, and remembers the state so Compose can prepend the plan-mode marker to outgoing turns.

func (*Controller) SetRAGScope

func (c *Controller) SetRAGScope(scope string)

SetRAGScope sets the knowledge-base collection that auto-injection searches. Pass "" to disable injection for this session ("不使用" in the Composer dropdown). The scope is read at the start of each turn (under c.mu) so a mid-flight change can't race the RAG call. Mirrors the per-tab pattern of SetToolApprovalMode.

func (*Controller) SetRiskOverrides

func (c *Controller) SetRiskOverrides(overrides map[string]permission.RiskClass)

SetRiskOverrides installs the per-tool risk-class map (SPEC v2 §3.2A) built from [[plugins]] risk config. Called once at boot after plugins are resolved. A nil/empty map keeps the safe default (MCP tools external-risk).

func (*Controller) SetSessionPath

func (c *Controller) SetSessionPath(p string)

SetSessionPath pins where auto-save lands (a fresh session file minted by the caller when no resume path applies).

func (*Controller) SetSkillEnabled

func (c *Controller) SetSkillEnabled(name string, enabled bool) error

SetSkillEnabled persists a skill enable/disable preference. The caller should rebuild the controller for the prompt/tool registry to reflect it immediately.

func (*Controller) SetToolApprovalMode

func (c *Controller) SetToolApprovalMode(mode string)

SetToolApprovalMode changes the runtime approval posture for permission-gated tools. It does not answer business asks or plan approval.

func (*Controller) SkillEnabled

func (c *Controller) SkillEnabled(name string) bool

SkillEnabled reports whether a discoverable skill is enabled.

func (*Controller) Skills

func (c *Controller) Skills() []skill.Skill

Skills returns the discoverable skills (for the slash menu and `/skills`). When a live Store is available, scan it on demand so skills installed during this session appear without rewriting the cache-stable system prompt.

func (*Controller) Snapshot

func (c *Controller) Snapshot() error

Snapshot writes the executor's conversation to the active session file. No-op when persistence is unavailable or the session has never been used (no user interaction). Called after every turn so a crash loses at most one in-flight prompt.

func (*Controller) SnapshotActivity

func (c *Controller) SnapshotActivity() error

SnapshotActivity writes the active conversation and marks the session as recently active. Use it only after a real user/model turn changes the transcript; switch/close snapshots should call Snapshot so they do not reorder recent-session pickers.

func (*Controller) Steer

func (c *Controller) Steer(text string)

Steer queues mid-turn guidance without interrupting the in-flight request.

func (*Controller) SteerConsumed

func (c *Controller) SteerConsumed() bool

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

func (*Controller) Submit

func (c *Controller) Submit(input string)

Submit is the one-call entry for a simple frontend: it takes raw user input and does everything — slash-command dispatch, @-reference expansion, plan-mode composition — emitting all output as events. The HTTP/SSE server uses this so a browser client only POSTs the typed line.

Slash commands route to the matching primitive: /compact, /new, and /clear run their session op and emit a Notice; /mcp__server__prompt and custom /commands resolve to a turn; an unknown slash emits a Notice. Anything else is a normal turn with its @-references resolved first.

func (*Controller) SubmitDisplay

func (c *Controller) SubmitDisplay(display, input string)

SubmitDisplay runs input as a turn while remembering the user-facing display text for transcript replay when controller-side composition expands input.

func (*Controller) SummarizeFrom

func (c *Controller) SummarizeFrom(ctx context.Context, turn int) error

SummarizeFrom compresses the conversation from turn onward into one summary; SummarizeUpTo compresses everything before it. Both are Claude Code's "summarize from/up to here" — they restructure the message log (keeping code untouched), so afterwards the per-turn boundaries no longer map and conversation rewind/fork report "unavailable" until new turns rebuild them (code rewind, file-based, is unaffected). Refused while a turn runs; need the live boundary.

func (*Controller) SummarizeUpTo

func (c *Controller) SummarizeUpTo(ctx context.Context, turn int) error

func (*Controller) SwitchBranch

func (c *Controller) SwitchBranch(ref string) (agent.BranchInfo, error)

func (*Controller) ToolApprovalMode

func (c *Controller) ToolApprovalMode() string

func (*Controller) TriggerDistill

func (c *Controller) TriggerDistill(ctx context.Context) (agent.DreamRun, bool)

TriggerDistill runs a Distill workflow-extraction pass on demand. See TriggerDream.

func (*Controller) TriggerDream

func (c *Controller) TriggerDream(ctx context.Context) (agent.DreamRun, bool)

TriggerDream runs a Dream consolidation pass on demand, blocking until it finishes or times out. Returns the run record (status/error included). A false "ran" means the run did not execute (e.g. disabled, or already running). Intended for the desktop "run now" button.

func (*Controller) Turn

func (c *Controller) Turn() int

Turn returns the current turn number (0 before the first submit).

func (*Controller) WorkspaceRoot

func (c *Controller) WorkspaceRoot() string

WorkspaceRoot returns the workspace root for this controller's session (the directory that file-writers and @-references are scoped to). Empty means no scoping is in effect.

type GoalCommand

type GoalCommand struct {
	Action GoalCommandAction
	Text   string
}

func ParseGoalCommand

func ParseGoalCommand(input string) (GoalCommand, bool)

type GoalCommandAction

type GoalCommandAction int
const (
	GoalCommandStatus GoalCommandAction = iota + 1
	GoalCommandSet
	GoalCommandClear
)

type Options

type Options struct {
	Runner        agent.Runner
	Executor      *agent.Agent
	DreamProvider provider.Provider // lightweight model for dream/distill; nil = main
	Sink          event.Sink
	Policy        permission.Policy
	Label         string
	SystemPrompt  string
	SessionDir    string
	SessionPath   string
	Host          *plugin.Host
	Commands      []command.Command
	Skills        []skill.Skill
	AllSkills     []skill.Skill
	SkillStore    *skill.Store
	AllSkillStore *skill.Store
	Hooks         *hook.Runner
	Memory        *memory.Set
	Cleanup       func()
	// Jobs is the session-scoped background-job manager (nil disables background jobs).
	Jobs *jobs.Manager
	// Registry is the executor's live tool set, and PluginCtx the session-scoped
	// context; both are needed for hot-adding MCP servers via AddMCPServer.
	Registry  *tool.Registry
	PluginCtx context.Context
	// WorkspaceRoot is the project root checkpoint restores are confined to ("" =
	// no confinement). Frontends pass the cwd they launched the session in.
	WorkspaceRoot string
	AutoPlan      string
	// GoalJudge enables the independent goal judge: when the model reports
	// [goal:complete], a separate LLM call verifies completion based on the
	// transcript. nil disables the judge (model self-report is trusted).
	GoalJudge  func(ctx context.Context, prov provider.Provider, transcript []provider.Message, condition string) agent.GoalVerdict
	Classifier autoPlanClassifier
	// OnRemember, when set, is invoked with a new allow rule the user chose to
	// persist to disk (e.g. "Bash(go test:*)"). The callback is wired into the
	// permission Gate on EnableInteractiveApproval.
	OnRemember func(rule string) RememberResult
	// OnTurnEnd, when set, is invoked after each turn completes (both the
	// interactive and headless paths). It receives the last user message and
	// the final assistant reply text, enabling passive memory capture: boot
	// wires this to an LLM fact extractor that saves candidate memories with
	// Status "pending" for the user to confirm. The callback must be
	// fire-and-forget — it runs on the turn's goroutine and any error must be
	// swallowed internally so it can never stall or crash the foreground turn.
	// A nil callback disables auto-capture entirely.
	OnTurnEnd func(ctx context.Context, lastUserMsg, lastAssistant string)
	// RAGContextFn, when set, is called with the user's message before each
	// turn to auto-retrieve knowledge-base context. The returned string is
	// prepended to the user's input (like @reference context). A nil callback
	// or "" return disables injection for that turn. collection scopes the
	// retrieval to one knowledge-base collection ("" = let the callback decide,
	// typically meaning "don't inject" for the auto-injection path).
	RAGContextFn func(ctx context.Context, query, collection string) string
}

Options carries the already-built pieces setup assembles. Lifecycle metadata lets the controller mint and rotate session files; Host/Commands are surfaced to frontends that resolve MCP prompts and slash commands.

type PlanCommand

type PlanCommand struct {
	Off  bool   // true for "/plan off"
	Text string // the planning task for "/plan <text>"; empty for a bare toggle on
}

PlanCommand is the parsed form of a /plan slash command. /plan with no args or /plan <text> enters plan mode (text, when present, is sent as the planning turn); /plan off exits. See Controller.applyPlanCommand for the dispatch.

func ParsePlanCommand

func ParsePlanCommand(input string) (PlanCommand, bool)

ParsePlanCommand recognizes "/plan", "/plan off", and "/plan <text>". Returns ok=false for anything that isn't a /plan command. Mirrors ParseGoalCommand's tolerant spacing (matches "/plan", "/plan\t…", "/plan …").

type ProfileView

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

ProfileView is the payload the workspace preference panel reads: the path of the active mode's portrait file and its current contents. Path is "" when the user config dir is unresolvable; Content is "" when the file does not exist yet (a fresh mode the user has never written to).

type ProviderAutoPlanClassifier

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

func NewProviderAutoPlanClassifier

func NewProviderAutoPlanClassifier(prov provider.Provider) *ProviderAutoPlanClassifier

func (*ProviderAutoPlanClassifier) NeedsPlan

func (c *ProviderAutoPlanClassifier) NeedsPlan(ctx context.Context, input string, score int) (bool, string, error)

type RememberResult

type RememberResult struct {
	Rule      string
	Path      string
	Saved     bool
	CoveredBy string
	Err       error
}

RememberResult describes what happened when an approval rule was persisted.

type RewindScope

type RewindScope int

RewindScope selects what a Rewind restores.

const (
	RewindCode         RewindScope = iota // files only
	RewindConversation                    // message log only
	RewindBoth                            // both
)

type SessionDestroyHandle

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

SessionDestroyHandle separates session teardown into phases so background jobs can drain before artifacts are removed.

func (*SessionDestroyHandle) Wait

func (h *SessionDestroyHandle) Wait()

Wait cancels background jobs for the session and waits for them to drain.

type SlashItem

type SlashItem struct {
	Label   string `json:"label"`
	Insert  string `json:"insert"`
	Hint    string `json:"hint"`
	Descend bool   `json:"descend"`
}

SlashItem is one slash-completion suggestion. Insert is the token text placed at the current argument position (callers replace from the token's start, see SlashArgItems' returned offset); Descend hints the menu to re-open one level deeper after accepting (e.g. "/mcp " → "/mcp add ").

func SlashArgItems

func SlashArgItems(line string, d ArgData) ([]SlashItem, int)

SlashArgItems completes the arguments of a management slash command (everything after the command word). It returns the suggestions filtered by the token being typed and the byte offset where that token begins, so a caller replaces just that token. Only structured commands participate (/mcp /model /skills /hooks /effort /auto-plan /theme /language); others yield nil. Single source of truth for CLI + desktop.

Jump to

Keyboard shortcuts

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