car-engine 0.36.0

Core runtime engine for Common Agent Runtime
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
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
//! Live bridge for the goal loop: project [`GoalInputs`] from a [`Runtime`]'s
//! **ground truth** — its event-log receipts, its shared state, its
//! transactional consistency — so `car-verify`'s pure
//! [`car_verify::goal::evaluate_goal`] decides completion against what actually
//! happened, not a transcript read.
//!
//! See `docs/proposals/goal-loop.md`. This is the deterministic Evaluator half
//! of the goal loop. It is the seam the flagship assistant and the external
//! (Codex / Claude Code) executors plug into: each iteration, the caller runs
//! any command / model-judge checks it owns, then calls
//! [`Runtime::gather_goal_inputs`] to fold in the runtime-owned signals, then
//! runs `evaluate_goal`. The loop driver itself
//! ([`car_verify::goal::run_goal_loop`]) stays injected and crate-free.

use crate::Runtime;
use car_eventlog::tool_receipts::ToolClaim;
use car_eventlog::EventKind;
use car_ir::ActionProposal;
use car_verify::goal::{GoalCondition, GoalInputs};
use car_verify::plan_check::{check_plan, PlanCheckRequest};
use car_verify::transaction::check_transaction;
use std::collections::HashMap;

/// What to project into a [`GoalInputs`] for one evaluation. The
/// runtime-owned signals (`claims`, `plan`, `transaction_proposal`,
/// `state_keys`) are gathered by [`Runtime::gather_goal_inputs`]; the
/// caller-owned checks (`command_exits`, `model_verdicts`) are passed through
/// verbatim, since running a shell check or a model judge belongs to the
/// executor, not the runtime (the same injected split the driver uses).
#[derive(Debug, Clone, Default)]
pub struct GoalGather {
    /// The model's tool claims for this iteration (from its tool_calls / IR),
    /// cross-checked against the event-log receipts. Empty ⇒ nothing claimed,
    /// which is vacuously grounded.
    pub claims: Vec<ToolClaim>,
    /// Scope the receipt check to one proposal's events.
    pub proposal_id: Option<String>,
    /// A STRIPS plan to forward-check (drives `plan_achieved`).
    pub plan: Option<PlanCheckRequest>,
    /// A proposal to check for transactional consistency against current state
    /// (drives `state_consistent` — the state-drift guard).
    pub transaction_proposal: Option<ActionProposal>,
    /// State keys to include in the snapshot for `StatePredicate` conditions;
    /// empty ⇒ the whole state.
    pub state_keys: Vec<String>,
    /// Exit codes of command checks the caller already ran on the substrate.
    pub command_exits: HashMap<String, i32>,
    /// Verdicts of model judges the caller already gathered.
    pub model_verdicts: HashMap<String, bool>,
}

impl Runtime {
    /// Project a [`GoalInputs`] from this runtime's ground truth plus the
    /// caller-supplied checks. Reads receipts from the event log
    /// ([`Runtime::verify_tool_receipts`]), the live state snapshot, and — when
    /// a proposal is supplied — transactional consistency. Never fabricates a
    /// signal: a check whose input wasn't requested is left `None`, so the
    /// pure evaluator fails it closed.
    pub async fn gather_goal_inputs(&self, gather: &GoalGather) -> GoalInputs {
        // Receipts: always computed (empty claims ⇒ grounded=true, vacuously),
        // so a no-claims iteration still satisfies a `ToolReceiptsGrounded`
        // leaf rather than failing it closed for lack of input.
        let receipts_grounded = Some(
            self.verify_tool_receipts(&gather.claims, gather.proposal_id.as_deref())
                .await
                .grounded,
        );

        let plan_achieved = gather.plan.as_ref().map(|p| check_plan(p).valid);

        let full = self.state.snapshot();
        let state = if gather.state_keys.is_empty() {
            full
        } else {
            gather
                .state_keys
                .iter()
                .filter_map(|k| full.get(k).map(|v| (k.clone(), v.clone())))
                .collect()
        };

        let state_consistent = gather.transaction_proposal.as_ref().map(|p| {
            let (values, versions) = self.state.versioned_snapshot();
            check_transaction(p, &versions, Some(&values)).consistent
        });

        GoalInputs {
            receipts_grounded,
            plan_achieved,
            state_consistent,
            state,
            command_exits: gather.command_exits.clone(),
            model_verdicts: gather.model_verdicts.clone(),
        }
    }

    /// Append a durable goal-verifier result to the runtime event log.
    ///
    /// This is the audit counterpart to the live `GoalEvaluated` stream event:
    /// hosts can miss a websocket frame, but the session journal still records
    /// why the verifier continued or accepted the run. `model`/`model_tier`
    /// attribute the verdict to the model that produced the turn, so an
    /// ungrounded completion is queryable as "fails on local, passes on cloud"
    /// — the same provenance `record_turn_completed` stamps on the default path.
    pub async fn record_goal_evaluated(
        &self,
        goal: &str,
        condition: &GoalCondition,
        iteration: u32,
        met: bool,
        grounded: bool,
        reason: &str,
        model: &str,
    ) {
        let mut data = HashMap::new();
        data.insert("goal".to_string(), serde_json::json!(goal));
        data.insert(
            "condition".to_string(),
            serde_json::to_value(condition).unwrap_or(serde_json::Value::Null),
        );
        data.insert("iteration".to_string(), serde_json::json!(iteration));
        data.insert("met".to_string(), serde_json::json!(met));
        data.insert("grounded".to_string(), serde_json::json!(grounded));
        data.insert("reason".to_string(), serde_json::json!(reason));
        data.insert("model_id".to_string(), serde_json::json!(model));
        data.insert(
            "model_tier".to_string(),
            serde_json::json!(model_tier(model)),
        );
        self.log
            .lock()
            .await
            .append(EventKind::GoalEvaluated, None, None, data);
    }

    /// Append a durable completion-decision record for an assistant/coder turn
    /// loop. The default (goal-less) loop declares success the instant the model
    /// emits no tool calls, with no truncation or outcome check — so a truncated
    /// or turn-capped run is indistinguishable from a real finish at the
    /// terminal. Recording `decision`/`stop_reason`/`was_truncated`/`turns` makes
    /// that decision a first-class, queryable event: the false-success and
    /// never-finished signals live here on the ungrounded default path, which
    /// emits no `GoalEvaluated`.
    pub async fn record_turn_completed(
        &self,
        decision: &str,
        stop_reason: Option<&str>,
        was_truncated: bool,
        turns: u32,
        model: &str,
    ) {
        let data = turn_completed_data(decision, stop_reason, was_truncated, turns, model);
        self.log
            .lock()
            .await
            .append(EventKind::TurnCompleted, None, None, data);
    }
}

/// Build the durable `TurnCompleted` event-data map.
///
/// Shared by the assistant loop's [`Runtime::record_turn_completed`] and the
/// coder loop's terminal (`EventSink::record_turn_completed` in
/// `car-server-core`), so the two paths never drift on the keys the harness
/// miners parse (`harness_adapt::diagnose`, `failed_trace_events`): `decision`,
/// `stop_reason`, `was_truncated`, `turns`, and the P0c provenance
/// `model_id` (authoritative) / `model_tier` (derived local|cloud grouping).
pub fn turn_completed_data(
    decision: &str,
    stop_reason: Option<&str>,
    was_truncated: bool,
    turns: u32,
    model: &str,
) -> HashMap<String, serde_json::Value> {
    let mut data = HashMap::new();
    data.insert("decision".to_string(), serde_json::json!(decision));
    data.insert("stop_reason".to_string(), serde_json::json!(stop_reason));
    data.insert(
        "was_truncated".to_string(),
        serde_json::json!(was_truncated),
    );
    data.insert("turns".to_string(), serde_json::json!(turns));
    data.insert("model_id".to_string(), serde_json::json!(model));
    data.insert(
        "model_tier".to_string(),
        serde_json::json!(model_tier(model)),
    );
    data
}

/// Best-effort `local` | `cloud` | `unknown` tier from a model id's provider
/// prefix (the segment before the first `/`).
///
/// `model_id` is the authoritative attribution field; `model_tier` is a
/// convenience grouping so "fails on local vs cloud" — the framing of Matt's
/// field report (works on Codex/Opus, breaks on a local model) — can be
/// queried without re-deriving the prefix. Shared by the `GoalEvaluated` /
/// `TurnCompleted` event emitters and the offline bench corpus (`car-bench`)
/// so the provider allow-lists never drift between call sites.
pub fn model_tier(model: &str) -> &'static str {
    match model.split('/').next().unwrap_or("") {
        "mlx" | "mlx-vlm" | "vllm-mlx" | "local" | "apple" | "qwen" => "local",
        "anthropic" | "openai" | "google" | "gemini" | "parslee" => "cloud",
        _ => "unknown",
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ToolExecutor;
    use car_eventlog::tool_receipts::{ClaimKind, ToolClaim};
    use car_ir::{Action, ActionProposal, ActionType, FailureBehavior};
    use car_verify::goal::{evaluate_goal, GoalCondition};
    use serde_json::Value;
    use std::sync::Arc;

    struct EchoExec;
    #[async_trait::async_trait]
    impl ToolExecutor for EchoExec {
        async fn execute(&self, tool: &str, params: &Value) -> Result<Value, String> {
            match tool {
                "echo" => Ok(params.get("message").cloned().unwrap_or(Value::Null)),
                _ => Err(format!("unknown tool: {tool}")),
            }
        }
    }

    fn tool_call(tool: &str, params: HashMap<String, Value>) -> Action {
        Action {
            id: uuid::Uuid::new_v4().simple().to_string()[..12].to_string(),
            action_type: ActionType::ToolCall,
            tool: Some(tool.to_string()),
            parameters: params,
            preconditions: vec![],
            expected_effects: HashMap::new(),
            state_dependencies: vec![],
            read_set: vec![],
            write_set: vec![],
            assumptions: vec![],
            invocation_mode: Default::default(),
            idempotent: false,
            max_retries: 3,
            failure_behavior: FailureBehavior::Abort,
            timeout_ms: None,
            metadata: HashMap::new(),
        }
    }

    fn state_write(key: &str, value: Value) -> Action {
        Action {
            id: uuid::Uuid::new_v4().simple().to_string()[..12].to_string(),
            action_type: ActionType::StateWrite,
            tool: None,
            parameters: [
                ("key".to_string(), Value::from(key)),
                ("value".to_string(), value),
            ]
            .into(),
            preconditions: vec![],
            expected_effects: HashMap::new(),
            state_dependencies: vec![],
            read_set: vec![],
            write_set: vec![],
            assumptions: vec![],
            invocation_mode: Default::default(),
            idempotent: false,
            max_retries: 3,
            failure_behavior: FailureBehavior::Abort,
            timeout_ms: None,
            metadata: HashMap::new(),
        }
    }

    fn proposal(actions: Vec<Action>) -> ActionProposal {
        ActionProposal {
            id: "goal-test".to_string(),
            source: "test".to_string(),
            actions,
            timestamp: chrono::Utc::now(),
            context: HashMap::new(),
        }
    }

    /// End-to-end over REAL ground truth: execute a proposal that calls a tool
    /// and writes state, then gather inputs and evaluate a goal condition —
    /// proving `receipts_grounded` reads the event log and `StatePredicate`
    /// reads live state.
    #[tokio::test]
    async fn gather_reads_real_receipts_and_state() {
        let rt = Runtime::new().with_executor(Arc::new(EchoExec));
        rt.register_tool("echo").await;

        let p = proposal(vec![
            tool_call("echo", [("message".to_string(), Value::from("hi"))].into()),
            state_write("tests_passing", Value::Bool(true)),
        ]);
        let r = rt.execute(&p).await;
        assert!(r.all_succeeded(), "setup proposal must succeed");

        // A truthful claim ("I invoked echo") + the real state write.
        let gather = GoalGather {
            claims: vec![ToolClaim {
                kind: ClaimKind::Invoked,
                tool: "echo".to_string(),
                call_id: None,
                count: None,
                text: None,
            }],
            proposal_id: Some("goal-test".to_string()),
            ..Default::default()
        };
        let inputs = rt.gather_goal_inputs(&gather).await;
        assert_eq!(inputs.receipts_grounded, Some(true));
        assert_eq!(inputs.state.get("tests_passing"), Some(&Value::Bool(true)));

        let cond = GoalCondition::AllOf {
            conditions: vec![
                GoalCondition::ToolReceiptsGrounded,
                GoalCondition::StatePredicate {
                    key: "tests_passing".into(),
                    equals: Value::Bool(true),
                },
            ],
        };
        let verdict = evaluate_goal(&cond, &inputs);
        assert!(verdict.met && verdict.grounded, "{}", verdict.reason);
    }

    /// The `/goal` blindspot, closed: a fabricated tool claim makes the
    /// grounded-receipts condition fail even though the model "said" it ran.
    #[tokio::test]
    async fn fabricated_claim_fails_the_receipts_condition() {
        let rt = Runtime::new().with_executor(Arc::new(EchoExec));
        rt.register_tool("echo").await;
        let r = rt
            .execute(&proposal(vec![tool_call(
                "echo",
                [("message".to_string(), Value::from("hi"))].into(),
            )]))
            .await;
        assert!(r.all_succeeded());

        // The model claims it deployed — but no deploy ever ran.
        let gather = GoalGather {
            claims: vec![ToolClaim {
                kind: ClaimKind::Invoked,
                tool: "deploy".to_string(),
                call_id: None,
                count: None,
                text: Some("I deployed it".to_string()),
            }],
            proposal_id: Some("goal-test".to_string()),
            ..Default::default()
        };
        let inputs = rt.gather_goal_inputs(&gather).await;
        assert_eq!(inputs.receipts_grounded, Some(false));

        let verdict = evaluate_goal(&GoalCondition::ToolReceiptsGrounded, &inputs);
        assert!(
            !verdict.met,
            "a hallucinated tool claim must not satisfy the goal"
        );
        assert!(verdict.reason.contains("hallucinated"));
    }

    #[tokio::test]
    async fn record_goal_evaluated_appends_auditable_event() {
        let rt = Runtime::new();
        let condition = GoalCondition::Command {
            id: "tests".to_string(),
            expect_exit: 0,
        };
        rt.record_goal_evaluated(
            "make tests pass",
            &condition,
            2,
            true,
            true,
            "command `test` exited 0",
            "mlx/qwen3-8b:4bit",
        )
        .await;

        let log = rt.log.lock().await;
        let event = log.events().last().expect("goal event should be recorded");
        assert_eq!(event.kind, car_eventlog::EventKind::GoalEvaluated);
        assert_eq!(
            event.data.get("goal"),
            Some(&serde_json::json!("make tests pass"))
        );
        assert_eq!(
            event.data.get("condition"),
            Some(&serde_json::json!({"kind": "command", "id": "tests", "expect_exit": 0}))
        );
        assert_eq!(event.data.get("iteration"), Some(&serde_json::json!(2)));
        assert_eq!(event.data.get("met"), Some(&serde_json::json!(true)));
        assert_eq!(event.data.get("grounded"), Some(&serde_json::json!(true)));
        assert_eq!(
            event.data.get("reason"),
            Some(&serde_json::json!("command `test` exited 0"))
        );
        // Model provenance stamped on the goal path too (mirrors TurnCompleted),
        // so an ungrounded completion is attributable to local vs cloud.
        assert_eq!(
            event.data.get("model_id"),
            Some(&serde_json::json!("mlx/qwen3-8b:4bit"))
        );
        assert_eq!(
            event.data.get("model_tier"),
            Some(&serde_json::json!("local"))
        );
    }

    #[test]
    fn model_tier_maps_provider_prefixes() {
        // Local inference backends.
        assert_eq!(model_tier("mlx/qwen3-8b:4bit"), "local");
        assert_eq!(model_tier("qwen/qwen3-8b"), "local");
        assert_eq!(model_tier("apple/foundation"), "local");
        // Cloud providers.
        assert_eq!(model_tier("anthropic/claude-opus-4-8"), "cloud");
        assert_eq!(model_tier("openai/gpt-5.5"), "cloud");
        assert_eq!(model_tier("google/gemini-2.5-pro"), "cloud");
        assert_eq!(model_tier("parslee/reasoning"), "cloud");
        // Unrecognized prefix and the empty-model (no generate completed) case
        // both fall through to "unknown" rather than being misattributed.
        assert_eq!(model_tier("acme/whatever"), "unknown");
        assert_eq!(model_tier(""), "unknown");
    }

    #[tokio::test]
    async fn record_turn_completed_appends_auditable_event() {
        let rt = Runtime::new();
        rt.record_turn_completed(
            "empty_tool_calls",
            Some("length"),
            true,
            7,
            "mlx/qwen3-8b:4bit",
        )
        .await;

        let log = rt.log.lock().await;
        let event = log.events().last().expect("turn event should be recorded");
        assert_eq!(event.kind, car_eventlog::EventKind::TurnCompleted);
        assert_eq!(
            event.data.get("decision"),
            Some(&serde_json::json!("empty_tool_calls"))
        );
        assert_eq!(
            event.data.get("stop_reason"),
            Some(&serde_json::json!("length"))
        );
        assert_eq!(
            event.data.get("was_truncated"),
            Some(&serde_json::json!(true))
        );
        assert_eq!(event.data.get("turns"), Some(&serde_json::json!(7)));
        assert_eq!(
            event.data.get("model_id"),
            Some(&serde_json::json!("mlx/qwen3-8b:4bit"))
        );
        // provider prefix "mlx" → local tier (the local-vs-cloud attribution).
        assert_eq!(
            event.data.get("model_tier"),
            Some(&serde_json::json!("local"))
        );
    }
}