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
|
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.
|
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
|
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)
|
// Generate file path with ISO 8601 timestamp prefix (RFC 0031)
|
||||||
let timestamp = blue_core::utc_timestamp();
|
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 file_path = PathBuf::from("dialogues").join(&file_name);
|
||||||
let docs_path = state.home.docs_path.clone();
|
let docs_path = state.home.docs_path.clone();
|
||||||
let dialogue_path = docs_path.join(&file_path);
|
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
|
// Build response — RFC 0023: inject protocol as prose in message field
|
||||||
let (message, judge_protocol) = if let Some(ref agents) = pastry_agents {
|
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(
|
let protocol = build_judge_protocol(
|
||||||
agents,
|
agents,
|
||||||
&dialogue_path.display().to_string(),
|
&dialogue_path.display().to_string(),
|
||||||
model,
|
model,
|
||||||
&sources,
|
&sources,
|
||||||
|
&output_dir,
|
||||||
);
|
);
|
||||||
// Extract instructions as prose so Claude reads them directly
|
// Extract instructions as prose so Claude reads them directly
|
||||||
let instructions = protocol["instructions"].as_str().unwrap_or("");
|
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\
|
## JUDGE PROTOCOL — FOLLOW THESE INSTRUCTIONS\n\n\
|
||||||
{instructions}\n\n\
|
{instructions}\n\n\
|
||||||
## AGENT PROMPT TEMPLATE\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}",
|
{template}",
|
||||||
title = title,
|
title = title,
|
||||||
file = dialogue_path.display(),
|
file = dialogue_path.display(),
|
||||||
|
|
@ -899,6 +907,7 @@ pub fn build_judge_protocol(
|
||||||
dialogue_file: &str,
|
dialogue_file: &str,
|
||||||
model: &str,
|
model: &str,
|
||||||
sources: &[String],
|
sources: &[String],
|
||||||
|
output_dir: &str,
|
||||||
) -> Value {
|
) -> Value {
|
||||||
let agent_list: Vec<Value> = agents
|
let agent_list: Vec<Value> = agents
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -909,6 +918,7 @@ pub fn build_judge_protocol(
|
||||||
"emoji": a.emoji,
|
"emoji": a.emoji,
|
||||||
"tier": a.tier,
|
"tier": a.tier,
|
||||||
"relevance": a.relevance,
|
"relevance": a.relevance,
|
||||||
|
"name_lowercase": a.name.to_lowercase(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
@ -953,7 +963,14 @@ OUTPUT LIMIT — THIS IS MANDATORY:
|
||||||
- If the topic needs more depth, save it for the next round
|
- If the topic needs more depth, save it for the next round
|
||||||
- Aim for under 2000 characters total
|
- Aim for under 2000 characters total
|
||||||
- DO NOT write essays, literature reviews, or exhaustive analyses
|
- 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!(
|
let instructions = format!(
|
||||||
|
|
@ -961,6 +978,9 @@ OUTPUT LIMIT — THIS IS MANDATORY:
|
||||||
|
|
||||||
=== HOW TO SPAWN EXPERT SUBAGENTS ===
|
=== 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.
|
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.
|
Multiple Task calls in one message run as parallel subagents.
|
||||||
|
|
||||||
|
|
@ -968,28 +988,32 @@ Each Task call:
|
||||||
- subagent_type: "alignment-expert"
|
- subagent_type: "alignment-expert"
|
||||||
- description: "🧁 Muffin expert deliberation"
|
- description: "🧁 Muffin expert deliberation"
|
||||||
- max_turns: 10
|
- 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
|
- 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 ===
|
=== ROUND WORKFLOW ===
|
||||||
|
|
||||||
1. SPAWN: One message, {agent_count} Task calls (parallel subagents)
|
1. MKDIR: Create round directory via Bash: mkdir -p {output_dir}/round-N
|
||||||
2. READ: Each subagent returns its output directly
|
2. SPAWN: One message, {agent_count} Task calls (parallel subagents)
|
||||||
3. SCORE: ALIGNMENT = Wisdom + Consistency + Truth + Relationships (UNBOUNDED)
|
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
|
- Score ONLY AFTER reading output — NEVER pre-fill scores
|
||||||
4. UPDATE {dialogue_file}:
|
5. UPDATE {dialogue_file}:
|
||||||
- Agent responses under the correct Round section
|
- Agent responses under the correct Round section
|
||||||
- Scoreboard with scores from this round
|
- Scoreboard with scores from this round
|
||||||
- Perspectives Inventory (one row per [PERSPECTIVE Pnn:] marker)
|
- Perspectives Inventory (one row per [PERSPECTIVE Pnn:] marker)
|
||||||
- Tensions Tracker (one row per [TENSION Tn:] 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
|
Otherwise, start next round with updated prompt including prior perspectives
|
||||||
Maximum 5 rounds (safety valve)
|
Maximum 5 rounds (safety valve)
|
||||||
6. SAVE via blue_dialogue_save
|
7. SAVE via blue_dialogue_save
|
||||||
|
|
||||||
AGENTS: {agent_names}
|
AGENTS: {agent_names}
|
||||||
|
OUTPUT DIR: {output_dir}
|
||||||
|
|
||||||
FORMAT RULES — MANDATORY:
|
FORMAT RULES — MANDATORY:
|
||||||
- ALWAYS prefix agent names with their emoji (🧁 Muffin) not bare name (Muffin)
|
- 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."##,
|
IMPORTANT: Each agent has NO memory of other agents. They see only the topic and their role."##,
|
||||||
agent_count = agents.len(),
|
agent_count = agents.len(),
|
||||||
dialogue_file = dialogue_file,
|
dialogue_file = dialogue_file,
|
||||||
|
output_dir = output_dir,
|
||||||
agent_names = agents
|
agent_names = agents
|
||||||
.iter()
|
.iter()
|
||||||
.map(|a| format!("{} {} ({})", a.emoji, a.name, a.role))
|
.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,
|
"dialogue_file": dialogue_file,
|
||||||
"model": model,
|
"model": model,
|
||||||
"sources": sources,
|
"sources": sources,
|
||||||
|
"output_dir": output_dir,
|
||||||
"convergence": {
|
"convergence": {
|
||||||
"max_rounds": 5,
|
"max_rounds": 5,
|
||||||
"velocity_threshold": 0.1,
|
"velocity_threshold": 0.1,
|
||||||
|
|
@ -1152,6 +1178,7 @@ mod tests {
|
||||||
"/tmp/test.dialogue.md",
|
"/tmp/test.dialogue.md",
|
||||||
"sonnet",
|
"sonnet",
|
||||||
&["/tmp/source.rs".to_string()],
|
&["/tmp/source.rs".to_string()],
|
||||||
|
"/tmp/blue-dialogue/system-design",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Must have instructions
|
// Must have instructions
|
||||||
|
|
@ -1161,6 +1188,10 @@ mod tests {
|
||||||
assert!(instructions.contains("ALIGNMENT"));
|
assert!(instructions.contains("ALIGNMENT"));
|
||||||
assert!(instructions.contains("Wisdom"));
|
assert!(instructions.contains("Wisdom"));
|
||||||
assert!(instructions.contains("convergence"));
|
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
|
// Must have agent prompt template with Read tool reference
|
||||||
let template = protocol.get("agent_prompt_template").unwrap().as_str().unwrap();
|
let template = protocol.get("agent_prompt_template").unwrap().as_str().unwrap();
|
||||||
|
|
@ -1169,11 +1200,15 @@ mod tests {
|
||||||
assert!(template.contains("PERSPECTIVE"));
|
assert!(template.contains("PERSPECTIVE"));
|
||||||
assert!(template.contains("TENSION"));
|
assert!(template.contains("TENSION"));
|
||||||
assert!(template.contains("Read tool"));
|
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();
|
let agents_arr = protocol.get("agents").unwrap().as_array().unwrap();
|
||||||
assert_eq!(agents_arr.len(), 3);
|
assert_eq!(agents_arr.len(), 3);
|
||||||
assert_eq!(agents_arr[0]["name"], "Muffin");
|
assert_eq!(agents_arr[0]["name"], "Muffin");
|
||||||
|
assert_eq!(agents_arr[0]["name_lowercase"], "muffin");
|
||||||
|
|
||||||
// Must have model
|
// Must have model
|
||||||
assert_eq!(protocol["model"], "sonnet");
|
assert_eq!(protocol["model"], "sonnet");
|
||||||
|
|
@ -1183,6 +1218,9 @@ mod tests {
|
||||||
assert_eq!(sources.len(), 1);
|
assert_eq!(sources.len(), 1);
|
||||||
assert_eq!(sources[0], "/tmp/source.rs");
|
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
|
// Must have convergence params
|
||||||
assert_eq!(protocol["convergence"]["max_rounds"], 5);
|
assert_eq!(protocol["convergence"]["max_rounds"], 5);
|
||||||
assert!(protocol["convergence"]["tension_resolution_gate"].as_bool().unwrap());
|
assert!(protocol["convergence"]["tension_resolution_gate"].as_bool().unwrap());
|
||||||
|
|
@ -1196,10 +1234,44 @@ mod tests {
|
||||||
"/tmp/test.dialogue.md",
|
"/tmp/test.dialogue.md",
|
||||||
"haiku",
|
"haiku",
|
||||||
&[],
|
&[],
|
||||||
|
"/tmp/blue-dialogue/quick-topic",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Template should NOT contain grounding instructions when no sources
|
// Template should NOT contain grounding instructions when no sources
|
||||||
let template = protocol.get("agent_prompt_template").unwrap().as_str().unwrap();
|
let template = protocol.get("agent_prompt_template").unwrap().as_str().unwrap();
|
||||||
assert!(!template.contains("GROUNDING"));
|
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