newt-core 0.8.0

Newt-Agent core types, errors, and the NeMoCode-style tier router
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
use super::*;

/// Regression for the live full-access incident: the model narrated an exec
/// denial without ever calling the advertised tool. The harness must spend one
/// bounded round demanding a real probe instead of presenting that invention
/// as the final answer.
#[tokio::test]
async fn openai_unverified_run_command_blocker_gets_ground_truth_retry() {
    let server = MockServer::start().await;
    let round = Arc::new(AtomicUsize::new(0));
    Mock::given(method("POST"))
        .and(path("/v1/chat/completions"))
        .respond_with(ScriptedOpenAi {
            round: round.clone(),
            script: vec![
                serde_json::json!({
                    "content": "I hit a capability wall: run_command is permission-denied; exec not granted."
                }),
                serde_json::json!({
                    "content": "I will inspect an actual run_command result before reporting a denial."
                }),
            ],
        })
        .mount(&server)
        .await;

    let messages = msgs();
    let caveats = Caveats::top();
    let uri = server.uri();
    let mut c = ctx(&uri, &messages, &caveats);
    c.kind = BackendKind::Openai;
    c.task = "you should have a \"gh\" command ... test \"gh auth status\" now to tell me if you can use it?";
    c.max_tool_rounds = 2;
    let (reply, _s, _u, _h) = chat_complete(c, &mut NoMcp).await.expect("dispatch");

    assert_eq!(round.load(Ordering::SeqCst), 2, "one corrective retry");
    assert!(
        reply.contains("actual run_command result"),
        "final reply: {reply}"
    );

    let requests = server.received_requests().await.expect("recorded requests");
    let second = body_json(&requests[1]).to_string();
    assert!(
        second.contains("no returned run_command result this turn contains an exec denial"),
        "corrective request: {second}"
    );
    assert!(
        second.contains("Report a denial only when an actual returned result contains one"),
        "corrective request: {second}"
    );
}

/// [`run_openai_script`] against a workspace the caller chooses, so a test can
/// point the loop at a directory that actually affords a verification.
async fn run_openai_script_in(
    script: Vec<serde_json::Value>,
    workspace: &str,
    task: &str,
) -> (String, usize) {
    let server = MockServer::start().await;
    let round = Arc::new(AtomicUsize::new(0));
    Mock::given(method("POST"))
        .and(path("/v1/chat/completions"))
        .respond_with(ScriptedOpenAi {
            round: round.clone(),
            script,
        })
        .mount(&server)
        .await;
    let messages = msgs();
    let caveats = Caveats::top();
    let uri = server.uri();
    let mut c = ctx(&uri, &messages, &caveats);
    c.kind = BackendKind::Openai;
    c.workspace = workspace;
    c.task = task;
    let (reply, _s, _u, _h) = chat_complete(c, &mut NoMcp).await.expect("dispatch");
    (reply, round.load(Ordering::SeqCst))
}

/// **The wiring #1943 arms, proved end to end through the loop.**
///
/// Every other test in this file points at [`NO_CHECKS_WORKSPACE`] so the gate
/// stays out of their way — which would leave the armed gate exactly as
/// unexercised as the env var left it, and that is the failure this whole PR
/// is about. So one test points the loop at a workspace that DOES ship a
/// verification explicitly requested by the operator and holds it to firing,
/// even when the turn makes no edit.
///
/// `.` under `cargo test -p newt-core` is this crate's directory, which ships
/// a `Cargo.toml`. That is a deliberate real-filesystem dependency in exactly
/// one test, and it is what **grounds** `self_verify`'s mocked scanner tests:
/// those encode a belief about what `read_dir` yields, and this is the test
/// that would fail if the belief were wrong.
#[tokio::test]
#[serial_test::serial(newt_self_verify_env)]
async fn an_armed_self_verify_gate_adds_a_round_when_the_workspace_ships_a_check() {
    // #2374: this pins the DEFAULT (attempted) mode, which `ctx` builds with
    // `verify_outcomes: false`; no loop reads NEWT_VERIFY_OUTCOMES. Serial with
    // the other writers of NEWT_SELF_VERIFY.
    struct Restore([(&'static str, Option<std::ffi::OsString>); 1]);
    impl Drop for Restore {
        fn drop(&mut self) {
            for (key, value) in &self.0 {
                match value {
                    Some(v) => std::env::set_var(key, v),
                    None => std::env::remove_var(key),
                }
            }
        }
    }
    let _restore = Restore(["NEWT_SELF_VERIFY"].map(|k| (k, std::env::var_os(k))));
    std::env::set_var("NEWT_SELF_VERIFY", "1");
    // The model answers without ever running a command, three times running.
    // Armed, the gate hands it another round each time — and then STOPS at
    // `SELF_VERIFY_CAP` (2), so a model that will not verify still ends its
    // turn. Pinning the exact count pins the cap with it: a gate that could
    // nudge forever would hang the turn it was meant to improve.
    let script = vec![
        serde_json::json!({ "content": "Done — the fix is in place." }),
        serde_json::json!({ "content": "Still done." }),
        serde_json::json!({ "content": "Confirmed complete." }),
    ];
    let (_, rounds) =
        run_openai_script_in(script.clone(), ".", "Run `cargo test` to verify.").await;
    assert_eq!(
        rounds, 3,
        "an unverified conclusion in a workspace shipping `cargo test` costs a round per nudge, capped at SELF_VERIFY_CAP = 2 — that is #1943"
    );

    // The anti-vacuous twin, in the same test so the two can never drift: the
    // extra round is the GATE, not something else in the loop. The same script
    // against a workspace that affords nothing concludes in one.
    let (_, rounds) =
        run_openai_script_in(script, NO_CHECKS_WORKSPACE, "Complete the request.").await;
    assert_eq!(
        rounds, 1,
        "with nothing to verify the gate must stay silent — otherwise the assertion above is measuring some other nudge"
    );
}

/// #1259: `request_user_input` is a legitimate escalation in an **Explain**
/// turn — the boxed-in model formally asks the human instead of being forced
/// into penalized narration (the #1257 double-bind). Headless (no gate), the
/// dispatch returns the recoverable no-human message — never the
/// disposition-refusal, never a hang — and the turn completes normally.
/// Contrast pin in the same run: `run_command` without a session exec grant
/// still gets an authority denial. The prose classification is not the boundary.
#[tokio::test]
async fn explain_turn_request_user_input_dispatches_and_completes() {
    let server = MockServer::start().await;
    let round = Arc::new(AtomicUsize::new(0));
    Mock::given(method("POST"))
        .and(path("/v1/chat/completions"))
        .respond_with(ScriptedOpenAi {
            round: round.clone(),
            script: vec![
                serde_json::json!({
                    "content": null,
                    "tool_calls": [{
                        "id": "c1", "type": "function",
                        "function": { "name": "request_user_input",
                                       "arguments": "{\"question\":\"Which directory should I size?\"}" }
                    }]
                }),
                serde_json::json!({
                    "content": null,
                    "tool_calls": [{
                        "id": "c2", "type": "function",
                        "function": { "name": "run_command",
                                       "arguments": "{\"command\":\"du -sh .\"}" }
                    }]
                }),
                serde_json::json!({ "content": "Understood — proceeding with the workspace root." }),
            ],
        })
        .mount(&server)
        .await;

    let messages = msgs();
    let caveats = Caveats {
        exec: crate::Scope::none(),
        ..Caveats::top()
    };
    let uri = server.uri();
    let mut c = ctx(&uri, &messages, &caveats);
    c.kind = BackendKind::Openai;
    c.prompt_disposition = PromptDisposition::Explain;
    c.exec_floor = Some(&caveats.exec);
    let (reply, _s, _u, _h) = chat_complete(c, &mut NoMcp)
        .await
        .expect("an Explain turn asking the human completes, never errors");
    assert!(
        reply.contains("proceeding with the workspace root"),
        "the turn ends on the final answer: {reply}"
    );

    // Wire-level: what the loop fed back for each tool call.
    let requests = server.received_requests().await.expect("recorded");
    let bodies: Vec<String> = requests
        .iter()
        .map(|r| String::from_utf8_lossy(&r.body).into_owned())
        .collect();
    let all = bodies.join("\n---\n");
    // The escalation DISPATCHED: its result is the recoverable headless
    // message, not the disposition refusal.
    assert!(
        all.contains("no human available this session"),
        "request_user_input must dispatch (headless => the recoverable no-human message): {all}"
    );
    assert!(
        !all.contains("Tool `request_user_input` is not available for this request"),
        "request_user_input must NOT be disposition-refused in an Explain turn"
    );
    // The actual exec boundary still holds in the same turn.
    assert!(
        all.contains("capability denied")
            && !all.contains("Tool `run_command` is not available for this request"),
        "run_command must respect the session exec denial regardless of Explain style: {all}"
    );
}

#[tokio::test]
async fn question_turns_are_never_nudged() {
    // Regression #1152/#1162 (the 2026-07-14 Opus session): the user asked
    // a QUESTION; the model's narrated answer classified as pending-action
    // and got nudged, seeding the "I'm genuinely finished" defense loop
    // (#1158). With the intent gate, a question turn takes its narration
    // as the final answer on ROUND ONE — no rescue, no residue.
    let server = MockServer::start().await;
    let round = Arc::new(AtomicUsize::new(0));
    Mock::given(method("POST"))
        .and(path("/v1/chat/completions"))
        .respond_with(ScriptedOpenAi {
            round: round.clone(),
            // Phrasing the classifier reads as pending-action ("Let me…").
            script: vec![
                serde_json::json!({ "content": "Let me look into the harness next." }),
                serde_json::json!({ "content": "SHOULD NEVER BE REQUESTED" }),
            ],
        })
        .mount(&server)
        .await;
    let question =
        "Give me your top 5 improvements to make LLM effectiveness better inside this harness please?";
    let messages = vec![
        MemMessage::system("you are a test"),
        MemMessage::user(question),
    ];
    let caveats = Caveats::top();
    let uri = server.uri();
    let mut c = ctx(&uri, &messages, &caveats);
    c.kind = BackendKind::Openai;
    c.task = question;
    c.narration_nudge_cap = 2; // budget available — the GATE must stop it
    let (reply, _s, _u, _h) = chat_complete(c, &mut NoMcp).await.expect("dispatch");
    assert_eq!(
        round.load(Ordering::SeqCst),
        1,
        "a question turn must never consume a rescue round"
    );
    assert!(
        reply.contains("look into"),
        "narration IS the answer: {reply}"
    );
}

#[tokio::test]
async fn stale_file_blocker_nudges_ground_truth_check_and_continues() {
    let blocker = "\
Summary

What happened: The lib.rs file I was editing grew from ~9400 to ~16808 lines \
between reads — likely modified concurrently by another agent or tool. This \
means my old edit contexts are stale.

Why I'm blocked: I cannot safely use edit_file on lib.rs because the file has \
been modified out from under me. My old line references and context are invalid \
for an 8400-line larger file.

Final Answer / Recommendation

The operator should restore lib.rs to a known-good state (e.g., git checkout \
newt-tui/src/lib.rs).";
    let (reply, rounds) = run_openai_script(vec![
        serde_json::json!({ "content": blocker }),
        serde_json::json!({ "content": "Ground truth checked; lib.rs is clean, so I am continuing." }),
    ])
    .await;
    assert_eq!(
        rounds, 2,
        "stale-file blocker should get one verification nudge"
    );
    assert!(
        reply.contains("lib.rs is clean"),
        "returns the post-nudge answer: {reply}"
    );
    assert!(
        !reply.contains("git checkout"),
        "must not accept the unverified revert recommendation: {reply}"
    );
}

#[test]
fn looks_like_unverified_stale_file_blocker_requires_file_stale_and_blocker_cues() {
    assert!(looks_like_unverified_stale_file_blocker(
        "The lib.rs file I was editing grew from ~9400 to ~16808 lines between reads. \
         Why I'm blocked: I cannot safely use edit_file because the file has been \
         modified out from under me. The operator should restore lib.rs."
    ));
    assert!(looks_like_unverified_stale_file_blocker(
        "My old line references are invalid and the context is stale. Any edit could \
         land in the wrong place and corrupt the code; recommendation: restore the file."
    ));
    assert!(!looks_like_unverified_stale_file_blocker(
        "The cache entry is stale, so I refreshed it and continued."
    ));
    assert!(!looks_like_unverified_stale_file_blocker(
        "I checked git diff and the file is clean, so I can continue from the verified contents."
    ));
}

#[test]
fn stale_file_ground_truth_nudge_names_read_only_checks_and_revert_guard() {
    let nudge = stale_file_ground_truth_nudge();
    assert!(nudge.contains("git status --short"), "{nudge}");
    assert!(nudge.contains("git diff -- <file>"), "{nudge}");
    assert!(nudge.contains("wc -l <file>"), "{nudge}");
    assert!(nudge.contains("re-read the exact target range"), "{nudge}");
    assert!(
        nudge.contains("Never recommend git checkout/revert"),
        "{nudge}"
    );
}

/// #2315: on the Responses wire with SmartHarness, a turn whose check already
/// ran must not be nudged to run it (#1961: the attempt satisfies the gate).
/// `verify_answer` saw no command there, because requested calls ride as
/// `function_call` items. Twin in the same test: the same wire with the check
/// NOT run is still nudged, so a gate that never fires cannot pass this.
#[cfg(unix)]
#[tokio::test]
#[serial_test::serial(newt_self_verify_env)]
async fn responses_smart_gate_sees_a_check_the_model_already_ran() {
    use crate::agentic::smart_harness::{AdjudicationSettings, SmartHarness};
    struct Restore(Option<std::ffi::OsString>);
    impl Drop for Restore {
        fn drop(&mut self) {
            match self.0.take() {
                Some(v) => std::env::set_var("NEWT_SELF_VERIFY", v),
                None => std::env::remove_var("NEWT_SELF_VERIFY"),
            }
        }
    }
    let _restore = Restore(std::env::var_os("NEWT_SELF_VERIFY"));
    std::env::set_var("NEWT_SELF_VERIFY", "1");

    const CHECK: &str = "sh -c true";
    for ran_check in [true, false] {
        let server = MockServer::start().await;
        let calls = Arc::new(AtomicUsize::new(0));
        Mock::given(method("POST"))
            .respond_with(move |_: &Request| {
                let first = calls.fetch_add(1, Ordering::SeqCst) == 0;
                ResponseTemplate::new(200).set_body_json(if first && ran_check {
                    serde_json::json!({"id":"resp_1","status":"completed","output":[{
                        "type":"function_call","id":"fc_1","call_id":"call_1","name":"run_command",
                        "arguments":serde_json::json!({"command": CHECK}).to_string()}]})
                } else {
                    serde_json::json!({"id":"resp_1","status":"completed","model":"test-model",
                        "output":[{"type":"message","id":"msg_1","role":"assistant","status":"completed",
                        "content":[{"type":"output_text","text":"done","annotations":[]}]}]})
                })
            })
            .mount(&server)
            .await;
        let harness = SmartHarness::new(
            agent_harness::Session::new(crate::test_guard::unbudgeted_session_config()).unwrap(),
            Arc::new(|_| Box::pin(async { Ok(("\"answer\"".to_string(), None)) })),
            AdjudicationSettings::default(),
        )
        .unwrap();
        let ws = tempfile::TempDir::new().unwrap();
        let workspace = ws.path().to_string_lossy().into_owned();
        let task = format!("Fix the bug. You can run `{CHECK}` to verify.");
        let (uri, messages, caveats) = (server.uri(), msgs(), Caveats::top());
        let mut context = ctx(&uri, &messages, &caveats);
        context.smart_harness = Some(&harness);
        context.kind = BackendKind::Openai;
        context.workspace = &workspace;
        context.task = &task;
        openai_responses_complete(context, &mut NoMcp)
            .await
            .expect("the turn completes");
        let nudged = server
            .received_requests()
            .await
            .unwrap()
            .iter()
            .any(|r| String::from_utf8_lossy(&r.body).contains("Before you finish"));
        assert_eq!(
            nudged, !ran_check,
            "ran_check={ran_check}: the gate nudges exactly when the check was not attempted"
        );
    }
}