Documentation
¶
Overview ¶
Package agentic implements the agentic coding assistant actors for use in both the CLI and server contexts.
Package agentic implements the agentic coding assistant actors. The LLMPromptExecutionActor manages conversation history and spawns OrchestratorActors for each user request. The OrchestratorActor runs the autonomous loop: call LLM, execute tools, request approval, repeat until done.
Index ¶
- Constants
- Variables
- func ApprovalRequestSubject(paneID string) string
- func ApprovalResponseSubject(paneID string) string
- func BuildSubAgentRegistry(parent *tools.ToolRegistry, allowedTools []string) *tools.ToolRegistry
- func BuildSubAgentSeedConversation(p *SubAgentParams) []provider.ConversationTurn
- func CloseDanglingToolCalls(turns []provider.ConversationTurn, note string) []provider.ConversationTurn
- func FormatSubAgentSummary(done *msg.MsgOrchestratorDone) string
- func LastGroundingReport(turns []provider.ConversationTurn) *msg.GroundingReportInfo
- func SetApprovalPolicy(f ApprovalPolicyFunc)
- func ShouldAutoRetry(kind string) bool
- func SubAgentCallsFromConversation(conv []provider.ConversationTurn) []provider.SubAgentCall
- func ValidGroundingMode(s string) bool
- type AgentConfig
- type ApprovalManager
- type ApprovalMemory
- type ApprovalPolicyFunc
- type GroundingReportParams
- type LLMPromptExecutionActor
- func (a *LLMPromptExecutionActor) Conversation() []provider.ConversationTurn
- func (a *LLMPromptExecutionActor) InboxSubject() string
- func (a *LLMPromptExecutionActor) Receive(ctx actor.Context)
- func (a *LLMPromptExecutionActor) Session() *SessionMemory
- func (a *LLMPromptExecutionActor) SetAgentName(name string)
- func (a *LLMPromptExecutionActor) SetAutoApproval(key string)
- func (a *LLMPromptExecutionActor) SetAutoApproveAll(b bool)
- func (a *LLMPromptExecutionActor) SetContextTokenLimit(limit int)
- func (a *LLMPromptExecutionActor) SetCwdResolver(fn func() string)
- func (a *LLMPromptExecutionActor) SetEnvBlockProvider(fn func() string)
- func (a *LLMPromptExecutionActor) SetGroundingMode(mode string)
- func (a *LLMPromptExecutionActor) SetMetricsSink(s MetricsSink)
- func (a *LLMPromptExecutionActor) SetScopeResolver(fn func(scopeHint string) *tools.ToolRegistry)
- func (a *LLMPromptExecutionActor) SetSecretNAT(s secretnat.SessionHandle)
- type MetricsSink
- type NoopMetricsSink
- type OrchestratorActor
- func (o *OrchestratorActor) Receive(ctx actor.Context)
- func (o *OrchestratorActor) SetAutoApproveAll(b bool)
- func (o *OrchestratorActor) SetCwdResolver(fn func() string)
- func (o *OrchestratorActor) SetGroundingMode(mode string)
- func (o *OrchestratorActor) SetMaxContextTokens(n int)
- func (o *OrchestratorActor) SetMaxDuration(d time.Duration)
- func (o *OrchestratorActor) SetMetricsSink(s MetricsSink)
- func (o *OrchestratorActor) SetPermissionPolicy(p *PermissionPolicy)
- func (o *OrchestratorActor) SetSecretNAT(s secretnat.SessionHandle)
- type OrchestratorPhase
- type PatchKind
- type PatchTarget
- type PermDecision
- type PermissionPolicy
- type PermissionRule
- type PolicyDecision
- type ReadTracker
- type SessionMemory
- func (s *SessionMemory) Append(t provider.ConversationTurn)
- func (s *SessionMemory) AppendUserTurn(content string, blocks []provider.ContentBlock)
- func (s *SessionMemory) Bound()
- func (s *SessionMemory) ClearPause()
- func (s *SessionMemory) GroundingOverride() string
- func (s *SessionMemory) Len() int
- func (s *SessionMemory) Pause(reason string)
- func (s *SessionMemory) Paused() (bool, string)
- func (s *SessionMemory) Persist(force bool)
- func (s *SessionMemory) Replace(turns []provider.ConversationTurn)
- func (s *SessionMemory) Restore() bool
- func (s *SessionMemory) SetGroundingOverride(mode string)
- func (s *SessionMemory) Turns() []provider.ConversationTurn
- type SessionMemoryState
- type SubAgentParams
Constants ¶
const ( GroundingOff = "off" GroundingPrompt = "prompt" GroundingEnforced = "enforced" )
Grounding: make the agent read the codebase the way a human does before it acts — locate the relevant code with search tools (glob / grep / symbol_search), read it, and iterate until the request maps to concrete files and symbols. If the codebase and documents don't yield the target, the agent asks the user WHERE the relevant code lives instead of guessing; if the prompt alone already makes clear that information is missing, it may ask immediately without ritually exhausting search first.
Three modes (per LLM-execution actor, threaded into each orchestrator):
GroundingOff — no protocol; legacy behavior.
GroundingPrompt — ADVISORY: the grounding protocol is appended to the
system prompt, but nothing is enforced. Default for
sub-agents (pre-scoped by their parent) and
agents/humanoids (chat users don't want a grep
ritual forced on quick questions).
GroundingEnforced — the GATE: the run starts in PhaseGrounding where only
read-only exploration tools are allowed; mutating
tools stay locked until the model calls
grounding_report{understood: true}. Default for panes.
grounding_report is an intercepted pseudo-tool (the sub_agent pattern): the registered executor is never run; the orchestrator handles the call.
understood=true → exit PhaseGrounding, unlock all tools.
understood=false + question → surface the question to the user and PAUSE
the run (StoppedReasonAwaitingInfo); the
user's reply arrives as the next prompt and
resumes from the checkpoint.
const DefaultContextTokenLimit = 160000
DefaultContextTokenLimit is the input-token budget used when the host does not specify one. Claude models accept ~200k input tokens; the limit leaves headroom for the response and tool output.
const DefaultGroundingMaxIterations = 6
DefaultGroundingMaxIterations caps how many loop iterations a run may stay in PhaseGrounding before the gate opens anyway (fail-open: exploration budget exhausted must never brick the task).
const DefaultSubAgentMaxIterations = 25
DefaultSubAgentMaxIterations bounds the child orchestrator's agentic loop independently of the parent's budget. Sub-tasks are intended to be focused and short; a tighter cap protects against runaway children.
const DefaultSubAgentTimeout = 15 * time.Minute
DefaultSubAgentTimeout is the wall-clock upper bound a parent will wait for a child orchestrator to report MsgOrchestratorDone. The parent's own cancellation propagates through the shared o.ctx as well.
const GroundingToolName = "grounding_report"
GroundingToolName is the pseudo-tool the model calls to declare its understanding (or lack of it). Intercepted in executeTool before normal dispatch — the registered executor is a defensive no-op.
const MaxSubAgentDepth = 2
MaxSubAgentDepth caps recursive sub-agent spawning. The top-level orchestrator runs at depth 0; the first sub-agent runs at depth 1; a child of that sub-agent would run at depth 2 and so on. Attempts to spawn beyond MaxSubAgentDepth return a structured error so the parent can recover.
const SubAgentCallsMetaKey = "sub_agent_tool_calls"
SubAgentCallsMetaKey is the sub_agent tool output Metadata key carrying the child's tool calls as JSON ([]provider.SubAgentCall). buildToolResultTurn moves it onto the result turn's SubAgentCalls; it never enters Content.
const SubAgentToolName = "sub_agent"
SubAgentToolName is the tool name the model invokes to delegate a focused task to a child orchestrator. The orchestrator intercepts this name in executeTool and routes it through handleSubAgent rather than the registered ToolExecutor (whose Execute is a defensive no-op).
Variables ¶
var ( // MaxToolOutputBytes is the byte threshold at which truncation kicks in. MaxToolOutputBytes = 50000 // MaxToolOutputLines is the line threshold at which truncation kicks in. // Whichever threshold (bytes or lines) fires first triggers truncation. MaxToolOutputLines = 600 // ToolOutputHeadLines is the number of lines preserved verbatim at the // HEAD of a truncated output. Tail uses the same count by default. ToolOutputHeadLines = 200 // ToolOutputTailLines is the number of lines preserved verbatim at the // TAIL of a truncated output. ToolOutputTailLines = 200 )
Tool-output shaping (Phase 2 item K). Large raw outputs from bash / grep / file_read inflate the parent context and degrade subsequent model turns. shapeToolOutput applies a head+tail truncation with an explicit marker so the model knows content was dropped, while preserving the most informative edges of the output. It also annotates the output's Metadata with line and byte counts so the model can reason about size without re-counting.
Configurable thresholds: kept as package-level vars (not consts) so a host can tune them per environment or via tests. The defaults are tuned for Claude's context window: 50 KB / ~12k tokens is comfortable; 600 lines is enough to capture most useful tool output without overwhelming the parent.
var DefaultCompactionSummarizePrompt = `` /* 302-byte string literal not displayed */
DefaultCompactionSummarizePrompt is the prompt body used by the compaction path when summarising dropped conversation turns. The token `{{transcript}}` is substituted with the rendered transcript at call time. As with the sub- agent prompt above this is a VARIABLE so rysh-cli can replace it from the embedded rysh-cli-agent-prompts/system_compaction_summarize.md file.
var DefaultGroundingPromptAdvisory = `` /* 1002-byte string literal not displayed */
DefaultGroundingPromptAdvisory is the grounding protocol appended to the system prompt in GroundingPrompt and GroundingEnforced modes. A VARIABLE (not const) so rysh-cli can replace it from an embedded prompt file, like DefaultSubAgentSystemPrompt.
var DefaultGroundingPromptEnforced = DefaultGroundingPromptAdvisory + `
The grounding gate is ACTIVE for this task: only read-only exploration tools
are available until you call grounding_report. When the request maps to
concrete files (or plainly needs no codebase context), call
grounding_report{understood: true, relevant_files: [...], evidence: "..."} to
unlock all tools and proceed. If you cannot locate what the request refers
to, call grounding_report{understood: false, question: "..."} — the question
is shown to the user and the session pauses until they reply.`
DefaultGroundingPromptEnforced extends the advisory protocol with the gate contract. Also a VARIABLE for host override.
var DefaultSubAgentSystemPrompt = `` /* 456-byte string literal not displayed */
DefaultSubAgentSystemPrompt is used when a sub_agent tool call doesn't supply its own `system_prompt`. It is a VARIABLE (not const) so that the hosting binary — rysh-cli — can replace its content at startup from the embedded rysh-cli-agent-prompts/system_sub_agent.md file. The fallback below keeps the package self-contained for tests and library callers.
var GroundingAllowedTools = map[string]bool{ "glob": true, "grep": true, "symbol_search": true, "file_read": true, "bash": true, "list_tools": true, "env_read": true, "web_search": true, "web_fetch": true, "pane_inspect": true, "session_history": true, "agents_list": true, "ask_user": true, "context": true, "todo": true, GroundingToolName: true, }
GroundingAllowedTools is the read-only exploration set permitted while the grounding gate is closed. Everything else returns a permission error telling the model to ground first. bash is included because its own approval allowlist already gates mutating commands behind explicit user consent; ask_user is included so the model can ask mid-grounding. The set is a package variable so hosts can extend it (e.g. read-only MCP tools).
var GroundingReadOnlyBrowserActions = map[string]bool{ "get_text": true, "get_html": true, "get_elements": true, "get_value": true, "get_tabs": true, "screenshot": true, "wait": true, }
GroundingReadOnlyBrowserActions is the subset of browser_action actions permitted while the grounding gate is closed: a web-mode pane must be able to OBSERVE the page (read text/elements, screenshot, inspect tabs) to ground itself, while mutating actions (click, type, navigate, execute_js…) stay locked until grounding_report. Package variable so hosts can extend.
var NewSubAgentOrchID = func() string { return uuid.New().String() }
NewSubAgentOrchID returns a UUID for a child orchestrator. Centralised so tests can swap it out if needed.
Functions ¶
func ApprovalRequestSubject ¶
ApprovalRequestSubject returns the NATS subject for approval requests.
func ApprovalResponseSubject ¶
ApprovalResponseSubject returns the NATS subject for approval responses.
func BuildSubAgentRegistry ¶
func BuildSubAgentRegistry(parent *tools.ToolRegistry, allowedTools []string) *tools.ToolRegistry
BuildSubAgentRegistry returns a new tools.ToolRegistry containing only the tools the sub-agent is allowed to use. If allowedTools is empty, the full parent registry is cloned (so the child inherits everything except the hard-coded depth cap on nested sub_agent calls).
Unknown tool names in allowedTools are silently skipped — surfacing them as errors would force the model to know the exact registry contents, which it doesn't.
func BuildSubAgentSeedConversation ¶
func BuildSubAgentSeedConversation(p *SubAgentParams) []provider.ConversationTurn
BuildSubAgentSeedConversation constructs the single-user-turn conversation the child orchestrator starts with. Task and optional context are joined with a labelled separator so the child can distinguish them.
func CloseDanglingToolCalls ¶
func CloseDanglingToolCalls(turns []provider.ConversationTurn, note string) []provider.ConversationTurn
CloseDanglingToolCalls heals the TAIL of a conversation: when the last assistant turn carries tool_use blocks that have no matching tool_result turns (the run was interrupted mid-tool), synthetic error results are appended so the Messages API accepts the transcript on resume. The synthetic results are categorized "tool" with the interrupted note, so the model knows the calls never ran and can re-issue them.
func FormatSubAgentSummary ¶
func FormatSubAgentSummary(done *msg.MsgOrchestratorDone) string
FormatSubAgentSummary renders a child orchestrator's MsgOrchestratorDone as the human-readable Content of a tools.ToolOutput. Only the final summary (plus terse counts of files-changed and errors) is included — by design, the child's tool transcript is not surfaced to the parent.
func LastGroundingReport ¶
func LastGroundingReport(turns []provider.ConversationTurn) *msg.GroundingReportInfo
LastGroundingReport scans the conversation backwards for the most recent grounding_report tool CALL (on an assistant turn) and returns its parsed projection for status display. The call's params — not the tool-result ack — carry the substance (files, evidence, question). Returns nil when the session has no grounded run yet.
func SetApprovalPolicy ¶
func SetApprovalPolicy(f ApprovalPolicyFunc)
SetApprovalPolicy installs the approval policy (nil clears it). Called by rysh-cli's WorkspaceActor on policy load and on every ##policy reload.
func ShouldAutoRetry ¶
ShouldAutoRetry returns true when a tool error of the given kind is worth a one-shot transparent retry by the orchestrator. Today only `transient` qualifies — adding kinds requires a behavioural argument, not just a categorisation one. Follow-up item 4.
func SubAgentCallsFromConversation ¶ added in v0.2.11
func SubAgentCallsFromConversation(conv []provider.ConversationTurn) []provider.SubAgentCall
SubAgentCallsFromConversation lists the tool calls recorded in a child orchestrator's conversation, in call order: every assistant ToolCalls entry, matched to its result turn by ToolCallID. A call with no result turn (the child stopped mid-tool) has Ended false. A nested sub_agent call carries the SubAgentCalls of its own result turn, so the list is recursive.
func ValidGroundingMode ¶
ValidGroundingMode reports whether s is a recognised grounding mode.
Types ¶
type AgentConfig ¶
type AgentConfig struct {
// APIKey is the Anthropic API key.
APIKey string
// APIURL is the base URL for the Anthropic Messages API.
// Defaults to "https://api.anthropic.com" if empty.
APIURL string
// DefaultModel is the model name to use.
// Empty means the provider's own default (provider.DefaultClaudeModel);
// this comment used to name a dated model id that the API has since
// retired, which is exactly why the value is not repeated here.
DefaultModel string
// MaxTokens is the maximum number of tokens per response.
// Defaults to 4096 if zero.
MaxTokens int
// SystemPrompt is the system prompt text (or file path) for the agent.
SystemPrompt string
// BraveAPIKey is the API key for the Brave Search tool.
BraveAPIKey string
// MaxIterations is the maximum number of orchestrator loop iterations.
// Defaults to 20 if zero.
MaxIterations int
}
AgentConfig holds the configuration needed to run an LLMPromptExecutionActor. Both the CLI (rysh-cli) and the server (rysh-server) populate this struct from their respective configuration systems before passing it to NewLLMPromptExecutionActor. This keeps the shared package free of any host-specific configuration dependency.
type ApprovalManager ¶
type ApprovalManager struct {
// contains filtered or unexported fields
}
ApprovalManager handles sending approval requests and receiving responses. It is used by the TUI to publish user decisions back to the orchestrator.
The TUI calls PublishApproval when the user makes a decision on a pending approval request (displayed as a diff or confirmation prompt).
func NewApprovalManager ¶
func NewApprovalManager(nc *nats.Conn, pub *msg.NATSPublisher, paneID string) *ApprovalManager
NewApprovalManager creates an ApprovalManager for a specific pane.
func (*ApprovalManager) PublishApproval ¶
func (am *ApprovalManager) PublishApproval(requestID string, decision msg.ApprovalDecision, reason string)
PublishApproval publishes an approval response to the orchestrator.
func (*ApprovalManager) SubscribeApprovalRequests ¶
func (am *ApprovalManager) SubscribeApprovalRequests(callback func(*msg.MsgApprovalRequest)) (*nats.Subscription, error)
SubscribeApprovalRequests subscribes to approval requests for this pane. The callback is invoked for each request, allowing the TUI to render the approval prompt.
type ApprovalMemory ¶ added in v0.2.6
type ApprovalMemory struct {
// contains filtered or unexported fields
}
ApprovalMemory is the session's "approve all like this" registry: the set of tool+context keys a human answered `yes_always` to (buildApprovalKey).
It is a shared, guarded object rather than a plain map because of WHO touches it and WHEN. The registry belongs to the pane's long-lived LLMPromptExecutionActor, but the answers arrive inside an OrchestratorActor — a fresh one per prompt, running on its own goroutine.
It used to be a `map[string]bool` COPIED into each orchestrator at construction. Every `yes_always` was therefore written to a copy that was discarded when the turn ended, and the next turn started from the actor's map, which nothing ever wrote to (`SetAutoApproval` had no callers). The registry the code described as session-scoped lasted exactly one prompt: answer "Always", and the same dialog is back on the next turn. In the desktop app, with an agent touching a dozen files, that is a dozen dialogs that never stop coming.
Sharing the object fixes the lifetime; the mutex is what makes sharing it legal, since the orchestrator's goroutine is not the actor's.
Every method is nil-safe: a nil *ApprovalMemory reads as empty and swallows writes, so an orchestrator constructed without one (tests, a headless caller) behaves exactly as before.
func ApprovalMemoryFrom ¶ added in v0.2.6
func ApprovalMemoryFrom(seed map[string]bool) *ApprovalMemory
ApprovalMemoryFrom builds a registry seeded from a plain map, for callers that still hold one.
func NewApprovalMemory ¶ added in v0.2.6
func NewApprovalMemory() *ApprovalMemory
NewApprovalMemory returns an empty registry.
func (*ApprovalMemory) Approve ¶ added in v0.2.6
func (m *ApprovalMemory) Approve(key string)
Approve records a `yes_always` answer. It applies from the next tool call on, for the life of the pane's execution actor — not just the current turn.
func (*ApprovalMemory) Approved ¶ added in v0.2.6
func (m *ApprovalMemory) Approved(key string) bool
Approved reports whether key was answered `yes_always` in this session.
func (*ApprovalMemory) Len ¶ added in v0.2.6
func (m *ApprovalMemory) Len() int
Len returns how many keys are remembered. Diagnostics and tests.
type ApprovalPolicyFunc ¶
type ApprovalPolicyFunc func(toolName string, input json.RawMessage) (PolicyDecision, string)
ApprovalPolicyFunc evaluates one tool call (name + raw JSON input) against policy-as-code and returns a decision plus the matched rule ID (e.g. "bash.deny[1]") for traceability. Returns (PolicyDefault, "") when no rule matches.
type GroundingReportParams ¶
type GroundingReportParams struct {
// Understood: true when the request now maps to concrete files/symbols
// (or needs no codebase context at all).
Understood bool `json:"understood"`
// RelevantFiles lists the files the understanding is grounded on.
RelevantFiles []string `json:"relevant_files,omitempty"`
// Evidence is a one-paragraph summary of what was found and where.
Evidence string `json:"evidence,omitempty"`
// MissingInfo describes what could not be located (understood=false).
MissingInfo string `json:"missing_info,omitempty"`
// Question is the question to surface to the user (understood=false) —
// typically "where does X live?".
Question string `json:"question,omitempty"`
}
GroundingReportParams mirrors the JSON schema of the grounding_report tool.
func ParseGroundingReport ¶
func ParseGroundingReport(raw json.RawMessage) (*GroundingReportParams, error)
ParseGroundingReport decodes a grounding_report tool-call payload.
type LLMPromptExecutionActor ¶
type LLMPromptExecutionActor struct {
// contains filtered or unexported fields
}
LLMPromptExecutionActor is the top-level agent manager for a pane. It receives user prompts, maintains conversation history, spawns orchestrators, and relays output to the pane's data-plane subjects.
func NewLLMPromptExecutionActor ¶
func NewLLMPromptExecutionActor( paneID string, maxIterations int, pub *msg.NATSPublisher, nc *nats.Conn, prov provider.AgenticProvider, toolRegistry *tools.ToolRegistry, systemPrompt string, pipelineOutputSubject string, chatOutputPaneID string, kvStore nats.KeyValue, ) *LLMPromptExecutionActor
NewLLMPromptExecutionActor creates a new LLMPromptExecutionActor for the given pane.
func (*LLMPromptExecutionActor) Conversation ¶
func (a *LLMPromptExecutionActor) Conversation() []provider.ConversationTurn
Conversation returns the current conversation history (for persistence).
func (*LLMPromptExecutionActor) InboxSubject ¶
func (a *LLMPromptExecutionActor) InboxSubject() string
InboxSubject returns the NATS subject this actor listens on.
func (*LLMPromptExecutionActor) Receive ¶
func (a *LLMPromptExecutionActor) Receive(ctx actor.Context)
Receive handles messages for the LLMPromptExecutionActor.
func (*LLMPromptExecutionActor) Session ¶
func (a *LLMPromptExecutionActor) Session() *SessionMemory
Session exposes the session memory (turns + pause checkpoint). Hosts use it for diagnostics; mutation goes through actor messages only.
func (*LLMPromptExecutionActor) SetAgentName ¶
func (a *LLMPromptExecutionActor) SetAgentName(name string)
SetAgentName marks this actor as serving a named autonomous agent or humanoid, so its usage records are attributed to that agent (design 003 "by agent"). Regular panes never call this, so their records carry an empty AgentName.
func (*LLMPromptExecutionActor) SetAutoApproval ¶
func (a *LLMPromptExecutionActor) SetAutoApproval(key string)
SetAutoApproval grants automatic approval for a tool+context combination.
func (*LLMPromptExecutionActor) SetAutoApproveAll ¶
func (a *LLMPromptExecutionActor) SetAutoApproveAll(b bool)
SetAutoApproveAll enables headless auto-approval for every orchestrator this actor spawns: all tool calls run without an approval prompt. Used for humanoids/agents that have no human approver.
func (*LLMPromptExecutionActor) SetContextTokenLimit ¶
func (a *LLMPromptExecutionActor) SetContextTokenLimit(limit int)
SetContextTokenLimit overrides the input-token budget used for context compaction. A value <= 0 resets it to DefaultContextTokenLimit.
func (*LLMPromptExecutionActor) SetCwdResolver ¶
func (a *LLMPromptExecutionActor) SetCwdResolver(fn func() string)
SetCwdResolver installs a working-directory resolver threaded into every orchestrator this actor spawns. The resolver is evaluated at the start of each run; a non-empty result re-points all working-directory-aware tools at that path. Pass nil to disable (tools keep their default workDir).
func (*LLMPromptExecutionActor) SetEnvBlockProvider ¶
func (a *LLMPromptExecutionActor) SetEnvBlockProvider(fn func() string)
SetEnvBlockProvider installs a per-turn environment-block provider appended to the effective system prompt before each prompt. Pass nil to disable.
func (*LLMPromptExecutionActor) SetGroundingMode ¶
func (a *LLMPromptExecutionActor) SetGroundingMode(mode string)
SetGroundingMode selects the grounding protocol (GroundingOff / GroundingPrompt / GroundingEnforced) threaded into every orchestrator this actor spawns. See grounding.go.
func (*LLMPromptExecutionActor) SetMetricsSink ¶
func (a *LLMPromptExecutionActor) SetMetricsSink(s MetricsSink)
SetMetricsSink installs a metrics sink that's threaded into every orchestrator this actor spawns. Pass nil to disable recording on future runs. Follow-up 3b.
func (*LLMPromptExecutionActor) SetScopeResolver ¶
func (a *LLMPromptExecutionActor) SetScopeResolver(fn func(scopeHint string) *tools.ToolRegistry)
SetScopeResolver installs a per-prompt scope resolver. Given a prompt's ScopeHint it returns the parent registry this actor's tool registry should chain to for that run (so an agent inherits the invoking pane's scope). Pass nil to disable. Used for agents/humanoids; panes leave it nil.
func (*LLMPromptExecutionActor) SetSecretNAT ¶
func (a *LLMPromptExecutionActor) SetSecretNAT(s secretnat.SessionHandle)
SetSecretNAT installs this conversation's SecretNAT session, threaded into every leg provider and every spawned orchestrator. Pass nil to disable.
type MetricsSink ¶
type MetricsSink interface {
// RecordTool fires after a tool's Execute returns (success or error).
// dur is wall-clock elapsed; out is the result (may be nil on a Go
// error before the tool produced a ToolOutput); err is the Go-level
// error if any.
RecordTool(name string, dur time.Duration, out *tools.ToolOutput, err error)
// RecordLLMCall fires after each provider.CompleteWithTools[Stream]
// returns. usage carries token accounting; cache hit ratio is
// derivable from CacheReadInputTokens / TotalInputTokens.
RecordLLMCall(model string, dur time.Duration, usage provider.Usage)
// RecordCompaction fires after maybeCompact summarises and drops old
// turns. droppedTurns is the number of original turns replaced.
RecordCompaction(droppedTurns int, dur time.Duration)
// RecordApproval fires for every approval decision (yes / yes_always
// / no / no_with_reason / timeout).
RecordApproval(toolName, decision string, dur time.Duration)
}
Follow-up item 3 — observability hooks for the agentic system.
MetricsSink is the small interface the orchestrator and Setup call into when significant events happen: a tool finishes, an LLM call returns, compaction runs, an approval decision lands. Implementations are expected to be cheap (return quickly; do real work in the background).
rysh-cli ships a Prometheus implementation under rysh-cli/internal/metrics/; every other binary / test uses the NoopMetricsSink default so nothing breaks when metrics aren't wired up.
type NoopMetricsSink ¶
type NoopMetricsSink struct{}
NoopMetricsSink is the default; safe to use everywhere. Production rysh-cli replaces it with a Prometheus-backed implementation at startup.
func (NoopMetricsSink) RecordApproval ¶
func (NoopMetricsSink) RecordApproval(string, string, time.Duration)
func (NoopMetricsSink) RecordCompaction ¶
func (NoopMetricsSink) RecordCompaction(int, time.Duration)
func (NoopMetricsSink) RecordLLMCall ¶
func (NoopMetricsSink) RecordTool ¶
func (NoopMetricsSink) RecordTool(string, time.Duration, *tools.ToolOutput, error)
type OrchestratorActor ¶
type OrchestratorActor struct {
// contains filtered or unexported fields
}
OrchestratorActor runs the agentic loop for a single user request. It calls the LLM, dispatches tool calls, handles approvals, and loops until the task is complete or limits are reached.
func NewOrchestratorActor ¶
func NewOrchestratorActor( id string, paneID string, pub *msg.NATSPublisher, nc *nats.Conn, prov provider.AgenticProvider, toolRegistry *tools.ToolRegistry, conversation []provider.ConversationTurn, systemPrompt string, autoApproved *ApprovalMemory, ctx context.Context, maxIterations int, pipelineOutputSubject string, chatOutputPaneID string, approvalPaneGroups []string, paneName string, contextTokenLimit int, subAgentDepth int, agentName string, ) *OrchestratorActor
NewOrchestratorActor creates an orchestrator for a single task.
func (*OrchestratorActor) Receive ¶
func (o *OrchestratorActor) Receive(ctx actor.Context)
Receive handles messages for the orchestrator.
func (*OrchestratorActor) SetAutoApproveAll ¶
func (o *OrchestratorActor) SetAutoApproveAll(b bool)
SetAutoApproveAll enables headless auto-approval: every tool call is treated as pre-approved, so no approval request is published and no approval pane is spawned. Used for humanoids/agents that have no human approver.
func (*OrchestratorActor) SetCwdResolver ¶
func (o *OrchestratorActor) SetCwdResolver(fn func() string)
SetCwdResolver installs the working-directory resolver. When set, runLoop evaluates it once at the start of the run and re-points working-directory aware tools at the returned path (the pane's live shell cwd).
func (*OrchestratorActor) SetGroundingMode ¶
func (o *OrchestratorActor) SetGroundingMode(mode string)
SetGroundingMode selects the grounding protocol for this run: GroundingOff, GroundingPrompt (advisory system-prompt section), or GroundingEnforced (read-only tool gate until grounding_report{understood:true}). See grounding.go.
func (*OrchestratorActor) SetMaxContextTokens ¶
func (o *OrchestratorActor) SetMaxContextTokens(n int)
SetMaxContextTokens installs a cumulative token cap: the loop pauses with StoppedReasonMaxTokens once this leg's total token usage (input+output) reaches n. The parent passes the run's remaining budget. 0 disables the cap.
func (*OrchestratorActor) SetMaxDuration ¶
func (o *OrchestratorActor) SetMaxDuration(d time.Duration)
SetMaxDuration installs a wall-clock cap on the agentic loop. 0 disables the cap (iteration limit remains the only fail-safe). Phase 3 smaller-wins.
func (*OrchestratorActor) SetMetricsSink ¶
func (o *OrchestratorActor) SetMetricsSink(s MetricsSink)
SetMetricsSink installs a metrics sink on this orchestrator. Pass nil to fall back to NoopMetricsSink. Follow-up item 3.
func (*OrchestratorActor) SetPermissionPolicy ¶
func (o *OrchestratorActor) SetPermissionPolicy(p *PermissionPolicy)
SetPermissionPolicy installs a fine-grained tool-permission policy. Pass nil to remove it. Phase 3 N.
func (*OrchestratorActor) SetSecretNAT ¶
func (o *OrchestratorActor) SetSecretNAT(s secretnat.SessionHandle)
SetSecretNAT installs the SecretNAT session for this run. Pass nil to disable. The provider decorator (secretnat.Wrap) handles the HTTP boundary; these hooks handle the tool seam and KV hygiene.
type OrchestratorPhase ¶
type OrchestratorPhase string
OrchestratorPhase represents the current phase of the orchestrator loop.
const ( PhasePlanning OrchestratorPhase = "planning" PhaseExecuting OrchestratorPhase = "executing" PhaseWaitingApproval OrchestratorPhase = "waiting_approval" PhaseEvaluating OrchestratorPhase = "evaluating" PhaseDone OrchestratorPhase = "done" PhaseError OrchestratorPhase = "error" PhaseCompacting OrchestratorPhase = "compacting" // PhasePaused: the run stopped without finishing (user interrupt via // double-Ctrl+C, iteration cap, or wall-clock cap) but its state is fully // preserved in session memory — "continue" resumes from this checkpoint. PhasePaused OrchestratorPhase = "paused" // PhaseGrounding: the run is reading the codebase like a human before // acting — only read-only exploration tools are allowed until the model // calls grounding_report{understood: true}. See grounding.go. PhaseGrounding OrchestratorPhase = "grounding" )
type PatchTarget ¶
PatchTarget is one file touched by a unified diff.
func ParsePatchTargets ¶
func ParsePatchTargets(patch string) ([]PatchTarget, error)
ParsePatchTargets scans a unified diff and returns the set of files it touches with their classification. Both standard `a/`/`b/` prefixes and `--no-prefix` form are accepted. Lines that don't match a header pair are ignored — this is a *best-effort* extractor; the executor remains responsible for validating that the patch actually applies.
Behavior:
- "--- a/x" + "+++ b/x" → modify(x)
- "--- /dev/null" + "+++ b/x" → create(x)
- "--- a/x" + "+++ /dev/null" → delete(x)
- any other pairing → skipped
Returns an empty slice (not nil) when the patch is empty or has no recognised headers. Errors are reserved for genuine parse failures (e.g. a `+++` with no preceding `---`).
type PermDecision ¶
type PermDecision int
PermDecision is one of the three policy outcomes.
const ( PermAsk PermDecision = iota // default: fall through to the existing approval flow PermAllow // bypass the approval prompt, run the tool PermDeny // refuse the tool call with a structured error )
type PermissionPolicy ¶
type PermissionPolicy struct {
Rules []PermissionRule
}
PermissionPolicy is an ordered list of rules. First match wins; if no rule matches, the default decision is PermAsk.
func NewPermissionPolicy ¶
func NewPermissionPolicy(rules []PermissionRule) *PermissionPolicy
NewPermissionPolicy constructs a policy from a slice of rules. The caller's slice is COPIED so later mutations of the source don't surprise the orchestrator.
func (*PermissionPolicy) Decide ¶
func (p *PermissionPolicy) Decide(toolName string, params json.RawMessage) PermDecision
Decide returns the decision for a tool call. params is the raw JSON the model emitted; the policy extracts a primary string field (see permissionPath) to glob against. Empty / nil policy returns PermAsk.
type PermissionRule ¶
type PermissionRule struct {
Tool string // tool name (exact); "" matches any tool
Match string // glob over the primary path/command; "" matches any
Decision PermDecision // allow / deny / ask
}
PermissionRule is a single match-and-decide entry. Tool must match exactly; Match is a doublestar-friendly glob over the tool's primary path / command argument (interpreted via PathMatch below). An empty Match means "any argument" so a tool-only rule applies broadly.
type PolicyDecision ¶
type PolicyDecision int
PolicyDecision is the outcome of consulting the approval policy for one call.
const ( // PolicyDefault — no rule matched; fall back to the tool's own classifier. PolicyDefault PolicyDecision = iota // PolicyAutoApprove — a permissive rule matched; skip approval. PolicyAutoApprove // PolicyGate — a restrictive rule matched; force approval. This OVERRIDES // the per-session "approve all like this" registry and headless // auto-approve, so an unattended humanoid still gates a policy-gated call. PolicyGate )
type ReadTracker ¶
type ReadTracker struct {
// contains filtered or unexported fields
}
ReadTracker remembers which files were read during an orchestrator run. Goroutine-safe: ALL access goes through ReadTracker methods, and the orchestrator's runLoop is single-threaded so no external locking is required (consistent with the rest of the orchestrator state).
func NewReadTracker ¶
func NewReadTracker() *ReadTracker
NewReadTracker returns an empty tracker. Each orchestrator creates one at construction time.
func (*ReadTracker) Forget ¶
func (t *ReadTracker) Forget(path string)
Forget removes a path from the tracker (e.g. after a write to a brand-new location). Useful when a tool re-creates a file from scratch.
func (*ReadTracker) Record ¶
func (t *ReadTracker) Record(path string)
Record stamps the current state of path into the tracker. Called after a successful file_read. Errors are silently ignored — the tracker is best- effort; a record we couldn't capture just means no stale-edit check will fire on that path until the next successful read.
func (*ReadTracker) Refresh ¶
func (t *ReadTracker) Refresh(path string)
Refresh re-stamps a path. Called after a successful edit so the next edit on the same file doesn't immediately re-fire the stale check.
type SessionMemory ¶
type SessionMemory struct {
// contains filtered or unexported fields
}
SessionMemory is the single owner of a user session's conversational state for one LLM execution actor (pane, agent, or humanoid). It consolidates what used to be scattered across the actor: the categorized conversation turns, the pause/resume checkpoint, and throttled KV persistence.
Responsibilities:
- Turns: append / replace / bound-and-heal the categorized conversation.
- Checkpoint: a paused run (double-Ctrl+C, iteration cap, wall-clock cap) records Paused + PausedReason; "continue" resumes from the same turns.
- Persistence: the full state (turns + checkpoint) is written to JetStream KV under "<owner>.llm_session" (wrapper format, versioned) and the bare turns array is mirrored to the legacy "<owner>.llm_conversation" key so pre-existing readers (PaneActor restore) keep working.
SessionMemory is NOT goroutine-safe by design: it is owned by a single actor and only touched from that actor's Receive (proto.actor guarantees sequential processing).
func NewSessionMemory ¶
func NewSessionMemory(ownerID string, kv nats.KeyValue) *SessionMemory
NewSessionMemory creates a SessionMemory for the given owner (pane / agent / humanoid id). kv may be nil (no persistence — e.g. tests).
func (*SessionMemory) Append ¶
func (s *SessionMemory) Append(t provider.ConversationTurn)
Append appends an arbitrary turn (already categorized by the producer).
func (*SessionMemory) AppendUserTurn ¶
func (s *SessionMemory) AppendUserTurn(content string, blocks []provider.ContentBlock)
AppendUserTurn appends a categorized user turn (Category "user") stamped with the current time.
func (*SessionMemory) Bound ¶
func (s *SessionMemory) Bound()
Bound trims the window to the retained turn budget and heals both ends: the head never starts with an orphaned tool_result and the tail never ends with an unanswered tool_use (synthetic interrupted results are appended for those — required for a paused run to resume cleanly).
func (*SessionMemory) ClearPause ¶
func (s *SessionMemory) ClearPause()
ClearPause marks the session as no longer paused (a new prompt or an explicit continue arrived).
func (*SessionMemory) GroundingOverride ¶
func (s *SessionMemory) GroundingOverride() string
GroundingOverride returns the persisted ##grounding override ("" = none).
func (*SessionMemory) Len ¶
func (s *SessionMemory) Len() int
Len returns the number of retained turns.
func (*SessionMemory) Pause ¶
func (s *SessionMemory) Pause(reason string)
Pause records the resumable checkpoint: the reason the run stopped. The turns already in memory ARE the checkpoint — nothing else is needed to resume, because the orchestrator loop is stateless between LLM calls.
func (*SessionMemory) Paused ¶
func (s *SessionMemory) Paused() (bool, string)
Paused reports whether the session holds a resumable checkpoint, and why.
func (*SessionMemory) Persist ¶
func (s *SessionMemory) Persist(force bool)
Persist writes the state to KV. Writes are throttled to at most one per minWriteInterval unless force is true (shutdown / pause — moments where losing state would break resume).
func (*SessionMemory) Replace ¶
func (s *SessionMemory) Replace(turns []provider.ConversationTurn)
Replace swaps the full conversation (used when an orchestrator returns its merged transcript). Turns missing a category are back-filled from their role so pre-categorisation transcripts stay well-formed.
func (*SessionMemory) Restore ¶
func (s *SessionMemory) Restore() bool
Restore loads state from KV, preferring the wrapper format and falling back to the legacy bare-array key. Returns true when anything was restored.
The wrapper's settings (grounding override) are restored even when it holds no turns yet — an override set on a fresh pane must survive a restart. The legacy bare-array key is only consulted for turns the wrapper didn't have.
func (*SessionMemory) SetGroundingOverride ¶
func (s *SessionMemory) SetGroundingOverride(mode string)
SetGroundingOverride records (or clears, with "") the user's explicit ##grounding mode override for this pane. Persisted with the session so it survives daemon restarts.
func (*SessionMemory) Turns ¶
func (s *SessionMemory) Turns() []provider.ConversationTurn
Turns returns the current conversation window. Callers must not mutate the returned slice's backing array; use Append/Replace instead.
type SessionMemoryState ¶
type SessionMemoryState struct {
Version int `json:"version"`
Turns []provider.ConversationTurn `json:"turns"`
Paused bool `json:"paused,omitempty"`
PausedReason string `json:"paused_reason,omitempty"`
// GroundingOverride is the user's explicit ##grounding mode override
// ("" = none). Additive field: wrappers persisted before it existed
// simply restore with no override.
GroundingOverride string `json:"grounding_override,omitempty"`
UpdatedMs int64 `json:"updated_ms"`
}
SessionMemoryState is the persisted JSON wrapper.
type SubAgentParams ¶
type SubAgentParams struct {
Task string `json:"task"`
Context string `json:"context,omitempty"`
SystemPrompt string `json:"system_prompt,omitempty"`
AllowedTools []string `json:"allowed_tools,omitempty"`
}
SubAgentParams mirrors the JSON schema exposed by the sub_agent tool. It is declared in the shared package so the orchestrator can parse the model's tool input directly without a runtime dependency on rysh-cli.
func ParseSubAgentParams ¶
func ParseSubAgentParams(raw json.RawMessage) (*SubAgentParams, error)
ParseSubAgentParams decodes a sub_agent tool-call payload and validates minimum required fields.