hive-ag3nt + docs: extract prompt-rendering prose (#716 batch 3)
This commit is contained in:
parent
2bb8f2a3f7
commit
84f3fe5f69
3 changed files with 57 additions and 80 deletions
|
|
@ -187,6 +187,26 @@ socket at `/run/hive/` once at startup:
|
|||
`services.hyperhive.c0re.operatorPronouns`, default `she/her`).
|
||||
Passed via `--system-prompt-file`.
|
||||
|
||||
**Marker grammar.** `<!-- role:X -->` opens a block; matching
|
||||
`<!-- /role:X -->` closes it. Nesting is NOT supported — a stray
|
||||
opener overrides until its closing tag (or end of file). A
|
||||
mismatched closer (`<!-- /role:manager -->` inside a `role:agent`
|
||||
block) is elided from the output but does NOT pop the active
|
||||
role: suppression stays conservative so a typo can't dump
|
||||
wrong-flavor content. Whitespace inside markers is tolerated
|
||||
(`<!--role:foo-->` parses the same as `<!-- role:foo -->`).
|
||||
Content outside any marker is always shared.
|
||||
|
||||
**`hive_identity` / `swarm_identity` shape.** Each carries a
|
||||
leading space + backticked name (` on hive \`pr1ma\``,
|
||||
` in swarm \`constellat1on\``) when the corresponding env var
|
||||
is set, otherwise empty string. The independence lets the
|
||||
template drop one or both into the opener prose without
|
||||
breaking single-hive deployments that never set the option;
|
||||
the renderer also treats `Some("")` from a caller as `None` so
|
||||
empty-string env vars and missing env vars round-trip the
|
||||
same way.
|
||||
|
||||
The shared per-turn plumbing lives in `hive_ag3nt::turn::{write_mcp_config,
|
||||
write_settings, write_system_prompt, run_turn, drive_turn,
|
||||
emit_turn_end, wait_for_login, compact_session}` so the two binaries
|
||||
|
|
|
|||
|
|
@ -100,12 +100,9 @@ async fn update_marketplaces() {
|
|||
/// Install every plugin in `/etc/hyperhive/claude-plugins.json`.
|
||||
/// Returns a list of human-readable failure messages so the caller can
|
||||
/// route them through their own per-role surface (turn-failure-style
|
||||
/// notification, see `Surface::send_to_parent`). Pre-#692 this function
|
||||
/// hardcoded `"manager"` as the failure-notification recipient via a
|
||||
/// `notify_recipient: Option<&str>` arg; mara on #778 wanted the
|
||||
/// manager-name special case gone. Now plugins.rs is wire-agnostic and
|
||||
/// the caller picks the recipient via the same `<parent>` sentinel
|
||||
/// failure-notify uses everywhere else (#703).
|
||||
/// notification, see `Surface::send_to_parent`). Wire-agnostic: the
|
||||
/// caller picks the recipient via the same `<parent>` sentinel that
|
||||
/// failure-notify uses everywhere else.
|
||||
pub async fn install_configured(socket: &Path) -> Vec<String> {
|
||||
let _ = socket; // Reserved for future telemetry; currently unused.
|
||||
let Ok(raw) = tokio::fs::read_to_string(PLUGINS_PATH).await else {
|
||||
|
|
|
|||
|
|
@ -1,31 +1,8 @@
|
|||
//! System-prompt renderer (closes #519).
|
||||
//!
|
||||
//! Both flavors (agent / manager) used to live in separate files
|
||||
//! (`prompts/agent.md`, `prompts/manager.md`) that drifted in lockstep
|
||||
//! whenever someone updated only one. Now there's a single
|
||||
//! `prompts/system.md` with HTML-comment markers gating role-specific
|
||||
//! blocks; this module assembles the final prompt for a given flavor.
|
||||
//!
|
||||
//! Marker syntax (HTML comments — invisible in rendered markdown,
|
||||
//! distinct from `{label}` / `{operator_pronouns}` placeholders):
|
||||
//!
|
||||
//! ```text
|
||||
//! <!-- role:agent -->
|
||||
//! sub-agent-only paragraph
|
||||
//! <!-- /role:agent -->
|
||||
//!
|
||||
//! shared paragraph
|
||||
//!
|
||||
//! <!-- role:manager -->
|
||||
//! manager-only paragraph
|
||||
//! <!-- /role:manager -->
|
||||
//! ```
|
||||
//!
|
||||
//! Content outside any marker is shared. Nesting is NOT supported;
|
||||
//! a stray opener overrides until its closing tag (or end of file).
|
||||
//! When #513 lands the marker grammar can grow `cap:<group>` blocks
|
||||
//! the same way without touching the renderer surface (the role
|
||||
//! distinction folds into a per-cap-set lookup).
|
||||
//! System-prompt renderer. Single `prompts/system.md` with
|
||||
//! HTML-comment markers gating role-specific blocks; this module
|
||||
//! assembles the final prompt for a given flavor. Marker grammar +
|
||||
//! placeholder substitution rules in
|
||||
//! `docs/turn-loop.md::On-boot files` (`claude-system-prompt.md`).
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
|
|
@ -34,31 +11,14 @@ use anyhow::{Context, Result};
|
|||
use crate::mcp::Flavor;
|
||||
|
||||
/// 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
|
||||
/// fixture and production reads it once at harness startup via
|
||||
/// [`hive_sh4re::assets::prompt_template`] (`$HIVE_ASSETS_DIR/prompts/
|
||||
/// system.md`).
|
||||
///
|
||||
/// 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\` …`).
|
||||
/// 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 fixture and production reads it once at
|
||||
/// harness startup via [`hive_sh4re::assets::prompt_template`]
|
||||
/// (`$HIVE_ASSETS_DIR/prompts/system.md`). Substitution placeholders +
|
||||
/// marker grammar documented in
|
||||
/// `docs/turn-loop.md::On-boot files` (`claude-system-prompt.md`).
|
||||
#[must_use]
|
||||
pub fn render(
|
||||
template: &str,
|
||||
|
|
@ -94,7 +54,7 @@ pub fn render(
|
|||
/// closer (`<!-- /role:manager -->` inside a `role:agent` block) is
|
||||
/// elided from the output but does NOT reset the active role — keeps
|
||||
/// the suppression conservative so a typo can't dump wrong-flavor
|
||||
/// content (per argus #527 review nit).
|
||||
/// content.
|
||||
fn filter_role_blocks(template: &str, target: &str) -> String {
|
||||
let mut out = String::with_capacity(template.len());
|
||||
// None = outside any block; Some(role) = inside role-tagged block.
|
||||
|
|
@ -166,9 +126,9 @@ pub async fn write_system_prompt(_socket: &Path, label: &str, flavor: Flavor) ->
|
|||
template_path.display()
|
||||
)
|
||||
})?;
|
||||
// #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).
|
||||
// Surface hive + swarm display names in the prompt opener when
|
||||
// configured. Both `None` falls back to the non-identity 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(
|
||||
|
|
@ -190,11 +150,11 @@ mod tests {
|
|||
use super::*;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
// #555: the production template lives at
|
||||
// `$HIVE_ASSETS_DIR/prompts/system.md` and is loaded at runtime.
|
||||
// The unit tests below want to assert against the actual production
|
||||
// wording (so the renderer + tool surface stay honest), so they
|
||||
// resolve the same path at test runtime via two fallbacks:
|
||||
// The production template lives at `$HIVE_ASSETS_DIR/prompts/system.md`
|
||||
// and is loaded at runtime. The unit tests below want to assert
|
||||
// against the actual production wording (so the renderer + tool
|
||||
// surface stay honest), so they resolve the same path at test
|
||||
// runtime via two fallbacks:
|
||||
// 1. `$HIVE_ASSETS_DIR/prompts/system.md` — the runtime contract
|
||||
// production uses. The flake's `checks.cargo-test` derivation
|
||||
// sets this to the `hyperhive-assets` output so `cargo test`
|
||||
|
|
@ -278,7 +238,6 @@ shared closer
|
|||
// closer inside: the manager-tagged close must NOT pop the agent
|
||||
// gate, else manager-target output would leak the agent block's
|
||||
// text (or vice-versa). Stray marker line itself is still elided.
|
||||
// Per argus #527 review nit.
|
||||
let template = "shared\n\
|
||||
<!-- role:agent -->\n\
|
||||
agent line 1\n\
|
||||
|
|
@ -337,7 +296,7 @@ shared closer
|
|||
// Spot-check: the manager-only tool block (request_init_config,
|
||||
// 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).
|
||||
// time it happens.
|
||||
let rendered = render(
|
||||
&PRODUCTION_TEMPLATE,
|
||||
Flavor::Agent,
|
||||
|
|
@ -395,14 +354,15 @@ shared closer
|
|||
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.
|
||||
// Inline fixture for the {hive_identity} / {swarm_identity}
|
||||
// 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}**.
|
||||
";
|
||||
|
|
@ -456,8 +416,8 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
|||
|
||||
#[test]
|
||||
fn render_omits_identity_when_unset() {
|
||||
// None / None must round-trip the pre-#709 opener verbatim —
|
||||
// single-hive deployments see zero diff.
|
||||
// None / None must round-trip the non-identity opener verbatim
|
||||
// — single-hive deployments see zero diff.
|
||||
let rendered = render(
|
||||
IDENTITY_FIXTURE,
|
||||
Flavor::Agent,
|
||||
|
|
|
|||
Loading…
Reference in a new issue