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.
This commit is contained in:
vhaudiquet committed 2026-10-20 18:41:48 +02:00
1 parent bcadeb193c
commit 8c14ea7749
2 files changed
+50 -25

No files matched your search

+29 -25
View File
@@ -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<String, String> {
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<Bootstrap>) -> BTreeMap<String, Arc<dyn ToolOp>> {
BTreeMap::from([(
"bootstrap.save".to_owned(),
Arc::new(BootstrapSave { bootstrap }) as Arc<dyn ToolOp>,
)])
}
/// 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<Bootstrap>) -> 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
}
@@ -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