diff --git a/crates/loopd/src/consult.rs b/crates/loopd/src/consult.rs new file mode 100644 index 0000000..d2dd1ac --- /dev/null +++ b/crates/loopd/src/consult.rs @@ -0,0 +1,21 @@ +//! The `consult` tool: `brokerd` decides it and POSTs an allowlisted body (M6 spec, section 4). +//! It is not core; the parent finds it with `find_tool` and calls it through `call_tool`. + +pub const CONSULT: &str = "consult"; + +pub fn schema() -> crate::llama::ToolSchema { + crate::llama::ToolSchema { + name: CONSULT.to_string(), + description: "Ask a remote model a question. The session history is not sent. Needs a grant from the owner.".to_string(), + parameters: serde_json::json!({ + "type": "object", + "properties": { + "question": { + "type": "string", + "description": "The question to send. Nothing else of the session is included." + } + }, + "required": ["question"] + }), + } +} diff --git a/crates/loopd/src/lib.rs b/crates/loopd/src/lib.rs index d9ea4da..5ab6c61 100644 --- a/crates/loopd/src/lib.rs +++ b/crates/loopd/src/lib.rs @@ -5,6 +5,7 @@ pub mod broker_port; pub mod channel; pub mod compact; pub mod config; +pub mod consult; pub mod cron; pub mod http; pub mod jobs; diff --git a/crates/loopd/src/tools.rs b/crates/loopd/src/tools.rs index f69ccfc..4e149dc 100644 --- a/crates/loopd/src/tools.rs +++ b/crates/loopd/src/tools.rs @@ -118,6 +118,10 @@ impl Registry { schema: crate::subagent::schema(), core: false, }); + entries.push(Entry { + schema: crate::consult::schema(), + core: false, + }); Registry::new(entries) } diff --git a/crates/loopd/tests/consult_registry.rs b/crates/loopd/tests/consult_registry.rs new file mode 100644 index 0000000..a2a1c9b --- /dev/null +++ b/crates/loopd/tests/consult_registry.rs @@ -0,0 +1,58 @@ +//! `consult` in the M5c registry, not core. Do not edit. + +mod support; + +use loopd::baseline::Baseline; +use loopd::consult::{self, CONSULT}; +use loopd::tools::Registry; +use std::path::Path; +use support::Home; + +fn core_names(registry: &Registry) -> Vec { + registry + .core_schemas() + .into_iter() + .map(|s| s.name) + .collect() +} + +#[test] +fn consult_is_in_m5c_and_is_not_core() { + let r = Registry::m5c(); + let entry = r.get(CONSULT).expect("consult"); + assert!(!entry.core); + assert_eq!(entry.schema, consult::schema()); + assert_eq!( + core_names(&r), + [ + "clock", + "recall", + "memory", + "ask_memory", + "find_tool", + "call_tool" + ] + ); + assert!(r.find("consult").iter().any(|s| s.name == CONSULT)); + assert!(r.find("remote").iter().any(|s| s.name == CONSULT)); + assert!(r.get("subagent").is_some()); +} + +#[test] +fn a_baseline_from_m5c_does_not_put_consult_in_the_tools_array() { + let home = Home::new(); + let cfg = home.config(Path::new("/tmp/unused.sock")); + let baseline = Baseline::assemble(&cfg, &Registry::m5c()).unwrap(); + let names: Vec<&str> = baseline.tools.iter().map(|t| t.name.as_str()).collect(); + assert_eq!( + names, + [ + "clock", + "recall", + "memory", + "ask_memory", + "find_tool", + "call_tool" + ] + ); +} diff --git a/docs/implementer-log.md b/docs/implementer-log.md index f40eb07..6131aeb 100644 --- a/docs/implementer-log.md +++ b/docs/implementer-log.md @@ -6,6 +6,7 @@ reviewer adds findings under "Reviews" once per milestone. | Task | Date | Status | Gate runs | First gate | Deviations | Notes | Model | |---|---|---|---|---|---|---|---| +| M6b/01-loopd-consult-registry | 2026-10-03 | done | 1 | pass | none | Copied the given test `crates/loopd/tests/consult_registry.rs` from `docs/plans/M6b/files/`. Created `crates/loopd/src/consult.rs` with `CONSULT` const and `schema()` returning the `consult` `ToolSchema`. Added `pub mod consult;` to lib.rs after `pub mod config;`. In tools.rs `m5c()` pushed the non-core `consult` entry after the `subagent` entry (`Registry::new(entries)` unchanged). `cargo check -p loopd` clean, no warning; `wc -l` prints 21 for consult.rs, 481 for tools.rs, both under 500. All suites pass: `consult_registry` 2, `subagent_registry` 2, `ask_memory_tool`, `tools` 11, `baseline` 6. `make gate` printed `gate: ok` with 1592 tests (1590 before, plus 2). No new dependency, so `Cargo.lock` unchanged. | ornith-1.5-35b-a3b | | M6a/05-brokerd-state-inherit | 2026-10-03 | done | 1 | pass | none | Copied the given test `crates/brokerd/tests/state_subagent.rs` from `docs/plans/M6a/files/`. Step 2 (`cargo test -p brokerd --test state_subagent`) failed as expected: `a_new_subagent_copies_the_parents_untrusted_flag` (child was default private/trusted) and `a_nested_subagent_copies_through_a_missing_middle_child` (leaf was Private not Secret) failed, `a_plain_session_still_creates_nothing_on_read` passed. Applied I1 in state.rs `read`: replaced the `NotFound` early return with `return inherit_or_default(self, session)`. Applied I2 after `impl StateStore` (after `persist`'s and the `impl`'s closing braces, before `/// One line on disk`): added `inherit_or_default` which calls `parent_of_sub`, returns default when `None`, else `store.read(&parent)?` then `store.raise(session, SessionState::default(), Label{class: inherited.taint, untrusted: inherited.untrusted})`, and `parent_of_sub` which strips `sub-`, `rsplit_once('-')`, requires non-empty ASCII-digit suffix and builds `SessionId::new(parent)`. Did not touch `raise` or `persist`. `cargo check -p brokerd` clean, no warning; `wc -l` prints 176 for state.rs, under 500. All suites pass: `state_subagent` 3, `state` 9. `make gate` printed `gate: ok` with 1590 tests (1587 before, plus 3). No new dependency, so `Cargo.lock` unchanged. | ornith-1.5-35b-a3b | | M6a/04-loopd-scheduler-skip | 2026-10-03 | done | 1 | pass | none | Copied the given test `crates/loopd/tests/scheduler_subagent.rs` from `docs/plans/M6a/files/`. Step 2 (`cargo test -p loopd --test scheduler_subagent`) failed as expected: both tests failed, `work` not empty (a `Heartbeat` and a `Job { name: "mail", session: "job-mail-20261003t0700" }`) and `requests_to(CHAT)` not empty while the child was held. Applied H1 in channel.rs: added `holds_prefix` after `hold`'s closing `}` inside `impl Context`, locking `busy` like `hold` and returning whether any id starts with the prefix. Applied K2 in scheduler.rs `heartbeat`: after the `if !due { return; }` block and before `SessionId::new("heartbeat")`, `if ctx.holds_prefix("sub-") { return; }`. Applied K1 in scheduler.rs `tick`: immediately after `self.heartbeat(...)` and before `jobs_dir`, `if ctx.holds_prefix("sub-") { return work; }` so leftover-close on the first tick is already done. `cargo check -p loopd` clean, no warning; `wc -l` prints 307 channel.rs, 310 scheduler.rs, both under 500. All suites pass: `scheduler_subagent` 2, `scheduler_heartbeat` 6, `scheduler_cron` 6, `scheduler_heartbeat_failed` 1. `make gate` printed `gate: ok` with 1587 tests (1585 before, plus 2). No new dependency, so `Cargo.lock` unchanged. | ornith-1.5-35b-a3b | | M6a/03-loopd-subagent-spawn | 2026-10-03 | done | 1 | pass | none | Copied the given test `crates/loopd/tests/subagent_spawn.rs` from `docs/plans/M6a/files/`. Step 2 (`cargo build --tests --keep-going -p loopd`) failed as expected: E0560 `Runtime` has no field `ctx`. Added `pub ctx: Option<&'a crate::channel::Context>` to `Runtime` in turn.rs R1, filled it `Some(&*ctx)` in channel.rs C1 (live turn), `Some(ctx)` in the cron and heartbeat literals in scheduler.rs S1/S2 (both already held an immutable borrow of `ctx`), and `None` in the seven test literals: support/turn.rs, compact_turn.rs (the helper and the inline one), session_slot.rs, end_to_end.rs, turn_broker.rs, tool_summary_call.rs. Replaced the stub `run` in subagent.rs with the full spawn: build `sub--` from the already-consumed `call_id.0`, overflow returns `denial_message(InvalidArguments, Some(...))` naming subagent-id without holding, then `rt.ctx` `None` and `hold Busy` and block/baseline/create errors each return `failed`, a `Create` `Exists` falls back to `open`, the child runs one `run_turn` plus `finish_turn` on `slots.background`, memory is updated, `untrusted` is read before the hold drops, and the result is `{id} completed\n{summary}` or `{id} failed`, all `Public`. Added `RUNBOOK` and the use lines the body needs. Appended `## subagent-id` to docs/runbook.md via the task's `{ echo; cat ...; }` (grep -c prints 1). `cargo check -p loopd` clean, no warning; `wc -l` prints 185 subagent.rs, 335 turn.rs, 301 channel.rs, 304 scheduler.rs, all under 500. All suites pass: `subagent_spawn` 5, `subagent_dispatch` 3, `subagent_registry` 2, `session_slot` 4, `tool_summary_call` 6, `compact_turn`, `channel`. `make gate` printed `gate: ok` with 1585 tests (1580 before, plus 5). No new dependency, so `Cargo.lock` unchanged. | ornith-1.5-35b-a3b |