fix(agents): propagate shared instruction edits

Refresh linked agent assignments from their definition at snapshot apply time and include the effective coordinates in restart drift detection.

Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
Taylor Ho
2026-08-25 00:41:30 -07:00
committed by Rizz
parent d4f02229be
commit 76df4bf1d1
5 changed files with 45 additions and 2 deletions

View File

@@ -433,6 +433,9 @@ pub struct PersonaSnapshot {
pub system_prompt: Option<String>,
pub model: Option<String>,
pub provider: Option<String>,
/// Exact publisher-pinned shared-instruction coordinates, copied verbatim
/// from the definition so edits reach an existing instance on restart.
pub assigned_relay_skills: Vec<String>,
/// Preferred ACP runtime ID, copied verbatim from the persona (including
/// `None`). Unlike `model`/`provider`, there is no record-fallback: the
/// materialized instance `runtime` must mirror the definition so that
@@ -454,14 +457,15 @@ pub fn persona_snapshot(persona: &AgentDefinition) -> PersonaSnapshot {
system_prompt: Some(persona.system_prompt.clone()),
model: persona.model.clone(),
provider: persona.provider.clone(),
assigned_relay_skills: persona.assigned_relay_skills.clone(),
runtime: persona.runtime.clone(),
source_version: persona_content_hash(&persona_event_content(persona)),
}
}
/// Re-pin `record` to `persona`: build a snapshot via [`persona_snapshot`]
/// and mirror it onto the record — the definition quad
/// (`system_prompt`/`model`/`provider`/`runtime`), the env-override
/// and mirror it onto the record — the definition fields
/// (`system_prompt`/`model`/`provider`/`runtime`/shared instructions), the env-override
/// self-heal, and the `persona_source_version` drift basis.
///
/// Definition-authoritative: blank definition model/provider produce `None`
@@ -484,6 +488,7 @@ pub fn apply_persona_snapshot(record: &mut ManagedAgentRecord, persona: &AgentDe
}
record.model = snapshot.model;
record.provider = snapshot.provider;
record.assigned_relay_skills = snapshot.assigned_relay_skills;
record.runtime = snapshot.runtime;
// Drop a stale create-time harness pin when the definition switches to a
// different known runtime (builtin, static preset, or loaded custom). A pin

View File

@@ -168,6 +168,18 @@ pub(super) fn sample_persona() -> AgentDefinition {
}
}
#[test]
fn apply_snapshot_refreshes_shared_instructions_for_existing_instance() {
let mut record = sample_record();
record.assigned_relay_skills = vec![format!("30023:{}:old", "a".repeat(64))];
let mut persona = sample_persona();
persona.assigned_relay_skills = vec![format!("30023:{}:new", "b".repeat(64))];
apply_persona_snapshot(&mut record, &persona);
assert_eq!(record.assigned_relay_skills, persona.assigned_relay_skills);
}
#[test]
fn monotonic_created_at_bumps_past_head() {
// No head: uses now (floor 0).

View File

@@ -111,6 +111,8 @@ pub(crate) struct SpawnConfigSnapshot {
pub system_prompt: Option<String>,
pub model: Option<String>,
pub provider: Option<String>,
/// Exact publisher-pinned shared-instruction coordinates exported to ACP.
pub assigned_relay_skills: Vec<String>,
/// `None` when a user env override shadows `BUZZ_ACP_SESSION_TITLE`: spawn
/// writes the title BEFORE the user env layer, so the override is what
/// actually runs and it already reaches this snapshot through `env`.
@@ -193,6 +195,7 @@ impl SpawnConfigSnapshot {
system_prompt: system_prompt.map(str::to_string),
model: model.map(str::to_string),
provider: provider.map(str::to_string),
assigned_relay_skills: record.assigned_relay_skills.clone(),
session_title: (!descriptor.env.contains_key(SESSION_TITLE_ENV_VAR))
.then(|| resolve_session_title(record.display_name.as_deref(), &record.name))
.flatten(),

View File

@@ -21,6 +21,7 @@ fn base() -> SpawnConfigSnapshot {
system_prompt: Some("You are a test agent.".into()),
model: Some("gpt-5".into()),
provider: Some("openai".into()),
assigned_relay_skills: vec![format!("30023:{}:review", "a".repeat(64))],
session_title: Some("Fizz".into()),
auth_tag: Some("tag-abcdefgh".into()),
respond_to: "owner-only".into(),
@@ -62,6 +63,7 @@ fn mutations() -> Vec<Mutation> {
("system_prompt", |s| s.system_prompt = None),
("model", |s| s.model = None),
("provider", |s| s.provider = None),
("assigned_relay_skills", |s| s.assigned_relay_skills.clear()),
("session_title", |s| s.session_title = None),
("auth_tag", |s| s.auth_tag = None),
("respond_to", |s| s.respond_to = "anyone".into()),

View File

@@ -223,6 +223,27 @@ fn persona_prompt_edit_changes_snapshot() {
);
}
#[test]
fn persona_shared_instruction_edit_changes_snapshot() {
let mut rec = record();
rec.persona_id = Some("pers".into());
let mut before = persona("pers", Some("goose"), "prompt");
before.assigned_relay_skills = vec![format!("30023:{}:before", "a".repeat(64))];
let mut after = before.clone();
after.assigned_relay_skills = vec![format!("30023:{}:after", "b".repeat(64))];
assert_ne!(
snapshot(
&rec,
&[before],
&[],
"wss://ws.example",
&Default::default()
),
snapshot(&rec, &[after], &[], "wss://ws.example", &Default::default())
);
}
#[test]
fn workspace_relay_change_trips_snapshot_even_for_stored_record_relay() {
// The legacy per-record relay pin is ignored (#2122): every record spawns