Skip to main content

scud/commands/spawn/
agent.rs

1//! Agent prompt generation for Claude Code sessions
2//!
3//! Creates prompts that provide task context and instructions for Claude Code agents.
4
5use crate::agents::AgentDef;
6use crate::commands::spawn::terminal::Harness;
7use crate::commands::swarm::session::WaveSummary;
8use crate::models::task::Task;
9use std::path::Path;
10
11/// Resolved configuration for spawning an agent
12#[derive(Debug, Clone)]
13pub struct ResolvedAgentConfig {
14    /// The harness to use (claude or opencode)
15    pub harness: Harness,
16    /// The model to use (if specified)
17    pub model: Option<String>,
18    /// The prompt to send to the agent
19    pub prompt: String,
20    /// Whether this config came from an agent definition
21    pub from_agent_def: bool,
22    /// The agent type name (if from agent def)
23    pub agent_type: Option<String>,
24}
25
26/// Resolve the agent configuration for a task.
27///
28/// If the task has an `agent_type`, attempts to load the agent definition
29/// from `.scud/agents/<agent_type>.toml` and uses its harness, model, and
30/// prompt template. Falls back to defaults if the agent definition is not found.
31///
32/// # Arguments
33/// * `task` - The task to resolve configuration for
34/// * `tag` - The task's tag/phase name
35/// * `default_harness` - Harness to use if no agent definition specifies one
36/// * `default_model` - Model to use if no agent definition specifies one
37/// * `working_dir` - Project root for loading agent definitions
38pub fn resolve_agent_config(
39    task: &Task,
40    tag: &str,
41    default_harness: Harness,
42    default_model: Option<&str>,
43    working_dir: &Path,
44) -> ResolvedAgentConfig {
45    if let Some(ref agent_type) = task.agent_type {
46        // Try to load agent definition
47        match AgentDef::try_load(agent_type, working_dir) {
48            Some(agent_def) => {
49                let harness = agent_def.harness().unwrap_or(default_harness);
50                let model = agent_def
51                    .model()
52                    .map(String::from)
53                    .or_else(|| default_model.map(String::from));
54
55                // Use custom prompt template if available
56                let prompt = match agent_def.prompt_template(working_dir) {
57                    Some(template) => generate_prompt_with_template(task, tag, &template),
58                    None => generate_prompt(task, tag),
59                };
60
61                ResolvedAgentConfig {
62                    harness,
63                    model,
64                    prompt,
65                    from_agent_def: true,
66                    agent_type: Some(agent_type.clone()),
67                }
68            }
69            None => {
70                // Agent type specified but no definition found - use defaults
71                ResolvedAgentConfig {
72                    harness: default_harness,
73                    model: default_model.map(String::from),
74                    prompt: generate_prompt(task, tag),
75                    from_agent_def: false,
76                    agent_type: Some(agent_type.clone()),
77                }
78            }
79        }
80    } else {
81        // No agent type - use defaults
82        ResolvedAgentConfig {
83            harness: default_harness,
84            model: default_model.map(String::from),
85            prompt: generate_prompt(task, tag),
86            from_agent_def: false,
87            agent_type: None,
88        }
89    }
90}
91
92impl ResolvedAgentConfig {
93    /// Get a display string for logging (e.g., "opencode:grok-code-fast-1@fast-builder")
94    pub fn display_info(&self) -> String {
95        let model_str = self
96            .model
97            .as_deref()
98            .map(|m| format!(":{}", m))
99            .unwrap_or_default();
100
101        if let Some(ref agent_type) = self.agent_type {
102            format!("{}{}@{}", self.harness.name(), model_str, agent_type)
103        } else {
104            format!("{}{}", self.harness.name(), model_str)
105        }
106    }
107}
108
109/// Generate a prompt for Claude Code with task context
110pub fn generate_prompt(task: &Task, tag: &str) -> String {
111    let mut prompt = format!(
112        r#"You are working on SCUD task {id}: {title}
113
114Tag: {tag}
115Complexity: {complexity}
116Priority: {priority:?}
117
118Description:
119{description}
120"#,
121        id = task.id,
122        title = task.title,
123        tag = tag,
124        complexity = task.complexity,
125        priority = task.priority,
126        description = task.description,
127    );
128
129    // Add details if present
130    if let Some(ref details) = task.details {
131        prompt.push_str(&format!(
132            r#"
133Technical Details:
134{}
135"#,
136            details
137        ));
138    }
139
140    // Add test strategy if present
141    if let Some(ref test_strategy) = task.test_strategy {
142        prompt.push_str(&format!(
143            r#"
144Test Strategy:
145{}
146"#,
147            test_strategy
148        ));
149    }
150
151    // Add dependencies info if any
152    if !task.dependencies.is_empty() {
153        prompt.push_str(&format!(
154            r#"
155Dependencies (should be done):
156{}
157"#,
158            task.dependencies.join(", ")
159        ));
160    }
161
162    // Add instructions
163    prompt.push_str(&format!(
164        r#"
165Instructions:
1661. Check for discoveries from other agents: scud log-all --limit 10
1672. Explore the codebase to understand the context for this task
1683. Implement the task following project conventions and patterns
1694. Log important discoveries to share with other agents:
170   scud log {id} "Found X in Y, useful for Z"
1715. Write tests if applicable based on the test strategy
1726. When complete, run: scud set-status {id} done
1737. If blocked by issues, run: scud set-status {id} blocked
174
175Discovery Logging:
176- Log findings that other agents might benefit from (file locations, patterns, gotchas)
177- Keep logs concise but informative (1-3 sentences)
178- Example: scud log {id} "Auth helpers are in lib/auth.rs, not utils/"
179
180Begin by checking recent logs and exploring relevant code.
181"#,
182        id = task.id
183    ));
184
185    prompt
186}
187
188/// Generate a shorter prompt for tasks with less context
189pub fn generate_minimal_prompt(task: &Task, tag: &str) -> String {
190    format!(
191        r#"SCUD Task {id}: {title}
192
193Tag: {tag}
194Description: {description}
195
196First: scud log-all --limit 5 (check recent discoveries)
197Log findings: scud log {id} "your discovery"
198When done: scud set-status {id} done
199If blocked: scud set-status {id} blocked
200"#,
201        id = task.id,
202        title = task.title,
203        tag = tag,
204        description = task.description
205    )
206}
207
208/// Generate a prompt using a custom template
209///
210/// Template placeholders:
211/// - {task.id} - Task ID
212/// - {task.title} - Task title
213/// - {task.description} - Task description
214/// - {task.complexity} - Complexity score
215/// - {task.priority} - Priority level
216/// - {task.details} - Technical details (empty if none)
217/// - {task.test_strategy} - Test strategy (empty if none)
218/// - {task.dependencies} - Comma-separated dependencies
219/// - {tag} - Phase/tag name
220pub fn generate_prompt_with_template(task: &Task, tag: &str, template: &str) -> String {
221    let mut result = template.to_string();
222
223    result = result.replace("{task.id}", &task.id);
224    result = result.replace("{task.title}", &task.title);
225    result = result.replace("{task.description}", &task.description);
226    result = result.replace("{task.complexity}", &task.complexity.to_string());
227    result = result.replace("{task.priority}", &format!("{:?}", task.priority));
228    result = result.replace("{task.details}", task.details.as_deref().unwrap_or(""));
229    result = result.replace(
230        "{task.test_strategy}",
231        task.test_strategy.as_deref().unwrap_or(""),
232    );
233    result = result.replace("{task.dependencies}", &task.dependencies.join(", "));
234    result = result.replace("{tag}", tag);
235
236    result
237}
238
239/// Generate a prompt for wave review
240pub fn generate_review_prompt(
241    summary: &WaveSummary,
242    tasks: &[(String, String)], // (task_id, title)
243    review_all: bool,
244) -> String {
245    let tasks_str = if review_all {
246        tasks
247            .iter()
248            .map(|(id, title)| format!("- {} | {}", id, title))
249            .collect::<Vec<_>>()
250            .join("\n")
251    } else {
252        // Sample: first task, last task, and one random middle task
253        let sample: Vec<_> = if tasks.len() <= 3 {
254            tasks.iter().collect()
255        } else {
256            vec![&tasks[0], &tasks[tasks.len() / 2], &tasks[tasks.len() - 1]]
257        };
258        sample
259            .iter()
260            .map(|(id, title)| format!("- {} | {}", id, title))
261            .collect::<Vec<_>>()
262            .join("\n")
263    };
264
265    let files_str = if summary.files_changed.len() <= 10 {
266        summary.files_changed.join("\n")
267    } else {
268        let mut s = summary.files_changed[..10].join("\n");
269        s.push_str(&format!(
270            "\n... and {} more files",
271            summary.files_changed.len() - 10
272        ));
273        s
274    };
275
276    format!(
277        r#"You are reviewing SCUD wave {wave_number}.
278
279## Tasks to Review
280{tasks}
281
282## Files Changed
283{files}
284
285## Review Process
2861. For each task, run: scud show <task_id>
2872. Read the changed files relevant to each task
2883. Check implementation quality and correctness
289
290## Output Format
291For each task:
292  PASS: <task_id> - looks good
293  IMPROVE: <task_id> - <specific issue>
294
295When complete, create marker file:
296  echo "REVIEW_COMPLETE: ALL_PASS" > .scud/review-complete-{wave_number}
297Or if improvements needed:
298  echo "REVIEW_COMPLETE: IMPROVEMENTS_NEEDED" > .scud/review-complete-{wave_number}
299  echo "IMPROVE_TASKS: <comma-separated task IDs>" >> .scud/review-complete-{wave_number}
300"#,
301        wave_number = summary.wave_number,
302        tasks = tasks_str,
303        files = files_str,
304    )
305}
306
307/// Generate a prompt for repair agent
308pub fn generate_repair_prompt(
309    task_id: &str,
310    task_title: &str,
311    failed_command: &str,
312    error_output: &str,
313    task_files: &[String],
314    error_files: &[String],
315) -> String {
316    let task_files_str = task_files.join(", ");
317    let error_files_str = error_files.join(", ");
318
319    format!(
320        r#"You are a repair agent fixing validation failures for SCUD task {task_id}: {task_title}
321
322## Validation Failure
323The following validation command failed:
324{failed_command}
325
326Error output:
327{error_output}
328
329## Attribution
330This failure has been attributed to task {task_id} based on git blame analysis.
331Files changed by this task: {task_files}
332
333## Your Mission
3341. Analyze the error output to understand what went wrong
3352. Read the relevant files: {error_files}
3363. Fix the issue while preserving the task's intended functionality
3374. Run the validation command to verify the fix: {failed_command}
338
339## Important
340- Focus on fixing the specific error, don't refactor unrelated code
341- If the fix requires changes to other tasks' code, note it but don't modify
342- After fixing, commit with: scud commit -m "fix: {task_id} - <description>"
343- Log what you fixed for other agents: scud log {task_id} "Fixed: <brief description>"
344
345When the validation passes:
346  scud log {task_id} "Repair successful: <what was fixed>"
347  scud set-status {task_id} done
348  echo "REPAIR_COMPLETE: SUCCESS" > .scud/repair-complete-{task_id}
349
350If you cannot fix it:
351  scud log {task_id} "Repair blocked: <reason>"
352  scud set-status {task_id} blocked
353  echo "REPAIR_COMPLETE: BLOCKED" > .scud/repair-complete-{task_id}
354  echo "REASON: <explanation>" >> .scud/repair-complete-{task_id}
355"#,
356        task_id = task_id,
357        task_title = task_title,
358        failed_command = failed_command,
359        error_output = error_output,
360        task_files = task_files_str,
361        error_files = error_files_str,
362    )
363}
364
365/// Generate a prompt for batch repair agent handling multiple tasks
366pub fn generate_batch_repair_prompt(
367    tasks: &[(String, String, Vec<String>)], // (task_id, title, files_changed)
368    failed_command: &str,
369    error_output: &str,
370    error_locations: &[(String, Option<u32>)], // (file, line)
371) -> String {
372    let tasks_str = tasks
373        .iter()
374        .map(|(id, title, files)| {
375            format!(
376                "- {} | {}\n  Files: {}",
377                id,
378                title,
379                files.join(", ")
380            )
381        })
382        .collect::<Vec<_>>()
383        .join("\n");
384
385    let error_locations_str = error_locations
386        .iter()
387        .take(20) // Limit to avoid prompt explosion
388        .map(|(file, line)| {
389            match line {
390                Some(l) => format!("  {}:{}", file, l),
391                None => format!("  {}", file),
392            }
393        })
394        .collect::<Vec<_>>()
395        .join("\n");
396
397    format!(
398        r#"You are a batch repair agent fixing validation failures for multiple SCUD tasks.
399
400## Validation Failure
401The following validation command failed:
402{failed_command}
403
404Error output:
405{error_output}
406
407## Error Locations
408{error_locations}
409
410## Responsible Tasks
411Based on git blame analysis, these tasks may be responsible:
412{tasks}
413
414## Your Mission
4151. Analyze the error output to understand ALL the issues
4162. Read the relevant files and understand what each task was trying to do
4173. Fix issues systematically - some errors may be related
4184. Run the validation command after each fix to check progress: {failed_command}
419
420## Process
421For each issue:
4221. Identify which task introduced it
4232. Read the task details: scud show <task_id>
4243. Fix the issue while preserving intended functionality
4254. Commit: scud commit -m "fix: <task_id> - <description>"
4265. Log: scud log <task_id> "Fixed: <brief description>"
427
428## Important
429- Fix ALL issues before signaling completion
430- Some issues may cascade - fix root causes first
431- If you cannot fix an issue, document why
432- Iterate until validation passes or you're truly blocked
433
434## Completion
435When ALL validation passes:
436  echo "BATCH_REPAIR_COMPLETE: SUCCESS" > .scud/batch-repair-complete
437  echo "FIXED_TASKS: <comma-separated task IDs that were fixed>" >> .scud/batch-repair-complete
438
439If blocked on some tasks:
440  echo "BATCH_REPAIR_COMPLETE: PARTIAL" > .scud/batch-repair-complete
441  echo "FIXED_TASKS: <task IDs fixed>" >> .scud/batch-repair-complete
442  echo "BLOCKED_TASKS: <task IDs blocked>" >> .scud/batch-repair-complete
443  echo "BLOCK_REASON: <explanation>" >> .scud/batch-repair-complete
444
445If completely blocked:
446  echo "BATCH_REPAIR_COMPLETE: BLOCKED" > .scud/batch-repair-complete
447  echo "REASON: <explanation>" >> .scud/batch-repair-complete
448"#,
449        failed_command = failed_command,
450        error_output = error_output,
451        error_locations = error_locations_str,
452        tasks = tasks_str,
453    )
454}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459    use crate::models::task::Task;
460
461    #[test]
462    fn test_generate_prompt_basic() {
463        let task = Task::new(
464            "auth:1".to_string(),
465            "Implement login".to_string(),
466            "Add user authentication flow".to_string(),
467        );
468
469        let prompt = generate_prompt(&task, "auth");
470
471        assert!(prompt.contains("auth:1"));
472        assert!(prompt.contains("Implement login"));
473        assert!(prompt.contains("Tag: auth"));
474        assert!(prompt.contains("scud set-status auth:1 done"));
475    }
476
477    #[test]
478    fn test_generate_prompt_with_details() {
479        let mut task = Task::new(
480            "api:2".to_string(),
481            "Add endpoint".to_string(),
482            "Create REST endpoint".to_string(),
483        );
484        task.details = Some("Use Express.js router pattern".to_string());
485        task.test_strategy = Some("Unit test with Jest".to_string());
486
487        let prompt = generate_prompt(&task, "api");
488
489        assert!(prompt.contains("Technical Details:"));
490        assert!(prompt.contains("Express.js router"));
491        assert!(prompt.contains("Test Strategy:"));
492        assert!(prompt.contains("Unit test with Jest"));
493    }
494
495    #[test]
496    fn test_generate_minimal_prompt() {
497        let task = Task::new(
498            "fix:1".to_string(),
499            "Quick fix".to_string(),
500            "Fix typo".to_string(),
501        );
502
503        let prompt = generate_minimal_prompt(&task, "fix");
504
505        assert!(prompt.contains("fix:1"));
506        assert!(prompt.contains("Quick fix"));
507        assert!(!prompt.contains("Technical Details"));
508    }
509
510    #[test]
511    fn test_generate_prompt_with_template() {
512        let mut task = Task::new(
513            "auth:1".to_string(),
514            "Login Feature".to_string(),
515            "Implement login".to_string(),
516        );
517        task.complexity = 5;
518        task.details = Some("Use OAuth".to_string());
519
520        let template = "Task: {task.id} - {task.title}\nTag: {tag}\nDetails: {task.details}";
521        let prompt = generate_prompt_with_template(&task, "auth", template);
522
523        assert_eq!(
524            prompt,
525            "Task: auth:1 - Login Feature\nTag: auth\nDetails: Use OAuth"
526        );
527    }
528
529    #[test]
530    fn test_generate_prompt_with_template_missing_fields() {
531        let task = Task::new("1".to_string(), "Title".to_string(), "Desc".to_string());
532
533        let template = "Details: {task.details} | Strategy: {task.test_strategy}";
534        let prompt = generate_prompt_with_template(&task, "test", template);
535
536        assert_eq!(prompt, "Details:  | Strategy: ");
537    }
538
539    #[test]
540    fn test_generate_review_prompt_all() {
541        let summary = WaveSummary {
542            wave_number: 1,
543            tasks_completed: vec!["auth:1".to_string(), "auth:2".to_string()],
544            files_changed: vec!["src/auth.rs".to_string(), "src/main.rs".to_string()],
545        };
546
547        let tasks = vec![
548            ("auth:1".to_string(), "Add login".to_string()),
549            ("auth:2".to_string(), "Add logout".to_string()),
550        ];
551
552        let prompt = generate_review_prompt(&summary, &tasks, true);
553
554        assert!(prompt.contains("wave 1"));
555        assert!(prompt.contains("auth:1 | Add login"));
556        assert!(prompt.contains("auth:2 | Add logout"));
557        assert!(prompt.contains("src/auth.rs"));
558    }
559
560    #[test]
561    fn test_generate_review_prompt_sampled() {
562        let summary = WaveSummary {
563            wave_number: 2,
564            tasks_completed: vec![
565                "t:1".to_string(),
566                "t:2".to_string(),
567                "t:3".to_string(),
568                "t:4".to_string(),
569                "t:5".to_string(),
570            ],
571            files_changed: vec!["a.rs".to_string()],
572        };
573
574        let tasks: Vec<_> = (1..=5)
575            .map(|i| (format!("t:{}", i), format!("Task {}", i)))
576            .collect();
577
578        let prompt = generate_review_prompt(&summary, &tasks, false);
579
580        // Should only include first, middle, and last (3 tasks sampled)
581        assert!(prompt.contains("t:1"));
582        assert!(prompt.contains("t:3")); // middle
583        assert!(prompt.contains("t:5")); // last
584                                         // t:2 and t:4 should not be present
585        assert!(!prompt.contains("t:2 | Task 2"));
586        assert!(!prompt.contains("t:4 | Task 4"));
587    }
588
589    #[test]
590    fn test_generate_repair_prompt() {
591        let prompt = generate_repair_prompt(
592            "auth:1",
593            "Add login",
594            "cargo build",
595            "error: mismatched types at src/main.rs:42",
596            &["src/auth.rs".to_string()],
597            &["src/main.rs".to_string()],
598        );
599
600        assert!(prompt.contains("auth:1"));
601        assert!(prompt.contains("Add login"));
602        assert!(prompt.contains("cargo build"));
603        assert!(prompt.contains("mismatched types"));
604        assert!(prompt.contains("src/auth.rs"));
605        assert!(prompt.contains("src/main.rs"));
606        assert!(prompt.contains("REPAIR_COMPLETE"));
607    }
608
609    // =========================================================================
610    // Tests for resolve_agent_config
611    // =========================================================================
612
613    #[test]
614    fn test_resolve_agent_config_no_agent_type() {
615        let temp = tempfile::TempDir::new().unwrap();
616        let task = Task::new("1".to_string(), "Test".to_string(), "Desc".to_string());
617
618        let config = resolve_agent_config(&task, "test", Harness::Claude, None, temp.path());
619
620        assert_eq!(config.harness, Harness::Claude);
621        assert_eq!(config.model, None);
622        assert!(!config.from_agent_def);
623        assert!(config.agent_type.is_none());
624    }
625
626    #[test]
627    fn test_resolve_agent_config_uses_default_model() {
628        let temp = tempfile::TempDir::new().unwrap();
629        let task = Task::new("1".to_string(), "Test".to_string(), "Desc".to_string());
630
631        let config =
632            resolve_agent_config(&task, "test", Harness::Claude, Some("opus"), temp.path());
633
634        assert_eq!(config.harness, Harness::Claude);
635        assert_eq!(config.model, Some("opus".to_string()));
636        assert!(!config.from_agent_def);
637    }
638
639    #[test]
640    fn test_resolve_agent_config_agent_type_not_found() {
641        let temp = tempfile::TempDir::new().unwrap();
642        let mut task = Task::new("1".to_string(), "Test".to_string(), "Desc".to_string());
643        task.agent_type = Some("nonexistent".to_string());
644
645        let config =
646            resolve_agent_config(&task, "test", Harness::Claude, Some("sonnet"), temp.path());
647
648        // Should fall back to defaults when agent def not found
649        assert_eq!(config.harness, Harness::Claude);
650        assert_eq!(config.model, Some("sonnet".to_string()));
651        assert!(!config.from_agent_def);
652        assert_eq!(config.agent_type, Some("nonexistent".to_string()));
653    }
654
655    #[test]
656    fn test_resolve_agent_config_from_agent_def() {
657        let temp = tempfile::TempDir::new().unwrap();
658        let agents_dir = temp.path().join(".scud").join("agents");
659        std::fs::create_dir_all(&agents_dir).unwrap();
660
661        // Create a fast-builder agent definition with opencode harness
662        let agent_file = agents_dir.join("fast-builder.toml");
663        std::fs::write(
664            &agent_file,
665            r#"
666[agent]
667name = "fast-builder"
668description = "Fast builder"
669
670[model]
671harness = "opencode"
672model = "xai/grok-code-fast-1"
673"#,
674        )
675        .unwrap();
676
677        let mut task = Task::new("1".to_string(), "Test".to_string(), "Desc".to_string());
678        task.agent_type = Some("fast-builder".to_string());
679
680        // Default harness is claude, but agent def should override to opencode
681        let config =
682            resolve_agent_config(&task, "test", Harness::Claude, Some("opus"), temp.path());
683
684        assert_eq!(config.harness, Harness::OpenCode);
685        assert_eq!(config.model, Some("xai/grok-code-fast-1".to_string()));
686        assert!(config.from_agent_def);
687        assert_eq!(config.agent_type, Some("fast-builder".to_string()));
688    }
689
690    #[test]
691    fn test_resolve_agent_config_agent_def_without_model_uses_default() {
692        let temp = tempfile::TempDir::new().unwrap();
693        let agents_dir = temp.path().join(".scud").join("agents");
694        std::fs::create_dir_all(&agents_dir).unwrap();
695
696        // Agent def with harness but no model
697        let agent_file = agents_dir.join("custom.toml");
698        std::fs::write(
699            &agent_file,
700            r#"
701[agent]
702name = "custom"
703
704[model]
705harness = "opencode"
706"#,
707        )
708        .unwrap();
709
710        let mut task = Task::new("1".to_string(), "Test".to_string(), "Desc".to_string());
711        task.agent_type = Some("custom".to_string());
712
713        let config =
714            resolve_agent_config(&task, "test", Harness::Claude, Some("opus"), temp.path());
715
716        // Harness comes from agent def, model falls back to default
717        assert_eq!(config.harness, Harness::OpenCode);
718        assert_eq!(config.model, Some("opus".to_string()));
719        assert!(config.from_agent_def);
720    }
721
722    #[test]
723    fn test_resolve_agent_config_uses_custom_prompt_template() {
724        let temp = tempfile::TempDir::new().unwrap();
725        let agents_dir = temp.path().join(".scud").join("agents");
726        std::fs::create_dir_all(&agents_dir).unwrap();
727
728        let agent_file = agents_dir.join("templated.toml");
729        std::fs::write(
730            &agent_file,
731            r#"
732[agent]
733name = "templated"
734
735[model]
736harness = "claude"
737
738[prompt]
739template = "Custom: {task.title} in {tag}"
740"#,
741        )
742        .unwrap();
743
744        let mut task = Task::new("1".to_string(), "My Task".to_string(), "Desc".to_string());
745        task.agent_type = Some("templated".to_string());
746
747        let config = resolve_agent_config(&task, "my-tag", Harness::Claude, None, temp.path());
748
749        assert_eq!(config.prompt, "Custom: My Task in my-tag");
750        assert!(config.from_agent_def);
751    }
752
753    #[test]
754    fn test_resolved_agent_config_display_info() {
755        let config = ResolvedAgentConfig {
756            harness: Harness::OpenCode,
757            model: Some("xai/grok-code-fast-1".to_string()),
758            prompt: "test".to_string(),
759            from_agent_def: true,
760            agent_type: Some("fast-builder".to_string()),
761        };
762
763        assert_eq!(
764            config.display_info(),
765            "opencode:xai/grok-code-fast-1@fast-builder"
766        );
767
768        let config_no_model = ResolvedAgentConfig {
769            harness: Harness::Claude,
770            model: None,
771            prompt: "test".to_string(),
772            from_agent_def: false,
773            agent_type: None,
774        };
775
776        assert_eq!(config_no_model.display_info(), "claude");
777    }
778}