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`).
|
`services.hyperhive.c0re.operatorPronouns`, default `she/her`).
|
||||||
Passed via `--system-prompt-file`.
|
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,
|
The shared per-turn plumbing lives in `hive_ag3nt::turn::{write_mcp_config,
|
||||||
write_settings, write_system_prompt, run_turn, drive_turn,
|
write_settings, write_system_prompt, run_turn, drive_turn,
|
||||||
emit_turn_end, wait_for_login, compact_session}` so the two binaries
|
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`.
|
/// Install every plugin in `/etc/hyperhive/claude-plugins.json`.
|
||||||
/// Returns a list of human-readable failure messages so the caller can
|
/// Returns a list of human-readable failure messages so the caller can
|
||||||
/// route them through their own per-role surface (turn-failure-style
|
/// route them through their own per-role surface (turn-failure-style
|
||||||
/// notification, see `Surface::send_to_parent`). Pre-#692 this function
|
/// notification, see `Surface::send_to_parent`). Wire-agnostic: the
|
||||||
/// hardcoded `"manager"` as the failure-notification recipient via a
|
/// caller picks the recipient via the same `<parent>` sentinel that
|
||||||
/// `notify_recipient: Option<&str>` arg; mara on #778 wanted the
|
/// failure-notify uses everywhere else.
|
||||||
/// 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).
|
|
||||||
pub async fn install_configured(socket: &Path) -> Vec<String> {
|
pub async fn install_configured(socket: &Path) -> Vec<String> {
|
||||||
let _ = socket; // Reserved for future telemetry; currently unused.
|
let _ = socket; // Reserved for future telemetry; currently unused.
|
||||||
let Ok(raw) = tokio::fs::read_to_string(PLUGINS_PATH).await else {
|
let Ok(raw) = tokio::fs::read_to_string(PLUGINS_PATH).await else {
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,8 @@
|
||||||
//! System-prompt renderer (closes #519).
|
//! System-prompt renderer. Single `prompts/system.md` with
|
||||||
//!
|
//! HTML-comment markers gating role-specific blocks; this module
|
||||||
//! Both flavors (agent / manager) used to live in separate files
|
//! assembles the final prompt for a given flavor. Marker grammar +
|
||||||
//! (`prompts/agent.md`, `prompts/manager.md`) that drifted in lockstep
|
//! placeholder substitution rules in
|
||||||
//! whenever someone updated only one. Now there's a single
|
//! `docs/turn-loop.md::On-boot files` (`claude-system-prompt.md`).
|
||||||
//! `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).
|
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
|
@ -34,31 +11,14 @@ use anyhow::{Context, Result};
|
||||||
use crate::mcp::Flavor;
|
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.
|
/// optional hive / swarm display names. Pure function — no I/O. Splits
|
||||||
/// Pure function — no I/O. Splits out from [`write_system_prompt`] so
|
/// out from [`write_system_prompt`] so the marker logic + substitution
|
||||||
/// the marker logic + substitution is unit-testable in isolation.
|
/// is unit-testable in isolation. The caller supplies the template body
|
||||||
/// The caller supplies the template body so tests can pass an inline
|
/// so tests can pass an inline fixture and production reads it once at
|
||||||
/// fixture and production reads it once at harness startup via
|
/// harness startup via [`hive_sh4re::assets::prompt_template`]
|
||||||
/// [`hive_sh4re::assets::prompt_template`] (`$HIVE_ASSETS_DIR/prompts/
|
/// (`$HIVE_ASSETS_DIR/prompts/system.md`). Substitution placeholders +
|
||||||
/// system.md`).
|
/// marker grammar documented in
|
||||||
///
|
/// `docs/turn-loop.md::On-boot files` (`claude-system-prompt.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\` …`).
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn render(
|
pub fn render(
|
||||||
template: &str,
|
template: &str,
|
||||||
|
|
@ -94,7 +54,7 @@ pub fn render(
|
||||||
/// closer (`<!-- /role:manager -->` inside a `role:agent` block) is
|
/// closer (`<!-- /role:manager -->` inside a `role:agent` block) is
|
||||||
/// elided from the output but does NOT reset the active role — keeps
|
/// elided from the output but does NOT reset the active role — keeps
|
||||||
/// the suppression conservative so a typo can't dump wrong-flavor
|
/// 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 {
|
fn filter_role_blocks(template: &str, target: &str) -> String {
|
||||||
let mut out = String::with_capacity(template.len());
|
let mut out = String::with_capacity(template.len());
|
||||||
// None = outside any block; Some(role) = inside role-tagged block.
|
// 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()
|
template_path.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
// #701 / #709: surface hive + swarm display names in the prompt
|
// Surface hive + swarm display names in the prompt opener when
|
||||||
// opener when configured. Both `None` falls back to the pre-#709
|
// configured. Both `None` falls back to the non-identity wording
|
||||||
// wording verbatim (single-hive deployments see no diff).
|
// verbatim (single-hive deployments see no diff).
|
||||||
let hive_name = crate::identity::hive_name();
|
let hive_name = crate::identity::hive_name();
|
||||||
let swarm_name = crate::identity::swarm_name();
|
let swarm_name = crate::identity::swarm_name();
|
||||||
let body = render(
|
let body = render(
|
||||||
|
|
@ -190,11 +150,11 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
// #555: the production template lives at
|
// The production template lives at `$HIVE_ASSETS_DIR/prompts/system.md`
|
||||||
// `$HIVE_ASSETS_DIR/prompts/system.md` and is loaded at runtime.
|
// and is loaded at runtime. The unit tests below want to assert
|
||||||
// The unit tests below want to assert against the actual production
|
// against the actual production wording (so the renderer + tool
|
||||||
// wording (so the renderer + tool surface stay honest), so they
|
// surface stay honest), so they resolve the same path at test
|
||||||
// resolve the same path at test runtime via two fallbacks:
|
// runtime via two fallbacks:
|
||||||
// 1. `$HIVE_ASSETS_DIR/prompts/system.md` — the runtime contract
|
// 1. `$HIVE_ASSETS_DIR/prompts/system.md` — the runtime contract
|
||||||
// production uses. The flake's `checks.cargo-test` derivation
|
// production uses. The flake's `checks.cargo-test` derivation
|
||||||
// sets this to the `hyperhive-assets` output so `cargo test`
|
// 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
|
// closer inside: the manager-tagged close must NOT pop the agent
|
||||||
// gate, else manager-target output would leak the agent block's
|
// gate, else manager-target output would leak the agent block's
|
||||||
// text (or vice-versa). Stray marker line itself is still elided.
|
// text (or vice-versa). Stray marker line itself is still elided.
|
||||||
// Per argus #527 review nit.
|
|
||||||
let template = "shared\n\
|
let template = "shared\n\
|
||||||
<!-- role:agent -->\n\
|
<!-- role:agent -->\n\
|
||||||
agent line 1\n\
|
agent line 1\n\
|
||||||
|
|
@ -337,7 +296,7 @@ shared closer
|
||||||
// Spot-check: the manager-only tool block (request_init_config,
|
// Spot-check: the manager-only tool block (request_init_config,
|
||||||
// kill, schedule_*) MUST NOT appear in the agent's rendered
|
// kill, schedule_*) MUST NOT appear in the agent's rendered
|
||||||
// prompt. Drift between flavor and tool surface bites every
|
// prompt. Drift between flavor and tool surface bites every
|
||||||
// time it happens (cf. #511 missing-allow-list bug).
|
// time it happens.
|
||||||
let rendered = render(
|
let rendered = render(
|
||||||
&PRODUCTION_TEMPLATE,
|
&PRODUCTION_TEMPLATE,
|
||||||
Flavor::Agent,
|
Flavor::Agent,
|
||||||
|
|
@ -395,14 +354,15 @@ shared closer
|
||||||
assert!(manager.starts_with("You are the hyperhive manager"));
|
assert!(manager.starts_with("You are the hyperhive manager"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inline fixture for the #709 placeholders. Cargo's `cargo test`
|
// Inline fixture for the {hive_identity} / {swarm_identity}
|
||||||
// resolves `PRODUCTION_TEMPLATE` against `$HIVE_ASSETS_DIR/prompts/
|
// placeholders. Cargo's `cargo test` resolves `PRODUCTION_TEMPLATE`
|
||||||
// system.md`, which the flake builds at derivation time — a fresh
|
// against `$HIVE_ASSETS_DIR/prompts/system.md`, which the flake
|
||||||
// placeholder added on the source side isn't in the shipped asset
|
// builds at derivation time — a fresh placeholder added on the
|
||||||
// until the flake rebuilds, so PRODUCTION_TEMPLATE can't be the
|
// source side isn't in the shipped asset until the flake rebuilds,
|
||||||
// fixture here. The string below carries just enough of the
|
// so PRODUCTION_TEMPLATE can't be the fixture here. The string
|
||||||
// opener shape to exercise the substitution logic; nothing here
|
// below carries just enough of the opener shape to exercise the
|
||||||
// depends on the production template's flavor markers.
|
// substitution logic; nothing here depends on the production
|
||||||
|
// template's flavor markers.
|
||||||
const IDENTITY_FIXTURE: &str = "\
|
const IDENTITY_FIXTURE: &str = "\
|
||||||
You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity}{swarm_identity} in a multi-agent system. Pronouns: **{operator_pronouns}**.
|
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]
|
#[test]
|
||||||
fn render_omits_identity_when_unset() {
|
fn render_omits_identity_when_unset() {
|
||||||
// None / None must round-trip the pre-#709 opener verbatim —
|
// None / None must round-trip the non-identity opener verbatim
|
||||||
// single-hive deployments see zero diff.
|
// — single-hive deployments see zero diff.
|
||||||
let rendered = render(
|
let rendered = render(
|
||||||
IDENTITY_FIXTURE,
|
IDENTITY_FIXTURE,
|
||||||
Flavor::Agent,
|
Flavor::Agent,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue