feat: RFC 0029 file-based subagent output for alignment dialogues
Replace JSONL extraction pipeline with direct file writes: alignment-expert
agents write perspectives to /tmp/blue-dialogue/{slug}/round-{n}/{name}.md,
and the Judge reads those files directly after Task completion.
Changes:
- alignment-expert.md: add Write tool
- dialogue.rs: create output_dir, pass to build_judge_protocol
- Add name_lowercase field to agent JSON for filename generation
- Add WRITE YOUR OUTPUT section to agent prompt template
- Update Judge instructions with mkdir + Read tool workflow
- Add output_dir to returned protocol JSON
- New test: test_build_judge_protocol_output_paths
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0fea499957
commit
37de7759b5
2 changed files with 85 additions and 13 deletions
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: alignment-expert
|
||||
description: Expert agent for alignment dialogues. Produces focused perspectives with inline markers. Use when orchestrating multi-expert alignment dialogues via blue_dialogue_create.
|
||||
tools: Read, Grep, Glob
|
||||
tools: Read, Grep, Glob, Write
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -346,7 +346,8 @@ pub fn handle_create(state: &mut ProjectState, args: &Value) -> Result<Value, Se
|
|||
|
||||
// Generate file path with ISO 8601 timestamp prefix (RFC 0031)
|
||||
let timestamp = blue_core::utc_timestamp();
|
||||
let file_name = format!("{}-{}.dialogue.recorded.md", timestamp, title_to_slug(title));
|
||||
let slug = title_to_slug(title);
|
||||
let file_name = format!("{}-{}.dialogue.recorded.md", timestamp, slug);
|
||||
let file_path = PathBuf::from("dialogues").join(&file_name);
|
||||
let docs_path = state.home.docs_path.clone();
|
||||
let dialogue_path = docs_path.join(&file_path);
|
||||
|
|
@ -410,11 +411,18 @@ pub fn handle_create(state: &mut ProjectState, args: &Value) -> Result<Value, Se
|
|||
|
||||
// Build response — RFC 0023: inject protocol as prose in message field
|
||||
let (message, judge_protocol) = if let Some(ref agents) = pastry_agents {
|
||||
// RFC 0029: Create output directory for file-based subagent output
|
||||
let output_dir = format!("/tmp/blue-dialogue/{}", slug);
|
||||
fs::create_dir_all(&output_dir).map_err(|e| {
|
||||
ServerError::CommandFailed(format!("Failed to create output dir {}: {}", output_dir, e))
|
||||
})?;
|
||||
|
||||
let protocol = build_judge_protocol(
|
||||
agents,
|
||||
&dialogue_path.display().to_string(),
|
||||
model,
|
||||
&sources,
|
||||
&output_dir,
|
||||
);
|
||||
// Extract instructions as prose so Claude reads them directly
|
||||
let instructions = protocol["instructions"].as_str().unwrap_or("");
|
||||
|
|
@ -425,7 +433,7 @@ pub fn handle_create(state: &mut ProjectState, args: &Value) -> Result<Value, Se
|
|||
## JUDGE PROTOCOL — FOLLOW THESE INSTRUCTIONS\n\n\
|
||||
{instructions}\n\n\
|
||||
## AGENT PROMPT TEMPLATE\n\n\
|
||||
Substitute {{{{NAME}}}}, {{{{EMOJI}}}}, {{{{ROLE}}}} for each agent:\n\n\
|
||||
Substitute {{{{NAME}}}}, {{{{EMOJI}}}}, {{{{ROLE}}}}, {{{{OUTPUT_FILE}}}} for each agent:\n\n\
|
||||
{template}",
|
||||
title = title,
|
||||
file = dialogue_path.display(),
|
||||
|
|
@ -899,6 +907,7 @@ pub fn build_judge_protocol(
|
|||
dialogue_file: &str,
|
||||
model: &str,
|
||||
sources: &[String],
|
||||
output_dir: &str,
|
||||
) -> Value {
|
||||
let agent_list: Vec<Value> = agents
|
||||
.iter()
|
||||
|
|
@ -909,6 +918,7 @@ pub fn build_judge_protocol(
|
|||
"emoji": a.emoji,
|
||||
"tier": a.tier,
|
||||
"relevance": a.relevance,
|
||||
"name_lowercase": a.name.to_lowercase(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
|
@ -953,7 +963,14 @@ OUTPUT LIMIT — THIS IS MANDATORY:
|
|||
- If the topic needs more depth, save it for the next round
|
||||
- Aim for under 2000 characters total
|
||||
- DO NOT write essays, literature reviews, or exhaustive analyses
|
||||
- Be pointed and specific, not comprehensive{source_read_instructions}"##
|
||||
- Be pointed and specific, not comprehensive
|
||||
|
||||
WRITE YOUR OUTPUT — THIS IS MANDATORY:
|
||||
Use the Write tool to write your COMPLETE response to:
|
||||
{{{{OUTPUT_FILE}}}}
|
||||
|
||||
Write your full perspective to this file. This is your primary output mechanism.
|
||||
After writing the file, you may stop.{source_read_instructions}"##
|
||||
);
|
||||
|
||||
let instructions = format!(
|
||||
|
|
@ -961,6 +978,9 @@ OUTPUT LIMIT — THIS IS MANDATORY:
|
|||
|
||||
=== HOW TO SPAWN EXPERT SUBAGENTS ===
|
||||
|
||||
BEFORE spawning each round, create the round directory:
|
||||
Use Bash: mkdir -p {output_dir}/round-N
|
||||
|
||||
Spawn ALL {agent_count} experts in a SINGLE message with {agent_count} Task tool calls.
|
||||
Multiple Task calls in one message run as parallel subagents.
|
||||
|
||||
|
|
@ -968,28 +988,32 @@ Each Task call:
|
|||
- subagent_type: "alignment-expert"
|
||||
- description: "🧁 Muffin expert deliberation"
|
||||
- max_turns: 10
|
||||
- prompt: the AGENT PROMPT TEMPLATE with {{{{NAME}}}}, {{{{EMOJI}}}}, {{{{ROLE}}}} substituted
|
||||
- prompt: the AGENT PROMPT TEMPLATE with {{{{NAME}}}}, {{{{EMOJI}}}}, {{{{ROLE}}}}, {{{{OUTPUT_FILE}}}} substituted
|
||||
- {{{{OUTPUT_FILE}}}} → {output_dir}/round-N/AGENT_NAME_LOWERCASE.md
|
||||
- run_in_background: true
|
||||
|
||||
All {agent_count} results return when complete. Read each agent's output from the results.
|
||||
All {agent_count} results return when complete.
|
||||
|
||||
=== ROUND WORKFLOW ===
|
||||
|
||||
1. SPAWN: One message, {agent_count} Task calls (parallel subagents)
|
||||
2. READ: Each subagent returns its output directly
|
||||
3. SCORE: ALIGNMENT = Wisdom + Consistency + Truth + Relationships (UNBOUNDED)
|
||||
1. MKDIR: Create round directory via Bash: mkdir -p {output_dir}/round-N
|
||||
2. SPAWN: One message, {agent_count} Task calls (parallel subagents)
|
||||
3. READ: After all agents complete, read each file with Read tool
|
||||
If a file is missing, fall back to blue_extract_dialogue(task_id=TASK_ID)
|
||||
4. SCORE: ALIGNMENT = Wisdom + Consistency + Truth + Relationships (UNBOUNDED)
|
||||
- Score ONLY AFTER reading output — NEVER pre-fill scores
|
||||
4. UPDATE {dialogue_file}:
|
||||
5. UPDATE {dialogue_file}:
|
||||
- Agent responses under the correct Round section
|
||||
- Scoreboard with scores from this round
|
||||
- Perspectives Inventory (one row per [PERSPECTIVE Pnn:] marker)
|
||||
- Tensions Tracker (one row per [TENSION Tn:] marker)
|
||||
5. CONVERGE: If velocity approaches 0 OR all tensions resolved → declare convergence
|
||||
6. CONVERGE: If velocity approaches 0 OR all tensions resolved → declare convergence
|
||||
Otherwise, start next round with updated prompt including prior perspectives
|
||||
Maximum 5 rounds (safety valve)
|
||||
6. SAVE via blue_dialogue_save
|
||||
7. SAVE via blue_dialogue_save
|
||||
|
||||
AGENTS: {agent_names}
|
||||
OUTPUT DIR: {output_dir}
|
||||
|
||||
FORMAT RULES — MANDATORY:
|
||||
- ALWAYS prefix agent names with their emoji (🧁 Muffin) not bare name (Muffin)
|
||||
|
|
@ -1001,6 +1025,7 @@ FORMAT RULES — MANDATORY:
|
|||
IMPORTANT: Each agent has NO memory of other agents. They see only the topic and their role."##,
|
||||
agent_count = agents.len(),
|
||||
dialogue_file = dialogue_file,
|
||||
output_dir = output_dir,
|
||||
agent_names = agents
|
||||
.iter()
|
||||
.map(|a| format!("{} {} ({})", a.emoji, a.name, a.role))
|
||||
|
|
@ -1015,6 +1040,7 @@ IMPORTANT: Each agent has NO memory of other agents. They see only the topic and
|
|||
"dialogue_file": dialogue_file,
|
||||
"model": model,
|
||||
"sources": sources,
|
||||
"output_dir": output_dir,
|
||||
"convergence": {
|
||||
"max_rounds": 5,
|
||||
"velocity_threshold": 0.1,
|
||||
|
|
@ -1152,6 +1178,7 @@ mod tests {
|
|||
"/tmp/test.dialogue.md",
|
||||
"sonnet",
|
||||
&["/tmp/source.rs".to_string()],
|
||||
"/tmp/blue-dialogue/system-design",
|
||||
);
|
||||
|
||||
// Must have instructions
|
||||
|
|
@ -1161,6 +1188,10 @@ mod tests {
|
|||
assert!(instructions.contains("ALIGNMENT"));
|
||||
assert!(instructions.contains("Wisdom"));
|
||||
assert!(instructions.contains("convergence"));
|
||||
// RFC 0029: file-based output instructions
|
||||
assert!(instructions.contains("/tmp/blue-dialogue/system-design"));
|
||||
assert!(instructions.contains("mkdir"));
|
||||
assert!(instructions.contains("Read tool"));
|
||||
|
||||
// Must have agent prompt template with Read tool reference
|
||||
let template = protocol.get("agent_prompt_template").unwrap().as_str().unwrap();
|
||||
|
|
@ -1169,11 +1200,15 @@ mod tests {
|
|||
assert!(template.contains("PERSPECTIVE"));
|
||||
assert!(template.contains("TENSION"));
|
||||
assert!(template.contains("Read tool"));
|
||||
// RFC 0029: WRITE YOUR OUTPUT section
|
||||
assert!(template.contains("WRITE YOUR OUTPUT"));
|
||||
assert!(template.contains("{{OUTPUT_FILE}}"));
|
||||
|
||||
// Must have agents array
|
||||
// Must have agents array with name_lowercase
|
||||
let agents_arr = protocol.get("agents").unwrap().as_array().unwrap();
|
||||
assert_eq!(agents_arr.len(), 3);
|
||||
assert_eq!(agents_arr[0]["name"], "Muffin");
|
||||
assert_eq!(agents_arr[0]["name_lowercase"], "muffin");
|
||||
|
||||
// Must have model
|
||||
assert_eq!(protocol["model"], "sonnet");
|
||||
|
|
@ -1183,6 +1218,9 @@ mod tests {
|
|||
assert_eq!(sources.len(), 1);
|
||||
assert_eq!(sources[0], "/tmp/source.rs");
|
||||
|
||||
// Must have output_dir
|
||||
assert_eq!(protocol["output_dir"], "/tmp/blue-dialogue/system-design");
|
||||
|
||||
// Must have convergence params
|
||||
assert_eq!(protocol["convergence"]["max_rounds"], 5);
|
||||
assert!(protocol["convergence"]["tension_resolution_gate"].as_bool().unwrap());
|
||||
|
|
@ -1196,10 +1234,44 @@ mod tests {
|
|||
"/tmp/test.dialogue.md",
|
||||
"haiku",
|
||||
&[],
|
||||
"/tmp/blue-dialogue/quick-topic",
|
||||
);
|
||||
|
||||
// Template should NOT contain grounding instructions when no sources
|
||||
let template = protocol.get("agent_prompt_template").unwrap().as_str().unwrap();
|
||||
assert!(!template.contains("GROUNDING"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_judge_protocol_output_paths() {
|
||||
let agents = assign_pastry_agents(4, "api design");
|
||||
let protocol = build_judge_protocol(
|
||||
&agents,
|
||||
"/tmp/test.dialogue.md",
|
||||
"sonnet",
|
||||
&[],
|
||||
"/tmp/blue-dialogue/api-design",
|
||||
);
|
||||
|
||||
// output_dir in JSON
|
||||
assert_eq!(protocol["output_dir"], "/tmp/blue-dialogue/api-design");
|
||||
|
||||
// All agents have name_lowercase
|
||||
let agents_arr = protocol["agents"].as_array().unwrap();
|
||||
assert_eq!(agents_arr[0]["name_lowercase"], "muffin");
|
||||
assert_eq!(agents_arr[1]["name_lowercase"], "cupcake");
|
||||
assert_eq!(agents_arr[2]["name_lowercase"], "scone");
|
||||
assert_eq!(agents_arr[3]["name_lowercase"], "eclair");
|
||||
|
||||
// WRITE YOUR OUTPUT in template
|
||||
let template = protocol["agent_prompt_template"].as_str().unwrap();
|
||||
assert!(template.contains("WRITE YOUR OUTPUT"));
|
||||
assert!(template.contains("{{OUTPUT_FILE}}"));
|
||||
assert!(template.contains("Write tool"));
|
||||
|
||||
// output_dir referenced in instructions
|
||||
let instructions = protocol["instructions"].as_str().unwrap();
|
||||
assert!(instructions.contains("/tmp/blue-dialogue/api-design"));
|
||||
assert!(instructions.contains("OUTPUT DIR:"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue