🤖 ## Requested rebase published —ef40744bRebased onto fetched main **47d068e2109d077414cbf2f4f1c927f6d051037a**, published **ef40744b3aeb4baaf8c81416e1a644fb5b315f91** with the exact expected-old `df7fad6a` force-with-lease. No merge. Manual conflicts were additive: preserve main's exact-key identity documentation alongside the availability contract, and retain both Bestie props and the shared availability reader in `UnifiedAgentsSection`. Range-diff confirms unchanged lifecycle policy: exact-key action-time authority, Unknown versus Offline, rejected shutdown retains record/memberships, and separate local/provider/owner gates. Main's exact-key profile routing survives. Both test-only CI synchronization repairs (natural toast expiry and bounded stderr wait) are byte-identical to the prior head. All ten original authors/messages/DCO/material coauthor trailers are preserved; configured signing policy was not changed. Fresh checks on the rebased candidate: - TypeScript, Biome on 26 changed TypeScript files, differential file-size gate, and diff whitespace: pass. - Focused production-hook/card/profile units: **58/58**. - Fresh E2E build, availability/deletion browser: **11/11**, no retries. - Main exact-key profile cases plus failed-DM send/startup retries: **6/6**, no retries. Previously reviewed full Desktop/buzz-agent package and mutation evidence is reused for unchanged behavior; no ceremonial full suite, new native/provider test, or `just ci` pass is claimed. Local configs, dependency links, and historical artifacts are preserved. Hosted observation: **MERGEABLE**, **BLOCKED / REVIEW_REQUIRED**, no new-head formal review. [CI 33699735990](https://github.com/block/buzz/actions/runs/33699735990) is running (including Rust and Desktop lanes), not a completed success. DCO and required Security aggregate passed at the observation; the separate Codex advisory review was skipped. No completed failing check or new inline feedback observed. Historical approvals are not new-head approvals. No reviewer/security authorization or merge action was performed. --- ## Feature summary and retained pre-rebase evidence ## Summary In Buzz Desktop, an agent could look online just because it had been started or deployed, even when there was no current sign it was connected. Cards and profiles now show availability from the agent's relay presence rather than a saved launch record, so you can distinguish an online agent from one that was merely deployed. Agents cards and profiles use presence reported through the shared server (the relay). A successful presence read with no online agent shows Offline; failed/disconnected evidence shows unknown, rather than retaining a misleading cached Online state. Lifecycle actions remain separate. An offline agent may still have a Shutdown action because the deployment record exists. Shutdown reports a **request**, not proof the process stopped. Offline does not imply that starting a duplicate agent is safe, and Online does not promise a response. ### Related issue Independent base: `main`; no stack parent or child among the replacements. Extracted from [#7114](https://github.com/block/buzz/pull/7114), retained as historical source (`98fe33ec`). [Behavior contract](f4bb2ed44e/docs/agent-availability.md). Originating [Buzz discussion](buzz://message?channel=f7a9536a-1738-4bad-a888-b3ea25010ef1&id=7aa1f0ab23dce514bd8a0221441cf005bf428914621171472b79747c50820848) · channel `f7a9536a-1738-4bad-a888-b3ea25010ef1`. ### Testing The same saved provider-backed agent, with only authored presence changing. These are mock-browser states, not a before/after deployment or live relay transport test; production UI is unchanged by the later fixture repairs. **No online presence:** gray dot, existing Shutdown control retained.  **Online presence:** green dot, same lifecycle control.  [Capture details](https://github.com/block/buzz/pull/7127#issuecomment-5482264824). To check manually, compare runtime-only transitions with presence updates, then disconnect/fail the presence read and verify it does not stay Online. A Shutdown request should not immediately claim confirmed termination. #### Historical pre-rebase evidence and limitations (df7fad6a) Lifecycle production source remains **`f4bb2ed44e5a989d93c5f51e93c0bbd2dca941be`**. Current published head is **`df7fad6ae65dda78508317186a95522d1bb22ed9`**: the prior browser synchronization at `b78d093e` plus an additive two-file Rust test-harness synchronization described below. No production bytes, dependency/configuration files, or prior commits were changed; no rebase. Current live main `0dbd036f5bff33e7ade75e7639f3218d424a6e73` has identical failing-test/toaster/send-flow source; the causal browser comparison used latest successfully tested main `04babf02655440b4dfd37f2e2df605ead0a030d8`. **Lifecycle/deletion correction:** both Agents and actual profile deletion now pass the shared exact-key availability reader, not raw cached data. It reads the canonical query state and connection at action time, including after awaited channel discovery. Failed/disconnected/pending evidence and unqueried persona siblings are unknown; successful missing means Offline only for a requested key. Successful background refetch cache remains usable; settled failure revokes it. No second cache or per-row polling was added. Provider record + channel + Online/Away/**unknown** awaits shutdown submission before local removal; rejection preserves record/membership for retry. Established Offline preserves intentional no-request removal. No route preserves warned local removal. Local agents retain native stop-before-remove, independent of presence. Profile consent now describes a shutdown **request**, not remote deletion or guaranteed termination. Existing ownership and force gates are unchanged. **Verified, reused exact-candidate validation:** the independently approved eleven-file patch (SHA-256 `2f69fe12ef0420e62dea1fd8db28cfa22cde5eecaf8080e656310a3e60d0cf86`) was committed without byte changes. Desktop **5,921 passed, 0 failed/skipped**, including **26 new mounted production hook/IPC regressions**; rebuilt availability browser suite **11/11 passed, no retries**, including four actual profile Delete journeys. Desktop check (existing 4 warnings/5 infos), typecheck, production/protected-feature artifact matrix, differential file-size/policy and diff checks passed. No blanket rerun or new full-repository `just ci` is claimed for this frontend correction. Production regressions cover cached Online **and Offline** failure/disconnection, genuine missing/Offline, pending, successful inflight refetch versus settled error, retained reader, error during awaited channel discovery, unqueried persona sibling, shutdown rejection/order/cancel, no route and local authority. Browser fixtures use safe mock IPC and a retained provider receipt, not a real deployment. Three restored mutation controls fail: unknown → skip shutdown (**15** regressions), Agents raw-cache reader (**6**), actual profile raw-cache caller (**1 browser journey**, false removal on failed cached Offline). Independent review approved the exact frozen bytes and added **4/4 cached-empty failure/disconnection probes** across both callers. This is local independent approval, not formal GitHub/A Team clearance. The prior native propagation/poll-count defects remain closed ([earlier response](https://github.com/block/buzz/pull/7127#issuecomment-5512278755)). The prior hover-popover correction at `b55423f6` remains covered by the full 11-journey browser run: pending/failed/disconnected means no badge or accessible status, genuine missing/Offline retains an Offline badge. Its earlier fallback-restoration mutation failed as expected (badge count 1 rather than 0); that historical witness is reused, not rerun. **Reused unchanged native/system boundary:** local `just ci` at `c59067d8` passed workspace/Tauri fmt/clippy, static/policy checks, Rust unit recipe, native workspace **3,159 passed / 20 ignored**, Web build and **2,019 mobile tests**. No native implementation changed in this lifecycle correction. These are historical boundary results, not new-head native/live certification. [Parent CI](https://github.com/block/buzz/actions/runs/33650549130) passed with **14 retry-recovered browser flakes**, not retry-free. Old-head CI/reviews are not current-head clearance. **Hosted gates:** [CI33662151103](https://github.com/block/buzz/actions/runs/33662151103) on `f4bb2ed4` **FAILED**: smoke shard1 had 322 pass, one failure, one retry-recovered flaky, two skipped. The failed first-DM retry test timed out on all three attempts because the error toast intercepted Send. That failure is preserved, not waived; the scoped test repair below is published as `b78d093e`. [CI33668171165](https://github.com/block/buzz/actions/runs/33668171165) on `b78d093e` subsequently **FAILED** the Rust unit budget regression described below. Both original failures remain visible; neither was retried to green. Exact `f4bb2ed4` and `b78d093e` APPROVED reviews cover unchanged reviewed bytes, not formal approval of the new head. Fresh exact-head CI and the established automated technical rereview are the next gates for `df7fad6a`. Historical deletion responses remain ([5092381800](https://github.com/block/buzz/pull/7127#issuecomment-5513759316), [5092391193](https://github.com/block/buzz/pull/7127#issuecomment-5513763497)). No formal review dismissed. The [security notice](https://github.com/block/buzz/pull/7127#issuecomment-5482231278) and latest-push maintainer/codeowner policy remain separate actionable gates: eligible Block organization members own current-range authorization. No merge/security authority exercised. **CI causal repair (`b78d093e`, test only):** the error `Message failed to send: Mock first DM send failed.` is deliberately injected by the existing fixture. CI screenshot and retry trace show the bottom-right Sonner notification over the actual enabled Send button. `fill()` leaves the pointer parked there; Sonner pauses its 4-second lifetime while hovered. A fast run can click before animation settles (unchanged local test passed in 2.7s; two actual tested-main CI cases passed first attempt in 3.3s), which does not disprove the failure. Independent controlled browser runs on `f4bb2ed4` and tested main `04babf` both reproduced the same toast hit-test at Send `(1203,627,32,32)`, persistent hover beyond 4s, and intercepted ordinary click with no second send. Moving the real pointer to the editor allows natural expiry and successful ordinary retry, preserving all original DM-channel/recipient assertions. This same synchronization already exists in the neighboring agent-startup-failure test. The one-file correction keeps the visible error assertion, scopes its toast locator, moves the pointer back to the editor and observes normal toast removal (bounded 10s) before retry. No forced click, direct toast dismissal, mocked clock, CSS override, skipped test, production behavior change, or new backend mock. Six focused browser executions pass (first-send/startup-failure, three repeats each, no retries); the held-toast control fails on original bytes at the Send click while the exact repaired test passes. Biome and diff checks pass. Reuse unchanged 5,921 Desktop / 11 availability browser / four independent probes above; no semantic production change warrants repeating those suites. Original failed CI attempt/retries, local fast pass, deliberate failing control and all traces remain in `WORK_LOGS/AVAILABILITY_CI_B9210A40`. Browser evidence is mock-IPC Chromium, not native/live-relay certification. The UI still temporarily overlays Send while a notification is hovered; the test exercises its real move-away/expiry recovery, not immediate click-through. **Rust CI causal repair (`df7fad6a`, test only):** [original Rust / Unit Tests failure, job100375291370](https://github.com/block/buzz/actions/runs/33668171165/job/100375291370) tested GitHub merge `223dee91a396d8cb4ebf18b9b8559e5a54951235`. `context_recovery_budget_exhaustion_surfaces_the_error` failed at `regressions.rs:2756` in **0.091s** because its immediate stderr snapshot lacked `context recovery budget spent`. ACP context-error assertions had already passed. The captured prefix shows all three budgets **32768 → 16384 → 8192 bytes**, above the 4096-byte floor, and ends during the third attempt. This is **not evidence of floor exhaustion**. The collector is an independent Tokio task; a stdout response is not a stderr barrier. Recovery, harness and test blobs were identical across the compared base/head/merge parents; no production regression was implicated. The shared test Harness now provides a bounded event/condition wait, registering for collector notifications before reading the buffer to avoid lost wakeups. The budget and adjacent terminal floor assertions wait for their own diagnostic and retain the matching snapshot. The budget test still requires the provider's ACP context error and exactly three recovery rungs, now corroborated by **exactly four provider calls** and no floor diagnostic. Timeout remains a real failure with captured stderr. No fixed sleep, weaker assertion, skip, provider-limit/logging change, dependency/config edit, or production change. **Deterministic causal control:** the same real agent/HTTP-provider/ACP scenario holds only stderr collection behind a one-shot gate until after stdout responds. The old immediate snapshot fails the original budget assertion (intentional exit101); the repaired wait explicitly remains Pending while held, then passes after release. No scheduler-speed assumption or fixed sleep. This reproduces the observation race under controlled delay, **not the exact historical CI schedule**. A missing-diagnostic test proves the wait actually times out. Original failure and deliberate failing-control logs/patch remain in `WORK_LOGS/RUST_TRIAGE_06E32D2D` and `WORK_LOGS/RUST_SYNC_BC5758B9`. **Final candidate validation:** focused recovery **9/9**, floor **1/1**, absent-diagnostic timeout **1/1** pass. One full touched-package run, `cargo test --locked -p buzz-agent`: **695 passed, 0 failed, 1 existing ignored**, including all **54 regressions**. Local nextest was unavailable, so this uses the repository-supported cargo-test fallback, not a claim of nextest reproduction. `cargo fmt --check`, package-scoped Clippy all-targets with `-D warnings`, differential file-size/policy and diff checks pass. Previously reviewed availability production and the Desktop/browser evidence above are unchanged and reused; no all-native blanket rerun. The test-only delta was self-reviewed against collector ordering, timeout and falsification evidence. Existing production approval remains valid for those bytes; exact-new-head technical/CI clearance is not assumed. The required **Security aggregate** is distinct from optional Codex advisory feedback; no security authorization, human review contact, or merge was requested. **Remaining policy limits:** shutdown submission is not harness acceptance or process termination; confirmed Offline/no-route local removal may leave a remote process; route discovery is best effort, membership cleanup uses `Promise.allSettled`, and multi-instance deletion is sequential/non-atomic. No distributed singleton, provider-health, tenant-switch cancellation, live relay TTL or packaged WebView/VoiceOver certification is claimed. The pre-existing DM-header raw-presence fallback (`ChannelScreenHeader`/`useActiveChannelHeader`) remains outside this repair and uncertified. Screenshots above remain historical mock-browser illustrations, not new deletion or native transport evidence. --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
buzz-agent
Minimal, unbreakable ACP-compliant LLM agent. Stdio in, tool calls out. Non-streaming. No persistence. No cleverness.
ACP is the Agent Client Protocol — JSON-RPC 2.0 over stdio between a client (Zed, JetBrains, buzz-acp, …) and an agent. MCP is how the agent talks to its tools.
buzz-agent is the agent.
What It Is
+--------+ stdio (JSON-RPC 2.0) +---------------+
| client | <----------------------> | buzz-agent |
+--------+ ACP frames +---------------+
│ │
│ │ rmcp (stdio)
│ ▼
│ MCP servers
│ (your tools)
▼
HTTPS
│
▼
Anthropic Messages API,
OpenRouter, or any OpenAI-compat
(vLLM, llama.cpp, Databricks,
Block Gateway, Ollama, …)
A client sends session/prompt. The agent loops: call the LLM → get tool calls → run them via MCP → feed results back → repeat. The loop terminates when the LLM stops asking for tools, the round cap is hit, or the client cancels.
The agent's output is its tool calls. Generated text is forwarded to the client as agent_message_chunk updates, but the real work happens in the tools. The LLM call is non-streaming — one HTTP POST, one response.
Quick Start
# Build
cargo build --release -p buzz-agent
# Run against Anthropic
BUZZ_AGENT_PROVIDER=anthropic \
ANTHROPIC_API_KEY=sk-ant-... \
ANTHROPIC_MODEL=claude-sonnet-4-5 \
./target/release/buzz-agent
# Or any OpenAI-compatible endpoint
BUZZ_AGENT_PROVIDER=openai \
OPENAI_COMPAT_API_KEY=sk-... \
OPENAI_COMPAT_MODEL=gpt-5 \
OPENAI_COMPAT_BASE_URL=https://api.openai.com/v1 \
./target/release/buzz-agent
# Or OpenRouter
BUZZ_AGENT_PROVIDER=openrouter \
OPENROUTER_API_KEY=sk-or-v1-... \
OPENROUTER_MODEL=anthropic/claude-sonnet-4.5 \
./target/release/buzz-agent
# Or Databricks model serving via OAuth 2.0 PKCE
BUZZ_AGENT_PROVIDER=databricks \
DATABRICKS_HOST=https://dbc-...cloud.databricks.com \
DATABRICKS_MODEL=goose-claude-4-6-sonnet \
./target/release/buzz-agent
That's the whole setup. The agent reads JSON-RPC frames from stdin, writes them to stdout, and logs to stderr.
ACP Transcript
A complete round-trip. Lines starting with → are client→agent (stdin); ← are agent→client (stdout). Each line is one newline-terminated JSON value. Comments are not part of the wire.
// 1. Handshake.
→ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{}}}
← {"jsonrpc":"2.0","id":1,"result":{
"protocolVersion":1,
"agentCapabilities":{
"loadSession":false,
"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false},
"mcpCapabilities":{"http":false,"sse":false}
},
"agentInfo":{"name":"buzz-agent","version":"0.1.0"}
}}
// 2. Open a session. The client passes the MCP servers to spawn.
→ {"jsonrpc":"2.0","id":2,"method":"session/new","params":{
"cwd":"/tmp",
"mcpServers":[{"name":"echo","command":"/usr/local/bin/echo-mcp","args":[],"env":[]}]
}}
← {"jsonrpc":"2.0","id":2,"result":{"sessionId":"ses_a1b2c3d4e5f6a7b8"}}
// 3. Prompt. The agent loops until the LLM stops calling tools.
→ {"jsonrpc":"2.0","id":3,"method":"session/prompt","params":{
"sessionId":"ses_a1b2c3d4e5f6a7b8",
"prompt":[{"type":"text","text":"echo hello"}]
}}
// 4. Agent emits tool_call (status: pending) — visible to the UI.
← {"jsonrpc":"2.0","method":"session/update","params":{
"sessionId":"ses_a1b2c3d4e5f6a7b8",
"update":{
"sessionUpdate":"tool_call",
"toolCallId":"toolu_01XYZ",
"title":"echo__say",
"kind":"other",
"status":"pending",
"rawInput":{"text":"hello"}
}
}}
// 5. Agent moves the call to in_progress, runs the MCP tool, then completed.
← {"jsonrpc":"2.0","method":"session/update","params":{
"sessionId":"ses_a1b2c3d4e5f6a7b8",
"update":{"sessionUpdate":"tool_call_update","toolCallId":"toolu_01XYZ","status":"in_progress"}
}}
← {"jsonrpc":"2.0","method":"session/update","params":{
"sessionId":"ses_a1b2c3d4e5f6a7b8",
"update":{
"sessionUpdate":"tool_call_update",
"toolCallId":"toolu_01XYZ",
"status":"completed",
"content":[{"type":"content","content":{"type":"text","text":"hello"}}]
}
}}
// 8. The model sees the result, decides it's done, and the prompt resolves.
← {"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}
That's ACP. Three request methods (initialize, session/new, session/prompt), one inbound notification (session/cancel), and three outbound update variants (agent_message_chunk, tool_call, tool_call_update). The full server is hand-rolled in main.rs.
Configuration
Everything is environment variables. No flags, no config files. (We are a subprocess; subprocess config is environment.)
| Variable | Default | Notes |
|---|---|---|
BUZZ_AGENT_PROVIDER |
— | Required. anthropic, openai, openrouter, databricks, or databricks_v2. No implicit fallback — the agent errors at startup when this is unset. |
ANTHROPIC_API_KEY |
— | Required when provider=anthropic. |
ANTHROPIC_MODEL |
— | Required when provider=anthropic. |
ANTHROPIC_BASE_URL |
https://api.anthropic.com |
|
ANTHROPIC_API_VERSION |
2023-06-01 |
|
OPENAI_COMPAT_API_KEY |
— | Required when provider=openai. |
OPENAI_COMPAT_MODEL |
— | Required when provider=openai. |
OPENAI_COMPAT_BASE_URL |
https://api.openai.com/v1 |
Point at vLLM, llama.cpp, Ollama, etc. |
OPENAI_COMPAT_API |
auto |
auto | chat | responses. auto picks Responses for *.openai.com, Chat Completions everywhere else. |
OPENROUTER_API_KEY |
— | Required when provider=openrouter. |
OPENROUTER_MODEL |
— | Required when provider=openrouter. Use OpenRouter's vendor/model id, e.g. anthropic/claude-sonnet-4.5. |
OPENROUTER_BASE_URL |
https://openrouter.ai/api/v1 |
|
DATABRICKS_HOST |
— | Required when provider=databricks or provider=databricks_v2. |
DATABRICKS_MODEL |
— | Required when provider=databricks or provider=databricks_v2. |
DATABRICKS_MODEL_FILTER |
— | Optional discovery-only, comma-separated full-string */? patterns OR-matched against raw Databricks endpoint and Unity Catalog model-service IDs. Blank/unset shows all; this is visibility filtering, not an authorization boundary. |
DATABRICKS_TOKEN |
— | Optional static bearer escape hatch. If unset, Databricks uses browser OAuth + refresh cache. |
BUZZ_AGENT_SYSTEM_PROMPT |
built-in | Inline system prompt. |
BUZZ_AGENT_SYSTEM_PROMPT_FILE |
— | File path. Mutually exclusive with the above. |
BUZZ_AGENT_MAX_ROUNDS |
0 |
Tool-loop iteration cap. 0 = unlimited. |
BUZZ_AGENT_MAX_OUTPUT_TOKENS |
65536 |
Desired per-call ceiling. Set this at or below the served model's output limit for each agent deployment. Proactive handoff is independently based on 90% of BUZZ_AGENT_MAX_CONTEXT_TOKENS. |
BUZZ_AGENT_MAX_TOKEN_RECOVERIES |
3 |
Retries after a successful response is truncated at the output-token limit. 0 disables recovery; the finite value and BUZZ_AGENT_MAX_ROUNDS prevent infinite retries. |
BUZZ_AGENT_MAX_CONTEXT_TOKENS |
200000 |
Provider context window used by the handoff gate. |
BUZZ_AGENT_MAX_HANDOFFS |
10 |
Max context handoffs per session before falling back to truncation. |
BUZZ_AGENT_LLM_TIMEOUT_SECS |
240 |
Max seconds with no response bytes before abandoning an LLM call (per-read inactivity, not wall-clock). |
BUZZ_AGENT_TOOL_TIMEOUT_SECS |
1260 |
Per-tool call timeout in seconds |
BUZZ_AGENT_MAX_PARALLEL_TOOLS |
8 |
Max concurrent tool calls per turn (1 = sequential) |
BUZZ_AGENT_MAX_SESSIONS |
unlimited | Max concurrent ACP sessions. Sessions are cheap; default has no cap. |
BUZZ_AGENT_MAX_LINE_BYTES |
4194304 |
4 MiB. Hard cap on inbound JSON-RPC frames. |
BUZZ_AGENT_MAX_HISTORY_BYTES |
1048576 |
1 MiB. Old turns are evicted past this. |
BUZZ_AGENT_MAX_TOOL_RESULT_TEXT_BYTES |
51200 |
50 KiB. Per-result cap on tool-output text; oversize is middle-elided (head + tail kept) with an inline marker. Images are exempt. |
BUZZ_AGENT_REQUIRE_REPLY |
0 (1 on mesh) |
1 enables the reply guard — remind the model to publish when a turn is about to end with nothing posted to Buzz. Desktop defaults it to 1 for Buzz shared-compute agents. |
Reply Guard
Off by default, except on Buzz shared-compute (mesh) agents, where Buzz Desktop
sets BUZZ_AGENT_REQUIRE_REPLY=1 automatically. With it enabled, a turn that is
about to end without any recognized attempt to post to Buzz gets a reminder that
its assistant text is invisible to humans, and is rerolled.
This exists because a Buzz agent's reasoning and tool output are not shown to anyone. A turn that does real work and never posts is a silent failure — the requester waits on a result that was produced and thrown away.
Mesh agents get it by default because they run on small local models, which are
the ones most likely to do the work and then end the turn without publishing it.
Setting BUZZ_AGENT_REQUIRE_REPLY=0 on the agent, persona, or global env opts a
mesh agent back out; the default never overrides an explicit value.
Advisory, never a trap. At most two reminders, then the turn ends whether or not anything was published. The guard catches accidental omission; it does not compel speech. The reminder text explicitly licenses silence, because the built-in system prompt says publishing is optional and silence is often the correct outcome.
Recognition contract. A turn counts as having replied when it issues a call that:
- resolves to a registered, non-hook tool (a hallucinated tool name is rejected at preflight and never runs, so it must not disarm the guard),
- whose qualified name ends in
__shell— i.e. the bare tool name is exactlyshell, which isbuzz-dev-mcp's shell tool and any other server's, and - whose
commandargument containsmessages sendorreactions add.
messages send also covers messages send-diff. Reactions count because the
built-in prompt directs agents to react rather than post a bare
acknowledgement, so nagging an agent that reacted would punish documented
behavior.
Detection is checked after the per-turn tool-call cap
(MAX_TOOL_CALLS_PER_TURN) is applied: a publish-shaped call that was discarded
never ran.
It recognizes an attempt, not a successful publish. Only the command text is inspected, never the exit status. A send that fails still satisfies the guard — which is fine, since a failed send already returns a non-zero exit and error JSON to the model, louder feedback than a reminder.
Known limits, both deliberate. A command assembled at runtime ($CMD) or
buried in a wrapper script is missed, so that turn is reminded despite having
posted. Text that merely quotes a send (echo "buzz messages send") matches, so
that turn is not reminded. Missing a real post is the expensive direction, and
substring matching is the forgiving one there. Neither edge is pinned by a test;
the matcher is free to improve.
Budget. Reminders ride the existing _Stop gate and share
BUZZ_AGENT_STOP_MAX_REJECTIONS — the outer cap on every end-turn objection.
At the default 3 both reminders fit; at 1 only one does; at 0 the guard is off
along with the hooks. A round carrying both a _Stop hook objection and a
reminder costs one rejection and delivers both texts. This is not a new
lifecycle hook — see MCP_DRIVEN_HOOKS.md.
Providers
buzz-agent speaks a few HTTP dialects. Pick with BUZZ_AGENT_PROVIDER.
| Provider | BUZZ_AGENT_PROVIDER |
Endpoint (auto) | Tested with |
|---|---|---|---|
| Anthropic | anthropic |
POST {base}/v1/messages |
claude-sonnet-4-5, claude-opus-4 |
| OpenAI | openai |
POST {base}/responses |
gpt-5, gpt-5-mini, o4-mini, gpt-4o |
| vLLM | openai |
POST {base}/chat/completions |
any tool-calling model |
| llama.cpp | openai |
POST {base}/chat/completions |
any tool-calling GGUF |
| Ollama | openai |
POST {base}/chat/completions |
llama3.1, qwen2.5-coder |
| Block Gateway | openai |
POST {base}/chat/completions |
gpt-5, claude |
| OpenRouter | openrouter |
POST {base}/chat/completions |
anything they route (extended-thinking replay, provider-agnostic tool calling) |
| Databricks | databricks |
POST {host}/serving-endpoints/{model}/invocations |
goose-claude-4-6-sonnet |
| Databricks AI Gateway v2 | databricks_v2 |
POST {host}/ai-gateway/{provider}/v1/... |
workspace endpoints and Unity Catalog model-service FQNs; UC FQNs use MLflow Chat Completions |
The optional DATABRICKS_MODEL_FILTER applies only to model discovery. Each comma-separated entry is trimmed and matched against the complete raw ID with case-sensitive * (zero or more characters) and ? (one Unicode character) semantics; patterns are OR-ed. Unset or blank preserves the full authenticated catalog. A nonblank value containing no usable patterns is rejected. This controls picker visibility only; Databricks and Unity Catalog permissions remain the authorization boundary. A filtered-empty result is authoritative and does not restore the built-in fallback models.
If BUZZ_AGENT_PROVIDER=anthropic is selected without ANTHROPIC_API_KEY, BUZZ_AGENT_PROVIDER=openai is selected without OPENAI_COMPAT_API_KEY, or BUZZ_AGENT_PROVIDER=openrouter is selected without OPENROUTER_API_KEY, the agent returns an error — there is no implicit fallback to another provider.
provider=openai speaks two HTTP dialects: the Responses API (/v1/responses, required for GPT-5 / o-series tool-calling on OpenAI's own service) and the Chat Completions API (/chat/completions, the broadly-supported OpenAI-compatible wire format).
By default (OPENAI_COMPAT_API=auto) the agent picks Responses when OPENAI_COMPAT_BASE_URL points at an *.openai.com host and Chat Completions everywhere else. Pin the choice explicitly with OPENAI_COMPAT_API=chat or OPENAI_COMPAT_API=responses for providers that diverge from the default (e.g. a Responses-compatible self-hosted gateway).
provider=openrouter is first-class, not routed through provider=openai: it speaks OpenAI's Chat Completions wire format but with OpenRouter-specific extensions layered on top —
reasoning.effortis set on the request when reasoning effort is configured. The request deliberately carries noprovider.require_parametersfilter: that filter routes only to endpoints advertising every parameter in the body, and 83 of 274 tools-capable OpenRouter models do not advertisereasoning, so it turns an effort setting into a hard 404 on a valid model id. A model that cannot reason answers without reasoning instead.- The response's
reasoning_detailsarray (opaque extended-thinking payload) is captured and replayed byte-for-byte on the next turn's assistant message, so multi-turn tool use keeps the model's chain-of-thought. anthropic/*models get Anthropic-stylecache_controlbreakpoints injected on the system message and the last two user messages.- Retryable statuses (429 and typed
provider_overloaded503) honor the documentedRetry-Afterheader (clamped to a small ceiling — seeRETRY_AFTER_CAP_SECSinllm.rs— since the sleep happens outsideBUZZ_AGENT_LLM_TIMEOUT_SECS); 502 and untyped 503 retry with jittered backoff instead.401is treated as an expired/invalid key and refreshed once, while402(no credits) and403(guardrail/moderation/permission) fail immediately without retry.
Provider is a Rust enum with one match in Llm::complete. There is no trait, no Box<dyn>, no async-trait. Adding a provider is a match arm and one body/parse pair in llm.rs.
MCP Servers
The client passes MCP server specs in session/new. The agent spawns each one as a stdio subprocess, calls tools/list, and merges everything into a single tool catalog the LLM sees. Tool names are namespaced as server__tool (double underscore separator). Bare tool names containing __ are rejected at registration.
Example: a single echo MCP server.
{
"jsonrpc": "2.0",
"id": 2,
"method": "session/new",
"params": {
"cwd": "/work",
"mcpServers": [
{
"name": "echo",
"command": "/usr/local/bin/echo-mcp",
"args": ["--mode", "stdio"],
"env": [
{ "name": "ECHO_VERBOSE", "value": "1" }
]
}
]
}
}
Multiple servers: just add more entries. Tool calls fan out to the right server by namespace prefix.
Transport: stdio only. No HTTP, no SSE. We advertise this in agentCapabilities (mcpCapabilities.http: false, mcpCapabilities.sse: false); spec-compliant clients won't ask for what we don't have.
Security Model
The trust boundary is the operator who launched the agent. The harness, MCP server binaries, and API keys are all trusted. Untrusted input — model output, tool results, prompts — is bounded.
| Boundary | Mechanism |
|---|---|
| Stdout discipline | Single-consumer mpsc channel feeding stdout. No two tasks can interleave bytes. All logs go to stderr. |
| MCP child env | Whitelist (PATH, HOME, TERM, LANG, LC_ALL, TMPDIR) plus what the client explicitly passes. Your ANTHROPIC_API_KEY does not leak into MCP children. |
| MCP child lifetime | Process group via setpgid(0,0) in pre_exec. On transport break or shutdown: killpg(SIGKILL). Grandchildren die too. |
| Server poisoning | After a timeout or transport break, the offending server is marked dead. Future calls trigger a lazy restart with exponential backoff. Other servers keep working. |
| Frame size | BUZZ_AGENT_MAX_LINE_BYTES (default 4 MiB). Oversize → connection killed. |
| LLM response size | 16 MiB hard cap. Both Content-Length precheck and streaming-buffer cap. |
| Cancellation | tokio::select! { biased; _ = cancel.changed() => ... } at every loop boundary. Cancel always wins the race. |
| Session isolation | Unlimited concurrent sessions by default (configurable via BUZZ_AGENT_MAX_SESSIONS). One prompt per session at a time. Each session gets its own MCP servers. |
tool_use ↔ tool_result pairing |
Encoded in the type system. Every ToolCall and ToolResult carries a provider_id: String (not Option). |
Bounded Everything
| Limit | Default | Where |
|---|---|---|
| Inbound JSON-RPC frame | 4 MiB | BUZZ_AGENT_MAX_LINE_BYTES |
| Single prompt | 1 MiB | MAX_PROMPT_BYTES |
| History window | 1 MiB | BUZZ_AGENT_MAX_HISTORY_BYTES |
| LLM response body | 16 MiB | MAX_LLM_RESPONSE_BYTES |
| LLM error body | 4 KiB | MAX_LLM_ERROR_BODY_BYTES |
| Tool result body (total, incl. images) | 8 MiB | MAX_TOOL_RESULT_BYTES |
| Tool result text | 50 KiB | BUZZ_AGENT_MAX_TOOL_RESULT_TEXT_BYTES |
| MCP servers / session | 16 | MAX_MCP_SERVERS |
| Tools / session | 128 | MAX_TOOLS_PER_SESSION |
| Tool description bytes | 1 KiB | MAX_DESCRIPTION_BYTES |
| Tool schema bytes | 4 KiB | MAX_SCHEMA_BYTES (oversize → replaced with {}) |
| Tool calls per turn | 64 | MAX_TOOL_CALLS_PER_TURN |
| Loop rounds | 0 (unlimited) | BUZZ_AGENT_MAX_ROUNDS |
| LLM read inactivity timeout | 240 s | BUZZ_AGENT_LLM_TIMEOUT_SECS |
| Tool call timeout | 1260 s | BUZZ_AGENT_TOOL_TIMEOUT_SECS |
What This Is NOT
A short list, because the answer is mostly "no":
- Not a framework. No plugins, no recipes, no slash commands, no modes. MCP servers can participate in agent lifecycle via hook tools (
_Stop,_PostCompact), but these are advisory, fail-open, and budget-bounded — not a plugin system. - Not streaming. One non-streaming HTTP POST per round. The LLM's generated text is forwarded to the client as
agent_message_chunk, but there is no token-level streaming. - Not persistent. Everything is in-memory, per-process. No SQLite. When context fills, the agent summarizes its own history and continues (context handoff). No external persistence.
- Not an SDK. This is a binary. The protocol seam is stdin/stdout. Use it from any language.
- Not a UI. No TUI, no web, no notifications. The client renders.
- Not authenticated. API keys come from env. Use systemd, Docker secrets, or a wrapper.
- Not networked MCP. Stdio transport only. No HTTP/SSE MCP transport.
- Not load-able. No
session/load. We advertiseloadSession: false. - Not a router. No agent-to-agent, no fan-out, no orchestration. One model. One loop.
Concurrency model:
┌──── reader task ──────────┐
│ (stdin → JSON-RPC → ...) │
│ │
stdin ─────────┤ dispatch │
│ │ │
│ ├── initialize │ (sync reply)
│ ├── session/new │ (sync reply)
│ ├── session/prompt ───┼─── spawn ──> prompt task
│ │ │ │
│ ├── session/cancel ───┼─> watch::send│ (biased select wins)
│ │ │ │
└───────────────────────────┘ │
│
┌── writer task ────────────────┐ │
stdout ────────┤ mpsc<WireMsg> consumer │<─────────┘
│ (the only stdout writer) │
└───────────────────────────────┘
One reader, one writer, up to 8 concurrent prompt tasks (one per session).
Building
cargo build --release -p buzz-agent
Testing
cargo test -p buzz-agent
Test strategy is real subprocess, no mocks:
- Fake LLM —
tests/fake_llm.rsand the helpers intests/regressions.rsspin up a realtokio::net::TcpListeneron port 0, parseContent-Length, and return scripted JSON. No HTTP mocking library. - Fake MCP server —
tests/bin/fake_mcp.rsis a separate binary controlled by env vars:FAKE_MCP_HANG_INIT,FAKE_MCP_TOOL_DELAY,FAKE_MCP_SPAWN_GRANDCHILD, etc. Each fault path is a real process being abused. - Regression tests are the changelog. Each
#[test]inregressions.rsis named for the bug it locks down:assistant_text_preserved_across_prompts,cancel_leaves_history_valid_for_next_prompt,mcp_init_timeout_kills_child,oversize_line_kills_connection. Read them in order to learn the protocol's failure modes.