Skip to main content

rhema_coding_agent/
config.rs

1//! The agent's configuration: model, prompt, workspace, turn budget.
2
3use std::path::PathBuf;
4use std::sync::Arc;
5
6use rhema_agent_tools::{ShellTools, Skill};
7use rhema_core::ToolResolver;
8use rhema_core::request::ThinkingEffort;
9
10/// Finished-work notifications a turn loop injects into the history.
11///
12/// The contract is deliberately text-shaped: the driver knows nothing
13/// about what the lines announce — a registry of background jobs, an
14/// inbox, anything — and the source knows nothing about turns or
15/// history. `drain` is a query, never a mutation of record: the same
16/// lines may come back again (a crash between the append and the
17/// checkpoint only re-delivers), and the driver deduplicates within a
18/// turn.
19pub trait Steering: Send + Sync {
20    /// The lines to append to the history now, one notification per
21    /// line, in announcement order. Empty means nothing to announce.
22    fn drain(&self) -> Vec<String>;
23}
24
25/// How a frontend resolves any provider kind to a turn provider: the
26/// credential policy behind the subagent tool's cross-provider
27/// `model` argument. A `None` answer means that kind has no usable
28/// credentials here — the same shape the model picker's catalog
29/// reports. The registry's `build` does the wiring; what this closure
30/// adds is the resolved credentials themselves.
31pub type SubagentProviders =
32    Arc<dyn Fn(&str) -> Option<Arc<dyn rhema_core::ModelProvider>> + Send + Sync>;
33
34/// Everything a [`CodingAgent`](crate::CodingAgent) needs besides the
35/// provider.
36#[derive(Clone)]
37pub struct CodingAgentConfig {
38    /// Model identifier, passed through to
39    /// [`ChatParams`](rhema_core::ChatParams).
40    pub model: String,
41    /// System prompt stamped into every request. `None` uses the base
42    /// prompt — [`base_system`](Self::base_system) when set, the
43    /// built-in default otherwise; setting it *replaces* everything, it
44    /// does not append.
45    pub system: Option<String>,
46    /// The base prompt from the user's `SYSTEM.md`, a Jinja template
47    /// rendered by [`render_system`](crate::render_system) — a template
48    /// error fails the agent's construction. `None` uses the built-in
49    /// [`default_system`](crate::default_system).
50    pub base_system: Option<String>,
51    /// Root all tools are scoped to; paths that escape it are rejected
52    /// in-band to the model.
53    pub workspace: PathBuf,
54    /// Skills already collected elsewhere, for the prompt's skills list.
55    /// `None` discovers from the filesystem — the global config
56    /// directory and the workspace's `.rhema/skills`. A frontend with no
57    /// filesystem to discover from (the cloud sandbox) seeds the skill
58    /// files itself and passes the list; the model reads bodies through
59    /// the shell, so each path must resolve inside the workspace.
60    pub skills: Option<Vec<Skill>>,
61    /// Skill folder names to drop from whichever list the prompt carries
62    /// — the persisted off switch a frontend reads out of its config.
63    /// The filter runs after collection and after discovery alike, so
64    /// both supply paths honor it; an empty set (the default) changes
65    /// nothing.
66    pub disabled_skills: std::collections::BTreeSet<String>,
67    /// Cap on steps per turn; a turn still requesting tools past it fails
68    /// with `AgentError::MaxIterations`.
69    pub max_turn_steps: u32,
70    /// How much reasoning the model should do; providers without a knob
71    /// ignore it, and [`ThinkingEffort::Default`] sends nothing.
72    pub thinking: ThinkingEffort,
73    /// The shell resolver the agent's tools run on. `None` builds one
74    /// over `workspace` — the default. A frontend that shares its shell
75    /// with an extension (the subagents extension reads the turn's
76    /// approval hook off it) brings its own.
77    pub tools: Option<Arc<ShellTools>>,
78    /// An extension resolver merged over the shell's tools through
79    /// [`Overlay`](rhema_agent_tools::Overlay): its specs join the
80    /// prompt's tool list and its tools resolve by name, shadowing a
81    /// built-in of the same name. `None` runs the built-ins alone.
82    pub extra_resolver: Option<Arc<dyn ToolResolver>>,
83    /// The transcript: a bounded request view over the unbounded
84    /// session, plus the `zoom` tool to read back through it. On by
85    /// default and below the budget a pass-through, so small sessions
86    /// are byte-identical to a transcript-less run. `None` is the off
87    /// switch: requests carry the whole history. See
88    /// `specs/rhema-transcript.md`.
89    pub transcript: Option<rhema_transcript::TranscriptSettings>,
90    /// The compaction's model fill: a cheap worker the transcript's
91    /// stepped compaction runs at flushes, rewriting each new block
92    /// section's extractive stand-in into a dense markdown block. The
93    /// worker never blocks or fails a request — a failed call leaves
94    /// the stand-in in place. `None` keeps the compaction extractive:
95    /// still stepped, still cache-stable, only less dense. See
96    /// `specs/rhema-transcript.md`.
97    pub summary_worker: Option<Arc<dyn rhema_transcript::Summarizer>>,
98    /// Extra environment the shell tool's commands run with, layered
99    /// over the inherited one — how a frontend provisions its agent's
100    /// client tools (a daemon URL, a scoped token, a PATH addition)
101    /// without touching the process environment.
102    pub shell_env: std::collections::BTreeMap<String, String>,
103    /// A steering source the turn loop drains: one User-role message per
104    /// drain lands in the history at a step boundary, so work that
105    /// finished outside the turn (a background subagent) is announced to
106    /// the model. `None` — the default — runs the turn exactly as
107    /// before: the loop is the provided `run_turn` with no injection.
108    pub steering: Option<Arc<dyn Steering>>,
109    /// The wired MCP servers, as the prompt's `mcp` section carries
110    /// them — the lines a frontend's wire step produced (spec:
111    /// `specs/rhema-mcp.md` §10). Empty renders no section: a
112    /// frontend that wires nothing advertises nothing.
113    pub mcp_servers: Vec<rhema_agent_tools::prompt::McpServerLine>,
114    /// MCP server names to drop from the prompt's section — the
115    /// persisted off switch a frontend reads out of its stored
116    /// settings, the [`disabled_skills`](Self::disabled_skills)
117    /// mechanism. Applied at stamp time over
118    /// [`mcp_servers`](Self::mcp_servers) by config name; the wire
119    /// step filters the same set before anything spawns.
120    pub disabled_mcp_servers: std::collections::BTreeSet<String>,
121    /// Subagent definition folder names the subagent wire drops from
122    /// discovery — the persisted off switch a frontend reads out of
123    /// its stored settings, the [`disabled_skills`](Self::disabled_skills)
124    /// mechanism. The filter runs before the catalog builds, so a
125    /// disabled name is in neither the prompt's tool list nor the
126    /// tools array; the built-in `general-purpose` answers to its name
127    /// like any other, and a set holding every discovered name leaves
128    /// no catalog, which drops the tool whole.
129    pub disabled_subagents: std::collections::BTreeSet<String>,
130    /// Resolves a provider kind to a turn provider, for the subagent
131    /// tool's `model` argument (`<provider>/<model>`) and the same
132    /// spelling in a definition's `model` frontmatter: a subagent can
133    /// run on another logged-in provider while the session stays put.
134    /// The wire hands it to the tool; `None` means the session's own
135    /// provider is the only one, and cross-provider requests fail with
136    /// that named. Credential resolution stays with the frontend —
137    /// this carries the answers, not the policy.
138    pub subagent_providers: Option<SubagentProviders>,
139    /// The environment section rendered into the system prompt: machine
140    /// facts probed where commands run (see the [`environment`
141    /// module](crate::environment)). `None` renders the section empty.
142    /// A verbatim `system` prompt ignores it, like every template
143    /// section.
144    pub environment: Option<String>,
145    /// The user-preferences section rendered into the system prompt —
146    /// the contents of the user's global `USER.md`, verbatim, a
147    /// frontend that has one reads it and passes it here. `None`
148    /// renders nothing.
149    pub user_preferences: Option<String>,
150    /// The prompt section documenting the wired
151    /// [`vision`](rhema_agent_tools::vision) macro — the `see` bash
152    /// function's documentation, produced by the frontend's
153    /// provisioning and passed verbatim. `None` renders no section: a
154    /// frontend that wired nothing advertises nothing, the same rule
155    /// the `mcp_servers` section follows.
156    pub vision: Option<String>,
157    /// The directory conventions skill discovery reads when
158    /// [`skills`](Self::skills) is `None`: the user-wide skills directory
159    /// and the cross-agent `.agents/skills` convention. `None` — the
160    /// default — discovers the reference layout
161    /// ([`Home::discover`](rhema_config::Home::discover)); a frontend
162    /// that owns its own directory hands a relocated `Home` instead.
163    pub home: Option<rhema_config::Home>,
164}
165
166impl std::fmt::Debug for CodingAgentConfig {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        // The resolvers are trait objects and stay opaque: their
169        // presence is the debug-worthy fact, not their internals.
170        f.debug_struct("CodingAgentConfig")
171            .field("model", &self.model)
172            .field("system", &self.system)
173            .field("base_system", &self.base_system)
174            .field("workspace", &self.workspace)
175            .field("skills", &self.skills.is_some())
176            .field("max_turn_steps", &self.max_turn_steps)
177            .field("thinking", &self.thinking)
178            .field("tools", &self.tools.is_some())
179            .field("extra_resolver", &self.extra_resolver.is_some())
180            .field("transcript", &self.transcript.is_some())
181            .field("summary_worker", &self.summary_worker.is_some())
182            .field("steering", &self.steering.is_some())
183            .field("shell_env", &self.shell_env.len())
184            .field("environment", &self.environment.is_some())
185            .field("user_preferences", &self.user_preferences.is_some())
186            .field("home", &self.home.is_some())
187            .finish()
188    }
189}
190
191impl CodingAgentConfig {
192    /// Default for `max_turn_steps`. Long autonomous sessions — builds,
193    /// rebases, multi-package sweeps — routinely run hundreds of steps on
194    /// cheap models under a watching eye; the cap exists to end a runaway
195    /// loop, not to ration the model. `--max-steps` is the tighter leash
196    /// for unattended turns on expensive models.
197    pub const DEFAULT_MAX_TURN_STEPS: u32 = 2048;
198
199    /// Config naming `model`, rooted at `workspace`, defaults elsewhere.
200    pub fn new(model: impl Into<String>, workspace: impl Into<PathBuf>) -> Self {
201        Self {
202            model: model.into(),
203            system: None,
204            base_system: None,
205            workspace: workspace.into(),
206            skills: None,
207            disabled_skills: std::collections::BTreeSet::new(),
208            max_turn_steps: Self::DEFAULT_MAX_TURN_STEPS,
209            thinking: ThinkingEffort::Default,
210            tools: None,
211            extra_resolver: None,
212            transcript: Some(rhema_transcript::TranscriptSettings::default()),
213            summary_worker: None,
214            steering: None,
215            shell_env: std::collections::BTreeMap::new(),
216            mcp_servers: Vec::new(),
217            disabled_mcp_servers: std::collections::BTreeSet::new(),
218            disabled_subagents: std::collections::BTreeSet::new(),
219            subagent_providers: None,
220            environment: None,
221            user_preferences: None,
222            vision: None,
223            home: None,
224        }
225    }
226}