From 8c14ea7749f540a6322a1415f4b70580550fc514 Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Tue, 20 Oct 2026 18:29:48 +0200 Subject: [PATCH] feat(claw): move the bootstrap tool spec into a TOOL.yaml manifest bootstrap_save now loads its spec from an embedded manifest, one folder under crates/rhema-claw/tools; the Rust side keeps only the invoke half, behind the run.builtin id. Drift tests follow in the registry's own suite shape once the family grows; for now the registry asserts the manifest loads, and the render tests exercise the manifest-served spec through the roster. --- crates/rhema-claw/src/bootstrap.rs | 54 ++++++++++--------- .../rhema-claw/tools/bootstrap_save/TOOL.yaml | 21 ++++++++ 2 files changed, 50 insertions(+), 25 deletions(-) create mode 100644 crates/rhema-claw/tools/bootstrap_save/TOOL.yaml diff --git a/crates/rhema-claw/src/bootstrap.rs b/crates/rhema-claw/src/bootstrap.rs index c1e8174..438977d 100644 --- a/crates/rhema-claw/src/bootstrap.rs +++ b/crates/rhema-claw/src/bootstrap.rs @@ -22,11 +22,12 @@ //! section renders only while either file is unwritten, so the offer //! self-terminates. +use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::sync::Arc; use rhema_agent_tools::render_template; -use rhema_autonomous_agent::{Tool, ToolRegistry, async_trait}; +use rhema_autonomous_agent::{ToolOp, ToolRegistry, async_trait}; use rhema_core::ToolSpec; use serde_json::{Value, json}; @@ -251,28 +252,7 @@ pub struct BootstrapSave { } #[async_trait] -impl Tool for BootstrapSave { - fn spec(&self) -> ToolSpec { - ToolSpec::new( - BOOTSTRAP_SAVE, - "Write one of your identity files. `kind` is `soul` (who you \ - are: name, tone, purpose) or `user` (who the user is). \ - Overwrites the file, so send the whole document every time. \ - Both files enter every request: keep each one small, \ - stable, and high-signal.", - json!({ - "type": "object", - "properties": { - "kind": { "type": "string", "enum": ["soul", "user"], - "description": "Which file to write: soul or user." }, - "content": { "type": "string", - "description": "The file's full markdown contents." } - }, - "required": ["kind", "content"] - }), - ) - } - +impl ToolOp for BootstrapSave { async fn invoke(&self, args: Value) -> Result { let args: SaveArgs = serde_json::from_value(args).map_err(|err| format!("bad arguments: {err}"))?; @@ -285,11 +265,35 @@ impl Tool for BootstrapSave { } } +/// The compiled operation behind the manifest, by `run.builtin` id. +fn builtins(bootstrap: Arc) -> BTreeMap> { + BTreeMap::from([( + "bootstrap.save".to_owned(), + Arc::new(BootstrapSave { bootstrap }) as Arc, + )]) +} + +/// The `TOOL.yaml` of the bootstrap tool, embedded so the daemon ships +/// it with no filesystem layout. +pub const MANIFESTS: &[(&str, &str)] = &[( + "bootstrap_save", + include_str!("../tools/bootstrap_save/TOOL.yaml"), +)]; + /// The bootstrap tool set over `store`: `bootstrap_save`, the shape the -/// interactive resolver chain composes. +/// interactive resolver chain composes — its spec from the embedded +/// manifest, the operation compiled here. pub fn registry(bootstrap: Arc) -> ToolRegistry { + let (tools, skipped) = + rhema_autonomous_agent::manifest::builtin_tools(MANIFESTS, &builtins(bootstrap)); + assert!( + skipped.is_empty(), + "the bootstrap tool manifest must load: {skipped:?}" + ); let mut registry = ToolRegistry::new(); - registry.register(Arc::new(BootstrapSave { bootstrap })); + for tool in tools { + registry.register(tool); + } registry } diff --git a/crates/rhema-claw/tools/bootstrap_save/TOOL.yaml b/crates/rhema-claw/tools/bootstrap_save/TOOL.yaml new file mode 100644 index 0000000..ed628da --- /dev/null +++ b/crates/rhema-claw/tools/bootstrap_save/TOOL.yaml @@ -0,0 +1,21 @@ +# The model-facing spec of the bootstrap_save tool; the operation is the +# compiled builtin named under `run`. +name: bootstrap_save +description: >- + Write one of your identity files. `kind` is `soul` (who you are: + name, tone, purpose) or `user` (who the user is). Overwrites the + file, so send the whole document every time. Both files enter every + request: keep each one small, stable, and high-signal. +parameters: + type: object + properties: + kind: + type: string + enum: [soul, user] + description: "Which file to write: soul or user." + content: + type: string + description: The file's full markdown contents. + required: [kind, content] +run: + builtin: bootstrap.save