rig_harness/lib.rs
1//! rig-harness: the harness of the rig coding agent, a Bevy app on the
2//! [`rig_ecs`] kernel. It holds what the binary needs to run and be
3//! relaunched (the session, the `rig` launcher protocol, how the agent was
4//! started: [`Invoked`](host::launcher::Invoked)) and the services every
5//! plugin shares. Everything else, the terminal view, `--print`, `/reload`
6//! and the built-in tools included, is a plugin crate in the repository's
7//! `plugins/` folder, built only on this crate's public API.
8//!
9//! [`HeadlessPlugins`] is what a terminal app takes from Bevy, and
10//! [`RigHarnessPlugins`] is the harness. The launcher's generated `main.rs`
11//! adds the plugins `plugins.toml` lists with [`load`]. The error handler is
12//! the application's to set:
13//!
14//! ```no_run
15//! use rig_harness::load;
16//! use rig_harness::prelude::*;
17//!
18//! #[derive(Default)]
19//! struct HelloPlugin;
20//!
21//! impl Plugin for HelloPlugin {
22//! fn build(&self, _app: &mut App) {}
23//! }
24//!
25//! fn main() -> AppExit {
26//! let mut app = App::new();
27//! app.set_error_handler(rig_harness::error::warn)
28//! .add_plugins((HeadlessPlugins, RigHarnessPlugins));
29//! load::<HelloPlugin>(&mut app, "rig-hello", "path plugins/hello");
30//! app.run()
31//! }
32//! ```
33//!
34//! [`plugin_guide`] is the guide to writing a plugin; `rig plugin new <name>`
35//! makes one.
36
37#[doc = include_str!("../PLUGINS.md")]
38pub mod plugin_guide {}
39
40/// Where the plugin guide ([`plugin_guide`]) is on disk, for an agent
41/// that reads it: in this crate's own source, from a checkout or from
42/// crates.io alike.
43pub const PLUGIN_GUIDE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/PLUGINS.md");
44pub mod agent_title;
45pub mod ask;
46pub mod clock;
47pub mod commands;
48pub mod describe;
49pub mod host;
50mod load;
51pub mod main_agent;
52pub mod per_call;
53pub mod pick;
54pub mod plugin_data;
55pub mod progress;
56pub mod secrets;
57pub mod services;
58pub mod settings;
59pub mod successor;
60pub mod timings;
61pub mod watch;
62pub mod workspace;
63
64pub use describe::run;
65pub use load::{
66 AppKeepsExt, Keeps, PluginEntity, PluginSource, PluginSystems, ProvidedBy, Provides, SavedAs,
67 check_stages_at_start, load, stage_conflicts,
68};
69
70use bevy::MinimalPlugins;
71use bevy::diagnostic::FrameCountPlugin;
72use bevy::time::TimePlugin;
73use bevy_app::{
74 Plugin, PluginGroup, PluginGroupBuilder, ScheduleRunnerPlugin, TaskPoolOptions, TaskPoolPlugin,
75 TaskPoolThreadAssignmentPolicy,
76};
77use bevy_log::LogPlugin;
78
79pub use bevy_app::{App, AppExit};
80/// Bevy's error handlers, for [`App::set_error_handler`]: `warn` logs a
81/// failing system, observer or command instead of stopping the app.
82pub use bevy_ecs::error;
83/// The rig-core this app is built on, for the conversation's messages
84/// (`rig_harness::rig_core::message::Message`) and rig-core's tools.
85pub use rig_core;
86/// The kernel this app is built on.
87pub use rig_ecs;
88/// The `rig` launcher protocol: the `RIG_HOME` layout, sessions, and the
89/// environment and exit codes the launcher and the agent share.
90pub use rig_harness_protocol as harness_protocol;
91/// The store this app keeps its sessions in, for reading and saving a
92/// session without restoring it (`rig_harness::rig_memory::DirStore`,
93/// `load`, `save`) and for compaction's pure functions.
94pub use rig_memory;
95/// The tool library this app is built on, for the process runner
96/// [`Processes`](services::Processes) hands out
97/// (`rig_harness::rig_tools::process::Spawn`) and the file tools' rules
98/// (`rig_harness::rig_tools::fs::place`).
99pub use rig_tools;
100
101/// What a plugin needs, so a typical one imports only this: the kernel's
102/// prelude and the harness's services. A plugin crate's own types come from
103/// that crate, such as the terminal view's panels from `rig-tui`.
104pub mod prelude {
105 pub use rig_ecs::prelude::*;
106
107 pub use std::time::Duration;
108
109 pub use bevy_app::OnAppExitSystems;
110
111 pub use crate::agent_title::AgentTitle;
112 pub use crate::ask::{Answer, Answerer, Answerers, Ask, AskSystems, Choice};
113 pub use crate::clock::{Due, KeepAwake, WakeAt};
114 pub use crate::commands::{
115 AppCommandsExt, CommandArgs, CommandSystems, RunCommand, SlashCommand,
116 };
117 pub use crate::harness_protocol::{Home, StartReason};
118 pub use crate::host::cli::{AppCliExt, Flag, Front};
119 pub use crate::host::exit::{Exit, ExitReason, ExitRequest};
120 pub use crate::host::launcher::{self, Invoked};
121 pub use crate::host::session::{
122 AppStart, DeleteSession, Logged, LoggedQueue, Scratch, SessionDeleteFailed, SessionDeleted,
123 SessionPaths, StoreOpener, edit_stopped, rewrite_stopped,
124 };
125 pub use crate::main_agent::{MainAgentQuery, ShownAgent, main_agent};
126 pub use crate::per_call::{AppPerCallExt, CallPlace, PerCallTools};
127 pub use crate::pick::{Chosen, Highlighted, Pick, PickItem, PickPreview, Picked, Picker};
128 pub use crate::plugin_data::PluginData;
129 pub use crate::progress::{CallOutput, ToolProgress};
130 pub use crate::secrets::{Secret, Secrets};
131 pub use crate::services::{Processes, ProjectRoot, RigHome, TokioRuntime, WorkDir};
132 pub use crate::settings::{AppSettingsExt, Settings};
133 pub use crate::successor::resend;
134 pub use crate::timings::{PluginTimings, SystemTimings};
135 pub use crate::watch::{FileChanges, FileWatch};
136 pub use crate::workspace::ProjectWorkspace;
137 pub use crate::{
138 AppKeepsExt, HeadlessPlugins, Keeps, PluginEntity, PluginSource, PluginSystems, ProvidedBy,
139 Provides, RigHarnessPlugins, SavedAs,
140 };
141 pub use rig_core::conversation::Note;
142 pub use rig_tools::attach::{first_typed, typed};
143 pub use rig_tools::{Spill, blocking};
144}
145
146/// The harness: the session and its log, the kernel
147/// ([`rig_ecs::AgentPlugin`], [`rig_ecs::session_log::SessionLogPlugin`]),
148/// the launcher protocol, and the services every agent uses: slash
149/// commands, the clock the loop sleeps by, file watches, the questions and
150/// pickers every front answers, and tool progress. Everything else is a
151/// plugin `plugins.toml` lists, which can leave it out.
152pub struct RigHarnessPlugins;
153
154impl PluginGroup for RigHarnessPlugins {
155 fn build(self) -> PluginGroupBuilder {
156 PluginGroupBuilder::start::<Self>()
157 .add(services::ServicesPlugin)
158 .add(host::session::SessionPlugin)
159 .add(rig_ecs::AgentPlugin)
160 .add(rig_ecs::session_log::SessionLogPlugin)
161 .add(commands::CommandPlugin)
162 .add(host::launcher::LauncherPlugin)
163 .add(host::exit::ExitPlugin)
164 .add(clock::ClockPlugin)
165 .add(watch::WatchPlugin)
166 .add(ask::AskPlugin)
167 .add(pick::PickPlugin)
168 .add(progress::ProgressPlugin)
169 }
170}
171
172/// What a terminal app takes from Bevy where a windowed one has
173/// `DefaultPlugins`: Bevy's `MinimalPlugins` with task pools sized for an
174/// agent and the frame count and clock, the log written to the session
175/// with its warnings and errors passed on as data, a clean exit on SIGINT,
176/// SIGTERM and SIGHUP, and in place of Bevy's `ScheduleRunnerPlugin` a
177/// loop that runs the schedules on the main thread and sleeps until
178/// [`Wake`](rig_ecs::tasks::Wake)d or until the clock's next deadline.
179/// Added before [`RigHarnessPlugins`], so the kernel runs on its clock;
180/// a windowing plugin added later through [`windowed`] sets its own runner
181/// in place of this loop (see [`plugin_guide`]).
182pub struct HeadlessPlugins;
183
184impl PluginGroup for HeadlessPlugins {
185 fn build(self) -> PluginGroupBuilder {
186 MinimalPlugins
187 .build()
188 .set(TaskPoolPlugin {
189 task_pool_options: task_pools(),
190 })
191 .disable::<ScheduleRunnerPlugin>()
192 .add(LogPlugin {
193 custom_layer: host::session::log_events,
194 fmt_layer: host::session::log_layer,
195 ..LogPlugin::default()
196 })
197 .add(host::signals::ExitOnSignalPlugin)
198 .add(host::runner::RunnerPlugin)
199 }
200}
201
202/// `plugins`, such as Bevy's `DefaultPlugins`, without what
203/// [`HeadlessPlugins`] already sets: the log, the task pools, the frame
204/// count, the clock, the signal handler and the loop. Its windowing plugin
205/// then runs the loop; [`plugin_guide`] shows how a window plugin wakes it.
206pub fn windowed(plugins: impl PluginGroup) -> PluginGroupBuilder {
207 let plugins = without::<LogPlugin>(plugins.build());
208 let plugins = without::<TaskPoolPlugin>(plugins);
209 let plugins = without::<FrameCountPlugin>(plugins);
210 let plugins = without::<TimePlugin>(plugins);
211 let plugins = without::<ScheduleRunnerPlugin>(plugins);
212 #[cfg(any(unix, windows))]
213 let plugins = without::<bevy_app::TerminalCtrlCHandlerPlugin>(plugins);
214 plugins
215}
216
217/// `plugins` with `P` disabled, if it has `P`.
218fn without<P: Plugin>(plugins: PluginGroupBuilder) -> PluginGroupBuilder {
219 if plugins.contains::<P>() {
220 plugins.disable::<P>()
221 } else {
222 plugins
223 }
224}
225
226/// Bevy's pools sized for an agent rather than a game. Model calls stream
227/// on the IO pool and tool calls run on the async compute pool, each with
228/// 2 to 4 threads, so a few agents' calls run side by side; blocking tool
229/// work runs on threads of its own through [`prelude::blocking`]. Bevy's
230/// own systems keep the rest of the cores. [`HeadlessPlugins`] sets them; a
231/// test app sets them too, so it runs its calls as the agent does.
232pub fn task_pools() -> TaskPoolOptions {
233 let calls = || TaskPoolThreadAssignmentPolicy {
234 min_threads: 2,
235 max_threads: 4,
236 percent: 0.25,
237 on_thread_spawn: None,
238 on_thread_destroy: None,
239 };
240 TaskPoolOptions {
241 io: calls(),
242 async_compute: calls(),
243 ..TaskPoolOptions::default()
244 }
245}