car-server-core 0.47.0

Transport-neutral library for the CAR daemon JSON-RPC dispatcher (used by car-server and tokhn-daemon)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
//! Engine selection: native CAR loop vs. delegation to an external CLI.
//!
//! Resolution order: an explicit choice always wins; `Auto` assesses the
//! intent with `car-inference`'s [`TaskComplexity`] heuristics and delegates
//! to the preferred ready external CLI only for `Complex` work, keeping
//! routine tasks on the native loop. Detection is injected as plain data so
//! resolution is unit-testable without binaries on `$PATH`.

use car_inference::adaptive_router::TaskComplexity;
use serde::{Deserialize, Serialize};

/// Which engine performs the coding work.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EngineChoice {
    /// Decide per task: complexity assessment + detected external CLIs.
    Auto,
    /// CAR's own inference + tool loop.
    Native,
    /// One session of a detected external agentic CLI (`claude-code`,
    /// `codex`, `gemini`).
    External(String),
    /// Foreman (#274): decompose the intent, farm subtasks to the external
    /// CLI in parallel worktrees, gate each patch + the integrated union,
    /// then land the verified union in the session worktree. Declines to
    /// single-session/native when the plan has no parallelism.
    Foreman(String),
}

impl EngineChoice {
    /// Parse the RPC/CLI form: `auto` | `native` | `external[:agent_id]` |
    /// `foreman[:agent_id]`.
    pub fn parse(s: &str) -> Result<Self, String> {
        match s.trim() {
            "auto" | "" => Ok(Self::Auto),
            "native" => Ok(Self::Native),
            "external" => Ok(Self::External(String::new())),
            "foreman" => Ok(Self::Foreman(String::new())),
            other => {
                if let Some(id) = other.strip_prefix("external:") {
                    if !id.is_empty() {
                        return Ok(Self::External(id.to_string()));
                    }
                }
                if let Some(id) = other.strip_prefix("foreman:") {
                    if !id.is_empty() {
                        return Ok(Self::Foreman(id.to_string()));
                    }
                }
                Err(format!(
                    "unknown engine '{other}' (expected auto | native | external[:agent_id] | foreman[:agent_id])"
                ))
            }
        }
    }

    pub fn label(&self) -> String {
        match self {
            Self::Auto => "auto".to_string(),
            Self::Native => "native".to_string(),
            Self::External(id) if id.is_empty() => "external".to_string(),
            Self::External(id) => format!("external:{id}"),
            Self::Foreman(id) if id.is_empty() => "foreman".to_string(),
            Self::Foreman(id) => format!("foreman:{id}"),
        }
    }
}

/// What resolution needs to know about one detected external CLI.
#[derive(Debug, Clone)]
pub struct DetectedAgent {
    pub id: String,
    /// Health bucket is `Ready` (authenticated, status command succeeded).
    pub ready: bool,
    /// Set when detection proved the binary cannot be executed at all —
    /// carries the diagnosis. Kept separate from `ready` because the two
    /// answer different questions, and collapsing them is what made a
    /// quarantined binary indistinguishable from a signed-out one. Sending
    /// a user to `codex login` when the login command is itself
    /// unrunnable is a dead end.
    pub unusable_reason: Option<String>,
}

impl DetectedAgent {
    /// The user-facing explanation for why this agent can't be used, or
    /// `None` when it is ready. Prefers the executability diagnosis over
    /// the auth guess.
    fn unavailable_reason(&self, kind: &str) -> Option<String> {
        if let Some(why) = &self.unusable_reason {
            return Some(format!(
                "{kind} '{}' is installed but cannot be executed: {why}",
                self.id
            ));
        }
        if !self.ready {
            return Some(format!(
                "{kind} '{}' is installed but not ready (not authenticated?)",
                self.id
            ));
        }
        None
    }
}

/// Default delegation preference when several CLIs are ready.
pub const DEFAULT_PREFERENCE: [&str; 3] = ["claude-code", "codex", "gemini"];

/// A resolved engine (never `Auto`) plus the reason, for the event stream.
#[derive(Debug, Clone, PartialEq)]
pub struct ResolvedEngine {
    pub engine: EngineChoice,
    pub reason: String,
}

/// Snapshot the currently-detected external CLIs (with health) into the
/// plain shape [`resolve_engine`] consumes.
pub async fn detect_ready_agents() -> Vec<DetectedAgent> {
    car_external_agents::detect_with_health(false)
        .await
        .into_iter()
        .map(|spec| DetectedAgent {
            ready: matches!(
                &spec.health,
                Some(h) if h.status == car_external_agents::HealthStatus::Ready
            ),
            unusable_reason: spec.unusable_reason().map(str::to_string),
            id: spec.id,
        })
        .collect()
}

fn first_ready(preference: &[&str], detected: &[DetectedAgent]) -> Option<String> {
    preference
        .iter()
        .find(|p| detected.iter().any(|d| d.ready && d.id == **p))
        .map(|p| p.to_string())
        // A ready agent outside the preference list still beats nothing.
        .or_else(|| detected.iter().find(|d| d.ready).map(|d| d.id.clone()))
}

/// Resolve the requested engine against the detected CLIs and the intent's
/// assessed complexity. Errors only when an *explicit* external request can't
/// be satisfied — `Auto` always resolves (falling back to native).
pub fn resolve_engine(
    requested: &EngineChoice,
    intent: &str,
    detected: &[DetectedAgent],
    preference: &[&str],
) -> Result<ResolvedEngine, String> {
    match requested {
        EngineChoice::Native => Ok(ResolvedEngine {
            engine: EngineChoice::Native,
            reason: "explicitly requested".into(),
        }),
        EngineChoice::External(id) if !id.is_empty() => {
            let agent = detected
                .iter()
                .find(|d| d.id == *id)
                .ok_or_else(|| format!("external agent '{id}' is not installed"))?;
            if let Some(why) = agent.unavailable_reason("external agent") {
                return Err(why);
            }
            Ok(ResolvedEngine {
                engine: EngineChoice::External(id.clone()),
                reason: "explicitly requested".into(),
            })
        }
        EngineChoice::External(_) => {
            let id = first_ready(preference, detected).ok_or(
                "external engine requested but no external agent CLI is installed and ready",
            )?;
            Ok(ResolvedEngine {
                engine: EngineChoice::External(id),
                reason: "first ready external agent".into(),
            })
        }
        EngineChoice::Foreman(id) if !id.is_empty() => {
            let agent = detected
                .iter()
                .find(|d| d.id == *id)
                .ok_or_else(|| format!("foreman adapter '{id}' is not installed"))?;
            if let Some(why) = agent.unavailable_reason("foreman adapter") {
                return Err(why);
            }
            Ok(ResolvedEngine {
                engine: EngineChoice::Foreman(id.clone()),
                reason: "explicitly requested".into(),
            })
        }
        EngineChoice::Foreman(_) => {
            let id = first_ready(preference, detected).ok_or(
                "foreman engine requested but no external agent CLI is installed and ready",
            )?;
            Ok(ResolvedEngine {
                engine: EngineChoice::Foreman(id),
                reason: "first ready external agent".into(),
            })
        }
        EngineChoice::Auto => {
            // `assess` buckets almost any coding intent as `Code` (repair
            // markers like "fix"/"refactor" dominate), so `Code` alone can't
            // mean "delegate". Frontier-worthy = genuinely Complex, or a Code
            // task whose intent is long/multi-step enough that a frontier CLI
            // is likely to outperform the native loop. The explicit engine
            // override is the real control; this is a default, not a promise.
            let complexity = TaskComplexity::assess(intent);
            let broad_scope = intent.split_whitespace().count() > 120;
            let frontier_worthy = complexity == TaskComplexity::Complex
                || (complexity == TaskComplexity::Code && broad_scope);
            if frontier_worthy {
                if let Some(id) = first_ready(preference, detected) {
                    // Foreman-first: its planner self-selects — a plan with
                    // no parallelism declines to single-session, so trivial
                    // tasks never pay the farm-out cost.
                    return Ok(ResolvedEngine {
                        engine: EngineChoice::Foreman(id),
                        reason: format!(
                            "task assessed as {complexity:?} with broad scope and a frontier CLI is ready; \
                             foreman gates the parallel farm-out"
                        ),
                    });
                }
            }
            Ok(ResolvedEngine {
                engine: EngineChoice::Native,
                reason: if frontier_worthy {
                    "task is complex but no external CLI is ready; using native loop".into()
                } else {
                    format!("task assessed as {complexity:?}; native loop suffices")
                },
            })
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn agents(ready: &[&str], installed_not_ready: &[&str]) -> Vec<DetectedAgent> {
        ready
            .iter()
            .map(|id| DetectedAgent {
                id: id.to_string(),
                ready: true,
                unusable_reason: None,
            })
            .chain(installed_not_ready.iter().map(|id| DetectedAgent {
                id: id.to_string(),
                ready: false,
                unusable_reason: None,
            }))
            .collect()
    }

    /// An installed-but-unrunnable CLI must be reported as unrunnable,
    /// not as an auth problem. Sending the user to `codex login` when
    /// the login command is itself SIGKILLed is a dead end, and this is
    /// the path `coder.start --engine codex` takes — the one the
    /// original Gatekeeper report came from.
    #[test]
    fn explicit_engine_reports_unrunnable_over_auth() {
        let detected = vec![DetectedAgent {
            id: "codex".to_string(),
            ready: false,
            unusable_reason: Some("killed by signal 9".to_string()),
        }];
        let err = resolve_engine(
            &EngineChoice::External("codex".to_string()),
            "add a test",
            &detected,
            &DEFAULT_PREFERENCE,
        )
        .unwrap_err();
        assert!(
            err.contains("cannot be executed") && err.contains("signal 9"),
            "want the executability diagnosis, got {err:?}"
        );
        assert!(
            !err.contains("not authenticated"),
            "must not blame auth for an unrunnable binary, got {err:?}"
        );
    }

    // A prompt TaskComplexity::assess reliably buckets as Complex: long,
    // multi-step, architecture-flavored.
    fn complex_intent() -> String {
        format!(
            "Refactor the authentication architecture across the whole system, design and \
             implement the migration step by step, then analyze the tradeoffs. {}",
            "Consider every module and integration in depth. ".repeat(30)
        )
    }

    #[test]
    fn explicit_choice_always_wins() {
        let detected = agents(&["claude-code"], &[]);
        let r = resolve_engine(
            &EngineChoice::Native,
            &complex_intent(),
            &detected,
            &DEFAULT_PREFERENCE,
        )
        .unwrap();
        assert_eq!(r.engine, EngineChoice::Native);

        let r = resolve_engine(
            &EngineChoice::External("claude-code".into()),
            "tiny task",
            &detected,
            &DEFAULT_PREFERENCE,
        )
        .unwrap();
        assert_eq!(r.engine, EngineChoice::External("claude-code".into()));
    }

    #[test]
    fn explicit_external_fails_clearly_when_unavailable() {
        let err = resolve_engine(
            &EngineChoice::External("codex".into()),
            "x",
            &agents(&[], &["codex"]),
            &DEFAULT_PREFERENCE,
        )
        .unwrap_err();
        assert!(err.contains("not ready"), "{err}");

        let err = resolve_engine(
            &EngineChoice::External("codex".into()),
            "x",
            &agents(&[], &[]),
            &DEFAULT_PREFERENCE,
        )
        .unwrap_err();
        assert!(err.contains("not installed"), "{err}");
    }

    #[test]
    fn auto_with_no_clis_is_native() {
        let r = resolve_engine(
            &EngineChoice::Auto,
            &complex_intent(),
            &[],
            &DEFAULT_PREFERENCE,
        )
        .unwrap();
        assert_eq!(r.engine, EngineChoice::Native);
        assert!(r.reason.contains("no external CLI"), "{}", r.reason);
    }

    #[test]
    fn auto_simple_task_stays_native_even_with_clis() {
        let detected = agents(&["claude-code"], &[]);
        let r = resolve_engine(
            &EngineChoice::Auto,
            "fix typo in README",
            &detected,
            &DEFAULT_PREFERENCE,
        )
        .unwrap();
        assert_eq!(r.engine, EngineChoice::Native, "{}", r.reason);
    }

    #[test]
    fn auto_complex_task_delegates_foreman_first_in_preference_order() {
        let detected = agents(&["gemini", "claude-code"], &["codex"]);
        let r = resolve_engine(
            &EngineChoice::Auto,
            &complex_intent(),
            &detected,
            &DEFAULT_PREFERENCE,
        )
        .unwrap();
        assert_eq!(
            r.engine,
            EngineChoice::Foreman("claude-code".into()),
            "foreman-first with preference order: {}",
            r.reason
        );
    }

    #[test]
    fn explicit_foreman_resolves_and_fails_clearly() {
        let detected = agents(&["codex"], &["claude-code"]);
        let r = resolve_engine(
            &EngineChoice::Foreman(String::new()),
            "x",
            &detected,
            &DEFAULT_PREFERENCE,
        )
        .unwrap();
        assert_eq!(r.engine, EngineChoice::Foreman("codex".into()));

        let err = resolve_engine(
            &EngineChoice::Foreman("claude-code".into()),
            "x",
            &detected,
            &DEFAULT_PREFERENCE,
        )
        .unwrap_err();
        assert!(err.contains("not ready"), "{err}");

        let err = resolve_engine(
            &EngineChoice::Foreman(String::new()),
            "x",
            &agents(&[], &[]),
            &DEFAULT_PREFERENCE,
        )
        .unwrap_err();
        assert!(err.contains("no external agent"), "{err}");
    }

    #[test]
    fn ready_agent_outside_preference_still_selected() {
        let detected = agents(&["future-cli"], &[]);
        let r = resolve_engine(
            &EngineChoice::External(String::new()),
            "x",
            &detected,
            &DEFAULT_PREFERENCE,
        )
        .unwrap();
        assert_eq!(r.engine, EngineChoice::External("future-cli".into()));
    }

    #[test]
    fn parse_round_trips() {
        for (input, expect) in [
            ("auto", EngineChoice::Auto),
            ("", EngineChoice::Auto),
            ("native", EngineChoice::Native),
            ("external", EngineChoice::External(String::new())),
            (
                "external:claude-code",
                EngineChoice::External("claude-code".into()),
            ),
            ("foreman", EngineChoice::Foreman(String::new())),
            ("foreman:codex", EngineChoice::Foreman("codex".into())),
        ] {
            assert_eq!(EngineChoice::parse(input).unwrap(), expect);
        }
        assert!(EngineChoice::parse("warp-drive").is_err());
        assert!(EngineChoice::parse("external:").is_err());
        assert!(EngineChoice::parse("foreman:").is_err());
    }
}