Implementation Plan: Pi Agent Support
- [ ] Step 1: Pi stream parser types and parsing - [ ] Step 2: Pi stream event dispatch - [ ] Step 3: CliBackend constructors and registration - [ ] Step 4: OutputFormat extension and PtyExecutor integration - [ ] Step 5: Auto-detection - [ ] Step 6: Smoke test fixture and replay test - [ ] Step 7: Documentation and ralph.yml examples
Overview
Implementation Plan: Pi Agent Support
Checklist
- Step 1: Pi stream parser types and parsing
- Step 2: Pi stream event dispatch
- Step 3: CliBackend constructors and registration
- Step 4: OutputFormat extension and PtyExecutor integration
- Step 5: Auto-detection
- Step 6: Smoke test fixture and replay test
- Step 7: Documentation and ralph.yml examples
Step 1: Pi stream parser types and parsing
Objective: Create pi_stream.rs in ralph-adapters with the PiStreamEvent enum, sub-types, and PiStreamParser::parse_line().
Implementation guidance:
- Create
crates/ralph-adapters/src/pi_stream.rs - Define
PiStreamEventenum with#[serde(tag = "type", rename_all = "snake_case")]and variants:MessageUpdate,ToolExecutionStart,ToolExecutionEnd,TurnEnd,Other - Define sub-types:
PiAssistantEvent,PiToolResult,PiContentBlock,PiTurnMessage,PiUsage,PiCost - Implement
PiStreamParser::parse_line()matchingClaudeStreamParser::parse_line()pattern - Add
mod pi_stream;andpub usetolib.rs
Test requirements:
- Parse each variant from JSON string literals
- Verify
#[serde(other)]handles unknowntypevalues - Verify
#[serde(other)]handles unknownassistantMessageEvent.typevalues - Verify malformed JSON returns
None(no panic) - Verify empty/whitespace lines return
None - Parse real captured NDJSON lines from research samples
Integration notes: No integration yet — pure types and parsing.
Demo: Unit tests pass. Parse a real pi NDJSON line and get the correct Rust enum variant.
Step 2: Pi stream event dispatch
Objective: Implement dispatch_pi_stream_event() and PiSessionState that map pi events to StreamHandler calls.
Implementation guidance:
- Add
PiSessionStatestruct withtotal_cost_usd: f64andnum_turns: u32 - Implement
dispatch_pi_stream_event()per the design doc TextDelta→handler.on_text()+extracted_textappendThinkingDelta→handler.on_text()only ifverboseError→handler.on_error()ToolExecutionStart→handler.on_tool_call()ToolExecutionEnd→handler.on_tool_result()orhandler.on_error()based onis_errorTurnEnd→ accumulate cost and turn count in stateOther→ no-op- Export from
lib.rs
Test requirements:
- Mock
StreamHandlerthat records calls - Verify
text_deltacallson_textwith correct delta - Verify
text_deltaappends toextracted_text - Verify
thinking_deltacallson_textwhen verbose=true, skips when false - Verify
tool_execution_startcallson_tool_callwith name, id, args - Verify
tool_execution_end(isError=false) callson_tool_result - Verify
tool_execution_end(isError=true) callson_error - Verify
turn_endaccumulates cost (3 turns → summed cost) - Verify
turn_endincrements turn count - Verify
Othervariant does nothing
Integration notes: Still self-contained in ralph-adapters. No changes to other crates.
Demo: Feed a sequence of PiStreamEvent values through dispatch, verify handler received correct calls and state has correct totals.
Step 3: CliBackend constructors and registration
Objective: Add pi() and pi_interactive() to CliBackend, register in all resolution paths.
Implementation guidance:
- Add
CliBackend::pi()— command:"pi", args:["-p", "--mode", "json", "--no-session"], prompt_mode:Arg, prompt_flag:None, output_format:PiStreamJson - Add
CliBackend::pi_interactive()— command:"pi", args:["--no-session"], prompt_mode:Arg, prompt_flag:None, output_format:Text - Add
"pi"arm tofrom_name() - Add
"pi"arm tofrom_config() - Add
"pi"arm tofor_interactive_prompt() - No large-prompt temp file handling (only Claude needs that)
Test requirements:
test_pi_backend()— verify command, args, prompt as positional argtest_pi_interactive_backend()— verify no-p, no--mode jsontest_from_name_pi()— verify resolutiontest_for_interactive_prompt_pi()— verify resolutiontest_from_config_pi()— verify resolutiontest_from_hat_backend_named_with_args_pi()— verify extra args appendedtest_pi_interactive_mode_unchanged()— verify interactive flag filtering is no-op for pi
Integration notes: Requires OutputFormat::PiStreamJson from Step 4, but the enum variant can be added here first since it's in the same file.
Demo: CliBackend::pi().build_command("test prompt", false) returns ("pi", ["-p", "--mode", "json", "--no-session", "test prompt"], None, None).
Step 4: OutputFormat extension and PtyExecutor integration
Objective: Add OutputFormat::PiStreamJson, wire pi parsing into PtyExecutor::run_observe_streaming().
Implementation guidance:
- Add
PiStreamJsonvariant toOutputFormatenum incli_backend.rs - In
pty_executor.rs, importPiStreamParser,dispatch_pi_stream_event,PiSessionState - Add
is_pi_streamcheck alongsideis_stream_json - Add pi parsing branch in the data processing loop (same line-buffering pattern as Claude)
- Initialize
PiSessionStatebefore the event loop - After event loop exits, if
is_pi_stream, callhandler.on_complete()with synthesizedSessionResultusing wall-clock duration and accumulated state - Pass
verboseflag through todispatch_pi_stream_event()
Test requirements:
- Existing Claude streaming tests still pass (no regression)
- Pi stream parsing branch activates for
OutputFormat::PiStreamJson on_complete()called with accumulated cost and turn count after pi session ends
Integration notes: This is the main integration point. Changes pty_executor.rs which is a critical path. Careful to only add a new branch, not modify existing Claude/Text paths.
Demo: Run Ralph with a mock pi binary that outputs NDJSON fixtures. TUI shows tool calls with ⚙️ icons and text output streams in real-time.
Step 5: Auto-detection
Objective: Add pi to DEFAULT_PRIORITY as the last entry.
Implementation guidance:
- Append
"pi"toDEFAULT_PRIORITYinauto_detect.rs - No
detection_command()mapping needed (binary name matches)
Test requirements:
test_default_priority_includes_pi()— verifypiis in the listtest_default_priority_pi_is_last()— verifypiis the last element- Existing auto-detection tests still pass
Integration notes: Minimal change. One line added to the constant.
Demo: ralph doctor (or equivalent) shows pi as a detected backend when installed.
Step 6: Smoke test fixture and replay test
Objective: Record a real pi NDJSON session and create a replay-based smoke test.
Implementation guidance:
- Capture a pi session:
pi -p --mode json --no-session --thinking off "create a file called test.txt with 'hello world', then read it back" > fixture.jsonl - Place fixture in
crates/ralph-adapters/tests/fixtures/pi-basic-session.jsonl(orcrates/ralph-core/tests/fixtures/) - Write a test that reads the fixture line by line, parses with
PiStreamParser, dispatches throughdispatch_pi_stream_event()with a recording handler - Verify: text output extracted, tool calls captured, cost accumulated, turn count correct
Test requirements:
- Fixture parses without errors
- All expected
on_text,on_tool_call,on_tool_resultcalls present extracted_textcontains the agent's text output- Cost > 0
- Turn count > 0
Integration notes: Tests the full parse+dispatch pipeline against real data.
Demo: cargo test -p ralph-adapters pi_smoke passes.
Step 7: Documentation and ralph.yml examples
Objective: Document pi backend in user-facing docs and provide example configurations.
Implementation guidance:
- Update
docs/installation.mdor equivalent to mention pi as a supported backend - Add pi to the backend comparison table in docs
- Add example
ralph.pi.ymlconfig file in project root orexamples/ - Update
AGENTS.md/CLAUDE.mdarchitecture section to mention pi in the backend list - Update the
NoBackendErrordisplay to include pi install instructions
Test requirements:
- Example config is valid YAML
NoBackendErrordisplay includes pi
Integration notes: Documentation only, no code behavior changes beyond NoBackendError message.
Demo: User can copy ralph.pi.yml, set backend: pi, and run ralph run -c ralph.pi.yml -p "prompt".