supercode-core 0.2.1

A lightweight, fully-customizable AI coding agent SDK in Rust. Talks to any model via OpenRouter or any OpenAI-compatible endpoint.
Documentation
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
//! PARITY-5 dev/01–02: a GENUINE, multi-session real-Pi corpus, committed
//! alongside (never replacing) `pi_session.jsonl` / `pi_session_live_corpus.jsonl`
//! and `pi_interop.rs`.
//!
//! Provenance of the five fixtures below (`pi_real_corpus_*.jsonl`): each was
//! produced by driving pi's own REAL `AgentSession`/`Agent` turn loop — the
//! exact code path the `pi` CLI itself uses (`packages/coding-agent/src/core/
//! agent-session.ts`'s `prompt()`), from a shallow clone of
//! `earendil-works/pi@351efc82` (the pin `docs/interop/research/pi-fields.md`
//! cites) — against pi's own keyless, OFFLINE `faux` provider
//! (`packages/ai/src/providers/faux.ts`, registered via
//! `@earendil-works/pi-ai/compat`'s `registerFauxProvider`, the exact helper
//! pi's OWN test suite uses, e.g. `packages/coding-agent/test/suite/
//! agent-session-retry-events.test.ts`). Unlike `pi_session_live_corpus.jsonl`
//! (which called `SessionManager.create()`/`appendMessage()` directly with a
//! single hand-assembled payload), these five went through the REAL turn
//! loop end to end: `AgentSession.prompt()` -> `Agent` streams a faux
//! response -> real built-in tools (`write`, `bash` — no test doubles,
//! `baseToolsOverride` was left unset) execute for real against a real
//! scratch `cwd` -> `SessionManager` (real disk instance, not
//! `SessionManager.inMemory()`, no `sessionDir` override so it used pi's own
//! default `getDefaultSessionDir` -> `~/.pi/agent/sessions/--<enc-cwd>--/`)
//! persists each entry. Every file landed at that real default location,
//! reopened cleanly via `SessionManager.open()` (no v3 migration rewrite),
//! then was copied here verbatim.
//!
//! Five sessions, five distinct genuine-loop shapes:
//! - `pi_real_corpus_plain_text.jsonl` — 3 turns, plain text only, no tools/thinking.
//! - `pi_real_corpus_thinking.jsonl` — 2 turns, each assistant message carries
//!   a real `thinking` block ahead of its `text` block (pi's own emission
//!   order for a reasoning turn that also answers).
//! - `pi_real_corpus_tool_call.jsonl` — a `write` tool call that REALLY wrote
//!   `hello.txt` to disk, then a `bash` tool call that REALLY ran
//!   `cat hello.txt` and captured genuine stdout ("hi") as the toolResult —
//!   neither tool result was faux-scripted, only the toolCall content was.
//! - `pi_real_corpus_error_retry.jsonl` — a real `stopReason: "error"`
//!   (`overloaded_error`) turn persisted by pi's own retry machinery, then a
//!   real recovered turn.
//! - `pi_real_corpus_aborted.jsonl` — a real `session.abort()` mid-stream,
//!   persisting `stopReason: "aborted"` / `errorMessage: "Request was
//!   aborted"` (both literal strings pi's own `createAbortedMessage` emits,
//!   not fabricated here), followed by a real recovered follow-up turn
//!   proving the file stays usable after an interrupt.
//!
//! This corpus also caught a REAL loader/writer bug (fixed alongside these
//! tests, see `pi_real_corpus_thinking_survives_pi_to_claude_code_export`
//! below): the Claude Code writer only re-emitted a retained `thinking`
//! block when the rest of `blocks` was otherwise empty ("a genuinely
//! reasoning-only turn"), silently dropping it whenever the SAME turn also
//! had text or a tool call — which is pi's own default shape for a thinking
//! turn, not the rare edge case the old gate's comment described. The
//! synthetic `pi_session.jsonl`/`pi_session_live_corpus.jsonl` fixtures never
//! exercised a thinking+text turn together, so this shipped unnoticed until
//! a genuine pi-driven corpus hit it.

mod interop_common;

use std::path::{Path, PathBuf};

use supercode::audit::{audit_dir, Corpus};
use supercode::session::{Session, SessionFormat};
use supercode::Role;

fn fixture(name: &str) -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests/fixtures")
        .join(name)
}

const REAL_CORPUS_FILES: [&str; 5] = [
    "pi_real_corpus_plain_text.jsonl",
    "pi_real_corpus_thinking.jsonl",
    "pi_real_corpus_tool_call.jsonl",
    "pi_real_corpus_error_retry.jsonl",
    "pi_real_corpus_aborted.jsonl",
];

/// Strip the `to_native_jsonl`/`to_native_jsonl_v2` envelope header line —
/// mirrors `pi_interop.rs`'s private helper of the same shape (kept
/// independent here since integration test binaries can't share private
/// items across files).
fn strip_native_header(native: &str) -> &str {
    let idx = native
        .find('\n')
        .expect("native output must have a header line");
    &native[idx + 1..]
}

// ---- dev/01: at least five REAL pi sessions load and summarize cleanly ----

#[test]
fn pi_real_corpus_five_sessions_load_with_expected_message_counts() {
    let expected: [(&str, usize); 5] = [
        ("pi_real_corpus_plain_text.jsonl", 6),
        ("pi_real_corpus_thinking.jsonl", 4),
        ("pi_real_corpus_tool_call.jsonl", 8),
        ("pi_real_corpus_error_retry.jsonl", 4),
        ("pi_real_corpus_aborted.jsonl", 6),
    ];
    assert_eq!(
        expected.len(),
        REAL_CORPUS_FILES.len(),
        "keep the summary table and the file list in sync"
    );

    for (name, expected_count) in expected {
        let session = Session::from_pi(fixture(name)).expect("real pi fixture must load");
        assert_eq!(
            session.messages.len(),
            expected_count,
            "{name}: unexpected message count: {:?}",
            session.messages
        );
        assert_eq!(
            session.meta.lineage.get("pi_version").map(String::as_str),
            Some("3")
        );
        println!(
            "{name}: {} messages, {} tool calls, cwd={:?} — REAL pi agent-loop output",
            session.messages.len(),
            session
                .messages
                .iter()
                .map(|m| m.tool_calls().len())
                .sum::<usize>(),
            session.meta.cwd
        );
    }
}

#[test]
fn pi_real_corpus_directory_audit_has_zero_gaps_over_genuine_output() {
    let tmp = std::env::temp_dir().join(format!("sc-pi-real-corpus-audit-{}", std::process::id()));
    std::fs::create_dir_all(&tmp).unwrap();
    for name in REAL_CORPUS_FILES {
        std::fs::copy(fixture(name), tmp.join(name)).unwrap();
    }

    let report = audit_dir(&tmp, Corpus::Pi, None);
    assert_eq!(report.files, REAL_CORPUS_FILES.len() as u64);
    assert_eq!(report.parse_errors, 0);
    assert!(
        report.records.keys().all(|k| !k.starts_with("<line>/")
            && !k.contains("UnknownRole")
            && !k.contains("UnknownImageShape")),
        "5 genuine pi-agent-loop sessions must hit zero unknown-record/role/image-shape \
         buckets: {:?}",
        report.records.keys().collect::<Vec<_>>()
    );

    std::fs::remove_dir_all(&tmp).ok();
}

// ---- turn-type coverage: thinking, real tool call+result, error, aborted --

#[test]
fn pi_real_corpus_thinking_session_preserves_thinking_metadata() {
    let session = Session::from_pi(fixture("pi_real_corpus_thinking.jsonl")).unwrap();
    let thinkers: Vec<&supercode::ChatMessage> = session
        .messages
        .iter()
        .filter(|m| m.role == Role::Assistant && m.metadata.contains_key("thinking"))
        .collect();
    assert_eq!(
        thinkers.len(),
        2,
        "both assistant turns in this REAL pi session carried a genuine thinking block: {:?}",
        session.messages
    );
    assert!(thinkers[0]
        .metadata
        .get("thinking")
        .unwrap()
        .contains("Quicksort is fast on average"));
    // Each assistant turn ALSO has real text alongside the thinking — this is
    // exactly the shape the Claude Code writer used to drop (see the export
    // test below).
    assert!(thinkers[0]
        .content
        .as_deref()
        .unwrap_or("")
        .contains("Timsort"));
}

#[test]
fn pi_real_corpus_tool_call_session_ran_real_tools_not_scripted_results() {
    let session = Session::from_pi(fixture("pi_real_corpus_tool_call.jsonl")).unwrap();

    let write_call = session
        .messages
        .iter()
        .flat_map(|m| m.tool_calls())
        .find(|c| c.function.name == "write")
        .expect("real write toolCall");
    assert_eq!(
        write_call.function.parsed_arguments().unwrap(),
        serde_json::json!({"path": "hello.txt", "content": "hi"})
    );
    let write_result = session
        .messages
        .iter()
        .find(|m| m.role == Role::Tool && m.tool_call_id.as_deref() == Some(write_call.id.as_str()))
        .expect("paired write toolResult");
    assert_eq!(
        write_result.content.as_deref(),
        Some("Successfully wrote 2 bytes to hello.txt"),
        "this string came from pi's REAL `write` tool executing against a real scratch cwd, \
         not a faux-scripted tool result"
    );

    let bash_call = session
        .messages
        .iter()
        .flat_map(|m| m.tool_calls())
        .find(|c| c.function.name == "bash")
        .expect("real bash toolCall");
    assert_eq!(
        bash_call.function.parsed_arguments().unwrap(),
        serde_json::json!({"command": "cat hello.txt"})
    );
    let bash_result = session
        .messages
        .iter()
        .find(|m| m.role == Role::Tool && m.tool_call_id.as_deref() == Some(bash_call.id.as_str()))
        .expect("paired bash toolResult");
    assert_eq!(
        bash_result.content.as_deref(),
        Some("hi"),
        "genuine stdout from a real `cat hello.txt` run against the file the `write` tool \
         really created — never scripted as a faux tool result"
    );
}

#[test]
fn pi_real_corpus_error_retry_session_preserves_error_then_recovery() {
    let session = Session::from_pi(fixture("pi_real_corpus_error_retry.jsonl")).unwrap();

    // The transient-error attempt itself carried NO text and NO tool call
    // (`{"content":[{"type":"text","text":""}],...,"stopReason":"error"}`) —
    // `push_assistant`'s shared drop-if-empty rule (`session.rs:5024`,
    // shared by every format's loader, not pi-specific) correctly excludes
    // it from the canonical/replayable `messages` list, same as an empty
    // Claude Code or Codex turn would be. It is NOT lost, though: `raw` is
    // captured strict-verbatim (T1), so the failed attempt is still on disk
    // and still round-trips byte-for-byte (see the native round-trip test).
    assert!(
        session.raw_verbatim().contains("overloaded_error"),
        "the failed attempt must still be preserved verbatim in T1 `raw`, even though it \
         correctly does not become a canonical (replayable) message"
    );
    assert!(
        !session
            .messages
            .iter()
            .any(|m| m.metadata.get("pi_stop_reason").map(String::as_str) == Some("error")),
        "an empty-content error turn has nothing to replay and is consistently excluded from \
         canonical messages, exactly like every other format's loader"
    );

    let recovered = session
        .messages
        .iter()
        .find(|m| {
            m.role == Role::Assistant
                && m.content.as_deref() == Some("Sorry about that -- recovered. The answer is 42.")
        })
        .expect("the retried, successful continuation must survive as a canonical message");
    assert_eq!(
        recovered.metadata.get("pi_stop_reason").map(String::as_str),
        Some("stop")
    );
}

#[test]
fn pi_real_corpus_aborted_session_preserves_aborted_turn_and_recovers() {
    let session = Session::from_pi(fixture("pi_real_corpus_aborted.jsonl")).unwrap();
    let aborted = session
        .messages
        .iter()
        .find(|m| m.metadata.get("pi_stop_reason").map(String::as_str) == Some("aborted"))
        .expect("the genuinely interrupted turn must survive as its own message");
    assert_eq!(
        aborted.metadata.get("pi_error_message").map(String::as_str),
        Some("Request was aborted"),
        "this literal string comes from pi's own faux.ts `createAbortedMessage`, not fabricated"
    );
    // Real session.abort() truncates the streamed text (5000-token faux
    // payload) mid-flight rather than completing it.
    assert!(aborted.content.as_deref().unwrap_or("").len() < 20_000);
    // And the session recovers afterward: a genuine follow-up turn exists.
    let recovered_after = session.messages.iter().any(|m| {
        m.role == Role::Assistant && m.content.as_deref() == Some("Okay, keeping it short instead.")
    });
    assert!(
        recovered_after,
        "a genuine turn after the abort proves the file stays usable/continuable"
    );
}

// ---- native round-trip: byte-lossless on all five genuine files -----------

#[test]
fn pi_real_corpus_native_round_trip_is_byte_lossless_for_all_five() {
    for name in REAL_CORPUS_FILES {
        let path = fixture(name);
        let original = std::fs::read(&path).unwrap();
        let session = Session::from_pi(&path).unwrap();
        let native = session.to_native_jsonl();
        let body = strip_native_header(&native);
        assert_eq!(
            body.as_bytes(),
            original.as_slice(),
            "{name}: native round-trip must reproduce genuine pi-agent-loop bytes exactly"
        );
        // The CLI's own `convert --to pi` path (same-format, no --session-id
        // override) uses `raw_verbatim()` — exercise that exact call too.
        assert_eq!(session.raw_verbatim().as_bytes(), original.as_slice());
    }
}

// ---- REGRESSION: the bug this real corpus exposed and this session fixed -

/// Locks in the fix: previously, `to_claude_code_jsonl`'s Role::Assistant
/// branch only re-emitted a retained `thinking`/`thinking_signature` block
/// when `blocks` (built from `msg.content`/images/tool_calls first) was
/// otherwise EMPTY — i.e. only for a turn with NO text and NO tool call.
/// `pi_real_corpus_thinking.jsonl` (genuine pi output, not hand-authored)
/// has two turns that each carry BOTH a real thinking block AND real text in
/// the SAME turn — pi's own default shape whenever a reasoning model also
/// answers — so the old gate dropped the entire thinking block on every
/// single turn in this file. Fixed by always prepending the retained
/// thinking/redacted_thinking blocks ahead of text/images/tool_use,
/// unconditionally, matching the writer's own documented intent ("Claude
/// Code's OWN format has real thinking/redacted_thinking block shapes, so
/// re-emit the retained metadata verbatim rather than dropping the
/// record").
#[test]
fn pi_real_corpus_thinking_survives_pi_to_claude_code_export() {
    let session = Session::from_pi(fixture("pi_real_corpus_thinking.jsonl")).unwrap();
    let claude_jsonl = session.to_jsonl(SessionFormat::ClaudeCode).unwrap();

    let mut thinking_blocks_seen = 0usize;
    let mut text_blocks_seen = 0usize;
    for line in claude_jsonl.lines() {
        if line.trim().is_empty() {
            continue;
        }
        let v: serde_json::Value = serde_json::from_str(line).expect("valid JSON per line");
        if v.get("type").and_then(serde_json::Value::as_str) != Some("assistant") {
            continue;
        }
        let content = v["message"]["content"]
            .as_array()
            .cloned()
            .expect("assistant content must be a block array");
        let block_types: Vec<&str> = content
            .iter()
            .filter_map(|b| b.get("type").and_then(serde_json::Value::as_str))
            .collect();
        assert_eq!(
            block_types.first(),
            Some(&"thinking"),
            "thinking must be re-emitted, and re-emitted FIRST (matching pi's/Claude's own \
             ordering), not dropped because the turn also has text: {block_types:?}"
        );
        assert!(
            block_types.contains(&"text"),
            "the fix must not have discarded the text block while restoring thinking: {block_types:?}"
        );
        thinking_blocks_seen += 1;
        text_blocks_seen += 1;
        let thinking_text = content[0]["thinking"].as_str().unwrap_or_default();
        assert!(
            !thinking_text.is_empty(),
            "the re-emitted thinking block must carry the real captured text, not a stub"
        );
    }
    assert_eq!(
        thinking_blocks_seen, 2,
        "both real thinking+text turns in this genuine pi session must survive export, not just \
         a reasoning-only edge case"
    );
    assert_eq!(text_blocks_seen, 2);
}

// ---- dev/02: Pi -> {Claude, Codex, OpenCode, Pi} exit cleanly on all five -

#[test]
fn pi_real_corpus_all_five_convert_to_every_target_format_and_stay_replayable() {
    for name in REAL_CORPUS_FILES {
        let session = Session::from_pi(fixture(name)).unwrap();
        let source_user_texts: Vec<String> = session
            .messages
            .iter()
            .filter(|m| m.role == Role::User)
            .filter_map(|m| m.content.clone())
            .collect();
        assert!(
            !source_user_texts.is_empty(),
            "{name}: fixture must have user turns to check replay"
        );

        for target in [
            SessionFormat::ClaudeCode,
            SessionFormat::Codex,
            SessionFormat::OpenCode,
            SessionFormat::Pi,
        ] {
            let out = session
                .to_jsonl(target)
                .unwrap_or_else(|e| panic!("{name} -> {target:?} must exit cleanly: {e}"));
            assert!(
                !out.trim().is_empty(),
                "{name} -> {target:?}: output must be non-empty"
            );

            // Replayable content check: reload through the SAME format's
            // loader (round-trip, not just "parses as JSON") and confirm
            // every source user message text still appears verbatim
            // somewhere in the reloaded conversation.
            let reloaded = Session::load_str(&out, target).unwrap_or_else(|e| {
                panic!("{name} -> {target:?}: writer output must itself reload cleanly: {e}")
            });
            for text in &source_user_texts {
                let found = reloaded.messages.iter().any(|m| {
                    m.content
                        .as_deref()
                        .map(|c| c.contains(text.as_str()))
                        .unwrap_or(false)
                });
                assert!(
                    found,
                    "{name} -> {target:?}: source user text {text:?} must still be present and \
                     replayable after round-tripping through the writer + that format's own loader"
                );
            }
        }
    }
}