feat(claude): expose the headless stream-json transport, with real streaming
`ClaudeCodeOptions.transport="stream_json"` runs the agent on `claude -p` with stream-json on both pipes instead of driving the TUI through a pseudo-tty. Opt-in: the default stays `pty`, so nothing moves until a config asks for it. The reason to ask for it is that the PTY path pays a multi-second readiness wait before every spawn and can silently lose a prompt to a swallowed paste; the headless path writes to a pipe and gets a native `result` record back. `--remote-control` and friends are dropped by the transport when set, since they'd take claude out of print mode. `include_partial_messages` then gives clients genuine token-level SSE. Blocks are emitted from the `StreamEvent`s rather than forwarded raw: the payload is already Anthropic-shaped but arrives as a dict, so it would need validating against the SDK union anyway, and rebuilding it through our own builders drops an unrecognized block or delta type the same way `_emit_block` already does instead of raising mid-stream on a claude release that adds one. When streaming, the whole-block records are kept for TurnCapture but not re-emitted — that would duplicate every block on the wire. Verified end-to-end against a live claude: both paths accumulate to the identical Message (same blocks, same stop_reason, same usage), the envelope stays 1:1, and block indices stay collision-free across a turn that spans several API requests.
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
"""Claude Code agent definition.
|
||||
|
||||
Notice the absence of a ``streaming`` field — claude-code does not emit
|
||||
token-level deltas, and that fact is encoded in the type, not in a
|
||||
runtime branch.
|
||||
There is no ``streaming`` field, but there is
|
||||
:attr:`ClaudeCodeOptions.include_partial_messages`, and the difference
|
||||
matters. Token-level deltas are not a property of the agent, they are a
|
||||
property of the transport: the PTY transport reads a JSONL that only
|
||||
ever contains finished blocks, so it cannot stream no matter what the
|
||||
agent asks for, while ``transport="stream_json"`` can. The flag lives
|
||||
next to the transport that grants it rather than pretending to be a
|
||||
free-standing capability.
|
||||
|
||||
``BaseAgent.system_prompt`` maps onto the claude CLI's
|
||||
``--system-prompt`` — i.e. it really *is* the agent's system prompt,
|
||||
@@ -57,6 +62,9 @@ HistoryInjectionMode = Literal["native_jsonl", "concat_message"]
|
||||
cycle on the user-facing path (configs may be loaded without
|
||||
``claude-code-api`` installed, e.g. ``--extra prod`` minus claude)."""
|
||||
|
||||
Transport = Literal["pty", "stream_json"]
|
||||
"""Mirrors ``claude_code_api.Transport``, for the same reason."""
|
||||
|
||||
|
||||
class ClaudeCodeOptions(BaseModel):
|
||||
"""Per-agent passthrough for ``claude_code_api.BackendOptions``.
|
||||
@@ -85,6 +93,32 @@ class ClaudeCodeOptions(BaseModel):
|
||||
|
||||
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)
|
||||
|
||||
transport: Transport = "pty"
|
||||
"""Which claude process backs this agent.
|
||||
|
||||
``stream_json`` runs ``claude -p`` with stream-json on stdin and
|
||||
stdout: prompts go into a pipe, events come back as they happen, and
|
||||
a native ``result`` record closes each turn. ``pty`` (the default,
|
||||
kept for backwards compatibility) drives the interactive TUI and
|
||||
tails the session JSONL, which costs a multi-second readiness wait
|
||||
per spawn — the thing ``startup_delay`` bounds — and brings the
|
||||
swallowed-paste failure mode with it.
|
||||
|
||||
Switching also makes the PTY-only knobs below inert
|
||||
(``startup_delay`` / ``file_wait_timeout`` /
|
||||
``turn_duration_timeout`` / ``wait_for_turn_duration``) and drops
|
||||
interactive-only ``extra_args`` such as ``--remote-control``, which
|
||||
would take claude out of print mode. Everything else — model,
|
||||
prompts, tools, MCPs, history seeding — behaves identically."""
|
||||
|
||||
include_partial_messages: bool = False
|
||||
"""Stream token-level deltas to the client instead of one delta per
|
||||
finished block. Requires ``transport="stream_json"``; inert on the
|
||||
PTY transport, whose JSONL contains only finished blocks. Off by
|
||||
default because it multiplies the SSE event count — worth it for
|
||||
interactive chat, pointless for a client that waits for the whole
|
||||
message anyway."""
|
||||
|
||||
append_system_prompt: str | None = None
|
||||
"""Maps to claude CLI's ``--append-system-prompt``. Opting in
|
||||
re-attaches claude-code's full built-in prompt (persona, planning
|
||||
|
||||
Reference in New Issue
Block a user