mirror of
https://github.com/block/buzz.git
synced 2026-11-06 01:11:33 +00:00
Closes the last behavioural gap. buzz-agent's most load-bearing non-standard behaviour -- the agent may not end its turn while its todo list has open items -- now works, with no changes to goose. Why not goose's hook system. Goose has a blocking Stop hook with exactly these semantics (agent.rs:2891-2917), but it is unreachable: hook_manager is private with a #[cfg(test)] setter, and hooks are otherwise discovered as subprocesses from <root>/.goose/plugins/*/hooks/hooks.json. The subprocess route looked viable until you notice where the answer lives -- buzz-dev-mcp's todo list is in-process state (todo.rs:49, a Mutex<Vec<Item>>) and that process's stdio is owned by goose. A hook binary goose spawns cannot see it, so a materialised hooks.json would produce a hook that always answers "no objection": worse than no hook, because it looks like it works. Instead we own the outer loop, so we ask the tool ourselves. Agent::dispatch_tool_call is public (agent.rs:1059). Between rounds we call _Stop on the same extension the model uses; on objection we re-enter reply() with the objection as an agent-visible/user-invisible message. Capped at 3 consecutive vetoes, mirroring goose's own stop_hook_block_cap. _PostCompact is wired to AgentEvent::HistoryReplaced and re-injects via steer(), which goose drains at the round boundary (agent.rs:1951-1974). Extension name is discovered by "___Stop" suffix rather than hardcoded -- buzz-acp derives it from the MCP binary's file stem (buzz-acp/src/lib.rs:4145), so it is not a fixed string. KNOWN DEVIATION: buzz-agent hid _-prefixed tools from the model (agent.rs:328-336) while still calling them itself. Goose's available_tools allowlist gates advertising and dispatch through the same cache (extension_manager.rs:1421, :1698), so hiding them would make them undispatchable and break the veto. They stay visible; a system-prompt extension tells the model not to call them. 4 new tests against the real fake-mcp binary (copied from crates/buzz-agent) counting provider generations: 2 objections => 3 calls, permanent objection capped at 4, no hook => 1 call, and discovery under a non-obvious extension name. 21 tests green. Signed-off-by: Michael Neale <michael.neale@gmail.com>
79 lines
3.1 KiB
TOML
79 lines
3.1 KiB
TOML
# buzz-agent-core — buzz-agent's ACP wire layer on top of the Goose Rust
|
|
# library (used as an SDK, not as a subprocess).
|
|
#
|
|
# WHY A SEPARATE CRATE, EXCLUDED FROM THE WORKSPACE
|
|
#
|
|
# `crates/buzz-agent` is a *library* linked into both `crates/sprig` and
|
|
# `desktop/src-tauri` (which uses only `catalog`/`config` for the Databricks
|
|
# model picker). Adding goose to that crate would pull goose's ~700-crate
|
|
# dependency graph into the Tauri app and into the workspace lockfile.
|
|
#
|
|
# So this crate is excluded from the workspace (see the root Cargo.toml
|
|
# `exclude` list) and carries its own Cargo.lock, exactly as PR #1526 did for
|
|
# `goose-acp`. Goose's ACP/MCP dependency versions move independently.
|
|
#
|
|
# Build: cargo build --release --manifest-path buzz-agent-core/Cargo.toml
|
|
#
|
|
# THIS IS NOT "GOOSE AS THE HARNESS". The binary keeps buzz-agent's identity
|
|
# (`agentInfo.name = "buzz-agent"`), its 6-method ACP surface, its
|
|
# `activeRunId` steering contract and its `usage_update` ordering. Only the
|
|
# agent loop underneath is Goose's instead of hand-written. Picking "Goose"
|
|
# from the harness gallery remains a separate thing: that shells out to a
|
|
# user-installed `goose` CLI.
|
|
|
|
[package]
|
|
name = "buzz-agent-core"
|
|
version = "0.1.0"
|
|
edition = "2021"
|
|
publish = false
|
|
|
|
[lib]
|
|
name = "buzz_agent_core"
|
|
path = "src/lib.rs"
|
|
|
|
[[bin]]
|
|
name = "buzz-agent-core"
|
|
path = "src/main.rs"
|
|
|
|
# Test-only fake MCP server, copied from crates/buzz-agent. Built
|
|
# unconditionally because cargo can't gate bins on `cfg(test)`.
|
|
[[bin]]
|
|
name = "fake-mcp"
|
|
path = "tests/bin/fake_mcp.rs"
|
|
|
|
[dependencies]
|
|
# Pinned to a release commit deliberately: goose's public `Agent` surface is
|
|
# not semver-stable (many fields are `pub(super)`) and its MSRV moves quickly.
|
|
# 305849b71 == goose 1.44.0.
|
|
#
|
|
# default-features = false drops: telemetry, otel, aws-providers,
|
|
# local-inference (candle/whisper), code-mode (pctx/V8), system-keyring, nostr.
|
|
# We name only rustls-tls, which is mandatory — `goose` pulls reqwest/sqlx/rmcp
|
|
# with default-features = false, so a TLS backend only arrives via this feature
|
|
# (and `rustls-tls` + `native-tls` are mutually exclusive: see the
|
|
# compile_error! at goose/src/lib.rs:3-4).
|
|
goose = { git = "https://github.com/aaif-goose/goose", rev = "305849b71709b95b86ed9f11bd3bc939899c0aab", default-features = false, features = ["rustls-tls"] }
|
|
goose-provider-types = { git = "https://github.com/aaif-goose/goose", rev = "305849b71709b95b86ed9f11bd3bc939899c0aab" }
|
|
|
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-std", "io-util", "sync", "time"] }
|
|
tokio-util = { version = "0.7", features = ["rt"] }
|
|
futures = "0.3"
|
|
serde = { version = "1", features = ["derive"] }
|
|
serde_json = "1"
|
|
anyhow = "1"
|
|
tracing = "0.1"
|
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|
rmcp = { version = "1.8", default-features = false, features = ["schemars"] }
|
|
|
|
[dev-dependencies]
|
|
tempfile = "3"
|
|
|
|
# Size-optimized. PR #1526 measured 32.5 MiB (macOS arm64) for a comparable
|
|
# goose-linked binary; re-measure rather than trusting that number.
|
|
[profile.release]
|
|
opt-level = "z"
|
|
lto = "fat"
|
|
codegen-units = 1
|
|
panic = "abort"
|
|
strip = true
|