prompt: thread hive + swarm display names into system prompt opener (#709)
This commit is contained in:
parent
37d99ed118
commit
3323bca677
2 changed files with 185 additions and 16 deletions
|
|
@ -1,8 +1,8 @@
|
|||
<!-- role:agent -->
|
||||
You are hyperhive agent `{label}` (qualified: `{qualified_label}`) in a multi-agent system. The operator (recipient `operator` in `send`, the human at the dashboard) uses **{operator_pronouns}** pronouns — use them naturally when you refer to them in third person (e.g. when relaying to a peer or the manager). When you're talking to or about a peer on a different hive, use the qualified form (`name@hive`) so the operator + the manager can disambiguate; within your own hive the short form is fine.
|
||||
You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity}{swarm_identity} in a multi-agent system. The operator (recipient `operator` in `send`, the human at the dashboard) uses **{operator_pronouns}** pronouns — use them naturally when you refer to them in third person (e.g. when relaying to a peer or the manager). When you're talking to or about a peer on a different hive, use the qualified form (`name@hive`) so the operator + the manager can disambiguate; within your own hive the short form is fine.
|
||||
<!-- /role:agent -->
|
||||
<!-- role:manager -->
|
||||
You are the hyperhive manager `{label}` (qualified: `{qualified_label}`) in a multi-agent system. You coordinate sub-agents and relay between them and the operator. The operator (recipient `operator`, the human at the dashboard) uses **{operator_pronouns}** pronouns — use them naturally when you refer to them in third person. When you're talking to or about a peer on a different hive, use the qualified form (`name@hive`); within your own hive the short form is fine.
|
||||
You are the hyperhive manager `{label}` (qualified: `{qualified_label}`){hive_identity}{swarm_identity} in a multi-agent system. You coordinate sub-agents and relay between them and the operator. The operator (recipient `operator`, the human at the dashboard) uses **{operator_pronouns}** pronouns — use them naturally when you refer to them in third person. When you're talking to or about a peer on a different hive, use the qualified form (`name@hive`); within your own hive the short form is fine.
|
||||
<!-- /role:manager -->
|
||||
|
||||
Tools (hyperhive surface):
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@ use anyhow::{Context, Result};
|
|||
|
||||
use crate::mcp::Flavor;
|
||||
|
||||
/// Assemble the system prompt for a given flavor + label + pronouns.
|
||||
/// Assemble the system prompt for a given flavor + label + pronouns +
|
||||
/// optional hive / swarm display names.
|
||||
/// Pure function — no I/O. Splits out from [`write_system_prompt`] so
|
||||
/// the marker logic + substitution is unit-testable in isolation.
|
||||
/// The caller supplies the template body so tests can pass an inline
|
||||
|
|
@ -41,23 +42,49 @@ use crate::mcp::Flavor;
|
|||
/// [`hive_sh4re::assets::prompt_template`] (`$HIVE_ASSETS_DIR/prompts/
|
||||
/// system.md`).
|
||||
///
|
||||
/// `{label}` and `{operator_pronouns}` are substituted in the filtered body.
|
||||
/// `{qualified_label}` (#589) is also substituted — it's `${label}@${hive}`
|
||||
/// in federated deployments, or the same as `{label}` when no hive domain is
|
||||
/// configured (single-hive deployments). Templates that always want the
|
||||
/// fully-qualified form can use `{qualified_label}` and stay correct in
|
||||
/// both shapes.
|
||||
/// Substitutions:
|
||||
///
|
||||
/// - `{label}` — short hive-local name (e.g. `iris`).
|
||||
/// - `{qualified_label}` (#589) — `${label}@${domain}` in federated
|
||||
/// deployments, same as `{label}` when no hive domain is configured.
|
||||
/// - `{operator_pronouns}` — `"she/her"` / `"they/them"` / etc.
|
||||
/// - `{hive_identity}` (#701 / #709) — ` on hive \`pr1ma\`` (with
|
||||
/// leading space + backticks) when `hive_name` is `Some`, **empty
|
||||
/// string** when `None`. Lets the template drop the names into the
|
||||
/// opener prose without breaking single-hive deployments that
|
||||
/// never set the option.
|
||||
/// - `{swarm_identity}` (#701 / #709) — same shape for the swarm:
|
||||
/// ` in swarm \`constellat1on\`` when `Some`, empty when `None`.
|
||||
///
|
||||
/// Both `hive_name` / `swarm_name` are independent: setting just one
|
||||
/// surfaces only that clause; setting both yields the full prose
|
||||
/// (`… on hive \`pr1ma\` in swarm \`constellat1on\` …`).
|
||||
#[must_use]
|
||||
pub fn render(template: &str, flavor: Flavor, label: &str, operator_pronouns: &str) -> String {
|
||||
pub fn render(
|
||||
template: &str,
|
||||
flavor: Flavor,
|
||||
label: &str,
|
||||
operator_pronouns: &str,
|
||||
hive_name: Option<&str>,
|
||||
swarm_name: Option<&str>,
|
||||
) -> String {
|
||||
let target = match flavor {
|
||||
Flavor::Agent => "agent",
|
||||
Flavor::Manager => "manager",
|
||||
};
|
||||
let body = filter_role_blocks(template, target);
|
||||
let qualified = crate::identity::qualify(label);
|
||||
let hive_identity = hive_name
|
||||
.filter(|n| !n.is_empty())
|
||||
.map_or(String::new(), |n| format!(" on hive `{n}`"));
|
||||
let swarm_identity = swarm_name
|
||||
.filter(|n| !n.is_empty())
|
||||
.map_or(String::new(), |n| format!(" in swarm `{n}`"));
|
||||
body.replace("{label}", label)
|
||||
.replace("{qualified_label}", &qualified)
|
||||
.replace("{operator_pronouns}", operator_pronouns)
|
||||
.replace("{hive_identity}", &hive_identity)
|
||||
.replace("{swarm_identity}", &swarm_identity)
|
||||
}
|
||||
|
||||
/// Walk `template` line-by-line. Inside a `<!-- role:X -->` block,
|
||||
|
|
@ -139,7 +166,19 @@ pub async fn write_system_prompt(_socket: &Path, label: &str, flavor: Flavor) ->
|
|||
template_path.display()
|
||||
)
|
||||
})?;
|
||||
let body = render(&template, flavor, label, &pronouns);
|
||||
// #701 / #709: surface hive + swarm display names in the prompt
|
||||
// opener when configured. Both `None` falls back to the pre-#709
|
||||
// wording verbatim (single-hive deployments see no diff).
|
||||
let hive_name = crate::identity::hive_name();
|
||||
let swarm_name = crate::identity::swarm_name();
|
||||
let body = render(
|
||||
&template,
|
||||
flavor,
|
||||
label,
|
||||
&pronouns,
|
||||
hive_name.as_deref(),
|
||||
swarm_name.as_deref(),
|
||||
);
|
||||
let path = parent.join("claude-system-prompt.md");
|
||||
tokio::fs::write(&path, body).await?;
|
||||
tracing::info!(path = %path.display(), "wrote claude system prompt");
|
||||
|
|
@ -279,7 +318,14 @@ shared closer
|
|||
// Real template's first agent line — keeps the renderer
|
||||
// honest about the {label} / {operator_pronouns} pair the
|
||||
// harness already relied on.
|
||||
let rendered = render(&PRODUCTION_TEMPLATE, Flavor::Agent, "alice", "they/them");
|
||||
let rendered = render(
|
||||
&PRODUCTION_TEMPLATE,
|
||||
Flavor::Agent,
|
||||
"alice",
|
||||
"they/them",
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert!(rendered.contains("hyperhive agent `alice`"));
|
||||
assert!(rendered.contains("**they/them** pronouns"));
|
||||
assert!(!rendered.contains("{label}"));
|
||||
|
|
@ -292,7 +338,14 @@ shared closer
|
|||
// kill, schedule_*) MUST NOT appear in the agent's rendered
|
||||
// prompt. Drift between flavor and tool surface bites every
|
||||
// time it happens (cf. #511 missing-allow-list bug).
|
||||
let rendered = render(&PRODUCTION_TEMPLATE, Flavor::Agent, "alice", "she/her");
|
||||
let rendered = render(
|
||||
&PRODUCTION_TEMPLATE,
|
||||
Flavor::Agent,
|
||||
"alice",
|
||||
"she/her",
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert!(!rendered.contains("request_init_config"));
|
||||
assert!(!rendered.contains("request_apply_commit"));
|
||||
assert!(!rendered.contains("get_logs"));
|
||||
|
|
@ -303,7 +356,14 @@ shared closer
|
|||
|
||||
#[test]
|
||||
fn render_manager_includes_manager_only_tools() {
|
||||
let rendered = render(&PRODUCTION_TEMPLATE, Flavor::Manager, "hm1nd", "she/her");
|
||||
let rendered = render(
|
||||
&PRODUCTION_TEMPLATE,
|
||||
Flavor::Manager,
|
||||
"hm1nd",
|
||||
"she/her",
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert!(rendered.contains("request_init_config"));
|
||||
assert!(rendered.contains("request_apply_commit"));
|
||||
assert!(rendered.contains("get_logs"));
|
||||
|
|
@ -315,9 +375,118 @@ shared closer
|
|||
|
||||
#[test]
|
||||
fn render_uses_correct_role_opener() {
|
||||
let agent = render(&PRODUCTION_TEMPLATE, Flavor::Agent, "alice", "she/her");
|
||||
let agent = render(
|
||||
&PRODUCTION_TEMPLATE,
|
||||
Flavor::Agent,
|
||||
"alice",
|
||||
"she/her",
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert!(agent.starts_with("You are hyperhive agent"));
|
||||
let manager = render(&PRODUCTION_TEMPLATE, Flavor::Manager, "hm1nd", "she/her");
|
||||
let manager = render(
|
||||
&PRODUCTION_TEMPLATE,
|
||||
Flavor::Manager,
|
||||
"hm1nd",
|
||||
"she/her",
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert!(manager.starts_with("You are the hyperhive manager"));
|
||||
}
|
||||
|
||||
// Inline fixture for the #709 placeholders. Cargo's `cargo test`
|
||||
// resolves `PRODUCTION_TEMPLATE` against `$HIVE_ASSETS_DIR/prompts/
|
||||
// system.md`, which the flake builds at derivation time — a fresh
|
||||
// placeholder added on the source side isn't in the shipped asset
|
||||
// until the flake rebuilds, so PRODUCTION_TEMPLATE can't be the
|
||||
// fixture here. The string below carries just enough of the
|
||||
// opener shape to exercise the substitution logic; nothing here
|
||||
// depends on the production template's flavor markers.
|
||||
const IDENTITY_FIXTURE: &str = "\
|
||||
You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity}{swarm_identity} in a multi-agent system. Pronouns: **{operator_pronouns}**.
|
||||
";
|
||||
|
||||
#[test]
|
||||
fn render_substitutes_hive_identity_when_set() {
|
||||
let rendered = render(
|
||||
IDENTITY_FIXTURE,
|
||||
Flavor::Agent,
|
||||
"alice",
|
||||
"she/her",
|
||||
Some("pr1ma"),
|
||||
None,
|
||||
);
|
||||
assert!(rendered.contains("on hive `pr1ma`"), "{rendered}");
|
||||
// swarm clause stays absent when only hive is set.
|
||||
assert!(!rendered.contains("in swarm"));
|
||||
// No raw placeholder leaks.
|
||||
assert!(!rendered.contains("{hive_identity}"));
|
||||
assert!(!rendered.contains("{swarm_identity}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_substitutes_swarm_identity_when_set() {
|
||||
let rendered = render(
|
||||
IDENTITY_FIXTURE,
|
||||
Flavor::Manager,
|
||||
"hm1nd",
|
||||
"she/her",
|
||||
None,
|
||||
Some("constellat1on"),
|
||||
);
|
||||
assert!(rendered.contains("in swarm `constellat1on`"));
|
||||
assert!(!rendered.contains("on hive"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_substitutes_both_when_both_set() {
|
||||
let rendered = render(
|
||||
IDENTITY_FIXTURE,
|
||||
Flavor::Agent,
|
||||
"iris",
|
||||
"she/her",
|
||||
Some("pr1ma"),
|
||||
Some("constellat1on"),
|
||||
);
|
||||
// Order: hive then swarm, both inline before "in a multi-agent
|
||||
// system" — keeps the opener grammar intact.
|
||||
assert!(rendered.contains("on hive `pr1ma` in swarm `constellat1on`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_omits_identity_when_unset() {
|
||||
// None / None must round-trip the pre-#709 opener verbatim —
|
||||
// single-hive deployments see zero diff.
|
||||
let rendered = render(
|
||||
IDENTITY_FIXTURE,
|
||||
Flavor::Agent,
|
||||
"alice",
|
||||
"she/her",
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert!(!rendered.contains("on hive"));
|
||||
assert!(!rendered.contains("in swarm"));
|
||||
assert!(!rendered.contains("{hive_identity}"));
|
||||
assert!(!rendered.contains("{swarm_identity}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_treats_empty_identity_as_none() {
|
||||
// Defensive: an env var set to empty string round-trips
|
||||
// through `identity::hive_name()` as None (the accessor
|
||||
// filters empty), but `render` should still no-op on a
|
||||
// direct `Some("")` from a test fixture or a future caller.
|
||||
let rendered = render(
|
||||
IDENTITY_FIXTURE,
|
||||
Flavor::Agent,
|
||||
"alice",
|
||||
"she/her",
|
||||
Some(""),
|
||||
Some(""),
|
||||
);
|
||||
assert!(!rendered.contains("on hive"));
|
||||
assert!(!rendered.contains("in swarm"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue