Skip to main content

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}