mirror of
https://github.com/block/buzz.git
synced 2026-10-28 04:51:40 +00:00
Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
137 lines
6.4 KiB
Rust
137 lines
6.4 KiB
Rust
//! Spawn-time config hash for the restart-required badge.
|
|
//!
|
|
//! [`spawn_config_hash`] digests the *effective spawned values* — what a
|
|
//! process launch of `record` would actually receive — so the UI can compare
|
|
//! a running process's hash (stamped on [`super::ManagedAgentProcess`] at
|
|
//! spawn) against a recomputation from current disk state and show a
|
|
//! "restart required" badge only when a restart would change what runs.
|
|
//!
|
|
//! Scope rules (decided in #centralize-personas-and-agents, revised in PR
|
|
//! #1602 review):
|
|
//! - Inputs mirror what a start would actually run: the start/restore paths
|
|
//! re-snapshot the linked persona's prompt/model/provider/env onto the
|
|
//! record immediately before spawning (`start_local_agent_with_preflight`,
|
|
//! `restore_managed_agents_on_launch`), so persona edits to those fields DO
|
|
//! apply on a plain restart and are hashed via the same prospective
|
|
//! re-snapshot. Harness command, args/mcp, env layering, and the record
|
|
//! fields the spawn env writes read are hashed as spawn resolves them.
|
|
//! - The relay URL is hashed in resolved form (`effective_agent_relay_url`):
|
|
//! a record with a blank relay spawns against the active workspace relay,
|
|
//! so a workspace relay change means a restart would change what runs.
|
|
//! - Channel membership is not an input: agents pick up channel changes live
|
|
//! (#1468), never via restart.
|
|
//!
|
|
//! The hash never crosses a process or persistence boundary, so
|
|
//! `DefaultHasher` (not stable across Rust releases) is sufficient.
|
|
|
|
use std::hash::{DefaultHasher, Hash, Hasher};
|
|
|
|
use super::{
|
|
known_acp_runtime, normalize_agent_args,
|
|
persona_events::apply_persona_snapshot,
|
|
resolve_effective_agent_env,
|
|
types::{AgentDefinition, ManagedAgentRecord},
|
|
GlobalAgentConfig,
|
|
};
|
|
|
|
/// The prompt a spawn would actually deliver: `Some("")` collapses to `None`
|
|
/// because an empty BUZZ_ACP_SYSTEM_PROMPT is no prompt — EXCEPT for
|
|
/// team-pack records, where buzz-acp falls back to the pack persona's prompt
|
|
/// when the env var is absent, making set-but-empty a deliberate suppression.
|
|
///
|
|
/// The single source of truth for the spawn env write AND the config hash:
|
|
/// both call this, so they cannot disagree (the hash's contract is "digest
|
|
/// what a spawn would actually run"). B5 hash row 2 depends on the collapse:
|
|
/// backfilled prompt-less records re-snapshot to `Some("")` from their
|
|
/// manufactured definition and must not trip the restart badge.
|
|
pub(crate) fn effective_spawn_prompt(record: &ManagedAgentRecord) -> Option<String> {
|
|
let has_pack_fallback =
|
|
record.persona_team_dir.is_some() && record.persona_name_in_team.is_some();
|
|
record
|
|
.system_prompt
|
|
.clone()
|
|
.filter(|p| has_pack_fallback || !p.is_empty())
|
|
}
|
|
|
|
/// Digest the effective spawn configuration of `record` under the current
|
|
/// `personas`, resolving a blank record relay against `workspace_relay`.
|
|
/// Pure — no `AppHandle`, no disk, no keyring.
|
|
pub(crate) fn spawn_config_hash(
|
|
record: &ManagedAgentRecord,
|
|
personas: &[AgentDefinition],
|
|
workspace_relay: &str,
|
|
global: &GlobalAgentConfig,
|
|
) -> u64 {
|
|
// Prospective re-snapshot: apply the same `apply_persona_snapshot` the
|
|
// start/restore paths run right before spawning, so the hash covers what a
|
|
// restart would actually run. Idempotent, so the spawn-time stamp
|
|
// (post-snapshot record) and later recomputes (persisted record) agree
|
|
// when nothing changed. The persona env itself reaches the hash through
|
|
// `resolve_effective_agent_env` below; `persona_source_version` is set on
|
|
// the clone but is not a hash input.
|
|
let mut record = record.clone();
|
|
if let Some(persona_id) = record.persona_id.clone() {
|
|
if let Some(persona) = personas.iter().find(|p| p.id == persona_id) {
|
|
apply_persona_snapshot(&mut record, persona);
|
|
}
|
|
}
|
|
let record = &record;
|
|
|
|
let effective_command = crate::managed_agents::record_agent_command(record, personas);
|
|
let runtime_meta = known_acp_runtime(&effective_command);
|
|
let effective = resolve_effective_agent_env(record, personas, runtime_meta, global);
|
|
|
|
let mut hasher = DefaultHasher::new();
|
|
|
|
// Harness identity and derivations (live-persona-resolved, like spawn).
|
|
record.acp_command.hash(&mut hasher);
|
|
effective_command.hash(&mut hasher);
|
|
normalize_agent_args(&effective_command, record.agent_args.clone()).hash(&mut hasher);
|
|
runtime_meta
|
|
.and_then(|r| r.mcp_command)
|
|
.unwrap_or("")
|
|
.hash(&mut hasher);
|
|
|
|
// Effective env layering (baked floor → runtime metadata → user env).
|
|
// BTreeMap iteration is ordered, so this is deterministic.
|
|
effective.env.hash(&mut hasher);
|
|
|
|
// Record fields the spawn env writes read directly. The relay is hashed
|
|
// resolved: a blank record relay spawns on the workspace relay, so a
|
|
// workspace relay change must trip the badge.
|
|
crate::relay::effective_agent_relay_url(&record.relay_url, workspace_relay).hash(&mut hasher);
|
|
// Prompt via the shared spawn-effective filter (see its doc for the
|
|
// Some("")/None collapse and the team-pack exception).
|
|
effective_spawn_prompt(record).hash(&mut hasher);
|
|
record.model.hash(&mut hasher);
|
|
record.provider.hash(&mut hasher);
|
|
record.auth_tag.hash(&mut hasher);
|
|
record.respond_to.as_str().hash(&mut hasher);
|
|
// The allowlist is hashed as the env receives it: spawn sets
|
|
// BUZZ_ACP_RESPOND_TO_ALLOWLIST only in allowlist mode, and normalized
|
|
// (trim/lowercase/dedup via `validate_respond_to_allowlist`) — so edits
|
|
// that don't survive normalization, or edits while another mode is
|
|
// active, must not badge. A list spawn would reject hashes raw: the
|
|
// stamped hash comes from a successful spawn, so any invalid edit
|
|
// correctly compares unequal.
|
|
if record.respond_to == super::types::RespondTo::Allowlist {
|
|
super::types::validate_respond_to_allowlist(&record.respond_to_allowlist)
|
|
.unwrap_or_else(|_| record.respond_to_allowlist.clone())
|
|
.hash(&mut hasher);
|
|
}
|
|
record.idle_timeout_seconds.hash(&mut hasher);
|
|
// Spawn writes BUZZ_ACP_MAX_TURN_DURATION with a default.
|
|
record
|
|
.max_turn_duration_seconds
|
|
.unwrap_or(super::types::DEFAULT_AGENT_MAX_TURN_DURATION_SECONDS)
|
|
.hash(&mut hasher);
|
|
record.parallelism.hash(&mut hasher);
|
|
record.persona_team_dir.hash(&mut hasher);
|
|
record.persona_name_in_team.hash(&mut hasher);
|
|
|
|
hasher.finish()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|