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)| format!("- {} | {}\n  Files: {}", id, title, files.join(", ")))
375        .collect::<Vec<_>>()
376        .join("\n");
377
378    let error_locations_str = error_locations
379        .iter()
380        .take(20) // Limit to avoid prompt explosion
381        .map(|(file, line)| match line {
382            Some(l) => format!("  {}:{}", file, l),
383            None => format!("  {}", file),
384        })
385        .collect::<Vec<_>>()
386        .join("\n");
387
388    format!(
389        r#"You are a batch repair agent fixing validation failures for multiple SCUD tasks.
390
391## Validation Failure
392The following validation command failed:
393{failed_command}
394
395Error output:
396{error_output}
397
398## Error Locations
399{error_locations}
400
401## Responsible Tasks
402Based on git blame analysis, these tasks may be responsible:
403{tasks}
404
405## Your Mission
4061. Analyze the error output to understand ALL the issues
4072. Read the relevant files and understand what each task was trying to do
4083. Fix issues systematically - some errors may be related
4094. Run the validation command after each fix to check progress: {failed_command}
410
411## Process
412For each issue:
4131. Identify which task introduced it
4142. Read the task details: scud show <task_id>
4153. Fix the issue while preserving intended functionality
4164. Commit: scud commit -m "fix: <task_id> - <description>"
4175. Log: scud log <task_id> "Fixed: <brief description>"
418
419## Important
420- Fix ALL issues before signaling completion
421- Some issues may cascade - fix root causes first
422- If you cannot fix an issue, document why
423- Iterate until validation passes or you're truly blocked
424
425## Completion
426When ALL validation passes:
427  echo "BATCH_REPAIR_COMPLETE: SUCCESS" > .scud/batch-repair-complete
428  echo "FIXED_TASKS: <comma-separated task IDs that were fixed>" >> .scud/batch-repair-complete
429
430If blocked on some tasks:
431  echo "BATCH_REPAIR_COMPLETE: PARTIAL" > .scud/batch-repair-complete
432  echo "FIXED_TASKS: <task IDs fixed>" >> .scud/batch-repair-complete
433  echo "BLOCKED_TASKS: <task IDs blocked>" >> .scud/batch-repair-complete
434  echo "BLOCK_REASON: <explanation>" >> .scud/batch-repair-complete
435
436If completely blocked:
437  echo "BATCH_REPAIR_COMPLETE: BLOCKED" > .scud/batch-repair-complete
438  echo "REASON: <explanation>" >> .scud/batch-repair-complete
439"#,
440        failed_command = failed_command,
441        error_output = error_output,
442        error_locations = error_locations_str,
443        tasks = tasks_str,
444    )
445}
446
447#[cfg(test)]
448mod tests {
449    use super::*;
450    use crate::models::task::Task;
451
452    #[test]
453    fn test_generate_prompt_basic() {
454        let task = Task::new(
455            "auth:1".to_string(),
456            "Implement login".to_string(),
457            "Add user authentication flow".to_string(),
458        );
459
460        let prompt = generate_prompt(&task, "auth");
461
462        assert!(prompt.contains("auth:1"));
463        assert!(prompt.contains("Implement login"));
464        assert!(prompt.contains("Tag: auth"));
465        assert!(prompt.contains("scud set-status auth:1 done"));
466    }
467
468    #[test]
469    fn test_generate_prompt_with_details() {
470        let mut task = Task::new(
471            "api:2".to_string(),
472            "Add endpoint".to_string(),
473            "Create REST endpoint".to_string(),
474        );
475        task.details = Some("Use Express.js router pattern".to_string());
476        task.test_strategy = Some("Unit test with Jest".to_string());
477
478        let prompt = generate_prompt(&task, "api");
479
480        assert!(prompt.contains("Technical Details:"));
481        assert!(prompt.contains("Express.js router"));
482        assert!(prompt.contains("Test Strategy:"));
483        assert!(prompt.contains("Unit test with Jest"));
484    }
485
486    #[test]
487    fn test_generate_minimal_prompt() {
488        let task = Task::new(
489            "fix:1".to_string(),
490            "Quick fix".to_string(),
491            "Fix typo".to_string(),
492        );
493
494        let prompt = generate_minimal_prompt(&task, "fix");
495
496        assert!(prompt.contains("fix:1"));
497        assert!(prompt.contains("Quick fix"));
498        assert!(!prompt.contains("Technical Details"));
499    }
500
501    #[test]
502    fn test_generate_prompt_with_template() {
503        let mut task = Task::new(
504            "auth:1".to_string(),
505            "Login Feature".to_string(),
506            "Implement login".to_string(),
507        );
508        task.complexity = 5;
509        task.details = Some("Use OAuth".to_string());
510
511        let template = "Task: {task.id} - {task.title}\nTag: {tag}\nDetails: {task.details}";
512        let prompt = generate_prompt_with_template(&task, "auth", template);
513
514        assert_eq!(
515            prompt,
516            "Task: auth:1 - Login Feature\nTag: auth\nDetails: Use OAuth"
517        );
518    }
519
520    #[test]
521    fn test_generate_prompt_with_template_missing_fields() {
522        let task = Task::new("1".to_string(), "Title".to_string(), "Desc".to_string());
523
524        let template = "Details: {task.details} | Strategy: {task.test_strategy}";
525        let prompt = generate_prompt_with_template(&task, "test", template);
526
527        assert_eq!(prompt, "Details:  | Strategy: ");
528    }
529
530    #[test]
531    fn test_generate_review_prompt_all() {
532        let summary = WaveSummary {
533            wave_number: 1,
534            tasks_completed: vec!["auth:1".to_string(), "auth:2".to_string()],
535            files_changed: vec!["src/auth.rs".to_string(), "src/main.rs".to_string()],
536        };
537
538        let tasks = vec![
539            ("auth:1".to_string(), "Add login".to_string()),
540            ("auth:2".to_string(), "Add logout".to_string()),
541        ];
542
543        let prompt = generate_review_prompt(&summary, &tasks, true);
544
545        assert!(prompt.contains("wave 1"));
546        assert!(prompt.contains("auth:1 | Add login"));
547        assert!(prompt.contains("auth:2 | Add logout"));
548        assert!(prompt.contains("src/auth.rs"));
549    }
550
551    #[test]
552    fn test_generate_review_prompt_sampled() {
553        let summary = WaveSummary {
554            wave_number: 2,
555            tasks_completed: vec![
556                "t:1".to_string(),
557                "t:2".to_string(),
558                "t:3".to_string(),
559                "t:4".to_string(),
560                "t:5".to_string(),
561            ],
562            files_changed: vec!["a.rs".to_string()],
563        };
564
565        let tasks: Vec<_> = (1..=5)
566            .map(|i| (format!("t:{}", i), format!("Task {}", i)))
567            .collect();
568
569        let prompt = generate_review_prompt(&summary, &tasks, false);
570
571        // Should only include first, middle, and last (3 tasks sampled)
572        assert!(prompt.contains("t:1"));
573        assert!(prompt.contains("t:3")); // middle
574        assert!(prompt.contains("t:5")); // last
575                                         // t:2 and t:4 should not be present
576        assert!(!prompt.contains("t:2 | Task 2"));
577        assert!(!prompt.contains("t:4 | Task 4"));
578    }
579
580    #[test]
581    fn test_generate_repair_prompt() {
582        let prompt = generate_repair_prompt(
583            "auth:1",
584            "Add login",
585            "cargo build",
586            "error: mismatched types at src/main.rs:42",
587            &["src/auth.rs".to_string()],
588            &["src/main.rs".to_string()],
589        );
590
591        assert!(prompt.contains("auth:1"));
592        assert!(prompt.contains("Add login"));
593        assert!(prompt.contains("cargo build"));
594        assert!(prompt.contains("mismatched types"));
595        assert!(prompt.contains("src/auth.rs"));
596        assert!(prompt.contains("src/main.rs"));
597        assert!(prompt.contains("REPAIR_COMPLETE"));
598    }
599
600    // =========================================================================
601    // Tests for resolve_agent_config
602    // =========================================================================
603
604    #[test]
605    fn test_resolve_agent_config_no_agent_type() {
606        let temp = tempfile::TempDir::new().unwrap();
607        let task = Task::new("1".to_string(), "Test".to_string(), "Desc".to_string());
608
609        let config = resolve_agent_config(&task, "test", Harness::Claude, None, temp.path());
610
611        assert_eq!(config.harness, Harness::Claude);
612        assert_eq!(config.model, None);
613        assert!(!config.from_agent_def);
614        assert!(config.agent_type.is_none());
615    }
616
617    #[test]
618    fn test_resolve_agent_config_uses_default_model() {
619        let temp = tempfile::TempDir::new().unwrap();
620        let task = Task::new("1".to_string(), "Test".to_string(), "Desc".to_string());
621
622        let config =
623            resolve_agent_config(&task, "test", Harness::Claude, Some("opus"), temp.path());
624
625        assert_eq!(config.harness, Harness::Claude);
626        assert_eq!(config.model, Some("opus".to_string()));
627        assert!(!config.from_agent_def);
628    }
629
630    #[test]
631    fn test_resolve_agent_config_agent_type_not_found() {
632        let temp = tempfile::TempDir::new().unwrap();
633        let mut task = Task::new("1".to_string(), "Test".to_string(), "Desc".to_string());
634        task.agent_type = Some("nonexistent".to_string());
635
636        let config =
637            resolve_agent_config(&task, "test", Harness::Claude, Some("sonnet"), temp.path());
638
639        // Should fall back to defaults when agent def not found
640        assert_eq!(config.harness, Harness::Claude);
641        assert_eq!(config.model, Some("sonnet".to_string()));
642        assert!(!config.from_agent_def);
643        assert_eq!(config.agent_type, Some("nonexistent".to_string()));
644    }
645
646    #[test]
647    fn test_resolve_agent_config_from_agent_def() {
648        let temp = tempfile::TempDir::new().unwrap();
649        let agents_dir = temp.path().join(".scud").join("agents");
650        std::fs::create_dir_all(&agents_dir).unwrap();
651
652        // Create a fast-builder agent definition with opencode harness
653        let agent_file = agents_dir.join("fast-builder.toml");
654        std::fs::write(
655            &agent_file,
656            r#"
657[agent]
658name = "fast-builder"
659description = "Fast builder"
660
661[model]
662harness = "opencode"
663model = "xai/grok-code-fast-1"
664"#,
665        )
666        .unwrap();
667
668        let mut task = Task::new("1".to_string(), "Test".to_string(), "Desc".to_string());
669        task.agent_type = Some("fast-builder".to_string());
670
671        // Default harness is claude, but agent def should override to opencode
672        let config =
673            resolve_agent_config(&task, "test", Harness::Claude, Some("opus"), temp.path());
674
675        assert_eq!(config.harness, Harness::OpenCode);
676        assert_eq!(config.model, Some("xai/grok-code-fast-1".to_string()));
677        assert!(config.from_agent_def);
678        assert_eq!(config.agent_type, Some("fast-builder".to_string()));
679    }
680
681    #[test]
682    fn test_resolve_agent_config_agent_def_without_model_uses_default() {
683        let temp = tempfile::TempDir::new().unwrap();
684        let agents_dir = temp.path().join(".scud").join("agents");
685        std::fs::create_dir_all(&agents_dir).unwrap();
686
687        // Agent def with harness but no model
688        let agent_file = agents_dir.join("custom.toml");
689        std::fs::write(
690            &agent_file,
691            r#"
692[agent]
693name = "custom"
694
695[model]
696harness = "opencode"
697"#,
698        )
699        .unwrap();
700
701        let mut task = Task::new("1".to_string(), "Test".to_string(), "Desc".to_string());
702        task.agent_type = Some("custom".to_string());
703
704        let config =
705            resolve_agent_config(&task, "test", Harness::Claude, Some("opus"), temp.path());
706
707        // Harness comes from agent def, model falls back to default
708        assert_eq!(config.harness, Harness::OpenCode);
709        assert_eq!(config.model, Some("opus".to_string()));
710        assert!(config.from_agent_def);
711    }
712
713    #[test]
714    fn test_resolve_agent_config_uses_custom_prompt_template() {
715        let temp = tempfile::TempDir::new().unwrap();
716        let agents_dir = temp.path().join(".scud").join("agents");
717        std::fs::create_dir_all(&agents_dir).unwrap();
718
719        let agent_file = agents_dir.join("templated.toml");
720        std::fs::write(
721            &agent_file,
722            r#"
723[agent]
724name = "templated"
725
726[model]
727harness = "claude"
728
729[prompt]
730template = "Custom: {task.title} in {tag}"
731"#,
732        )
733        .unwrap();
734
735        let mut task = Task::new("1".to_string(), "My Task".to_string(), "Desc".to_string());
736        task.agent_type = Some("templated".to_string());
737
738        let config = resolve_agent_config(&task, "my-tag", Harness::Claude, None, temp.path());
739
740        assert_eq!(config.prompt, "Custom: My Task in my-tag");
741        assert!(config.from_agent_def);
742    }
743
744    #[test]
745    fn test_resolved_agent_config_display_info() {
746        let config = ResolvedAgentConfig {
747            harness: Harness::OpenCode,
748            model: Some("xai/grok-code-fast-1".to_string()),
749            prompt: "test".to_string(),
750            from_agent_def: true,
751            agent_type: Some("fast-builder".to_string()),
752        };
753
754        assert_eq!(
755            config.display_info(),
756            "opencode:xai/grok-code-fast-1@fast-builder"
757        );
758
759        let config_no_model = ResolvedAgentConfig {
760            harness: Harness::Claude,
761            model: None,
762            prompt: "test".to_string(),
763            from_agent_def: false,
764            agent_type: None,
765        };
766
767        assert_eq!(config_no_model.display_info(), "claude");
768    }
769}