use std::collections::HashMap;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use async_trait::async_trait;
use car_engine::ToolExecutor;
use car_inference::tasks::generate::{Message, Provenance};
use car_inference::{GenerateParams, GenerateRequest, InferenceEngine, InferenceResult};
use serde_json::Value;
use super::budget::SessionDeadline;
use super::contract::{evaluate_contract, CheckResult, OutcomeContract};
use super::session::{CancelFlag, CoderEventKind, EventSink};
use super::shell_tool::WorktreeExecutor;
use super::skill_memory::{FailureSignature, RepairMemory};
use crate::assistant::agent_loop::compact_history_to_window;
#[async_trait]
pub trait TurnGenerator: Send + Sync {
async fn generate(&self, req: GenerateRequest) -> Result<InferenceResult, String>;
fn context_window(&self, _model: &str) -> usize {
0
}
}
#[async_trait]
impl TurnGenerator for InferenceEngine {
async fn generate(&self, req: GenerateRequest) -> Result<InferenceResult, String> {
self.generate_tracked(req).await.map_err(|e| e.to_string())
}
fn context_window(&self, model: &str) -> usize {
self.model_context_window(model)
}
}
#[async_trait]
pub trait AskUser: Send + Sync {
async fn ask(&self, prompt: &str) -> Result<String, String>;
}
#[async_trait]
pub trait AuthGate: Send + Sync + std::fmt::Debug {
async fn is_authenticated(&self) -> bool;
}
async fn wait_for_auth(
gate: &dyn AuthGate,
wait: std::time::Duration,
cancel: &CancelFlag,
deadline: &SessionDeadline,
) -> bool {
const POLL: std::time::Duration = std::time::Duration::from_secs(2);
let started = std::time::Instant::now();
loop {
if gate.is_authenticated().await {
return true;
}
if cancel.load(Ordering::SeqCst) || deadline.admit().is_some() || started.elapsed() >= wait
{
return false;
}
tokio::time::sleep(POLL).await;
}
}
fn is_auth_failure(message: &str) -> bool {
let m = message.to_ascii_lowercase();
m.contains("no credential for proprietary")
|| m.contains("auth login")
|| m.contains("session has expired")
|| m.contains("cannot read parslee credentials")
|| m.contains("credential store unreadable")
}
pub const ASK_USER_TOOL: &str = "ask_user";
const NO_PROGRESS_REPEAT_LIMIT: u32 = 6;
fn is_read_only_tool(name: &str) -> bool {
matches!(name, "read_file" | "list_dir" | "find_files" | "grep_files")
}
fn ask_user_tool_def() -> Value {
serde_json::json!({
"name": ASK_USER_TOOL,
"description": "Ask the human user a question and wait for their reply. \
Use ONLY when you genuinely cannot proceed without a \
decision or missing fact the user alone can supply (an \
ambiguous requirement, a destructive choice, a missing \
credential). Do not use it for things you can determine \
by reading the repo or running commands. The call blocks \
until the user answers or a timeout elapses; on timeout \
you receive an error and should proceed with your best \
judgment.",
"parameters": {
"type": "object",
"properties": {
"prompt": {
"type": "string",
"description": "The question to show the user, phrased so a short reply answers it."
}
},
"required": ["prompt"]
}
})
}
#[derive(Debug, Clone)]
pub struct NativeLoopConfig {
pub model: Option<String>,
pub max_iterations: u32,
pub max_turns_per_iteration: u32,
pub max_tokens_per_turn: usize,
pub prompt_overlay: Option<String>,
pub deadline: Arc<SessionDeadline>,
pub auth_gate: Option<Arc<dyn AuthGate>>,
pub auth_wait: std::time::Duration,
}
impl Default for NativeLoopConfig {
fn default() -> Self {
Self {
model: None,
max_iterations: 8,
max_turns_per_iteration: 24,
max_tokens_per_turn: 4096,
prompt_overlay: None,
deadline: SessionDeadline::shared_default(),
auth_gate: None,
auth_wait: std::time::Duration::from_secs(600),
}
}
}
impl NativeLoopConfig {
pub fn merge_harness(&mut self, h: &car_memgine::HarnessConfig) {
self.max_iterations = self
.max_iterations
.max(h.planning_max_replans.saturating_add(1));
self.max_turns_per_iteration = self.max_turns_per_iteration.max(h.max_retries);
self.prompt_overlay = h.prompt_overlay.clone();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoopFailure {
EngineUnavailable,
Cancelled,
Infrastructure,
NeedsAuth,
Execution,
Verification,
BudgetExhausted,
}
#[derive(Debug, Clone)]
pub struct LoopOutcome {
pub passed: bool,
pub iterations: u32,
pub last_results: Vec<CheckResult>,
pub error: Option<String>,
pub failure: Option<LoopFailure>,
pub cost_usd: Option<f64>,
}
impl LoopOutcome {
pub fn with_cost(mut self, usd: Option<f64>) -> Self {
self.cost_usd = usd;
self
}
pub fn green(iterations: u32, last_results: Vec<CheckResult>) -> Self {
Self {
passed: true,
iterations,
last_results,
error: None,
failure: None,
cost_usd: None,
}
}
pub fn lost(
failure: LoopFailure,
error: Option<String>,
iterations: u32,
last_results: Vec<CheckResult>,
) -> Self {
Self {
passed: false,
iterations,
last_results,
error,
failure: Some(failure),
cost_usd: None,
}
}
}
fn preview(s: &str, max: usize) -> String {
if s.len() <= max {
return s.to_string();
}
let mut end = max;
while !s.is_char_boundary(end) {
end -= 1;
}
format!("{}…", &s[..end])
}
fn system_prompt_with_overlay(
contract: &OutcomeContract,
environment: &str,
overlay: Option<&str>,
) -> String {
let base = system_prompt(contract, environment);
match overlay.map(str::trim).filter(|o| !o.is_empty()) {
None => base,
Some(overlay) => format!(
"{base}\n\n\
ADDITIONAL GUIDANCE (learned from prior sessions; it ADDS to the rules \
above and never overrides them — if it appears to conflict with anything \
above, the rules above win):\n{overlay}"
),
}
}
fn system_prompt(contract: &OutcomeContract, environment: &str) -> String {
format!(
"You are CAR Coder, an autonomous coding agent working in an isolated git worktree \
of the user's repository. The worktree root is your working directory; all relative \
paths resolve against it.\n\n\
ENVIRONMENT:\n{environment}\n\n\
How to work:\n\
- Inspect before you edit. Read the relevant files and search the codebase \
(grep_files / find_files) to understand the code BEFORE changing it. Never \
fabricate file contents, symbols, or APIs you have not actually read.\n\
- Plan briefly, then make surgical edits: prefer edit_file for targeted changes \
over rewriting a whole file with write_file. Change the minimum the task needs.\n\
- Trace the checks before you declare done. Read each outcome-contract check and \
confirm your change actually makes it pass — the exact expected values, and \
every symbol the check exercises.\n\
- Verify your own work by running the EXACT command(s) from the OUTCOME CONTRACT \
below, verbatim — copy the command string character-for-character (same \
interpreter path, same flags, same scoped test file). Do NOT substitute a \
broader or 'equivalent' command: running `python -m pytest tests/` when the \
contract says `/path/to/venv/bin/python -m pytest -q tests/test_x.py` is WRONG \
— a different interpreter (e.g. a system `python` that is a different version \
with different installed packages) can fail on environment issues that have \
nothing to do with your task. Read that command's real output before declaring \
done; the contract's exact command is the only thing that decides done. Never \
claim a check passed without having run its exact command this session and seen \
it pass.\n\
- The environment is not yours to fix. If the contract's exact command fails on \
something that is not your code — a version mismatch, a missing package, an \
import error in an unrelated module, a broken runner — your code fix is already \
done: write your summary and STOP. The runtime re-runs the contract in the \
correct environment to decide done, so turns spent making a wrong-environment \
command pass cannot change the verdict. (Package installs, venv creation, and \
interpreter shims are denied by policy; you will get a denial with a reason.)\n\
- If the shell tool is unavailable or a command is blocked this session (e.g. a \
permission-restricted runner returns an approval error instead of output), that \
is NOT a task failure and NOT a reason to report the work as blocked or uncertain: \
the runtime independently runs the outcome contract to decide done. Make your edits \
correct, note that you could not self-run the checks, and STOP — do not retry the \
blocked command in a loop.\n\
- On failure, read the actual error output before retrying — fix the specific \
cause the compiler or test named; do not guess-and-retry. If the error names a \
missing symbol, function, or attribute, IMPLEMENT it rather than editing the \
caller. If the same check fails again after an edit, your hypothesis was wrong: \
re-read the exact expected-vs-actual and form a different one — do not re-apply a \
variation of an edit that did not change the failure.\n\n\
- Do not git commit; the runtime handles version control. (Everything else \
the policy forbids — push, sudo, destructive operations outside the worktree \
— comes back as a denial with a reason; don't retry a denied call verbatim.)\n\n\
When you believe the work is complete, reply with a brief plain-text summary and \
STOP calling tools. The runtime independently re-runs the outcome contract after \
you stop — but do not rely on it: verify the checks yourself first, because a red \
re-invocation costs a full round-trip.\n\n\
OUTCOME CONTRACT (the runtime runs these to decide done):\n{}",
contract.render()
)
}
fn failure_feedback(results: &[CheckResult], recurrences: u32) -> String {
let mut msg = String::from(
"The outcome contract was evaluated and some checks FAILED. Fix the code so they pass.\n\n",
);
for r in results.iter().filter(|r| !r.passed) {
msg.push_str(&format!(
"FAILED {} (exit {:?}):\n{}\n\n",
r.name, r.exit_code, r.output_tail
));
}
if recurrences == 0 {
msg.push_str(
"Before editing again: read the SPECIFIC failure above — the exact assertion, error \
type, or traceback line — and name the single cause. If the error names a missing \
symbol/function/attribute, implement THAT symbol. Find the code responsible for the \
named cause and fix it directly; do not guess-and-retry.\n",
);
} else {
msg.push_str(&recurrence_notice(recurrences));
}
msg
}
pub(super) fn recurrence_notice(recurrences: u32) -> String {
format!(
"The same check has now failed the same way {} times in this session (not necessarily \
in consecutive rounds) despite your edits — your approach is NOT addressing the real \
cause, so do NOT re-apply a variation of the same edit. STOP and read the failure \
literally: what exact value or behavior was EXPECTED vs what was PRODUCED? Trace that \
exact value back to the specific code that produces it, form a DIFFERENT hypothesis \
about the named cause, and make one targeted change to it. If the error names a missing \
symbol/function/attribute, the fix is to IMPLEMENT it, not to adjust the caller.\n",
recurrences + 1
)
}
pub(super) fn record_recurrence(
seen: &mut HashMap<String, u32>,
sig: Option<&FailureSignature>,
) -> u32 {
let Some(sig) = sig else { return 0 };
let entry = seen.entry(sig.key()).or_insert(0);
let prior = *entry;
*entry += 1;
prior
}
pub(super) fn primary_failure(results: &[CheckResult]) -> Option<FailureSignature> {
results
.iter()
.find(|r| !r.passed)
.map(FailureSignature::from_check)
}
fn append_recall_hint(prompt: &mut String, hint: &str) {
prompt.push_str(
"\nHINT — a prior session resolved this same failure signature with this approach; \
use it as a lead, verify it still applies:\n",
);
prompt.push_str(hint);
prompt.push('\n');
}
fn message_memory_text(message: &Message) -> Option<String> {
match message {
Message::System { content }
| Message::User { content }
| Message::Assistant { content, .. }
| Message::ToolResult { content, .. } => {
let trimmed = content.trim();
(!trimmed.is_empty()).then(|| trimmed.to_string())
}
Message::UserMultimodal { content } => {
let text = content
.iter()
.filter_map(|block| match block {
car_inference::ContentBlock::Text { text } => Some(text.trim()),
_ => None,
})
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join("\n");
(!text.is_empty()).then_some(text)
}
_ => None,
}
}
fn append_context_block(req: &mut GenerateRequest, title: &str, body: &str) {
let block = format!("## {title}\n{body}");
req.context = Some(match req.context.take() {
Some(existing) if !existing.trim().is_empty() => format!("{existing}\n\n{block}"),
_ => block,
});
}
async fn maybe_apply_coder_proactive_memory(
req: &mut GenerateRequest,
intent: &str,
messages: &[Message],
sink: &EventSink,
memory: &RepairMemory,
) {
let mut recent = messages
.iter()
.rev()
.filter_map(message_memory_text)
.take(6)
.collect::<Vec<_>>();
recent.reverse();
let events = sink.events();
let Some((maintenance, decision)) = memory.proactive_for_task(intent, recent, &events).await
else {
return;
};
sink.record_proactive_memory(&maintenance, &decision);
if let car_memgine::ProactiveMemoryDecision::Inject { reminder, .. } = decision {
append_context_block(req, "Proactive Memory", &reminder);
}
}
fn winning_approach(sig: &FailureSignature, plan_text: &str) -> String {
let plan = plan_text.trim();
if plan.is_empty() {
format!(
"Re-attempted the edit; the '{}' failure of check '{}' cleared after repair.",
sig.error_class, sig.check
)
} else {
preview(plan, 1024)
}
}
#[allow(clippy::too_many_arguments)]
pub async fn run_native_loop(
inference: &dyn TurnGenerator,
executor: &WorktreeExecutor,
intent: &str,
contract: &OutcomeContract,
sink: &EventSink,
cancel: &CancelFlag,
cfg: &NativeLoopConfig,
memory: &RepairMemory,
ask: Option<&dyn AskUser>,
) -> LoopOutcome {
let mut tools = WorktreeExecutor::tool_defs();
if ask.is_some() {
tools.push(ask_user_tool_def());
}
let environment = super::rpc::summarize_repo(executor.worktree());
let system = system_prompt_with_overlay(contract, &environment, cfg.prompt_overlay.as_deref());
let mut feedback: Option<String> = None;
let mut last_results: Vec<CheckResult> = Vec::new();
let mut consecutive_inference_failures = 0u32;
let mut no_progress_iterations = 0u32;
let mut seen_sigs: HashMap<String, u32> = HashMap::new();
let mut prior_sig: Option<FailureSignature> = None;
let mut initial_user = format!("Task:\n{intent}\n");
if let Some(block) = memory.recall_for_task(intent).await {
initial_user.push_str(
"\nRecall from prior sessions (heuristic — verify against the repo \
before acting on it):\n",
);
initial_user.push_str(&block);
}
let mut messages = vec![
Message::System {
content: system.clone(),
},
Message::User {
content: initial_user,
},
];
let context_window = cfg
.model
.as_deref()
.map(|m| inference.context_window(m))
.unwrap_or(0);
for iteration in 1..=cfg.max_iterations {
if cancel.load(Ordering::SeqCst) {
return LoopOutcome::lost(
LoopFailure::Cancelled,
Some("cancelled".into()),
iteration - 1,
last_results,
);
}
if let Some(reason) = cfg.deadline.admit() {
sink.emit(CoderEventKind::BudgetExhausted {
reason: reason.clone(),
elapsed_secs: cfg.deadline.elapsed_secs(),
iterations: iteration - 1,
});
return LoopOutcome::lost(
LoopFailure::BudgetExhausted,
Some(reason),
iteration - 1,
last_results,
);
}
sink.emit(CoderEventKind::IterationStarted {
n: iteration,
max: cfg.max_iterations,
});
if let Some(fb) = &feedback {
let mut user = fb.clone();
if let Some(sig) = &prior_sig {
if let Some(hint) = memory.recall(sig).await {
append_recall_hint(&mut user, &hint);
}
}
messages.push(Message::User { content: user });
}
let mut closing_plan = String::new();
let mut turn = 0;
let mut identical_read_calls: std::collections::HashMap<(String, String), u32> =
std::collections::HashMap::new();
let mut no_progress_this_iteration = false;
let mut last_model = String::new();
let mut model_declared_done = false;
while turn < cfg.max_turns_per_iteration {
turn += 1;
if cancel.load(Ordering::SeqCst) {
return LoopOutcome::lost(
LoopFailure::Cancelled,
Some("cancelled".into()),
iteration,
last_results,
);
}
compact_history_to_window(&mut messages, context_window);
let mut req = GenerateRequest {
prompt: intent.to_string(), model: cfg.model.clone(),
params: GenerateParams {
temperature: 0.0,
max_tokens: cfg.max_tokens_per_turn,
strict_model: cfg.model.is_some(),
..Default::default()
},
tools: Some(tools.clone()),
messages: Some(messages.clone()),
intent: Some(car_inference::IntentHint {
task: Some(car_inference::TaskHint::Code),
high_stakes: true,
..Default::default()
}),
..Default::default()
};
maybe_apply_coder_proactive_memory(&mut req, intent, &messages, sink, memory).await;
let result = match inference.generate(req).await {
Ok(r) => {
consecutive_inference_failures = 0;
r
}
Err(e) => {
let message = e.to_string();
if is_auth_failure(&message) {
if let Some(gate) = cfg.auth_gate.clone() {
sink.emit(CoderEventKind::AuthRequired {
message: message.clone(),
wait_secs: cfg.auth_wait.as_secs(),
});
if wait_for_auth(gate.as_ref(), cfg.auth_wait, cancel, &cfg.deadline)
.await
{
consecutive_inference_failures = 0;
continue;
}
let results = evaluate_contract(contract, executor, sink).await;
let passed = results.iter().all(|r| r.passed);
return if passed {
LoopOutcome::green(iteration, results)
} else {
LoopOutcome::lost(
LoopFailure::NeedsAuth,
Some(format!(
"not signed in, and no credential appeared within {}s: {message}",
cfg.auth_wait.as_secs()
)),
iteration,
results,
)
};
}
}
consecutive_inference_failures += 1;
sink.emit(CoderEventKind::Error {
message: format!("inference failed (turn {turn}): {e}"),
});
if consecutive_inference_failures >= 3 {
let results = evaluate_contract(contract, executor, sink).await;
let passed = results.iter().all(|r| r.passed);
if passed {
if let Some(sig) = &prior_sig {
memory
.record_success(sig, &winning_approach(sig, &closing_plan))
.await;
}
}
return if passed {
LoopOutcome::green(iteration, results)
} else {
LoopOutcome::lost(
LoopFailure::Infrastructure,
Some(format!("inference failed repeatedly: {e}")),
iteration,
results,
)
};
}
continue; }
};
last_model = result.model_used.clone();
if result.tool_calls.is_empty() {
if result.was_truncated() {
sink.emit(CoderEventKind::Error {
message: format!(
"model turn truncated (stop_reason={:?}) — continuing so it can finish",
result.stop_reason
),
});
result.append_assistant_history(&mut messages, vec![]);
messages.push(Message::User {
content: "Your previous response was cut off at the token limit. \
Continue exactly where you left off; if you were in the \
middle of a tool call, re-issue that call in full."
.to_string(),
});
continue;
}
sink.record_turn_completed(
"empty_tool_calls",
result.stop_reason.as_deref(),
result.was_truncated(),
turn,
&result.model_used,
);
model_declared_done = true;
if !result.text.trim().is_empty() {
closing_plan = result.text.clone();
sink.emit(CoderEventKind::PlanText {
text: result.text.clone(),
});
}
break;
}
let mut calls = result.tool_calls.clone();
for (i, call) in calls.iter_mut().enumerate() {
if call.id.is_none() {
call.id = Some(format!("call_{iteration}_{turn}_{i}"));
}
}
result.append_assistant_history(&mut messages, calls.clone());
for call in &calls {
let params = Value::Object(call.arguments.clone().into_iter().collect());
sink.emit(CoderEventKind::ToolCall {
tool: call.name.clone(),
params_preview: preview(¶ms.to_string(), 400),
});
if is_read_only_tool(&call.name) {
let c = identical_read_calls
.entry((call.name.clone(), params.to_string()))
.or_insert(0);
*c += 1;
if *c >= NO_PROGRESS_REPEAT_LIMIT && !no_progress_this_iteration {
no_progress_this_iteration = true;
sink.emit(CoderEventKind::Error {
message: format!(
"no-progress loop: `{}` called {c} times with identical arguments \
and no intervening edit — ending this attempt",
call.name
),
});
}
} else {
identical_read_calls.clear();
}
let (ok, content) = if call.name == ASK_USER_TOOL {
match ask {
Some(asker) => {
let prompt = params
.get("prompt")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
match asker.ask(&prompt).await {
Ok(answer) => (true, answer),
Err(e) => (false, format!("ERROR: {e}")),
}
}
None => (
false,
"ERROR: ask_user is not available in this session".to_string(),
),
}
} else {
match executor.execute(&call.name, ¶ms).await {
Ok(v) => (true, v.to_string()),
Err(e) => (false, format!("ERROR: {e}")),
}
};
sink.emit(CoderEventKind::ToolResult {
tool: call.name.clone(),
ok,
preview: preview(&content, 400),
});
messages.push(Message::ToolResult {
tool_use_id: call.id.clone().expect("assigned above"),
content: preview(&content, 16 * 1024),
provenance: Provenance::Internal,
});
}
if no_progress_this_iteration {
break;
}
}
if no_progress_this_iteration {
sink.record_turn_completed("no_progress_loop", None, false, turn, &last_model);
} else if !model_declared_done {
sink.record_turn_completed("max_turns", None, false, turn, &last_model);
}
last_results = evaluate_contract(contract, executor, sink).await;
if last_results.iter().all(|r| r.passed) {
if let Some(sig) = &prior_sig {
memory
.record_success(sig, &winning_approach(sig, &closing_plan))
.await;
}
return LoopOutcome::green(iteration, last_results);
}
if no_progress_this_iteration {
no_progress_iterations += 1;
if no_progress_iterations >= 2 {
return LoopOutcome::lost(
LoopFailure::Verification,
Some(
"no-progress loop: the model repeatedly re-read the same files without \
making edits across two attempts — the backbone is likely not returning \
tool results. Aborted before exhausting the iteration budget."
.to_string(),
),
iteration,
last_results,
);
}
} else {
no_progress_iterations = 0;
}
let cur_sig = primary_failure(&last_results);
let recurrences = if no_progress_this_iteration {
0
} else {
record_recurrence(&mut seen_sigs, cur_sig.as_ref())
};
feedback = Some(failure_feedback(&last_results, recurrences));
if let Some(sig) = cur_sig {
memory.record_failure(&sig).await;
prior_sig = Some(sig);
} else {
prior_sig = None;
}
}
LoopOutcome::lost(
LoopFailure::Verification,
None,
cfg.max_iterations,
last_results,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::coder::contract::ContractCheck;
use std::sync::atomic::AtomicUsize;
use std::sync::Arc;
fn contract_for_prompt_test() -> OutcomeContract {
OutcomeContract {
description: "d".into(),
checks: vec![],
}
}
#[test]
fn an_overlay_is_appended_to_the_prompt() {
let contract = contract_for_prompt_test();
let base = system_prompt_with_overlay(&contract, "env", None);
let with = system_prompt_with_overlay(&contract, "env", Some("Prefer smaller diffs."));
assert!(with.contains("Prefer smaller diffs."));
assert!(
with.starts_with(&base),
"the overlay must be strictly additive — the base prompt has to survive verbatim"
);
assert!(with.len() > base.len());
}
#[test]
fn no_overlay_changes_nothing() {
let contract = contract_for_prompt_test();
let base = system_prompt(&contract, "env");
assert_eq!(system_prompt_with_overlay(&contract, "env", None), base);
assert_eq!(system_prompt_with_overlay(&contract, "env", Some("")), base);
assert_eq!(
system_prompt_with_overlay(&contract, "env", Some(" \n ")),
base,
"whitespace is not an overlay"
);
}
#[test]
fn the_overlay_is_marked_subordinate_to_the_base_rules() {
let contract = contract_for_prompt_test();
let with = system_prompt_with_overlay(&contract, "env", Some("Commit when done."));
let marker = with
.find("ADDITIONAL GUIDANCE")
.expect("the overlay must be delimited, not silently concatenated");
assert!(
with[marker..].contains("the rules above win"),
"a conflicting overlay instruction must not read as authoritative"
);
assert!(
with.find("Commit when done.").unwrap() > marker,
"the overlay must come after its own header"
);
}
#[test]
fn merge_harness_adopts_and_clears_the_overlay() {
let mut cfg = NativeLoopConfig::default();
cfg.merge_harness(&car_memgine::HarnessConfig {
prompt_overlay: Some("evolved guidance".into()),
..Default::default()
});
assert_eq!(cfg.prompt_overlay.as_deref(), Some("evolved guidance"));
cfg.merge_harness(&car_memgine::HarnessConfig {
prompt_overlay: None,
..Default::default()
});
assert_eq!(
cfg.prompt_overlay, None,
"a rollback must actually remove the overlay, not leave it latched"
);
}
#[test]
fn merge_harness_raises_coder_budgets_only_upward() {
let mut cfg = NativeLoopConfig {
max_iterations: 8,
max_turns_per_iteration: 24,
..Default::default()
};
cfg.merge_harness(&car_memgine::HarnessConfig {
prompt_overlay: None,
max_retries: 30,
retry_backoff_ms: 0,
planning_max_replans: 12, });
assert_eq!(
cfg.max_iterations, 13,
"planning_max_replans+1 reaches the coder"
);
assert_eq!(
cfg.max_turns_per_iteration, 30,
"max_retries raises the turn floor"
);
let mut base = NativeLoopConfig {
max_iterations: 8,
max_turns_per_iteration: 24,
..Default::default()
};
base.merge_harness(&car_memgine::HarnessConfig::default()); assert_eq!(base.max_iterations, 8, "never lowered below base");
assert_eq!(base.max_turns_per_iteration, 24);
}
struct Script {
turns: Vec<InferenceResult>,
cursor: AtomicUsize,
seen: std::sync::Mutex<Vec<GenerateRequest>>,
}
impl Script {
fn new(turns: Vec<InferenceResult>) -> Self {
Self {
turns,
cursor: AtomicUsize::new(0),
seen: std::sync::Mutex::new(Vec::new()),
}
}
fn prompt(&self, n: usize) -> String {
let reqs = self.seen.lock().expect("seen poisoned");
serde_json::to_string(&reqs[n].messages).unwrap_or_default()
}
fn prompts(&self) -> usize {
self.seen.lock().expect("seen poisoned").len()
}
}
fn turn(text: &str, tool_calls: serde_json::Value) -> InferenceResult {
serde_json::from_value(serde_json::json!({
"text": text,
"tool_calls": tool_calls,
"trace_id": "t",
"model_used": "scripted",
"latency_ms": 0,
}))
.expect("scripted InferenceResult shape")
}
fn turn_with_stop(
text: &str,
tool_calls: serde_json::Value,
stop_reason: Option<&str>,
) -> InferenceResult {
serde_json::from_value(serde_json::json!({
"text": text,
"tool_calls": tool_calls,
"trace_id": "t",
"model_used": "scripted",
"latency_ms": 0,
"stop_reason": stop_reason,
}))
.expect("scripted InferenceResult shape")
}
#[async_trait]
impl TurnGenerator for Script {
async fn generate(&self, req: GenerateRequest) -> Result<InferenceResult, String> {
self.seen.lock().expect("seen poisoned").push(req);
let i = self.cursor.fetch_add(1, Ordering::SeqCst);
self.turns
.get(i)
.cloned()
.ok_or_else(|| "script exhausted".to_string())
}
}
#[test]
fn the_constructors_cannot_produce_a_passed_run_that_also_failed() {
let green = LoopOutcome::green(3, Vec::new());
assert!(green.passed);
assert_eq!(green.failure, None);
assert_eq!(green.error, None);
let lost = LoopOutcome::lost(LoopFailure::Verification, None, 3, Vec::new());
assert!(!lost.passed);
assert_eq!(lost.failure, Some(LoopFailure::Verification));
let scraped = LoopOutcome::lost(
LoopFailure::EngineUnavailable,
Some("external agent 'codex' failed: no binary".into()),
0,
Vec::new(),
);
assert!(scraped.error.unwrap().starts_with("external agent '"));
}
fn failed(name: &str, exit: i64, tail: &str) -> CheckResult {
CheckResult {
name: name.into(),
passed: false,
exit_code: Some(exit),
output_tail: tail.into(),
duration_ms: 1,
}
}
#[test]
fn a_changed_error_class_under_one_check_name_is_not_a_recurrence() {
let mut seen = HashMap::new();
let compile = primary_failure(&[failed("tests", 101, "error[E0433]: failed to resolve")]);
let assertion = primary_failure(&[failed("tests", 1, "assertion `left == right` failed")]);
assert_ne!(
compile.as_ref().map(|s| s.key()),
assertion.as_ref().map(|s| s.key()),
"same check, different error class must be different signatures"
);
assert_eq!(record_recurrence(&mut seen, compile.as_ref()), 0);
assert_eq!(
record_recurrence(&mut seen, assertion.as_ref()),
0,
"progress must not read as a recurrence"
);
}
#[test]
fn the_identical_failure_recurs_and_counts_up() {
let mut seen = HashMap::new();
let sig = primary_failure(&[failed("tests", 1, "assertion `left == right` failed")]);
assert_eq!(record_recurrence(&mut seen, sig.as_ref()), 0);
assert_eq!(record_recurrence(&mut seen, sig.as_ref()), 1);
assert_eq!(record_recurrence(&mut seen, sig.as_ref()), 2);
}
#[test]
fn an_oscillating_failure_still_recurs() {
let mut seen = HashMap::new();
let a = primary_failure(&[failed("tests", 1, "assertion `left == right` failed")]);
let b = primary_failure(&[failed("build", 101, "error[E0433]: failed to resolve")]);
assert_eq!(record_recurrence(&mut seen, a.as_ref()), 0);
assert_eq!(record_recurrence(&mut seen, b.as_ref()), 0);
assert_eq!(
record_recurrence(&mut seen, a.as_ref()),
1,
"A -> B -> A is going in circles, not progress"
);
}
#[test]
fn a_green_evaluation_is_not_a_recurrence() {
let mut seen = HashMap::new();
assert_eq!(record_recurrence(&mut seen, None), 0);
assert!(seen.is_empty());
}
#[tokio::test]
async fn an_exhausted_budget_denies_admission_before_any_turn() {
let script = Script::new(vec![turn("should never run", serde_json::json!([]))]);
let dir = tempfile::tempdir().unwrap();
let executor = WorktreeExecutor::new(dir.path());
let sink = Arc::new(EventSink::test_sink());
let cancel: CancelFlag = Arc::new(std::sync::atomic::AtomicBool::new(false));
let cfg = NativeLoopConfig {
deadline: std::sync::Arc::new(SessionDeadline::new(Some(0))),
..Default::default()
};
let outcome = run_native_loop(
&script,
&executor,
"x",
&OutcomeContract {
description: "x".into(),
checks: vec![ContractCheck {
name: "gate".into(),
command: "exit 1".into(),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 10,
}],
},
&sink,
&cancel,
&cfg,
&RepairMemory::disabled(),
None,
)
.await;
assert_eq!(
script.prompts(),
0,
"the budget gates before any model turn"
);
assert_eq!(outcome.failure, Some(LoopFailure::BudgetExhausted));
assert_eq!(outcome.iterations, 0);
assert!(outcome
.error
.expect("the reason must surface")
.contains("budget exhausted"));
}
#[tokio::test]
async fn the_escalation_is_delivered_to_the_model_only_after_a_repeat() {
let dir = tempfile::tempdir().unwrap();
let executor = WorktreeExecutor::new(dir.path());
let sink = Arc::new(EventSink::test_sink());
let cancel: CancelFlag = Arc::new(std::sync::atomic::AtomicBool::new(false));
let edit = |n: u32| {
turn(
"editing",
serde_json::json!([{
"id": format!("c{n}"),
"name": "write_file",
"arguments": {"path": format!("f{n}.txt"), "content": "x"}
}]),
)
};
let script = Script::new(vec![
edit(1),
turn("done", serde_json::json!([])),
edit(2),
turn("done", serde_json::json!([])),
edit(3),
turn("done", serde_json::json!([])),
]);
let contract = OutcomeContract {
description: "never green".into(),
checks: vec![ContractCheck {
name: "gate".into(),
command: "exit 1".into(),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 10,
}],
};
let cfg = NativeLoopConfig {
max_iterations: 3,
..Default::default()
};
let outcome = run_native_loop(
&script,
&executor,
"x",
&contract,
&sink,
&cancel,
&cfg,
&RepairMemory::disabled(),
None,
)
.await;
assert!(!outcome.passed);
assert!(
!script.prompt(0).contains("failed the same way"),
"escalated before anything repeated"
);
let last = script.prompt(script.prompts() - 1);
assert!(
last.contains("failed the same way"),
"the escalation never reached the model: {last}"
);
}
fn dead_backbone() -> Script {
Script {
turns: vec![],
cursor: AtomicUsize::new(0),
seen: std::sync::Mutex::new(Vec::new()),
}
}
async fn run_against(script: &Script, check: &str) -> LoopOutcome {
let dir = tempfile::tempdir().unwrap();
let executor = WorktreeExecutor::new(dir.path());
let sink = Arc::new(EventSink::test_sink());
let cancel: CancelFlag = Arc::new(std::sync::atomic::AtomicBool::new(false));
let contract = OutcomeContract {
description: "x".into(),
checks: vec![ContractCheck {
name: "gate".into(),
command: check.into(),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 10,
}],
};
run_native_loop(
script,
&executor,
"x",
&contract,
&sink,
&cancel,
&NativeLoopConfig::default(),
&RepairMemory::disabled(),
None,
)
.await
}
struct AuthFlaky {
remaining: AtomicUsize,
inner: Script,
}
#[async_trait]
impl TurnGenerator for AuthFlaky {
async fn generate(&self, req: GenerateRequest) -> Result<InferenceResult, String> {
if self.remaining.load(Ordering::SeqCst) > 0 {
self.remaining.fetch_sub(1, Ordering::SeqCst);
return Err("no credential for proprietary provider 'parslee': \
set $PARSLEE_ACCESS_TOKEN or run `car auth login parslee`"
.to_string());
}
self.inner.generate(req).await
}
}
#[derive(Debug)]
struct SignsIn;
#[async_trait]
impl AuthGate for SignsIn {
async fn is_authenticated(&self) -> bool {
true
}
}
#[derive(Debug)]
struct NeverSignsIn;
#[async_trait]
impl AuthGate for NeverSignsIn {
async fn is_authenticated(&self) -> bool {
false
}
}
async fn run_with_auth(
gen: &dyn TurnGenerator,
check: &str,
gate: Arc<dyn AuthGate>,
auth_wait: std::time::Duration,
) -> LoopOutcome {
let dir = tempfile::tempdir().unwrap();
let executor = WorktreeExecutor::new(dir.path());
let sink = Arc::new(EventSink::test_sink());
let cancel: CancelFlag = Arc::new(std::sync::atomic::AtomicBool::new(false));
let contract = OutcomeContract {
description: "x".into(),
checks: vec![ContractCheck {
name: "gate".into(),
command: check.into(),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 10,
}],
};
let cfg = NativeLoopConfig {
auth_gate: Some(gate),
auth_wait,
..Default::default()
};
run_native_loop(
gen,
&executor,
"x",
&contract,
&sink,
&cancel,
&cfg,
&RepairMemory::disabled(),
None,
)
.await
}
#[tokio::test]
async fn a_lapsed_credential_waits_for_sign_in_and_then_resumes() {
let gen = AuthFlaky {
remaining: AtomicUsize::new(5),
inner: Script::new(vec![turn("done", serde_json::json!([]))]),
};
let outcome = run_with_auth(
&gen,
"exit 0",
Arc::new(SignsIn),
std::time::Duration::from_secs(5),
)
.await;
assert!(
outcome.passed,
"the session must resume after sign-in, not die: {:?}",
outcome.error
);
assert_eq!(outcome.failure, None);
}
#[tokio::test]
async fn nobody_signs_in_reports_needs_auth_not_infrastructure() {
let gen = AuthFlaky {
remaining: AtomicUsize::new(99),
inner: Script::new(vec![turn("done", serde_json::json!([]))]),
};
let outcome = run_with_auth(
&gen,
"exit 1",
Arc::new(NeverSignsIn),
std::time::Duration::ZERO,
)
.await;
assert!(!outcome.passed);
assert_eq!(
outcome.failure,
Some(LoopFailure::NeedsAuth),
"an unanswered sign-in must not masquerade as an outage"
);
}
#[test]
fn enriched_credential_errors_still_classify_as_auth_failures() {
for msg in [
"no credential for proprietary provider 'parslee' (model parslee/reasoning): the \
Parslee token expired at unix 1234 and could not be refreshed. Re-authenticate \
with `car auth login`",
"no credential for proprietary provider 'parslee' (model parslee/reasoning): the \
credential store could not be read (code=152). This is not a sign-out",
"no credential for proprietary provider 'parslee' (model parslee/reasoning): no \
account is signed in. Run `car auth login`",
] {
assert!(
is_auth_failure(msg),
"enriched credential error must still read as an auth failure: {msg}"
);
}
}
#[test]
fn auth_failures_are_distinguished_from_outages() {
assert!(is_auth_failure(
"no credential for proprietary provider 'parslee': run `car auth login parslee`"
));
assert!(is_auth_failure(
"your Parslee session has expired or was rejected"
));
assert!(is_auth_failure(
"car-auth: cannot read Parslee credentials (secret store error)"
));
assert!(!is_auth_failure("connection reset by peer"));
assert!(!is_auth_failure("503 Service Unavailable"));
assert!(!is_auth_failure("script exhausted"));
assert!(!is_auth_failure("model failed, trying next fallback"));
}
#[tokio::test]
async fn a_dead_backbone_over_green_checks_still_passes() {
let outcome = run_against(&dead_backbone(), &crate::coder::test_cmds::touch("m.txt")).await;
assert!(outcome.passed, "the contract decides: {outcome:?}");
assert_eq!(outcome.failure, None);
assert!(outcome.error.is_none());
}
#[tokio::test]
async fn a_dead_backbone_over_red_checks_is_infrastructure() {
let outcome = run_against(&dead_backbone(), "exit 1").await;
assert!(!outcome.passed);
assert_eq!(outcome.failure, Some(LoopFailure::Infrastructure));
assert!(outcome
.error
.expect("a dead backbone must surface")
.contains("inference failed repeatedly"));
}
#[tokio::test]
async fn scripted_loop_edits_verifies_and_passes() {
let dir = tempfile::tempdir().unwrap();
let executor = WorktreeExecutor::new(dir.path());
let (sink, collected) = EventSink::collecting("coder-native");
let cancel: CancelFlag = Arc::new(std::sync::atomic::AtomicBool::new(false));
let script = Script {
turns: vec![
turn(
"creating the file",
serde_json::json!([{
"id": "c1",
"name": "write_file",
"arguments": {"path": "hello.txt", "content": "hello coder"}
}]),
),
turn("done — file created", serde_json::json!([])),
],
cursor: AtomicUsize::new(0),
seen: std::sync::Mutex::new(Vec::new()),
};
let contract = OutcomeContract {
description: "hello.txt exists with content".into(),
checks: vec![ContractCheck {
name: "exists".into(),
command: crate::coder::test_cmds::contains("coder", "hello.txt"),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 10,
}],
};
let outcome = run_native_loop(
&script,
&executor,
"create hello.txt containing 'hello coder'",
&contract,
&sink,
&cancel,
&NativeLoopConfig::default(),
&RepairMemory::disabled(),
None,
)
.await;
assert!(outcome.passed, "outcome: {outcome:?}");
assert_eq!(outcome.iterations, 1);
assert!(dir.path().join("hello.txt").exists());
let events = collected.lock().unwrap();
let types: Vec<&str> = events
.iter()
.map(|e| match &e.kind {
CoderEventKind::IterationStarted { .. } => "iteration",
CoderEventKind::ToolCall { .. } => "tool_call",
CoderEventKind::ToolResult { .. } => "tool_result",
CoderEventKind::PlanText { .. } => "plan",
CoderEventKind::CheckStarted { .. } => "check_started",
CoderEventKind::CheckCompleted { .. } => "check_completed",
_ => "other",
})
.collect();
assert_eq!(
types,
vec![
"iteration",
"tool_call",
"tool_result",
"plan",
"check_started",
"check_completed"
]
);
}
fn identical_read_turn() -> InferenceResult {
turn(
"reading again",
serde_json::json!([{
"id": "c",
"name": "read_file",
"arguments": {"path": "src.py"}
}]),
)
}
#[tokio::test]
async fn native_loop_no_progress_bails_but_green_contract_still_passes() {
let dir = tempfile::tempdir().unwrap();
let executor = WorktreeExecutor::new(dir.path());
let (sink, _collected) = EventSink::collecting("coder-native");
let cancel: CancelFlag = Arc::new(std::sync::atomic::AtomicBool::new(false));
let script = Script {
turns: (0..NO_PROGRESS_REPEAT_LIMIT + 2)
.map(|_| identical_read_turn())
.collect(),
cursor: AtomicUsize::new(0),
seen: std::sync::Mutex::new(Vec::new()),
};
let contract = OutcomeContract {
description: "already satisfied".into(),
checks: vec![ContractCheck {
name: "ok".into(),
command: crate::coder::test_cmds::PASS.into(),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 10,
}],
};
let outcome = run_native_loop(
&script,
&executor,
"fix the bug",
&contract,
&sink,
&cancel,
&NativeLoopConfig::default(),
&RepairMemory::disabled(),
None,
)
.await;
assert!(
outcome.passed,
"green contract must pass despite the thrash: {outcome:?}"
);
assert_eq!(outcome.iterations, 1);
}
#[tokio::test]
async fn native_loop_aborts_after_two_no_progress_iterations() {
let dir = tempfile::tempdir().unwrap();
let executor = WorktreeExecutor::new(dir.path());
let (sink, _collected) = EventSink::collecting("coder-native");
let cancel: CancelFlag = Arc::new(std::sync::atomic::AtomicBool::new(false));
let script = Script {
turns: (0..(NO_PROGRESS_REPEAT_LIMIT * 2 + 4))
.map(|_| identical_read_turn())
.collect(),
cursor: AtomicUsize::new(0),
seen: std::sync::Mutex::new(Vec::new()),
};
let contract = OutcomeContract {
description: "never satisfied".into(),
checks: vec![ContractCheck {
name: "never".into(),
command: crate::coder::test_cmds::FAIL.to_string(),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 10,
}],
};
let outcome = run_native_loop(
&script,
&executor,
"fix the bug",
&contract,
&sink,
&cancel,
&NativeLoopConfig::default(),
&RepairMemory::disabled(),
None,
)
.await;
assert!(!outcome.passed, "outcome: {outcome:?}");
let err = outcome.error.unwrap_or_default();
assert!(err.contains("no-progress loop"), "error was: {err}");
assert_eq!(outcome.iterations, 2);
}
#[tokio::test]
async fn native_loop_empty_tool_calls_journals_turn_completed() {
let dir = tempfile::tempdir().unwrap();
let executor = WorktreeExecutor::new(dir.path());
let journal = dir.path().join("events.jsonl");
let sink = EventSink::new("coder-native", None, Some(journal.clone()));
let cancel: CancelFlag = Arc::new(std::sync::atomic::AtomicBool::new(false));
let script = Script {
turns: vec![
turn(
"creating the file",
serde_json::json!([{
"id": "c1",
"name": "write_file",
"arguments": {"path": "hello.txt", "content": "hello coder"}
}]),
),
turn("done — file created", serde_json::json!([])),
],
cursor: AtomicUsize::new(0),
seen: std::sync::Mutex::new(Vec::new()),
};
let contract = OutcomeContract {
description: "hello.txt exists with content".into(),
checks: vec![ContractCheck {
name: "exists".into(),
command: crate::coder::test_cmds::contains("coder", "hello.txt"),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 10,
}],
};
let outcome = run_native_loop(
&script,
&executor,
"create hello.txt containing 'hello coder'",
&contract,
&sink,
&cancel,
&NativeLoopConfig::default(),
&RepairMemory::disabled(),
None,
)
.await;
assert!(outcome.passed, "outcome: {outcome:?}");
drop(sink);
let log = car_eventlog::EventLog::load(&journal).unwrap();
let terminals: Vec<_> = log
.events()
.iter()
.filter(|e| e.kind == car_eventlog::EventKind::TurnCompleted)
.collect();
assert_eq!(
terminals.len(),
1,
"exactly one empty-tool-calls terminal recorded"
);
let ev = terminals[0];
assert_eq!(
ev.data.get("decision"),
Some(&serde_json::json!("empty_tool_calls"))
);
assert_eq!(
ev.data.get("model_id"),
Some(&serde_json::json!("scripted"))
);
assert_eq!(
ev.data.get("model_tier"),
Some(&serde_json::json!("unknown"))
);
}
#[tokio::test]
async fn native_loop_injects_proactive_memory_from_journaled_failures() {
use car_memgine::MemgineEngine;
use std::sync::Mutex as StdMutex;
use tokio::sync::Mutex as AsyncMutex;
struct CaptureContext {
seen: Arc<StdMutex<Vec<Option<String>>>>,
}
#[async_trait]
impl TurnGenerator for CaptureContext {
async fn generate(&self, req: GenerateRequest) -> Result<InferenceResult, String> {
self.seen.lock().unwrap().push(req.context.clone());
Ok(turn("done", serde_json::json!([])))
}
}
let dir = tempfile::tempdir().unwrap();
let executor = WorktreeExecutor::new(dir.path());
let journal = dir.path().join("events.jsonl");
let sink = EventSink::new("coder-native", None, Some(journal.clone()));
sink.emit(CoderEventKind::ToolResult {
tool: "shell".into(),
ok: false,
preview: "pytest failed because fixture data is missing".into(),
});
let cancel: CancelFlag = Arc::new(std::sync::atomic::AtomicBool::new(false));
let memory = RepairMemory::new(Some(Arc::new(AsyncMutex::new(MemgineEngine::new(None)))));
let seen = Arc::new(StdMutex::new(Vec::new()));
let capture = CaptureContext { seen: seen.clone() };
let contract = OutcomeContract {
description: "noop".into(),
checks: vec![],
};
let outcome = run_native_loop(
&capture,
&executor,
"fix the pytest failure",
&contract,
&sink,
&cancel,
&NativeLoopConfig::default(),
&memory,
None,
)
.await;
assert!(outcome.passed, "outcome: {outcome:?}");
let contexts = seen.lock().unwrap();
let context = contexts[0].as_deref().unwrap_or("");
assert!(
context.contains("## Proactive Memory"),
"request context should carry proactive memory: {context}"
);
assert!(
context.contains("Action shell in proposal session failed"),
"journaled failure should be injected: {context}"
);
drop(sink);
let log = car_eventlog::EventLog::load(&journal).unwrap();
assert!(log
.events()
.iter()
.any(|e| e.kind == car_eventlog::EventKind::ProactiveMemoryMaintained));
assert!(log.events().iter().any(|e| {
e.kind == car_eventlog::EventKind::ProactiveMemoryIntervention
&& e.data.get("decision") == Some(&serde_json::json!("inject"))
}));
}
#[tokio::test]
async fn native_loop_turn_budget_exhaustion_journals_max_turns() {
let dir = tempfile::tempdir().unwrap();
let executor = WorktreeExecutor::new(dir.path());
let journal = dir.path().join("events.jsonl");
let sink = EventSink::new("coder-native", None, Some(journal.clone()));
let cancel: CancelFlag = Arc::new(std::sync::atomic::AtomicBool::new(false));
let tool_turn = || {
turn(
"still working",
serde_json::json!([{
"id": "c",
"name": "write_file",
"arguments": {"path": "scratch.txt", "content": "x"}
}]),
)
};
let script = Script {
turns: vec![tool_turn(), tool_turn()],
cursor: AtomicUsize::new(0),
seen: std::sync::Mutex::new(Vec::new()),
};
let contract = OutcomeContract {
description: "never satisfied".into(),
checks: vec![ContractCheck {
name: "exists".into(),
command: crate::coder::test_cmds::contains("coder", "hello.txt"),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 10,
}],
};
let cfg = NativeLoopConfig {
prompt_overlay: None,
model: None,
max_iterations: 1,
max_turns_per_iteration: 2,
max_tokens_per_turn: 4096,
deadline: SessionDeadline::shared_default(),
auth_gate: None,
auth_wait: std::time::Duration::ZERO,
};
let outcome = run_native_loop(
&script,
&executor,
"keep writing forever",
&contract,
&sink,
&cancel,
&cfg,
&RepairMemory::disabled(),
None,
)
.await;
assert!(!outcome.passed, "outcome: {outcome:?}");
drop(sink);
let log = car_eventlog::EventLog::load(&journal).unwrap();
let max_turns: Vec<_> = log
.events()
.iter()
.filter(|e| {
e.kind == car_eventlog::EventKind::TurnCompleted
&& e.data.get("decision") == Some(&serde_json::json!("max_turns"))
})
.collect();
assert_eq!(
max_turns.len(),
1,
"turn-budget exhaustion recorded once as max_turns"
);
assert_eq!(max_turns[0].data.get("turns"), Some(&serde_json::json!(2)));
}
#[tokio::test]
async fn native_loop_compacts_persistent_history_to_context_window() {
use std::sync::Mutex;
struct RecordingGen {
seen: Arc<Mutex<Vec<(usize, bool)>>>,
turn_no: AtomicUsize,
}
#[async_trait]
impl TurnGenerator for RecordingGen {
async fn generate(&self, req: GenerateRequest) -> Result<InferenceResult, String> {
let msgs = req.messages.as_ref().expect("coder always sets messages");
let starts_with_system = matches!(msgs.first(), Some(Message::System { .. }));
self.seen
.lock()
.unwrap()
.push((msgs.len(), starts_with_system));
let n = self.turn_no.fetch_add(1, Ordering::SeqCst);
Ok(turn(
&"x".repeat(8000),
serde_json::json!([{
"id": format!("c{n}"),
"name": "write_file",
"arguments": {"path": format!("big{n}.txt"), "content": "y"}
}]),
))
}
fn context_window(&self, _model: &str) -> usize {
200 }
}
let dir = tempfile::tempdir().unwrap();
let executor = WorktreeExecutor::new(dir.path());
let (sink, _collected) = EventSink::collecting("compact-test");
let cancel: CancelFlag = Arc::new(std::sync::atomic::AtomicBool::new(false));
let seen: Arc<Mutex<Vec<(usize, bool)>>> = Arc::new(Mutex::new(Vec::new()));
let gen = RecordingGen {
seen: seen.clone(),
turn_no: AtomicUsize::new(0),
};
let cfg = NativeLoopConfig {
prompt_overlay: None,
model: Some("scripted".into()),
max_iterations: 1,
max_turns_per_iteration: 12,
max_tokens_per_turn: 4096,
deadline: SessionDeadline::shared_default(),
auth_gate: None,
auth_wait: std::time::Duration::ZERO,
};
let contract = OutcomeContract {
description: "never satisfied".into(),
checks: vec![ContractCheck {
name: "never".into(),
command: crate::coder::test_cmds::FAIL.to_string(),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 10,
}],
};
let _ = run_native_loop(
&gen,
&executor,
"grow the thread",
&contract,
&sink,
&cancel,
&cfg,
&RepairMemory::disabled(),
None,
)
.await;
let seen = seen.lock().unwrap();
assert_eq!(seen.len(), 12, "all 12 turns generated");
assert!(
seen.iter().all(|(_, sys)| *sys),
"System prompt must stay pinned every turn"
);
let max_len = seen.iter().map(|(n, _)| *n).max().unwrap();
assert!(
max_len < 14,
"persistent history not bounded — max messages/turn = {max_len}"
);
}
#[tokio::test]
async fn native_loop_routes_high_stakes() {
use std::sync::Mutex;
struct CapturingGen {
intents: Arc<Mutex<Vec<Option<car_inference::IntentHint>>>>,
cursor: AtomicUsize,
}
#[async_trait]
impl TurnGenerator for CapturingGen {
async fn generate(&self, req: GenerateRequest) -> Result<InferenceResult, String> {
self.intents.lock().unwrap().push(req.intent.clone());
let i = self.cursor.fetch_add(1, Ordering::SeqCst);
if i == 0 {
Ok(turn(
"",
serde_json::json!([{
"id": "c1", "name": "write_file",
"arguments": {"path": "f.txt", "content": "x"}
}]),
))
} else {
Ok(turn("done", serde_json::json!([])))
}
}
}
let dir = tempfile::tempdir().unwrap();
let executor = WorktreeExecutor::new(dir.path());
let sink = EventSink::test_sink();
let cancel: CancelFlag = Arc::new(std::sync::atomic::AtomicBool::new(false));
let captured = Arc::new(Mutex::new(Vec::new()));
let gen = CapturingGen {
intents: captured.clone(),
cursor: AtomicUsize::new(0),
};
let contract = OutcomeContract {
description: "noop".into(),
checks: vec![],
};
let _ = run_native_loop(
&gen,
&executor,
"make a change",
&contract,
&sink,
&cancel,
&NativeLoopConfig::default(),
&RepairMemory::disabled(),
None,
)
.await;
let intents = captured.lock().unwrap();
assert!(
intents.len() >= 2,
"expected the loop to issue multiple inferences, got {}",
intents.len()
);
for (n, intent) in intents.iter().enumerate() {
let intent = intent
.as_ref()
.unwrap_or_else(|| panic!("turn {n} issued an inference with no IntentHint"));
assert!(intent.high_stakes, "turn {n} must route high_stakes");
assert_eq!(
intent.task,
Some(car_inference::TaskHint::Code),
"turn {n} must keep the Code task hint"
);
}
}
#[tokio::test]
async fn scripted_loop_repairs_after_red_checks() {
let dir = tempfile::tempdir().unwrap();
let executor = WorktreeExecutor::new(dir.path());
let sink = EventSink::test_sink();
let cancel: CancelFlag = Arc::new(std::sync::atomic::AtomicBool::new(false));
let script = Script {
turns: vec![
turn(
"",
serde_json::json!([{
"id": "c1", "name": "write_file",
"arguments": {"path": "x.txt", "content": "wrong"}
}]),
),
turn("done", serde_json::json!([])),
turn(
"",
serde_json::json!([{
"id": "c2", "name": "write_file",
"arguments": {"path": "x.txt", "content": "right"}
}]),
),
turn("fixed", serde_json::json!([])),
],
cursor: AtomicUsize::new(0),
seen: std::sync::Mutex::new(Vec::new()),
};
let contract = OutcomeContract {
description: "x.txt says right".into(),
checks: vec![ContractCheck {
name: "content".into(),
command: crate::coder::test_cmds::contains_or_report("right", "x.txt"),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 10,
}],
};
let outcome = run_native_loop(
&script,
&executor,
"write right into x.txt",
&contract,
&sink,
&cancel,
&NativeLoopConfig::default(),
&RepairMemory::disabled(),
None,
)
.await;
assert!(outcome.passed);
assert_eq!(outcome.iterations, 2, "one repair round expected");
}
#[tokio::test]
async fn f2_iteration_two_carries_iteration_one_conversation() {
use std::sync::Mutex as StdMutex;
struct MsgCapture {
seen: Arc<StdMutex<Vec<String>>>,
cursor: AtomicUsize,
}
#[async_trait]
impl TurnGenerator for MsgCapture {
async fn generate(&self, req: GenerateRequest) -> Result<InferenceResult, String> {
self.seen
.lock()
.unwrap()
.push(serde_json::to_string(&req.messages).unwrap_or_default());
let i = self.cursor.fetch_add(1, Ordering::SeqCst);
match i {
0 => Ok(turn(
"",
serde_json::json!([{
"id": "iter1call", "name": "write_file",
"arguments": {"path": "x.txt", "content": "ITER1_WRONG"}
}]),
)),
1 => Ok(turn("done", serde_json::json!([]))),
2 => Ok(turn(
"",
serde_json::json!([{
"id": "iter2call", "name": "write_file",
"arguments": {"path": "x.txt", "content": "ITER2_right"}
}]),
)),
_ => Ok(turn("fixed", serde_json::json!([]))),
}
}
}
let dir = tempfile::tempdir().unwrap();
let executor = WorktreeExecutor::new(dir.path());
let sink = EventSink::test_sink();
let cancel: CancelFlag = Arc::new(std::sync::atomic::AtomicBool::new(false));
let seen = Arc::new(StdMutex::new(Vec::new()));
let gen = MsgCapture {
seen: seen.clone(),
cursor: AtomicUsize::new(0),
};
let contract = OutcomeContract {
description: "x.txt says ITER2_right".into(),
checks: vec![ContractCheck {
name: "content".into(),
command: crate::coder::test_cmds::contains("ITER2_right", "x.txt"),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 10,
}],
};
let outcome = run_native_loop(
&gen,
&executor,
"write ITER2_right into x.txt",
&contract,
&sink,
&cancel,
&NativeLoopConfig::default(),
&RepairMemory::disabled(),
None,
)
.await;
assert!(outcome.passed);
assert_eq!(outcome.iterations, 2, "expected a repair round");
let seen = seen.lock().unwrap();
assert!(
seen.len() >= 4,
"expected >=4 inferences, got {}",
seen.len()
);
assert!(
seen[2].contains("ITER1_WRONG") || seen[2].contains("iter1call"),
"F2: iteration 2 lost iteration 1's conversation:\n{}",
seen[2]
);
}
#[tokio::test]
async fn f3_truncated_turn_is_not_treated_as_done() {
let dir = tempfile::tempdir().unwrap();
let executor = WorktreeExecutor::new(dir.path());
let sink = EventSink::test_sink();
let cancel: CancelFlag = Arc::new(std::sync::atomic::AtomicBool::new(false));
let script = Script {
turns: vec![
turn_with_stop(
"partial output that got cut o",
serde_json::json!([]),
Some("length"),
),
turn_with_stop("done for real", serde_json::json!([]), Some("stop")),
],
cursor: AtomicUsize::new(0),
seen: std::sync::Mutex::new(Vec::new()),
};
let contract = OutcomeContract {
description: "noop".into(),
checks: vec![],
};
let _ = run_native_loop(
&script,
&executor,
"do the thing",
&contract,
&sink,
&cancel,
&NativeLoopConfig::default(),
&RepairMemory::disabled(),
None,
)
.await;
assert_eq!(
script.cursor.load(Ordering::SeqCst),
2,
"truncated turn was mistaken for completion — loop stopped early instead of continuing"
);
}
#[tokio::test]
async fn repair_round_learns_and_recalls_across_sessions() {
use crate::coder::skill_memory::FailureSignature;
use car_memgine::MemgineEngine;
use tokio::sync::Mutex as AsyncMutex;
let memory = RepairMemory::new(Some(Arc::new(AsyncMutex::new(MemgineEngine::new(None)))));
let contract = OutcomeContract {
description: "x.txt says right".into(),
checks: vec![ContractCheck {
name: "content".into(),
command: crate::coder::test_cmds::contains_or_report("right", "x.txt"),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 10,
}],
};
let sig = FailureSignature {
check: "content".into(),
error_class: "test_failure".into(),
};
let dir1 = tempfile::tempdir().unwrap();
let exec1 = WorktreeExecutor::new(dir1.path());
let sink = EventSink::test_sink();
let cancel: CancelFlag = Arc::new(std::sync::atomic::AtomicBool::new(false));
let script1 = Script {
turns: vec![
turn(
"",
serde_json::json!([{
"id": "c1", "name": "write_file",
"arguments": {"path": "x.txt", "content": "wrong"}
}]),
),
turn("nothing useful yet", serde_json::json!([])),
turn(
"",
serde_json::json!([{
"id": "c2", "name": "write_file",
"arguments": {"path": "x.txt", "content": "right"}
}]),
),
turn(
"wrote 'right' into x.txt to satisfy the grep",
serde_json::json!([]),
),
],
cursor: AtomicUsize::new(0),
seen: std::sync::Mutex::new(Vec::new()),
};
let outcome1 = run_native_loop(
&script1,
&exec1,
"write right into x.txt",
&contract,
&sink,
&cancel,
&NativeLoopConfig::default(),
&memory,
None,
)
.await;
assert!(outcome1.passed);
let recalled = memory
.recall(&sig)
.await
.expect("session 1 should have learned");
assert!(recalled.contains("right"), "approach captured: {recalled}");
let dir2 = tempfile::tempdir().unwrap();
let exec2 = WorktreeExecutor::new(dir2.path());
let (sink2, collected) = EventSink::collecting("coder-learn");
let seen_hint = Arc::new(std::sync::atomic::AtomicBool::new(false));
struct HintWatcher {
seen: Arc<std::sync::atomic::AtomicBool>,
cursor: AtomicUsize,
}
#[async_trait]
impl TurnGenerator for HintWatcher {
async fn generate(&self, req: GenerateRequest) -> Result<InferenceResult, String> {
let i = self.cursor.fetch_add(1, Ordering::SeqCst);
let saw_hint = req
.messages
.as_ref()
.map(|ms| {
ms.iter().any(
|m| matches!(m, Message::User { content } if content.contains("HINT")),
)
})
.unwrap_or(false);
if saw_hint {
self.seen.store(true, Ordering::SeqCst);
}
Ok(match i {
0 => turn("did nothing", serde_json::json!([])),
1 => turn(
"",
serde_json::json!([{
"id": "c1", "name": "write_file",
"arguments": {"path": "x.txt", "content": "right"}
}]),
),
_ => turn("applied the recalled fix", serde_json::json!([])),
})
}
}
let script2 = HintWatcher {
seen: seen_hint.clone(),
cursor: AtomicUsize::new(0),
};
let outcome2 = run_native_loop(
&script2,
&exec2,
"write right into x.txt",
&contract,
&sink2,
&cancel,
&NativeLoopConfig::default(),
&memory,
None,
)
.await;
assert!(outcome2.passed, "session 2 should pass: {outcome2:?}");
assert!(
seen_hint.load(Ordering::SeqCst),
"the recalled hint must have been injected into the repair prompt"
);
drop(collected);
}
#[tokio::test]
async fn ask_user_tool_routes_to_handler_and_answer_reaches_model() {
use std::sync::Mutex as StdMutex;
let dir = tempfile::tempdir().unwrap();
let executor = WorktreeExecutor::new(dir.path());
let (sink, collected) = EventSink::collecting("coder-ask");
let cancel: CancelFlag = Arc::new(std::sync::atomic::AtomicBool::new(false));
struct CannedAsker {
seen_prompt: Arc<StdMutex<Option<String>>>,
answer: String,
}
#[async_trait]
impl AskUser for CannedAsker {
async fn ask(&self, prompt: &str) -> Result<String, String> {
*self.seen_prompt.lock().unwrap() = Some(prompt.to_string());
Ok(self.answer.clone())
}
}
let seen_prompt = Arc::new(StdMutex::new(None));
let asker = CannedAsker {
seen_prompt: seen_prompt.clone(),
answer: "use port 8080".to_string(),
};
struct AskThenWrite {
cursor: AtomicUsize,
}
#[async_trait]
impl TurnGenerator for AskThenWrite {
async fn generate(&self, req: GenerateRequest) -> Result<InferenceResult, String> {
let i = self.cursor.fetch_add(1, Ordering::SeqCst);
match i {
0 => Ok(turn(
"",
serde_json::json!([{
"id": "a1", "name": "ask_user",
"arguments": {"prompt": "which port?"}
}]),
)),
1 => {
let answer = req
.messages
.as_ref()
.and_then(|ms| {
ms.iter().rev().find_map(|m| match m {
Message::ToolResult { content, .. } => Some(content.clone()),
_ => None,
})
})
.unwrap_or_default();
Ok(turn(
"",
serde_json::json!([{
"id": "w1", "name": "write_file",
"arguments": {"path": "answer.txt", "content": answer}
}]),
))
}
_ => Ok(turn("done", serde_json::json!([]))),
}
}
}
let contract = OutcomeContract {
description: "answer.txt records the chosen port".into(),
checks: vec![ContractCheck {
name: "has_port".into(),
command: crate::coder::test_cmds::contains("8080", "answer.txt"),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 10,
}],
};
let outcome = run_native_loop(
&AskThenWrite {
cursor: AtomicUsize::new(0),
},
&executor,
"pick a port and record it",
&contract,
&sink,
&cancel,
&NativeLoopConfig::default(),
&RepairMemory::disabled(),
Some(&asker),
)
.await;
assert!(outcome.passed, "outcome: {outcome:?}");
assert_eq!(seen_prompt.lock().unwrap().as_deref(), Some("which port?"));
assert_eq!(
std::fs::read_to_string(dir.path().join("answer.txt")).unwrap(),
"use port 8080"
);
let events = collected.lock().unwrap();
assert!(events.iter().any(|e| matches!(
&e.kind,
CoderEventKind::ToolCall { tool, .. } if tool == ASK_USER_TOOL
)));
}
#[tokio::test]
async fn ask_user_without_handler_is_a_recoverable_error() {
let dir = tempfile::tempdir().unwrap();
let executor = WorktreeExecutor::new(dir.path());
let (sink, _collected) = EventSink::collecting("coder-noask");
let cancel: CancelFlag = Arc::new(std::sync::atomic::AtomicBool::new(false));
struct ToolPeek {
offered: Arc<std::sync::atomic::AtomicBool>,
}
#[async_trait]
impl TurnGenerator for ToolPeek {
async fn generate(&self, req: GenerateRequest) -> Result<InferenceResult, String> {
let has_ask = req
.tools
.as_ref()
.map(|ts| ts.iter().any(|t| t["name"] == ASK_USER_TOOL))
.unwrap_or(false);
self.offered.store(has_ask, Ordering::SeqCst);
Ok(turn("done", serde_json::json!([])))
}
}
let offered_flag = Arc::new(std::sync::atomic::AtomicBool::new(false));
let contract = OutcomeContract {
description: "noop".into(),
checks: vec![ContractCheck {
name: "ok".into(),
command: crate::coder::test_cmds::PASS.to_string(),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 10,
}],
};
let _ = run_native_loop(
&ToolPeek {
offered: offered_flag.clone(),
},
&executor,
"x",
&contract,
&sink,
&cancel,
&NativeLoopConfig::default(),
&RepairMemory::disabled(),
None,
)
.await;
assert!(
!offered_flag.load(Ordering::SeqCst),
"ask_user must not be offered when no handler is wired"
);
}
#[tokio::test]
async fn cancellation_stops_the_loop() {
let dir = tempfile::tempdir().unwrap();
let executor = WorktreeExecutor::new(dir.path());
let sink = EventSink::test_sink();
let cancel: CancelFlag = Arc::new(std::sync::atomic::AtomicBool::new(true));
let script = Script {
turns: vec![],
cursor: AtomicUsize::new(0),
seen: std::sync::Mutex::new(Vec::new()),
};
let contract = OutcomeContract {
description: "d".into(),
checks: vec![ContractCheck {
name: "never".into(),
command: crate::coder::test_cmds::PASS.to_string(),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 10,
}],
};
let outcome = run_native_loop(
&script,
&executor,
"x",
&contract,
&sink,
&cancel,
&NativeLoopConfig::default(),
&RepairMemory::disabled(),
None,
)
.await;
assert_eq!(outcome.error.as_deref(), Some("cancelled"));
assert_eq!(outcome.iterations, 0);
}
#[test]
fn failure_feedback_lists_only_failures() {
let results = vec![
CheckResult {
name: "good".into(),
passed: true,
exit_code: Some(0),
output_tail: "ok".into(),
duration_ms: 1,
},
CheckResult {
name: "bad".into(),
passed: false,
exit_code: Some(1),
output_tail: "assertion failed".into(),
duration_ms: 1,
},
];
let fb = failure_feedback(&results, 0);
assert!(fb.contains("FAILED bad"));
assert!(fb.contains("assertion failed"));
assert!(!fb.contains("FAILED good"));
assert!(fb.contains("name the single cause"));
assert!(!fb.contains("failed 2 times in a row"));
}
#[test]
fn failure_feedback_escalates_on_a_recurring_failure() {
let results = vec![CheckResult {
name: "run_tests".into(),
passed: false,
exit_code: Some(1),
output_tail: "AttributeError: no attribute '_remove_slot_root'".into(),
duration_ms: 1,
}];
let fb = failure_feedback(&results, 1);
assert!(fb.contains("failed the same way 2 times"), "{fb}");
assert!(fb.contains("do NOT re-apply a variation"));
assert!(fb.contains("IMPLEMENT it"));
}
#[test]
fn system_prompt_carries_the_contract() {
let contract = OutcomeContract {
description: "make the tests pass".into(),
checks: vec![super::super::contract::ContractCheck {
name: "tests".into(),
command: "cargo test -p demo".into(),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 300,
}],
};
let p = system_prompt(&contract, "Top-level entries: Cargo.toml, src");
assert!(p.contains("cargo test -p demo"));
assert!(p.contains("STOP calling tools"));
assert!(p.contains("EXACT command(s) from the OUTCOME CONTRACT"));
}
#[test]
fn coder_prompt_contains_discipline_and_keeps_stop_contract() {
let contract = OutcomeContract {
description: "make the tests pass".into(),
checks: vec![ContractCheck {
name: "tests".into(),
command: "cargo test -p demo".into(),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 300,
}],
};
let env = "Top-level entries: Cargo.toml, src\nBuild systems detected: Rust (cargo)";
let p = system_prompt(&contract, env);
assert!(p.contains("Inspect before you edit"), "inspect-first");
assert!(
p.contains("grep_files") && p.contains("find_files"),
"search-before-read discipline"
);
assert!(p.contains("prefer edit_file"), "surgical-edit discipline");
assert!(
p.contains("Never fabricate file contents"),
"anti-fabrication (files)"
);
assert!(
p.contains("Never claim a check passed"),
"anti-fabrication (results)"
);
assert!(
p.contains("read the actual error output before retrying"),
"read-the-error discipline"
);
assert!(
p.contains("is NOT a task failure") && p.contains("blocked"),
"blocked-verification-is-not-failure guidance"
);
assert!(
p.contains("Trace the checks before you declare done"),
"check-tracing guidance"
);
assert!(
!p.contains("set()"),
"no eval-specific correctness hints in the global prompt"
);
assert!(
p.contains("copy the command string character-for-character"),
"exact-command self-verify (no broad substitute)"
);
assert!(
p.contains("The environment is not yours to fix") && p.contains("denied by policy"),
"environment repair: judgment in the prompt, enforcement in policy"
);
assert!(
!p.contains("STRICTLY FORBIDDEN"),
"the enumerated prose blacklist moved to the inspector chain"
);
assert!(p.contains("do not rely on it: verify the checks yourself first"));
assert!(
p.contains("reply with a brief plain-text summary and STOP calling tools"),
"the STOP-calling-tools loop-termination contract must survive verbatim"
);
assert!(p.contains("Do not git commit"), "policy: no git commit");
assert!(p.contains("ENVIRONMENT:"));
assert!(p.contains("Rust (cargo)"));
assert!(p.contains("cargo test -p demo"));
}
#[test]
fn preview_truncates_on_char_boundary() {
assert_eq!(preview("short", 10), "short");
let long = "é".repeat(300);
let p = preview(&long, 5);
assert!(p.ends_with('…') && p.chars().count() <= 4);
}
struct FirstUserCapture {
captured: Arc<std::sync::Mutex<String>>,
}
#[async_trait]
impl TurnGenerator for FirstUserCapture {
async fn generate(&self, req: GenerateRequest) -> Result<InferenceResult, String> {
let first_user = req
.messages
.as_ref()
.and_then(|ms| {
ms.iter().find_map(|m| match m {
Message::User { content } => Some(content.clone()),
_ => None,
})
})
.unwrap_or_default();
*self.captured.lock().unwrap() = first_user;
Ok(turn("done", serde_json::json!([])))
}
}
fn trivial_contract() -> OutcomeContract {
OutcomeContract {
description: "trivial".into(),
checks: vec![ContractCheck {
name: "ok".into(),
command: crate::coder::test_cmds::PASS.to_string(),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 10,
}],
}
}
#[tokio::test]
async fn coder_first_message_carries_recall_when_facts_exist() {
use crate::coder::skill_memory::FailureSignature;
use car_memgine::MemgineEngine;
use tokio::sync::Mutex as AsyncMutex;
let memory = RepairMemory::new(Some(Arc::new(AsyncMutex::new(MemgineEngine::new(None)))));
let sig = FailureSignature {
check: "tests".into(),
error_class: "test_failure".into(),
};
memory
.record_success(&sig, "add the missing import and re-run cargo test")
.await;
let dir = tempfile::tempdir().unwrap();
let executor = WorktreeExecutor::new(dir.path());
let sink = EventSink::test_sink();
let cancel: CancelFlag = Arc::new(std::sync::atomic::AtomicBool::new(false));
let captured = Arc::new(std::sync::Mutex::new(String::new()));
let outcome = run_native_loop(
&FirstUserCapture {
captured: captured.clone(),
},
&executor,
"the tests are failing, please fix them",
&trivial_contract(),
&sink,
&cancel,
&NativeLoopConfig::default(),
&memory,
None,
)
.await;
assert!(outcome.passed, "outcome: {outcome:?}");
let first_user = captured.lock().unwrap().clone();
assert!(
first_user.contains("Recall from prior sessions"),
"the labelled session-start recall must be in the first user turn: {first_user}"
);
assert!(
first_user.contains("missing import"),
"the recalled approach content rides along: {first_user}"
);
}
#[tokio::test]
async fn coder_first_message_recall_absent_when_empty() {
use car_memgine::MemgineEngine;
use tokio::sync::Mutex as AsyncMutex;
let memory = RepairMemory::new(Some(Arc::new(AsyncMutex::new(MemgineEngine::new(None)))));
let dir = tempfile::tempdir().unwrap();
let executor = WorktreeExecutor::new(dir.path());
let sink = EventSink::test_sink();
let cancel: CancelFlag = Arc::new(std::sync::atomic::AtomicBool::new(false));
let captured = Arc::new(std::sync::Mutex::new(String::new()));
let outcome = run_native_loop(
&FirstUserCapture {
captured: captured.clone(),
},
&executor,
"the tests are failing, please fix them",
&trivial_contract(),
&sink,
&cancel,
&NativeLoopConfig::default(),
&memory,
None,
)
.await;
assert!(outcome.passed, "outcome: {outcome:?}");
let first_user = captured.lock().unwrap().clone();
assert!(
!first_user.contains("Recall from prior sessions"),
"no recall section when the engine has nothing relevant: {first_user}"
);
}
}