Editable install:
pip install -e ipyaiRun tests:
cd ipyai
./tools/test.shThe test harness keeps setup small: tools/test.sh redirects XDG_CONFIG_HOME to a repo-local temp dir so ipyai config writes stay out of a normal user config tree, then runs pytest. It does not redirect CLAUDE_CONFIG_DIR, because on macOS claude -p OAuth reads credentials from the login keychain keyed by the userID in ~/.claude.json; redirecting the config dir breaks that lookup. Instead, ClaudeBackend sweeps any session jsonls it can identify as its own (or as known claude side-effect stubs) after each turn.
- ipyai/core.py: IPython extension logic, prompt transforms, SQLite bookkeeping, notebook save/load, prompt mode, keybindings, Rich streaming display, backend selection
- ipyai/backends.py: backend registry, canonical backend names, default models
- ipyai/backend_common.py: shared backend context/base classes, typed conversation seed types, common stream formatter, replay helpers, and shared tool/command display helpers
- ipyai/claude_client.py: Claude backend that spawns
claude -pper turn, writes a synthetic session JSONL for context seeding, bridges custom tools through a unix socket + stdio MCP sidecar, and translates stream-json events into canonical backend events - ipyai/mcp_server.py: in-kernel unix socket server that exposes the live
ToolRegistry(list_tools, call_tool) to the MCP bridge subprocess - ipyai/mcp_bridge.py: stdio MCP server subprocess entry point (
ipyai-mcp-bridge) thatclaude -pspawns; forwards MCP tool calls over the unix socket - ipyai/api_client.py: shared
_LisetteBackendplus two backends on top of it —ClaudeAPIBackend(Anthropic vialisette) andCodexAPIBackend(Codexresponsesendpoint vialisette.CodexChat); this is the explicit exception to the common canonical-event formatter path and still uses lisette's native formatter - ipyai/codex_client.py: Codex app-server backend, thread/session orchestration, and app-server event translation into canonical backend events
- ipyai/tooling.py: shared custom
ToolRegistry, schema generation, and local tool calling helpers - ipyai/cli.py:
ipyaiconsole entry point - tests/conftest.py: minimal shell/history harness with repo-local config paths
- tests/test_backends.py: shared backend test helpers
- tests/test_backend_claude_cli.py: Claude CLI end-to-end test
- tests/test_backend_claude_api.py: Claude API end-to-end test
- tests/test_backend_codex.py: Codex end-to-end test
- tests/test_backend_codex_api.py: Codex API end-to-end test
- tests/test_mcp_server.py: unit tests for the in-kernel tool socket server
- tests/test_mcp_bridge.py: end-to-end test that spawns
ipyai-mcp-bridgeand exercises it over real MCP stdio - tests/test_core.py: small local guardrail tests for transforms and backend session filtering
- samples/capture_sdk_shapes.py: legacy Claude Agent SDK capture script (kept for reference while porting to
claude -p --output-format=stream-json) - samples/outputs/: committed normalized payload captures
ipyai uses ipythonng.cli.parse_flags() to split CLI args into ipyai flags and IPython args. parse_flags scans sys.argv[1:] for short flags (e.g. -b, -r, -l) that are not IPython's own short flags, collects them and their values into IPYTHONNG_FLAGS env var, and passes the rest through to IPython. When the ipyai extension loads, _parse_ng_flags() in core.py reads IPYTHONNG_FLAGS and parses it with argparse. This two-stage approach lets ipyai flags coexist with IPython flags on the same command line (e.g. ipyai -b codex -r 5 --pdb).
- Input starting with
.is rewritten into%ipyai. IPyAIExtension.run_prompt()reconstructs recent code/output/note context from IPython history.- Variable refs like
$nameand shell refs like `!`cmdare injected above the prompt. - The selected backend streams the turn:
core.pyfirst builds a typedConversationSeed- each backend then
prepare_turn(...)s using that seed - Claude CLI writes a synthetic session JSONL per turn, spawns
claude -p --resume, and starts a unix-socket MCP bridge for custom tools - Claude API and Codex API both rebuild flat history from the typed seed through the shared
_LisetteBackend - Codex resumes or bootstraps an app-server thread from the typed seed
astream_to_stdout()renders the response through Rich in TTY mode and stores the final transcript text locally.
Completion policy is shared in BaseBackend.complete():
- empty
ConversationSeed provider_session_id=Nonetool_mode="off"ephemeral=Truethink=COMPLETION_THINK(fixed low effort for inline completions, independent ofDEFAULT_THINK)
Backends can still override complete() if a provider genuinely requires it, but the default path is now the contract.
There are two layers of state:
- IPython shell session state, stored in IPython's own SQLite DB
- backend conversation state, stored as provider session IDs or thread IDs when the backend supports them
ipyai uses:
claude_promptsfor AI prompt historysessions.remarkJSON forcwd,backend, andprovider_session_id
If prompt history exists locally but provider_session_id is missing, provider bootstrap is backend-specific:
- Claude CLI always writes a fresh synthetic transcript JSONL per turn and resumes from it, then deletes the file afterward (
--no-session-persistencekeeps claude from writing anything further) - Claude API and Codex API use the typed flat-history seed directly
- Codex starts a new thread and sends the typed notebook-XML seed once
Notebook save/load is explicit only:
%ipyai save <filename>%ipyai load <filename>ipyai -l <filename>
There is no implicit startup.ipynb behavior.
The custom tool story is intentionally small:
- shared custom tools across all backends:
pyrun,bash,start_bgterm,write_stdin,close_bgterm,lnhashview_file,exhash_file - built-ins:
Bash,Edit,Read,Skill,WebFetch,WebSearch,Write
pyrun does not call back into InteractiveShell.run_cell*. It delegates to safepyrun, looked up in shell.user_ns, matching the old ipycodex direct-call boundary and avoiding nested IPython cell execution.
Provider-specific tool exposure now fans out from the shared ToolRegistry:
- Claude CLI: unix-socket MCP bridge (
ipyai-mcp-bridge) exposes the registry toclaude -pvia--mcp-config; allowed tool names use themcp__ipy__...prefix - Claude API and Codex API: OpenAI-style function schemas through
lisette - Codex: app-server
dynamicTools
The ipyai CLI loads safepyrun before ipyai, so normal terminal sessions get pyrun automatically. ipyai seeds the other custom tools into shell.user_ns directly.
Skills are Claude-native:
- built-in
Skilltool is enabled --setting-sources user,projectis passed toclaude -p- optional plugin directories are discovered from
.claude/pluginsup the cwd parent chain and passed as repeated--plugin-dirflags
The samples/ directory holds committed stream-shape artifacts so event-wiring spelunking does not need to be repeated. The capture scripts there still import claude_agent_sdk and are kept only as historical reference; the live Claude backend no longer uses the SDK. To re-capture against claude -p, run it directly with --output-format=stream-json --include-partial-messages --verbose and save the output.
The test suite is intentionally small and integration-heavy.
Current coverage focuses on:
- one real round-trip test for each backend
- notebook save/load followed by a real follow-up prompt
- backend-specific session metadata persistence
- prompt transform behavior
- backend session filtering in resume listings
ipyairesolves config paths via XDG.- The repo-local test harness sets
XDG_CONFIG_HOMEso config writes stay out of a normal user config tree.CLAUDE_CONFIG_DIRis intentionally not redirected (keychain-based OAuth depends on~/.claude.json).