Help us shape the future of Go! Take the 2026 Go Developer Survey

agent

package
v0.57.0 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: AGPL-3.0 Imports: 42 Imported by: 0

Documentation

Index

Constants

View Source
const DefaultMaxSteps = defaultMaxSteps

DefaultMaxSteps is what a zero-value MaxSteps resolves to (RunLoop maps 0 → this). The server's board answers it for background task children, whose loop budget has to match a synchronous sub-agent's run rather than its own turn's.

View Source
const MaxStepsUncapped = -1

MaxStepsUncapped asks RunLoop for no step ceiling. It has to be negative: 0 keeps meaning "use the default budget", because a zero-value LoopRunner (tests, a sub-agent's runner) relies on that. Only `ogcode run` opts in.

View Source
const ReminderCause = "reminder"

remindersOfCause partitions notices into the two labeled groups the content builder renders: reminders under their own heading, everything else — team reports, any unknown cause — under the teammate one, so a new cause value falls back to the heading that was always there rather than to no heading.

ReminderCause is exported: the server's reminder paths stamp it on the notices they push (internal/server/reminder.go imports this package), so one cause value is read from here by both sides of a push — in place of the spelled-alike literals whose silent drift would demote a reminder to the teammate heading.

Variables

View Source
var BuildAgent = Agent{
	ID:          "build",
	Name:        "Build",
	Description: "Full-access coding agent",
	Tools:       buildAgentTools,
	System:      codingAgentSystem(),
}

BuildAgent is the default full-access coding agent for interactive Build Mode.

View Source
var IndexAgent = Agent{
	ID:          "index",
	Name:        "Index",
	Description: "Analyzes page keyword corpora and produces semantic topic labels per page",
	Tools:       []string{"submit_doc_index"},
	System: fmt.Sprintf(`You are a document indexing agent. You receive keyword corpora for one or more documents and must produce detailed, descriptive labels that precisely capture what each page covers.

## Your process

1. **Read the page keyword corpora** from the user message. Each page has a set of unique words extracted from that page. When multiple documents are provided, each is clearly delimited.

2. **Analyze each page's keywords** deeply — identify the main topics, specific concepts, named functions/types/commands, and any sub-themes present.

3. **Produce as many labels per page as the content supports** — aim for depth, not brevity (up to %d per page, the tool's ceiling). Every distinct topic, concept, or named entity the page supports should get its own label, so a reader can judge what the page covers without opening it. Do not stop at a handful when there is more to name.
   - Specific and descriptive (prefer "Goroutine Scheduling" over "Concurrency")
   - Named entities where present: function names, types, commands, algorithms (e.g. "sync.WaitGroup", "HTTP Handler", "Binary Search")
   - Title case, 1-6 words each
   - Varied — cover different angles of the page content (topic + subtopic + key term)
   - Each a distinct topic; never pad with near-duplicates of one already given

4. **Call submit_doc_index** for EACH document separately. When multiple documents are provided, call the tool once per document — each call covers all pages of that one document. Include ALL pages for each document — do not skip any.

## Rules
- Every page must receive labels, even if the keyword corpus is sparse (use best-guess from available words).
- Be specific: "Interface Embedding" beats "Interfaces"; "defer and panic" beats "Error Handling".
- For code-heavy pages, include the specific APIs, types, or patterns being demonstrated.
- When indexing multiple documents, call submit_doc_index once per document, not once per page.
- Do not output raw JSON — use the submit_doc_index tool to submit results.
`, tool.MaxLabelsPerPage),
}

IndexAgent analyzes page keyword corpora and produces semantic topic labels.

View Source
var MemoryRecallAgent = Agent{
	ID:               "memory-recall",
	Name:             "Memory Recall",
	Description:      "Read-only agent that answers a question from the project's markdown turn memory",
	Tools:            []string{"memory_map", "file_map", "read"},
	FinalInstruction: "Reminder: answer the recall question directly and briefly — the specific facts, decisions, paths, or values asked for, and nothing else. No preamble, no methodology, no restating the question. If the memory does not cover it, say so in one line.",
	System: `You answer a single recall question using ONLY this project's persistent memory: dated markdown files, one per past turn, each a structured summary of what was asked and done. You cannot see the live conversation — the question is your complete input. You are read-only.

## Workflow (follow it exactly — it is what keeps this cheap)

1. **Call memory_map first.** It lists turn summaries as one line each — file name and topic labels — with conversations collapsed to one line per conversation at the project level. This is your table of contents — do not read files blindly.
2. **Pick the summaries that bear on the question** using their file names (UTC timestamp, session tag, title slug) and topics. Reason about time from the timestamps: a more recent summary supersedes an older one when they disagree.
3. **Read only what you need.** Call file_map on a chosen summary for its heading outline with line ranges, then read(path, ranges=[{start_line, end_line}]) for just that range. Never read a whole summary when a section will do, and never read a file the map already answered.
4. **Answer briefly and concretely.** Synthesize across the summaries you read into a short, direct answer: the facts, decisions, file paths, and values the question asks for. Attribute to a date when it matters (e.g. "as of 2026-09-09"). If the memory does not contain the answer, say so plainly rather than guessing.

## Rules

- Ground every claim in a summary you actually read — never invent facts, paths, or decisions.
- Be terse. This answer is consumed by another agent to save it re-reading history; precision and brevity matter more than prose.
- Prefer the most recent evidence when summaries conflict, and note the supersession if it is relevant.`,
}

MemoryRecallAgent is the read-only sub-agent that answers a recall question from the project's per-turn markdown memory. It backs the memory_recall and project_memory_recall tools when the turn-summary memory feature is on. Its toolset is deliberately minimal — memory_map to browse the index, file_map to outline a chosen summary, read to pull only the lines that matter — with no write/edit/bash and no recall tools, so it can neither mutate anything nor recurse into itself. It inherits the caller's model.

View Source
var NoteAgent = Agent{
	ID:               "note",
	Name:             "Note",
	Description:      "Note-taking agent — researches a query and produces a comprehensive, structured markdown note",
	Tools:            []string{"bash", "read", "file_map", "glob", "grep", "deep_search", "codebase_map", "pdf_index", "read_pdf_page", "docx_index", "read_docx_page", "query_sheet"},
	FinalInstruction: "Reminder: your entire final response must be the note itself — start with the `#` title and output only markdown. No preamble, no \"here is the note:\", no trailing commentary.",
	System: `You are a note-taking agent. Your job is to research the given query using the project codebase and any existing notes, then produce a single, comprehensive, well-structured note in markdown format.

` + projectIndexPrompt("note", true, true) + `

## Your process

1. **Research the query.** Start with codebase_map to locate relevant files, then use read, glob, and grep to explore the codebase and gather all information relevant to the query. Be thorough — your note is the primary reference a developer will reach for on this topic.

2. **Write the note.** Produce a single well-structured markdown document:
   - Clear H1 title that captures the topic
   - Sections with H2/H3 headers
   - Code blocks with language tags for all code examples
   - Mermaid diagrams, LaTeX math, LaTeX documents, Plotly charts, or Rough diagrams where they add genuine clarity (see Markdown output capabilities below)
   - Bullet lists for enumerations, tables for comparisons
   - Concrete file paths, function names, and line references (verified against the actual codebase)

3. **Output ONLY the note.** Your final response must be the complete note in markdown format and nothing else — no preamble, no "here is the note:", no trailing commentary. Just the raw markdown starting with the # title.

` + parallelToolCallsPrompt(false, true) + `

## Hard rules

- Only reference file paths and symbols you have actually read. Never invent details.
- Be specific and concrete. A note that says "see the config file" is useless — give the exact path and relevant fields.
` + "\n" + noPackageManagerDirsPrompt() + `
- Your output is saved verbatim as a markdown file. Make it self-contained — readable without access to this conversation.

` + markdownCapabilitiesPrompt(false, true),
}

NoteAgent researches a query and produces a comprehensive markdown note.

View Source
var SearchAgent = Agent{
	ID:               "search",
	Name:             "Search",
	Description:      "Deep research agent — runs one web search of the query, ranks results, fetches the top pages in parallel, and returns synthesised findings with sources",
	Tools:            []string{"web_search", "fetch_page", "read", "grep"},
	FinalInstruction: "Reminder: output only the synthesised markdown answer, including the mandatory Sources section at the bottom. No preamble. Write it as your plain message text, not inside a reasoning/thinking block.",
	System: `You are a deep research agent. Your job is to thoroughly research a question using the web and return a single, comprehensive, well-cited answer.

Your system context includes today's exact date — always use it. When the query involves anything time-sensitive (news, events, releases, "current", "latest", "today"), include the full date (day, month, year) explicitly in every search query so Google returns results for the right period.

## Strategy — complete in exactly 2 tool-call rounds

You MUST complete in exactly 2 rounds of tool calls. Going beyond 2 rounds wastes time and tokens.

**Round 1 — Search (web_search):**
Decompose the query into 3–5 focused sub-queries and call web_search for ALL of them in ONE response. Each query targets a different angle. For time-sensitive topics, append the current month and year.

**Round 2 — Fetch + Done (fetch_page):**
From the search results, pick the 2–3 most relevant URLs per sub-query (up to 9 total). Call fetch_page for ALL of them in ONE response. Do NOT write any text in this response — just the fetch_page calls. After the results arrive, your next response will be the final synthesis.

**Final response:** Synthesise the fetched content into a single well-structured markdown answer with:
- Clear H1 title
- Sections with H2/H3 headers
- A **Sources** section at the very bottom listing every URL you fetched or cited, formatted as numbered links. This section is mandatory — never omit it.

Do NOT add a third round of searches or fetches unless the results are clearly inadequate (missing key facts). 2 rounds is almost always sufficient.

## Rules

- ALWAYS fan out — never search or fetch sequentially when you can parallelize.
- If a page fails to fetch, skip it and proceed with what you have.
- Be specific and concrete. Name exact versions, APIs, and tradeoffs.
- Your final response MUST be written as plain text/markdown in your message — not inside a reasoning/thinking block. The text response is what gets returned to the caller.
- Output ONLY the synthesised answer, no preamble.
- Prefer official documentation, GitHub repos, and authoritative blogs over SEO-heavy aggregator sites.

` + parallelToolCallsPrompt(false, false),
}

SearchAgent performs deep parallel web research and synthesises findings.

View Source
var SubagentAgent = Agent{
	ID:               "subagent",
	Name:             "Subagent",
	Description:      "Read-only investigation sub-agent invoked via the task tool",
	Tools:            []string{"read", "file_map", "glob", "grep", "memory_recall", "project_memory_recall", "read_pdf_page", "pdf_index", "read_docx_page", "docx_index", "query_sheet", "codebase_map", "deep_search", "view_image"},
	FinalInstruction: "Reminder: your entire final message is what the caller receives. Answer the task directly and completely — findings, file paths, and specifics — with no preamble like \"here is what I found\". If you could not determine something, say so plainly.",
	System: `You are an autonomous investigation sub-agent. Another agent has delegated a single, self-contained task to you. You work from a clean context: you cannot see the parent's live conversation, only the task you were given. (project_memory_recall can still surface decisions recorded in this project's memory — use it when the task turns on history you were not given.) You are read-only — you explore and report, you never change anything.

` + projectIndexPrompt("subagent", false, true) + `

## Your job

1. **Read the task carefully.** It is your complete and only source of truth. Do exactly what it asks — no more, no less.

2. **Investigate efficiently.** Start with codebase_map (scoped to the relevant area) to orient, then use read, glob, and grep to gather the specific facts the task needs. Focus tightly on what the task asks — do not explore the whole codebase.

3. **Report back.** Produce a single, self-contained written answer that fully addresses the task. Be concrete: exact file paths, symbol names, line references, and short relevant snippets. Your answer is consumed by another agent that will act on it, so precision matters more than prose.

` + parallelToolCallsPrompt(false, true) + `

## Hard rules

- You are READ-ONLY. You have no write, edit, or shell tools — do not claim to have made any change.
- Only reference file paths and symbols you have actually read. Never invent paths, names, or details.
- Stay strictly within the delegated task. Do not expand scope or start unrelated work.
- If the task is ambiguous or you hit a dead end, report what you found and what remains uncertain — do not guess.
` + "\n" + noPackageManagerDirsPrompt(),
}

SubagentAgent is the autonomous, read-only sub-agent invoked via the `task` tool. It runs headless from a clean context to investigate a self-contained question, then returns a written answer. It is deliberately depth-1 — its toolset omits `task`, so it cannot spawn further sub-agents — and read-only — no write/edit and no bash, so a headless, ungated child can never mutate the project or run shell commands.

Functions

func AnnouncedPreviewPorts added in v0.40.0

func AnnouncedPreviewPorts(text string) []int

AnnouncedPreviewPorts returns the ports of the live-preview URLs in text — the <port>.<domain> form the agent is told to hand the user when it starts a service. It is deliberately narrow: a bare loopback URL (127.0.0.1:5432) an agent merely mentions is not an announcement, so it is left out, and so is a port label inside a longer hostname (a.3000.<domain>, 3000.<domain>.evil) the dispatcher would not route. The match is case-insensitive, like the Host the dispatcher reads; the result is deduped and sorted.

func CompactionThreshold added in v0.40.0

func CompactionThreshold(contextWindow int) int

CompactionThreshold is the request size, in tokens, above which the loop compacts before sending — exported so the UI can mark it on the meter.

func EffectiveContextWindow added in v0.40.0

func EffectiveContextWindow(reg *provider.Registry, database *db.DB, modelID string) (window int, learned bool)

EffectiveContextWindow is the context window the loop sizes compaction against for modelID: the smallest figure anything knows — the catalogue's, the serving host's (see Registry.ContextWindow), or one learned from an earlier overflow error, which is the host's own word that it serves less — else 0 (unknown). learned reports that the learned figure was used. The web UI's context meter reads the same value through /api/models, so what it shows and when the loop compacts cannot drift apart.

func LoadAgentMD

func LoadAgentMD(dir string) string

LoadAgentMD discovers and loads AGENT.md files by walking from dir up to the filesystem root. Files are returned in root-to-leaf order (outermost first), so that closer/leaf files appear later in the concatenated result and naturally take precedence for the LLM.

There is no size limit by default — the whole of every discovered file reaches the model. Set OGCODE_AGENT_MD_MAX_BYTES to a positive number of bytes to cap the total, in which case the first file that would overrun it is cut (on a rune boundary, with a marker) and the remainder are skipped.

Missing files are silently skipped. Permission or read errors are logged as warnings and the file is skipped.

func LoadMemoryMD added in v0.4.0

func LoadMemoryMD(dir string) string

LoadMemoryMD discovers and loads MEMORY.md files by walking from dir up to the filesystem root. Files are returned in root-to-leaf order (outermost first), so that closer/leaf files appear later in the concatenated result and naturally take precedence for the LLM.

There is no size limit by default — the whole of every discovered file reaches the model. Set OGCODE_MEMORY_MD_MAX_BYTES to a positive number of bytes to cap the total, in which case the first file that would overrun it is cut (on a rune boundary, with a marker) and the remainder are skipped.

Missing files are silently skipped. Permission or read errors are logged as warnings and the file is skipped.

func ParsePreviewPort added in v0.40.0

func ParsePreviewPort(s string) (int, bool)

ParsePreviewPort reads a port spelled as a preview host label or path segment: 1–5 ASCII digits, no sign and no leading zero, in 1–65535. It is stricter than strconv.Atoi, which also takes "+3000" and "03000", because one port must have exactly one spelling — a preview origin and the published-port check keyed on it must not be reachable under an alias.

func PermissionGatingEnabled added in v0.21.0

func PermissionGatingEnabled(ctx context.Context) bool

PermissionGatingEnabled reports whether this context was started as an interactive, permission-gated session.

func PreviewDomain added in v0.40.0

func PreviewDomain() string

PreviewDomain is the host suffix a loopback service is served under: the service on port 3000 is at http://3000.<domain>/. It is fixed for the process (an env-derived value), so a prompt section naming it stays byte-identical for the whole session and can live in the cacheable base.

The configured value is normalized — case folded, a leading "*." or "." and a trailing "." dropped — and one that is not a bare DNS name (a scheme, a port, a path, an IP address) falls back to the default with a warning. The domain is matched against every request's Host and written into URLs and the system prompt, so a malformed one would otherwise match nothing and fail silently.

func PreviewDomainIsLoopbackOnly added in v0.40.0

func PreviewDomainIsLoopbackOnly(domain string) bool

PreviewDomainIsLoopbackOnly reports whether domain is a *.localhost name. Such a name resolves to the machine the BROWSER runs on, so its preview URLs reach this server only from a browser on this server's own machine (or through a tunnel that forwards this server's port to the browser's machine); a browser anywhere else needs a real wildcard domain.

func PreviewURL added in v0.40.0

func PreviewURL(origin string, port int) string

PreviewURL is the URL the user's browser opens to reach the loopback service on port, given the origin the browser is already using for this server. Each service gets its own hostname (3000.preview.localhost) rather than a path prefix, so an app that bootstraps off window.location.pathname — Next.js and friends — sees its own origin root and starts normally. The origin's scheme and port are carried over so the URL works the same locally and through a tunnel; a preview host always carries the same numeric port as the service name, so it never collides with the server's own host.

func ServerURLFromContext added in v0.40.0

func ServerURLFromContext(ctx context.Context) string

ServerURLFromContext returns the client origin, or "" when unset (CLI, indexer, sub-agents, tests).

func WithLoopControl added in v0.18.0

func WithLoopControl(ctx context.Context, lc *LoopControl) context.Context

WithLoopControl returns a context carrying the given LoopControl. RunLoop retrieves it via LoopControlFromContext. Call sites that do not need guidance (CLI one-shot, search sessions, indexer) simply don't wrap the context — LoopControlFromContext returns nil and the loop behaves exactly as before.

func WithPermissionGating added in v0.21.0

func WithPermissionGating(ctx context.Context) context.Context

WithPermissionGating marks a context as belonging to an interactive session that has a UI able to answer permission prompts. Only loops started with this flag will pause on an "Ask" permission decision; headless loops (task, note, search, CLI) never carry it, so they never block waiting for an approval that no one is there to give.

func WithServerURL added in v0.40.0

func WithServerURL(ctx context.Context, url string) context.Context

WithServerURL carries the origin (scheme://host[:port]) the client used to reach this server, so the agent can hand back absolute URLs to /preview/ or /public/ paths.

func WithoutLoopControl added in v0.19.1

func WithoutLoopControl(ctx context.Context) context.Context

WithoutLoopControl returns a copy of ctx with any LoopControl value cleared (set to nil). This is used by nested loop invocations — notably the deep_search tool's RunSearchSession — so the child loop does not inherit the parent session's LoopControl. Without this, the child loop would drain the parent's pending guidance (stealing mid-loop instructions) and overwrite the parent's stream/tool cancel funcs, hijacking the parent's guidance side-channel for the duration of the search.

Types

type Agent

type Agent struct {
	ID          string
	Name        string
	Description string
	Tools       []string
	System      string
	// FinalInstruction, if set, is appended as the very last line of the fully
	// assembled system prompt — after all dynamic sections (project context,
	// viewport, etc.). Output-only agents use it to keep their "respond with
	// only X" constraint adjacent to the model's response, where it has the most
	// influence, instead of being buried mid-prompt.
	FinalInstruction string
}

Agent defines an agent configuration with available tools and system prompt.

func GetAgent

func GetAgent(name string) Agent

GetAgent returns the agent by name, defaulting to BuildAgent.

func (*Agent) HasTool

func (a *Agent) HasTool(toolID string) bool

HasTool reports whether toolID is in the agent's allowed toolset. An entry may be a literal id (matched exactly) or a "*" glob pattern (matched the same way Registry.ForAgent expands globs), so an agent listing "mcp_*" authorizes any "mcp_<server>/<tool>" at call time — not just at the point tools are offered to the model. Without this, a glob entry would pass the offer step but fail the executeTool guard, and the call would be rejected as "not available to the <agent> agent".

type BoardReport added in v0.51.0

type BoardReport struct {
	// Text is the notice text the report was queued with; a dispatched request
	// carrying that text concludes the report.
	Text string
	// Mark stamps the reported row on the board.
	Mark func() error
}

BoardReport is one completion report a server pushed into a running turn, held by the TrackBoardReport tracker until the request that actually carries the notice text is dispatched. The board's notified_at is stamped only by Mark — at the dispatch, not at the push or the drain — so a step that takes a notice but never sends it (a pre-stream gap restart, a dispatch that fails every retry) leaves the row unreported: the model never read the text, so the spawner's next turn hands it over again instead of the report counting as delivered. The mark closure belongs to the server: this package owns no board.

type LoopControl added in v0.18.0

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

LoopControl provides a side-channel for injecting mid-loop guidance into a running agent loop without starting a new user turn. It lets the user send a new instruction that the loop picks up at the top of the next iteration, and optionally cancel the currently-running tool call without killing the whole loop.

The guidance is ephemeral: it is never persisted to the database message history. Instead it is appended to the user's turn message content on the next LLM call — the model sees it as additional user input, not as a system directive. This avoids interactions with compaction boundaries, findLastTextUserMessageIndex turn-slicing, and agentic-memory <prior_context> filtering — all of which key off persisted user messages.

func LoopControlFromContext added in v0.18.0

func LoopControlFromContext(ctx context.Context) *LoopControl

LoopControlFromContext extracts the LoopControl from a context, or nil.

func NewLoopControl added in v0.18.0

func NewLoopControl() *LoopControl

NewLoopControl creates a fresh LoopControl.

func (*LoopControl) CancelAll added in v0.19.0

func (lc *LoopControl) CancelAll() bool

CancelAll cancels both the LLM stream and any running tool execution. This is the full "stop whatever you're doing right now" entry point used by the guidance handler so the user does not have to wait for the model to finish generating or for a long-running tool to complete before the loop picks up the new guidance. Returns true if at least one cancellation was issued.

func (*LoopControl) CancelStream added in v0.19.0

func (lc *LoopControl) CancelStream() bool

CancelStream cancels the currently-running LLM stream, if any. Returns true if a stream cancellation was issued, false if no stream is in progress (or the control is nil). This is the primary mechanism for making mid-loop guidance feel responsive: it interrupts the model's generation so the loop can immediately proceed to the next iteration where it drains the guidance.

func (*LoopControl) CancelTool added in v0.18.0

func (lc *LoopControl) CancelTool() bool

CancelTool cancels the currently-running tool execution batch, if any. Returns true if a tool cancellation was issued, false if no tools are currently running (or the control is nil).

func (*LoopControl) ClearStreamCancel added in v0.19.0

func (lc *LoopControl) ClearStreamCancel()

ClearStreamCancel removes the stored stream-cancel func without calling it. Called by RunLoop after the stream is fully consumed (or the guidance handler after it has called the cancel and the stream has wound down).

func (*LoopControl) ClearToolCancel added in v0.18.0

func (lc *LoopControl) ClearToolCancel()

ClearToolCancel removes the stored tool-cancel func without calling it. Called by RunLoop after tool execution completes normally.

func (*LoopControl) DeliveredGuidance added in v0.19.4

func (lc *LoopControl) DeliveredGuidance() string

DeliveredGuidance returns all guidance texts that have been drained (and thus injected) during this loop run, joined into a single string. This is called by RunLoop on every iteration to re-append accumulated guidance to the user's turn message so the model continuously sees all mid-loop guidance. Returns "" when no guidance has been delivered yet.

func (*LoopControl) DeliveredNotices added in v0.51.0

func (lc *LoopControl) DeliveredNotices() []Notice

DeliveredNotices returns every notice this turn has already injected, oldest first. The loop re-appends them to each step's request, so a report that arrived mid-turn is still in view three steps later.

func (*LoopControl) DrainGuidance added in v0.18.0

func (lc *LoopControl) DrainGuidance() string

DrainGuidance returns and clears all pending guidance texts. Called at the top of each loop iteration by RunLoop. Returns "" when nothing is pending. Drained texts are moved into the delivered accumulator so they can be re-injected on every subsequent iteration of this loop run — the guidance accumulates below the user's message rather than being a one-shot injection.

func (*LoopControl) DrainNotices added in v0.51.0

func (lc *LoopControl) DrainNotices() []Notice

DrainNotices returns and clears the next batch of pending notices, oldest first and capped at one injection. Anything past the cap stays queued rather than being dropped; drained notices move into the delivered accumulator and unshown, so the next request tail carries them in full and later steps keep them in view as headline digests. The board rows behind them are stamped only when the request that carries the text is actually dispatched — a step that drains and then restarts before sending (RestoreNotices) or fails to dispatch leaves the row for the next turn's catch-up.

func (*LoopControl) HasPendingGuidance added in v0.18.0

func (lc *LoopControl) HasPendingGuidance() bool

HasPendingGuidance reports whether there is undelivered guidance waiting.

func (*LoopControl) HasPendingNotice added in v0.51.0

func (lc *LoopControl) HasPendingNotice() bool

HasPendingNotice reports whether a pushed notice is still waiting to be injected.

func (*LoopControl) PendingNoticeCount added in v0.51.0

func (lc *LoopControl) PendingNoticeCount() int

PendingNoticeCount is how many notices are still queued.

func (*LoopControl) PushGuidance added in v0.18.0

func (lc *LoopControl) PushGuidance(text string)

PushGuidance appends a guidance text to the pending queue. Safe for concurrent use (called from the HTTP handler goroutine while the loop runs in its own goroutine).

func (*LoopControl) PushNotice added in v0.51.0

func (lc *LoopControl) PushNotice(n Notice)

PushNotice queues one notice for the next step. Safe for concurrent use: the board's fan-out pushes from its own goroutine while the loop runs. The same report is injected at most once per turn — catch-up runs at turn start and a teammate finishing a moment later can push the same text — so an identical notice already queued or delivered is dropped.

func (*LoopControl) RestoreNotices added in v0.51.0

func (lc *LoopControl) RestoreNotices(fresh []Notice)

RestoreNotices re-queues fresh notices at the FRONT of unshown, so the next TakeNoticesForInjection carries them in full again. The take runs before the request is built and dispatched, so a step that re-checks the queues in the pre-stream gap and restarts is holding notices it never sent — a later take would otherwise ship them only as a one-line digest claiming "already delivered above", pointing at text no request ever carried.

deliveredNotices is deliberately left alone: the restored notice is already in there from its original drain, and TakeNoticesForInjection keeps any notice present in fresh out of the digest block, so a restored notice rides the next take as full text, never shadowed by its own headline. PushNotice's dedup against deliveredNotices is unchanged by that — a same-text push was already a no-op before this method existed. Restores are idempotent: a notice already sitting in unshown is skipped, so two restarts in a row cannot double-queue it. nil-safe.

func (*LoopControl) SetStreamCancel added in v0.19.0

func (lc *LoopControl) SetStreamCancel(cancel context.CancelFunc)

SetStreamCancel registers the cancel func for the currently-running LLM stream. Called by RunLoop just before calling StreamChat. The stored func is used by CancelStream to interrupt the stream without killing the loop. RunLoop clears it (and calls it to release the child context) after the stream is fully consumed.

func (*LoopControl) SetToolCancel added in v0.18.0

func (lc *LoopControl) SetToolCancel(cancel context.CancelFunc)

SetToolCancel registers the cancel func for the currently-running tool execution batch. Called by RunLoop before launching parallel tool calls. The stored func is used by CancelTool to interrupt only the tools, not the loop. RunLoop clears it (and calls it to release the context) after wg.Wait() returns.

func (*LoopControl) TakeNoticesForInjection added in v0.51.0

func (lc *LoopControl) TakeNoticesForInjection() (fresh, digest []Notice)

TakeNoticesForInjection returns what a step's request tail should carry. fresh is every notice drained since the last injection, in full text — the one time the model reads the report whole. digest reprints everything the turn has already been shown as headlines, so a mid-turn report stays in view three steps later without re-shipping its full text at every step — re-showing the whole report each step made a model re-acknowledge it and narrate the re-check instead of working. The take happens before the request is built and sent, so a step that restarts between take and dispatch must hand what it took back via RestoreNotices — otherwise the next take would present the notice as a digest of text the model never read.

type LoopRunner

type LoopRunner struct {
	Store           *session.Store
	Bus             *bus.Bus
	Registry        *provider.Registry
	DefaultProvider provider.Provider
	Tools           *tool.Registry
	Dir             string
	MaxSteps        int
	// MaxAutoResumes lets a turn that exhausts its MaxSteps budget extend itself
	// instead of stranding the user mid-task — "resetting the limit so it resumes
	// working". The turn extends up to MaxAutoResumes times, for a hard ceiling of
	// MaxSteps*(1+MaxAutoResumes) iterations, after which it stops but stays
	// continuable (it ends on paired tool results, so a follow-up message or Resume
	// picks up from there). 0 (the default) disables auto-extend, so headless CLI
	// runs, the indexer, and sub-agents keep their exact MaxSteps cap. Only the
	// interactive server sets it.
	MaxAutoResumes int
	// MemFiles is the per-turn markdown memory index. When TurnMemory is on and
	// this is set, a completed turn is summarized to a dated markdown file and
	// indexed here, and the recall tools read it via the memory-recall sub-agent.
	// MemBarrier serializes those background writes against recall so a lookup
	// never sees a half-written index. nil (CLI, tests) disables memory.
	MemFiles   *memfile.Store
	MemBarrier *memfile.Manager
	// Search is the full-text index over the same memory corpus, answering
	// "which conversation was this about?" rather than "what did this one turn
	// contain?". It is a separate store from MemFiles because it re-reads files
	// a session memory file rewrites every turn — the one invariant the
	// memory_turn_index contract deliberately does not bend for. nil (CLI,
	// tests) disables content search.
	Search     *memfile.SearchStore
	TurnMemory bool
	NoteStore  *note.Store
	// SearchBridge is the web-search backend used by the deep-research pipeline
	// (RunSearchSession) and by web_search and fetch_page. nil when search is
	// disabled — deep_search is only registered when it is non-nil.
	SearchBridge search.Backend
	// SearchFetchTopK and SearchPageChars are the native engine's deep-research
	// tuning, set from the stored search config. Zero leaves the package default
	// in effect, and OGCODE_SEARCH_FETCH_TOP_K / OGCODE_SEARCH_PAGE_CHARS
	// override either way (see tuning).
	SearchFetchTopK int
	SearchPageChars int
	// BrowserStatus, when set, reports the live state of the browser takeover
	// bridge: whether the companion extension is connected and how many tabs
	// the user has granted. RunLoop consults it once per turn and, when a
	// connection is live, appends a system entry steering current-web checks
	// to the takeover tools while deep_search stays available for synthesis.
	// It is a closure rather than the bridge itself because the bridge lives in
	// the server (internal/browser), which this package must not import. nil
	// (CLI, tests) leaves the prompt silent and the deep_search guidance
	// unchanged — exactly the behaviour the steer refines, never removes.
	BrowserStatus func() (connected bool, granted int)
	// IndexedFileCount, when set, reports how many files the project index holds
	// for a directory. It lets the system prompt state up front whether
	// codebase_map has anything to return, instead of making every session in an
	// unindexed project spend a call discovering that it does not. A closure
	// rather than the store itself so this package keeps no dependency on
	// docindex. nil (CLI, tests) leaves the prompt silent and the agent probing,
	// which is the behaviour that predates this field.
	IndexedFileCount func(dir string) int
	// AutoIndex, when set, refreshes the project index in the background after a
	// completed turn, so files a session added or edited stop being missing from
	// codebase_map until someone presses the manual button. It is a closure
	// rather than the indexer itself because internal/indexer already imports
	// this package (it drives the IndexAgent), so the dependency has to run this
	// way round. nil (CLI, tests) leaves indexing manual, which is the behaviour
	// that predates this field — the caller decides, so the env gate stays in the
	// package that owns the index.
	//
	// model and provider are the endpoint the finished turn resolved to. They
	// are passed rather than re-derived because an index session created with
	// neither resolves through the registry default, which is frequently a
	// different provider than the session it is catching up with — spending the
	// tokens somewhere the user is not working. Inheriting them keeps the
	// refresh on the same endpoint as the turn that triggered it.
	AutoIndex func(dir, model, provider string)
	// TurnResources, when set, reports what the process consumed while one turn
	// ran — resident memory, CPU, goroutines — to telemetry. It is called once
	// where the turn starts and returns the finisher the loop defers for the end
	// of the turn: this package owns no sampler, and collection is suspended
	// while nothing watches it, so beginning and ending have to be one call.
	// Only turns a user asked for are reported (turnResourceAgents). nil (CLI,
	// tests) records nothing.
	TurnResources func(agentID string) func()
	// Permissions gates mutating tool calls (bash/write/edit) behind user
	// approval. nil disables gating entirely (CLI, tests). Even when set, a loop
	// only prompts when its context carries WithPermissionGating — so headless
	// runs (task, note, search) never block on an approval UI.
	Permissions *permission.Manager
	// Questions carries the ask_user round trip: a batch of questions the agent
	// cannot answer itself, put to the user through the UI, blocking until they
	// reply. nil disables it (CLI, tests, sub-agents). Like Permissions, it is
	// only offered when the context carries WithPermissionGating, so headless
	// runs never block on a dialog nobody can see. A teammate's loop IS gated
	// and shares this manager: its batch mirrors to the parent (user-facing)
	// session, where the human answers from the composer they are watching.
	Questions *question.Manager
	// Skills resolves the skills available in a project directory. nil (CLI,
	// tests) means no skill is ever listed and the skill tool has nothing to
	// load, which is the behaviour that predates the feature.
	Skills *skill.Loader
	// Usage is the global spend ledger: every step and utility call is recorded
	// there with the model it ran on, so one view can total spend across
	// projects and models. nil (tests) records nothing.
	Usage *usage.Ledger
	// NotesEnabled points at the notes feature-flag decision. While it reads
	// true, agents that operate on the project get the "## Project notes"
	// section in their system prompt. The server points it at the flag its
	// background refresher keeps current, so it can change while turns run —
	// hence atomic — and it is a pointer so the runners a sub-agent or task
	// copies from this one read the same live flag rather than a snapshot
	// (copying the atomic itself is what go vet's copylocks check rejects).
	// nil — the default in tests — reads as off.
	NotesEnabled *atomic.Bool
	// CatchUpTeammates, when set, hands this turn the completions its spawned
	// teammates produced since it last heard, as pending notices, before the
	// history is loaded. They reach the model at the tail of the first request.
	// It never starts a turn and never fails the turn: nil (CLI, tests) leaves
	// it unwired.
	CatchUpTeammates func(ctx context.Context, sessionID session.SessionID, dir string) error
	// CatchUpReminders, when set, hands this turn the one-shot reminders its
	// session scheduled that came due while the session was between turns, as
	// pending notices, before the history is loaded. They reach the model at
	// the tail of the first request. Like CatchUpTeammates it never starts a
	// turn and never fails the turn: nil (CLI, tests) leaves it unwired.
	CatchUpReminders func(ctx context.Context, sessionID session.SessionID, dir string) error
	// StepBudgetFor, when set, overrides the step limits for one session. This
	// runner owns no board of its own to know which sessions are teammates, so
	// the server answers for it. Returning zero leaves stepBudgets' answer in
	// place; a negative maxSteps asks for no ceiling at all (MaxStepsUncapped).
	StepBudgetFor func(sessionID session.SessionID) (maxSteps, maxAutoResumes int)
	// ParentSessionOf, when set, resolves where a permission request is also
	// surfaced: for a teammate, the user-facing session that started it (the
	// root of the nesting) plus the teammate's own handle, so the prompt shows
	// up in the composer the human is watching rather than only on the
	// teammate's page. Like StepBudgetFor, this runner owns no board, so the
	// server answers for it. An empty parent means "own session only"; nil
	// (CLI, tests) publishes nothing beyond it. The ask_user mirror rides the
	// same resolve: a teammate's question batch also surfaces under the parent,
	// so the human watching that composer sees the dialog.
	ParentSessionOf func(sessionID session.SessionID) (parent session.SessionID, teammate string)
	// FileLog is the read-bookmark gate write and edit consult before mutating a
	// file: a path another session changed after this one last read it is refused
	// rather than overwritten. nil (tests) leaves every mutation exactly as it
	// was before the gate existed.
	FileLog *filechange.Log
}

LoopRunner orchestrates the agent loop for a session.

func (*LoopRunner) AskUser added in v0.38.0

func (lr *LoopRunner) AskUser(ctx context.Context, sessionID string, questions []question.Question) (question.Reply, error)

AskUser puts a batch of questions to the user and blocks until they answer, the loop is cancelled, or the client goes away. It is the ask_user tool's implementation, wired in as tool.AskUserTool{Ask: lr.AskUser} so the tool package keeps no dependency on this one.

It mirrors requestPermission exactly: register the pending request, announce it on the bus, then wait on a buffered channel the HTTP handler writes to from another goroutine — the first client to answer wins, because the manager deletes the entry before handing it over. And like requestPermission, a teammate's batch is also mirrored to its parent (user-facing) session, so the human answers from the composer they are watching no matter which page the dialog opened on.

func (*LoopRunner) BoardNoticesDelivered added in v0.51.0

func (lr *LoopRunner) BoardNoticesDelivered(lc *LoopControl, sessionID session.SessionID, notices []Notice)

BoardNoticesDelivered concludes the reports whose text a dispatched request just carried — exactly the fresh full-text wires plus the repeat headlines the step injected at the request's tail. RunLoop calls it only after StreamChat returned a stream, so a notice is stamped exactly when the model is reading its text — never while a step has merely drained a notice it may still fail to send. Errors from the board are logged, not returned: an unstamped row is still unreported, so the next turn's catch-up delivers it again — never lost, at worst duplicated across turns.

func (*LoopRunner) PrepareResume added in v0.26.0

func (lr *LoopRunner) PrepareResume(sessionID session.SessionID) (bool, error)

PrepareResume readies a session for the loop to be started again on it, and reports whether there was anything to resume.

What it does with the interrupted turn depends on what the turn managed to produce, and the distinction is the whole design:

  • A turn that made tool calls is kept. Some of those calls may have run to completion before the loop died, and their results are on disk — real work, sometimes with real side effects. Throwing the turn away would throw those away too and invite the model to run them a second time, which for a shell command or a write is not a repeat but a second effect. ReconcileSession has already closed whichever calls went unanswered, so the turn is consistent: the model sees the results it got, and an error saying the rest never finished.

  • A turn that produced only text is deleted. What survives is a fragment that stops mid-word, and re-sending it asks the model to continue from its own truncated output rather than to take the step again. Nothing is lost by dropping it, because nothing outside the message was done.

This is the same rule the loop already applies to a stream cancelled by mid-loop guidance, and for the same reasons.

Neither branch needs to clear the finish reason. The loop only stops on a finished assistant turn when that turn is the last message, and after either branch it is not: a kept turn is followed by its tool results, and a deleted one leaves the user message before it.

func (*LoopRunner) ReconcileSession added in v0.26.0

func (lr *LoopRunner) ReconcileSession(sessionID session.SessionID) (*session.MessageInfo, error)

ReconcileSession repairs the tail of a session so the next request to the provider is valid, and returns the assistant message a resume would restart from, or nil when the session needs no resuming.

The repair that matters is pairing tool calls. A turn that died between the model emitting tool_use blocks and the loop writing their results leaves the history with a tool_use nothing answers, and both the Anthropic and OpenAI APIs reject that outright — every tool_use must have a tool_result. So the session is not merely missing its last turn; it cannot be continued at all until each unanswered call is closed with an error result.

It is written to run from persisted state rather than from what a loop held in memory, because the case it most needs to handle is the one where no loop is left to ask: the process died mid-stream. That makes it safe to call from anywhere — the error path, startup, and the resume request itself all reach the same fixed point, and calling it twice changes nothing the first call did not already.

func (*LoopRunner) RunLoop

func (lr *LoopRunner) RunLoop(ctx context.Context, sessionID session.SessionID, agentName string, viewportWidth int, viewportHeight int) (runErr error)

The error is named so the deferred loop.done publish can report it. Every `return someErr` in the body assigns to it regardless of how `err` is shadowed in inner scopes, which is why the publish can stay a single defer up here rather than being threaded through a dozen exit paths.

func (*LoopRunner) RunMemoryRecallSession added in v0.34.0

func (lr *LoopRunner) RunMemoryRecallSession(ctx context.Context, question, scope, targetSessionID, dir, model, providerID string) (string, error)

RunMemoryRecallSession runs the read-only memory-recall sub-agent for a recall question and returns its concise written answer. It mirrors RunTaskSession: an ephemeral session deleted on completion, a capped child loop with the parent's LoopControl and permissions stripped, and the parent model inherited via the session's Model. The scope (project vs one session) is placed on the child context for the memory_map tool, so the model cannot widen it. Wired to the recall tools via the tool.RecallFunc contract.

func (*LoopRunner) RunSearchSession added in v0.8.0

func (lr *LoopRunner) RunSearchSession(ctx context.Context, query, dir, model, providerID string) (string, error)

RunSearchSession runs the deep-research pipeline for a query and returns the synthesised markdown answer. It is called by tool.DeepSearchTool via the tool.DeepSearchFunc contract.

Unlike a free-form agent loop, this is a deterministic 4-stage pipeline — search → rank → fetch → synthesise — with exactly two LLM calls. The query is searched verbatim: the calling agent already phrases a focused research question, so expanding it into sub-queries cost a blocking LLM round trip up front for little gain. Breadth now comes from asking the bridge for more results on the one query instead. It never depends on the (session-inherited) model emitting parallel tool calls or converging on its own: the searches and fetches are orchestrated in parallel on the Go side, and the final stage is always a plain synthesis, so the result can never come back empty the way the old tool-calling loop did on weaker models. The model is still inherited from the caller (dir is accepted for signature compatibility but unused — the pipeline needs no working dir).

func (*LoopRunner) RunTaskSession added in v0.21.0

func (lr *LoopRunner) RunTaskSession(ctx context.Context, description, prompt, dir, model, providerID string) (string, error)

RunTaskSession creates an ephemeral read-only sub-agent session, runs the full loop for a delegated investigation, and returns the sub-agent's final written answer. The session is deleted on completion. Called by tool.TaskTool via the tool.TaskFunc contract. The sub-agent (SubagentAgent) is depth-1 — its toolset omits `task`, so it cannot spawn further sub-agents.

func (*LoopRunner) TrackBoardReport added in v0.51.0

func (lr *LoopRunner) TrackBoardReport(lc *LoopControl, sessionID session.SessionID, rep BoardReport)

TrackBoardReport records a report the board pushed into lc, to be concluded the first time a request dispatched for lc carries that notice's text. nil safe on every part — a runner without a board tracks nothing.

func (*LoopRunner) TrimBoardReports added in v0.51.0

func (lr *LoopRunner) TrimBoardReports(sessionID session.SessionID, live *LoopControl)

TrimBoardReports drops the session's reports whose turn is not live: the turn that just took over replaced them, their notices died with their LoopControl, and their rows stay unreported for this turn's catch-up to hand over itself.

type Notice added in v0.51.0

type Notice struct {
	// Text is the rendered report, fencing included.
	Text string
	// Cause names where the notice came from — "teammate" — for logs and
	// tests. The model never reads it.
	Cause string
}

Notice is one report pushed into a running loop: a spawned teammate finished, was stopped, or went stale, and its last message is the result. Unlike guidance it is not the user speaking.

func (Notice) Headline added in v0.51.0

func (n Notice) Headline() string

Headline is the notice's first line: the one-line digest the loop reprints on the steps after the notice's full text first went out.

Jump to

Keyboard shortcuts

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