Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ crates/
buzz-dev-mcp # Developer MCP server — shell + file-edit tools
buzz-persona # Agent persona packs
buzz-workflow # YAML-as-code workflow engine (evalexpr conditions)
buzz-budget # Sliding-window cost accounting for agent-to-agent exchanges
# Clients + interop
buzz-pair-relay # Ephemeral sidecar relay for NIP-AB device pairing
buzz-pairing-cli # CLI for NIP-AB device pairing interop testing
Expand Down
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ members = [
"crates/buzz-dev-mcp",
"crates/buzz-voice",
"crates/buzz-backend-kubernetes",
"crates/buzz-budget",
"examples/countdown-bot",
]
exclude = ["desktop/src-tauri"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,17 @@ async def run(
)
# The task arrives exactly as it would in production Buzz: a
# user prompt @mentioning the orchestrator. The harness never
# speaks as any agent.
# speaks as any agent. The orchestrator is mentioned by pubkey,
# not by name resolution: task text is untrusted payload, and any
# @-token inside it (e.g. Vim's `:%normal! @a`) would otherwise
# fail member resolution and kill the trial before the agent
# ever saw the task. An explicit --mention demotes unresolved
# @-tokens in the text to presentation-only.
await self._send(
trial.user, trial, f"@{orchestrator.agent_id} {instruction}"
trial.user,
trial,
f"@{orchestrator.agent_id} {instruction}",
mention=orchestrator.nostr_pubkey,
)
final_message = await asyncio.wait_for(
self._wait_for_done(environment, orchestrator, trial, agents + infra),
Expand Down Expand Up @@ -519,18 +527,24 @@ async def _verify_m1_output(
)

async def _send(
self, credential: AgentCredential, trial: TrialHandle, content: str
self,
credential: AgentCredential,
trial: TrialHandle,
content: str,
*,
mention: str | None = None,
) -> None:
await self._buzz_json(
credential,
trial,
args = [
"messages",
"send",
"--channel",
trial.channel_id,
"--content",
content,
)
]
if mention is not None:
args += ["--mention", mention]
await self._buzz_json(credential, trial, *args)

async def _buzz_json(
self, credential: AgentCredential, trial: TrialHandle, *args: str
Expand Down
31 changes: 31 additions & 0 deletions benchmarks/harbor-buzz-orchestra/tests/test_container_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,37 @@ async def test_m1_output_probe_matches_grader_and_is_condition_scoped(
assert bool(probed) == (condition == "M1-hello-world")


async def test_send_mentions_by_pubkey_so_task_text_stays_inert(
tmp_path, monkeypatch
):
"""Task text is untrusted payload: `:%normal! @a` in a task statement must
not be fed to member-name resolution (it would fail and kill the trial).
An explicit --mention pins delivery to the orchestrator's pubkey."""
rt = runtime(tmp_path)
orch = credential("orch-1", "orchestrator", "orch-model")
trial = trial_handle((orch,))
calls = []

async def buzz_json(credential, trial, *args):
calls.append(args)
return {}

monkeypatch.setattr(rt, "_buzz_json", buzz_json)

await rt._send(
trial.user,
trial,
"@orch-1 run `:%normal! @a` on the file",
mention=orch.nostr_pubkey,
)
assert calls[-1][-2:] == ("--mention", "pubkey-orch-1")

# Without an explicit mention the send is unchanged (name resolution).
await rt._send(trial.user, trial, "plain content")
assert "--mention" not in calls[-1]
assert calls[-1][-2:] == ("--content", "plain content")


async def test_wait_for_done_requires_orchestrator_authorship(tmp_path, monkeypatch):
rt = runtime(tmp_path, poll_seconds=0)
orch = credential("orch-1", "orchestrator", "orch-model")
Expand Down
19 changes: 19 additions & 0 deletions crates/buzz-budget/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "buzz-budget"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "Sliding-window cost accounting for agent-to-agent exchanges"

[dependencies]
buzz-core = { workspace = true }
chrono = { workspace = true }
nostr = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
uuid = { workspace = true }

[dev-dependencies]
tracing-subscriber = { workspace = true }
85 changes: 85 additions & 0 deletions crates/buzz-budget/examples/runaway.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//! Drive the budget at its real runtime surface and show the sawtooth.
//!
//! Decision D4 of wayfinder ticket #7 justifies a self-healing window by the
//! signature it leaves in the logs — "a runaway stops within minutes and
//! restarts only to stop again, producing a **sawtooth in the logs**, a
//! diagnosable signature rather than a silent drain."
//!
//! That claim is only true if something is actually emitted. This example
//! exists so it can be *observed* rather than asserted: it runs a simulated
//! two-agent runaway alongside an owner working in another channel, with a real
//! `tracing` subscriber attached, and prints what an operator would see.
//!
//! Run with:
//! cargo run -p buzz-budget --example runaway

use chrono::{DateTime, Duration, Utc};
use uuid::Uuid;

use buzz_budget::{Supervisor, DEFAULT_BUDGET_USD, DEFAULT_WINDOW_SECS};

fn main() {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG)
.with_target(false)
.without_time()
.init();

let runaway_channel = Uuid::from_u128(1);
let human_channel = Uuid::from_u128(2);
let t0: DateTime<Utc> =
DateTime::from_timestamp(1_700_000_000, 0).expect("valid fixed timestamp");

let mut sup = Supervisor::new(DEFAULT_BUDGET_USD, DEFAULT_WINDOW_SECS);

println!("--- two agents talking to each other, nobody watching ---");
let mut tripped = 0usize;
for i in 0..30i64 {
let at = t0 + Duration::seconds(i * 2);
let (speaker, listener) = if i % 2 == 0 {
("otto", "eva")
} else {
("eva", "otto")
};
sup.observe_message(runaway_channel, speaker, false, at);
let v = sup.on_turn_completed(
runaway_channel,
listener,
Some(0.60),
at + Duration::seconds(1),
);
if v.is_exhausted() {
tripped += 1;
}
}
println!("=> exhausted verdicts in the first window: {tripped}");

println!();
println!("--- the same agents, one hour later: the window has slid ---");
let later = t0 + Duration::seconds(DEFAULT_WINDOW_SECS + 60);
sup.observe_message(runaway_channel, "otto", false, later);
let v = sup.on_turn_completed(
runaway_channel,
"eva",
Some(0.60),
later + Duration::seconds(1),
);
println!("=> after the window slid: {v:?}");

println!();
println!("--- meanwhile, the owner working hard in another channel ---");
let mut blocked = 0usize;
for i in 0..40i64 {
let at = t0 + Duration::seconds(i * 2);
sup.observe_message(human_channel, "owner", true, at);
let v = sup.on_turn_completed(human_channel, "eva", Some(2.50), at + Duration::seconds(1));
if v.is_exhausted() {
blocked += 1;
}
}
println!("=> owner turns blocked (must be 0): {blocked}");
println!(
"=> owner-channel spend recorded (must be 0.00): {:.2}",
sup.spent(human_channel, "eva", "owner", t0 + Duration::seconds(200))
);
}
Loading