Skip to content
Merged
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
12 changes: 7 additions & 5 deletions crates/buzz-acp/src/base_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@ Run `buzz --help` or `buzz <group> --help` for full usage.

### Threading

- **To a human** (updates, questions, deliverables): Use `--reply-to <thread-root-id>` (from your `[Context]` block) and `@mention` the human. Keeps messages at layer 1 where humans read.
- **To another agent** (dispatching, collaborating): Thread however you want.
- **When in doubt**, reply to thread root.
- **Thread scope:** Respond in the thread where you were tagged. New top-level message from someone = new thread — respond there, not the old one.
- **New topic → new top-level message.** Don't graft unrelated work onto an existing thread.
Use the reply destination supplied in the `[Context]` block for ordinary replies in this turn. Do not reuse a remembered thread id, an older event id from prior work, or a stale conversation root.

For human-facing work, keep the conversation flat and easy to read. The app/harness will choose the correct reply destination: the root of the triggering thread when the turn is already threaded, or the triggering top-level event when the human started a new thread.

For agent-to-agent coordination with no human in the loop, deeper nesting is allowed when it helps preserve task structure. Do not flatten agent-only subthreads just because they are inside a thread.

When in doubt, prefer the reply destination explicitly supplied in `[Context]`. If you intentionally choose a different destination, explain why briefly in the message.

### General

Expand Down
43 changes: 43 additions & 0 deletions crates/buzz-acp/src/pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1627,6 +1627,24 @@ fn collect_prompt_pubkeys(
pubkeys
}

/// Detect whether a kind:0 profile event belongs to an owned agent.
///
/// Agents carry a NIP-OA `["auth", owner_pk, conditions, sig]` tag in their
/// profile; humans do not. This checks for the tag's presence/shape only — a
/// cheap routing heuristic for reply anchoring, not a verified security gate
/// (the signing path in `lib.rs::check_sibling_via_profile` does full
/// verification where it matters).
fn profile_event_is_agent(ev: &serde_json::Value) -> bool {
ev.get("tags")
.and_then(|t| t.as_array())
.is_some_and(|tags| {
tags.iter().any(|tag| {
tag.as_array()
.is_some_and(|parts| parts.len() == 4 && parts[0].as_str() == Some("auth"))
})
})
}

/// Parse kind:0 profile events into a `PromptProfileLookup`.
///
/// Each kind:0 event has `pubkey` and JSON `content` with optional fields:
Expand All @@ -1649,11 +1667,13 @@ fn parse_kind0_profile_lookup(json: serde_json::Value) -> Option<PromptProfileLo
.get("nip05")
.and_then(|v| v.as_str())
.map(str::to_string);
let is_agent = profile_event_is_agent(ev);
lookup.insert(
pk.to_ascii_lowercase(),
PromptProfile {
display_name,
nip05_handle,
is_agent,
},
);
}
Expand Down Expand Up @@ -2784,10 +2804,33 @@ mod tests {
Some(&PromptProfile {
display_name: Some("Wes".into()),
nip05_handle: Some("wes@example.com".into()),
is_agent: false,
})
);
}

#[test]
fn test_profile_event_is_agent_detects_nip_oa_auth_tag() {
// Agent profile carries a 4-element NIP-OA ["auth", owner, cond, sig] tag.
let agent_ev = json!({
"pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"tags": [["auth", "owner_pk", "conditions", "sig"]],
});
assert!(profile_event_is_agent(&agent_ev));

// Human profile: no auth tag.
let human_ev = json!({ "pubkey": "bbbb", "tags": [["t", "topic"]] });
assert!(!profile_event_is_agent(&human_ev));

// Empty / missing tags → not an agent.
assert!(!profile_event_is_agent(&json!({ "tags": [] })));
assert!(!profile_event_is_agent(&json!({})));

// Malformed auth tag (wrong arity) → not treated as an agent.
let malformed = json!({ "tags": [["auth", "owner_pk"]] });
assert!(!profile_event_is_agent(&malformed));
}

#[test]
fn test_parse_kind0_profile_lookup_returns_none_for_empty() {
assert!(parse_kind0_profile_lookup(json!([])).is_none());
Expand Down
Loading
Loading