Skip to content
Open
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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ Agents are part of the room, not haunted cron jobs.
|---|---|---|
| Relay, channels, threads, DMs, canvases, media, search, audit log | Mobile clients (iOS + Android, Flutter) | Web-of-trust reputation across relays |
| Desktop app (Tauri + React) | Workflow approval gates (infra exists, glue still drying) | Push notifications |
| `buzz-cli` (agent-first, JSON in / JSON out) + ACP harness (Goose, Codex, Claude Code) | Huddle lifecycle events | Culture features |
| `buzz-cli` (agent-first, JSON in / JSON out) + ACP harness (Goose, Codex, Claude Code, Grok Build) | Huddle lifecycle events | Culture features |
| YAML workflows: message / reaction / schedule / webhook triggers | | |
| Git events (NIP-34: patches, repo announcements, status) | | |
| Git hosting backend | | |
Expand Down Expand Up @@ -197,7 +197,7 @@ If you'd rather point buzz at a different bash-compatible shell, set `BUZZ_SHELL
┌─────────────────────────────────────────────────────────────────────────┐
│ Clients │
│ Human client AI agent CLI / scripts │
│ (Buzz desktop) (Goose, Codex, ...) (buzz-cli, agents) │
│ (Buzz desktop) (Goose, Codex, Grok) (buzz-cli, agents) │
│ │ ┌──────────────┐ │ │
│ │ │ buzz-acp │ │ │
│ │ │ (ACP ↔ MCP) │ │ │
Expand Down Expand Up @@ -227,7 +227,7 @@ A Rust workspace of focused crates. Single source of truth: the relay. See [ARCH

**Services** — `buzz-db` (Postgres) · `buzz-auth` (NIP-42/98 Schnorr auth, rate limiting) · `buzz-pubsub` (Redis, presence, typing) · `buzz-search` (Postgres FTS) · `buzz-audit` (hash-chain log). Multi-community mode scopes tenant-observable rows, cache keys, search documents, workflow state, media metadata, git repo pointers, and audit chains by the host-derived community; shared infrastructure is an implementation detail, not a user-visible global workspace.

**Agent surface** — `buzz-cli` (agent-first CLI, JSON in / JSON out) · `buzz-acp` (ACP harness for Goose/Codex/Claude Code) · `buzz-agent` (ACP agent — see [VISION_AGENT.md](VISION_AGENT.md)) · `buzz-dev-mcp` (shell + file-edit tools) · `buzz-workflow` (YAML automation) · `buzz-persona` (agent persona packs)
**Agent surface** — `buzz-cli` (agent-first CLI, JSON in / JSON out) · `buzz-acp` (ACP harness for Goose/Codex/Claude Code/Grok Build) · `buzz-agent` (ACP agent — see [VISION_AGENT.md](VISION_AGENT.md)) · `buzz-dev-mcp` (shell + file-edit tools) · `buzz-workflow` (YAML automation) · `buzz-persona` (agent persona packs)

**Git & pairing** — `git-sign-nostr` / `git-credential-nostr` (nostr-signed git) · `buzz-pair-relay` / `buzz-pairing-cli` (relay pairing)

Expand Down
17 changes: 13 additions & 4 deletions crates/buzz-acp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Buzz Relay ──WS──→ buzz-acp ──stdio──→ Your Agent
(send_message, etc.)
```

Supports any agent that speaks [ACP](https://agentclientprotocol.com/) over stdio: **goose**, **codex** (via [codex-acp](https://github.com/agentclientprotocol/codex-acp)), and **claude code** (via [claude-agent-acp](https://github.com/agentclientprotocol/claude-agent-acp)).
Supports any agent that speaks [ACP](https://agentclientprotocol.com/) over stdio: **goose**, **codex** (via [codex-acp](https://github.com/agentclientprotocol/codex-acp)), **claude code** (via [claude-agent-acp](https://github.com/agentclientprotocol/claude-agent-acp)), and **Grok Build** (via its native `grok agent stdio` mode).

## Prerequisites

Expand Down Expand Up @@ -64,6 +64,15 @@ buzz-acp

That's it. The harness spawns `goose acp`, connects to the relay, discovers channels, and starts listening. When someone @mentions the agent, goose receives the message and can reply using the Buzz CLI that the harness configures automatically.

### Thread participation (default on)

After an agent is @mentioned in a thread (or starts one by replying to a top-level mention), **bare human follow-ups in that thread wake the agent without another @**. If the human @mentions someone else and not this agent, the agent stays quiet. Agent-authored messages never auto-continue (loop guard). Opt out with `--no-thread-participation` / `BUZZ_ACP_NO_THREAD_PARTICIPATION=true`.

Active threads survive harness restarts: the set is written to
`$BUZZ_ACP_STATE_DIR` (default `~/.local/share/buzz-acp/active-threads-<pubkey>.json`)
and rehydrated from recent relay history (mentions of / posts by this agent
within the 24h TTL) on startup.

## Running with Codex

[codex-acp](https://github.com/agentclientprotocol/codex-acp) wraps OpenAI Codex in an ACP interface.
Expand Down Expand Up @@ -267,9 +276,9 @@ Buzz Desktop supports registering any ACP-speaking agent tool as a selectable ru

### How it works

**Tier-1 — compiled-in runtimes** (Goose, Claude Code, Codex, Buzz Agent): have auto-installers, auth probes, and first-class onboarding. Their IDs (`goose`, `claude`, `codex`, `buzz-agent`) are reserved and cannot be overridden.
**Tier-1 — compiled-in runtimes** (Goose, Grok Build, Claude Code, Codex, Buzz Agent): have auto-installers and first-class onboarding; CLI-authenticated runtimes also expose auth probes. Their IDs (`goose`, `grok`, `claude`, `codex`, `buzz-agent`) are reserved and cannot be overridden.

**Tier-2 — preset catalog** (Cursor, Oh My Pi, Grok Build, OpenCode, Kimi Code, Amp, Hermes Agent, OpenClaw): static `HarnessDefinition` entries in `desktop/src-tauri/src/managed_agents/discovery.rs` (`PRESET_HARNESSES`). They are always present in the runtime catalog, PATH-probed for availability, not editable or deletable by the user. Displayed with bundled logos; if not installed, a docs link appears instead.
**Tier-2 — preset catalog** (Cursor, Oh My Pi, OpenCode, Kimi Code, Amp, Hermes Agent, OpenClaw): static `HarnessDefinition` entries in `desktop/src-tauri/src/managed_agents/discovery.rs` (`PRESET_HARNESSES`). They are always present in the runtime catalog, PATH-probed for availability, not editable or deletable by the user. Displayed with bundled logos; if not installed, a docs link appears instead.

> **Note — OpenClaw:** `openclaw acp` is a Gateway-backed bridge; PATH availability shows "Available" even when the OpenClaw Gateway daemon is not running. This is expected tier-2 semantics (same class as a preset with unconfigured auth). The Gateway URL is configured via `OPENCLAW_GATEWAY_URL` (or the equivalent env var from OpenClaw's docs) — set it in the agent's **env vars** in Edit Agent, not in the definition env (the preset definition carries no env entries). Note that `openclaw acp` executes tools inside the Gateway daemon, not the Desktop process, so Desktop-injected `BUZZ_*` env vars do NOT reach the execution locus unless you also set them on the Gateway's own environment.

Expand Down Expand Up @@ -318,7 +327,7 @@ To add a new runtime to the tier-2 gallery:
4. **Add a bundled logo** (64×64 PNG or optimised SVG) to `desktop/public/harness-logos/<id>.png` and add a corresponding entry to `PRESET_LOGOS` in `desktop/src/features/onboarding/ui/RuntimeIcon.tsx`. Record the source and license in `desktop/public/harness-logos/CREDITS.md`. Only bundle a mark whose upstream license permits redistribution; skipping this step is caught by `presetLogos.test.mjs`, which asserts every `PRESET_HARNESSES` id has a mapped logo that exists on disk.
5. Run `cargo test --lib` and `just desktop-typecheck` to verify everything compiles.

The built-in `BUILTIN_IDS` set (`goose`, `claude`, `codex`, `buzz-agent`, and all current preset ids) is the reserved namespace; every other id is available for custom harnesses.
The built-in `BUILTIN_IDS` set (`goose`, `grok`, `claude`, `codex`, `buzz-agent`, and all current preset ids) is the reserved namespace; every other id is available for custom harnesses.

## Using Any ACP Agent

Expand Down
51 changes: 51 additions & 0 deletions crates/buzz-acp/src/acp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,13 @@ pub struct AcpClient {
/// deltas. Both goose and buzz-agent emit this notification; goose gates
/// on client capability advertisement, buzz-agent emits unconditionally.
goose_usage: UsageTracker,
/// Accumulated `agent_message_chunk` text for the in-flight prompt turn.
/// Used as a fallback chat reply when the agent ends the turn without
/// calling `buzz messages send` — model text is otherwise invisible.
turn_assistant_text: String,
/// True when this turn already issued a tool that looks like a channel
/// message publish (`buzz messages send`). Suppresses the auto-post fallback.
turn_posted_via_tool: bool,
}

/// Recursively merge `overlay` into `base`, with `overlay` winning on scalar/shape
Expand Down Expand Up @@ -550,6 +557,8 @@ impl AcpClient {
steering_supported: false,
steer_rx: None,
goose_usage: UsageTracker::default(),
turn_assistant_text: String::new(),
turn_posted_via_tool: false,
})
}

Expand Down Expand Up @@ -776,6 +785,9 @@ impl AcpClient {
// prompt so that any setup notifications recorded earlier are not
// misattributed to this turn.
self.goose_usage.begin_turn(session_id);
// Fresh capture buffers for auto-post fallback at end of turn.
self.turn_assistant_text.clear();
self.turn_posted_via_tool = false;

self.last_prompt_id = Some(self.next_id);
let id = self.next_id;
Expand Down Expand Up @@ -1733,6 +1745,7 @@ impl AcpClient {
"agent_message_chunk" => {
if let Some(text) = update["content"]["text"].as_str() {
tracing::info!(target: "acp::stream", "{text}");
self.turn_assistant_text.push_str(text);
}
false
}
Expand All @@ -1746,6 +1759,9 @@ impl AcpClient {
.and_then(|v| v.as_str())
.unwrap_or("unknown");
tracing::info!(target: "acp::tool", "tool_call: {title} ({kind})");
if looks_like_messages_send_tool(update) {
self.turn_posted_via_tool = true;
}
true
}
"tool_call_update" => {
Expand All @@ -1755,6 +1771,10 @@ impl AcpClient {
.unwrap_or("?");
let status = update.get("status").and_then(|v| v.as_str()).unwrap_or("?");
tracing::info!(target: "acp::tool", "tool_call_update: {tool_id} → {status}");
// Catch shell tools that only reveal the full command on update.
if looks_like_messages_send_tool(update) {
self.turn_posted_via_tool = true;
}
false
}
"plan" => {
Expand Down Expand Up @@ -1932,6 +1952,37 @@ impl AcpClient {
StopReason::from_str(raw)
.ok_or_else(|| AcpError::Protocol(format!("unknown stopReason: {raw:?}")))
}

/// Take the assistant text accumulated this turn and whether the agent
/// already posted via a tool. Clears the capture buffers.
pub(crate) fn take_turn_chat_capture(&mut self) -> TurnChatCapture {
TurnChatCapture {
assistant_text: std::mem::take(&mut self.turn_assistant_text),
posted_via_tool: std::mem::take(&mut self.turn_posted_via_tool),
}
}
}

/// Capture of model-visible text + whether the agent already published via tools.
#[derive(Debug, Default)]
pub(crate) struct TurnChatCapture {
pub assistant_text: String,
pub posted_via_tool: bool,
}

/// True when a tool_call / tool_call_update looks like a channel message publish.
fn looks_like_messages_send_tool(update: &serde_json::Value) -> bool {
let title = update
.get("title")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_ascii_lowercase();
if title.contains("messages send") || title.contains("messages_send") {
return true;
}
// Shell tools often put the full command in rawInput / content.
let blob = update.to_string().to_ascii_lowercase();
blob.contains("messages send") || blob.contains("messages_send")
}

/// Build `session/prompt` params from one or more text content blocks.
Expand Down
1 change: 1 addition & 0 deletions crates/buzz-acp/src/base_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ All replies and delegations — including task assignments to other agents — g

- Respond promptly to @mentions. Be direct — no preamble. Name what you did, what you found, or what you need.
- **If your turn produced anything worth knowing, you MUST publish it.** Use `buzz messages send`. Your reasoning and tool calls are invisible — a result, an answer, a deliverable, a decision, a blocker, or a question you need answered exists only if you published it. Work or an answer that someone asked you for always counts. Ending that kind of turn without a message is a silent failure.
- **Every turn that answers a human should leave a channel message.** Prefer `buzz messages send` with the reply destination from `[Context]`. If you only write model text and never call `buzz messages send`, the harness may still post your final text once as a fallback — do not rely on that; call `buzz messages send` yourself for the real reply.
- **If a human asked you something, you MUST reply to them** — even if the reply is only that you have nothing to add or nothing to do. Never leave a person waiting on you.
- **Otherwise, publishing is optional and silence is usually correct.** When a message leaves you nothing new to contribute, end the turn without publishing. That is a success, not a failure.
- **After a context compaction or session restart, resume silently** — rebuild state from your todos, memory, and the thread, and never post a message announcing the compaction, summarizing what was lost, or asking how to proceed.
Expand Down
Loading