chore(buzz-agent): bump goose pin to bf332b9 (unrolled loop) and fix rmcp 3.x breaks

Moves the goose git pin from 305849b to bf332b9, which is past ca52cce
(#9574, the unrolled agent loop). Consequences of the bump:

- goose now uses rmcp 3.x; buzz-agent's own rmcp dep moves 1 -> 3 so the
  two do not resolve to different crate versions of the same types.
- rmcp 3 renames Content -> ContentBlock and adds fields to
  ListToolsResult; builtin_client.rs updated to match.
- buzz-model-catalog needs an explicit dirs dep after the merge.
- TurnTotalState / PricingIdentity, added on main while this branch was
  away, ported back into types.rs for wire.rs's usage_update_payload.

cargo check -p buzz-agent passes.

Co-authored-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
Michael Neale
2026-08-11 14:14:14 +10:00
parent d35f7042a5
commit 80a9f9a360
5 changed files with 1157 additions and 155 deletions

1192
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -29,8 +29,8 @@ path = "tests/bin/fake_mcp.rs"
# agent loop itself is goose's; this is the interactive PKCE login that seeds
# the token cache that goose (and the desktop model picker) then read.
buzz-model-catalog = { path = "../buzz-model-catalog" }
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" }
goose = { git = "https://github.com/aaif-goose/goose", rev = "bf332b983756eaf80353e82e4bb052d589629bf4", default-features = false, features = ["rustls-tls"] }
goose-provider-types = { git = "https://github.com/aaif-goose/goose", rev = "bf332b983756eaf80353e82e4bb052d589629bf4" }
# Resolution guard: goose pins icu_locale "=2.1.1" (needs icu_collections
# ~2.1.1) while url 2.5.x -> idna -> idna_adapter 1.2.2 pulls icu_normalizer
# 2.2.0 (needs icu_collections ~2.2.0). Only one 2.x icu_collections can be
@@ -46,7 +46,7 @@ serde = { workspace = true }
serde_json = { workspace = true }
serde_yaml = { workspace = true }
reqwest = { workspace = true, features = ["json", "rustls", "form"] }
rmcp = { version = "1", default-features = false, features = ["client", "transport-child-process"] }
rmcp = { version = "3", default-features = false, features = ["client", "transport-child-process"] }
arc-swap = "1"
getrandom = "0.4"
tracing = { workspace = true }

View File

@@ -25,7 +25,9 @@ use std::sync::Arc;
use async_trait::async_trait;
use goose::agents::mcp_client::McpClientTrait;
use goose::agents::{ExtensionConfig, ToolCallContext};
use rmcp::model::{CallToolResult, Content, InitializeResult, JsonObject, ListToolsResult, Tool};
use rmcp::model::{
CallToolResult, ContentBlock, InitializeResult, JsonObject, ListToolsResult, Tool,
};
use rmcp::service::ServiceError as Error;
use tokio_util::sync::CancellationToken;
@@ -79,6 +81,7 @@ impl McpClientTrait for BuiltinClient {
tools: vec![Self::load_skill_tool()],
next_cursor: None,
meta: None,
..Default::default()
})
}
@@ -90,7 +93,7 @@ impl McpClientTrait for BuiltinClient {
_cancel_token: CancellationToken,
) -> Result<CallToolResult, Error> {
if name != crate::builtin::LOAD_SKILL_TOOL {
return Ok(CallToolResult::error(vec![Content::text(format!(
return Ok(CallToolResult::error(vec![ContentBlock::text(format!(
"unknown tool: {name}"
))]));
}
@@ -100,10 +103,10 @@ impl McpClientTrait for BuiltinClient {
.unwrap_or(serde_json::Value::Null);
let result = crate::builtin::call_load_skill(&args, &self.skills).await;
let content: Vec<Content> = result
let content: Vec<ContentBlock> = result
.content
.iter()
.map(|c| Content::text(c.as_text_lossy()))
.map(|c| ContentBlock::text(c.as_text_lossy()))
.collect();
Ok(if result.is_error {

View File

@@ -163,3 +163,105 @@ pub fn truncate_at_boundary(s: &str, max: usize) -> &str {
}
&s[..cut]
}
/// Publisher-side billing identity for a turn.
///
/// Mirrors `buzz_core::agent_turn_metric::PricingIdentity` but is local to
/// `buzz-agent` (which does not depend on `buzz-core`). The two structs have
/// the same camelCase wire representation and are deserialized identically by
/// `buzz-acp`.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PricingIdentity {
/// Registered billing-namespace identifier (bare lowercase hostname).
pub authority: String,
/// Actually-requested billable model identifier.
pub model: String,
/// Cache-write class when applicable; omitted otherwise.
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_class: Option<String>,
}
/// Tri-state accumulator for provider-reported total tokens within one ACP turn.
///
/// - `Unseen`: no usage-bearing response observed yet (initial state for each turn).
/// - `Exact(n)`: every response so far reported a total; `n` is their sum.
/// - `Unknown`: at least one response lacked a total — permanently poisoned for
/// this turn. The session-cumulative also transitions to Unknown when any turn
/// lands Unknown, and stays there until a new session resets it.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TurnTotalState {
#[default]
Unseen,
Exact(u64),
Unknown,
}
impl TurnTotalState {
/// Add two exact token counts with overflow protection.
///
/// Returns `Exact(acc + n)` on success or `Unknown` on overflow.
/// This is the single implementation of the checked-add / overflow-poisons
/// contract; both `fold()` and `merge_session()` call this helper so a
/// change to overflow semantics needs to be made in exactly one place.
fn checked_exact_sum(acc: u64, n: u64) -> TurnTotalState {
match acc.checked_add(n) {
Some(sum) => TurnTotalState::Exact(sum),
None => TurnTotalState::Unknown,
}
}
/// Fold one provider-reported total into the current state.
///
/// `total`: `Some(n)` when the provider included a genuine total on this
/// response; `None` when it was absent (e.g. Anthropic, or an OpenAI
/// response that omits usage). Absence of a total on any usage-bearing
/// response poisons the whole turn.
///
/// Overflow is handled by `checked_exact_sum`: a saturated value would
/// not be a genuine provider-reported total, so overflow → `Unknown`.
pub fn fold(self, total: Option<u64>) -> TurnTotalState {
match (self, total) {
// Already poisoned — stays Unknown regardless.
(TurnTotalState::Unknown, _) => TurnTotalState::Unknown,
// No total from this response — poison the accumulator.
(_, None) => TurnTotalState::Unknown,
// First response with a total.
(TurnTotalState::Unseen, Some(n)) => TurnTotalState::Exact(n),
// Subsequent response — delegate to the shared checked-sum helper.
(TurnTotalState::Exact(acc), Some(n)) => Self::checked_exact_sum(acc, n),
}
}
/// Merge a completed turn's total state into the session-cumulative state.
///
/// This is the turn→session boundary accumulation:
/// - An `Unseen` turn (no usage-bearing responses) leaves the cumulative unchanged.
/// - Any `Unknown` side poisons the session permanently.
/// - Two `Exact` values are summed via `checked_exact_sum`; overflow → `Unknown`.
///
/// The checked-add logic lives in `checked_exact_sum`; both this function and
/// `fold()` call that helper so overflow semantics are defined once.
pub fn merge_session(self, turn: TurnTotalState) -> TurnTotalState {
match (self, turn) {
// Either side poisoned → session is poisoned.
(TurnTotalState::Unknown, _) | (_, TurnTotalState::Unknown) => TurnTotalState::Unknown,
// Turn had no usage-bearing responses → no change to cumulative.
(acc, TurnTotalState::Unseen) => acc,
// First exact turn — adopt its value.
(TurnTotalState::Unseen, TurnTotalState::Exact(n)) => TurnTotalState::Exact(n),
// Add to running exact sum — delegate to the shared checked-sum helper.
(TurnTotalState::Exact(acc), TurnTotalState::Exact(n)) => {
Self::checked_exact_sum(acc, n)
}
}
}
/// Consume the exact value if present; `None` for `Unseen` or `Unknown`.
pub fn exact_value(self) -> Option<u64> {
match self {
TurnTotalState::Exact(n) => Some(n),
_ => None,
}
}
}

View File

@@ -33,6 +33,7 @@ urlencoding = "2"
webbrowser = "1"
hex = { workspace = true }
getrandom = "0.4"
dirs = "6"
[dev-dependencies]
tempfile = "3"