Description
Implement ClaudeCodeJSONLSession — the concrete AgentChatSession that wraps a TerminalSession (local PTY or remote gRPC), consumes its rawStdout, runs bytes through ClaudeStreamJSONParser, and exposes the resulting AgentStreamEvent stream via transcript.
Spec: Epic #250 §4.4 (ClaudeCodeJSONLSession first implementation of AgentChatSession).
Scope
File
MacApp/Packages/AgentChat/Sources/AgentChat/Adapters/ClaudeCodeJSONLSession.swift
Responsibilities
- Own a
TerminalSession injected at init (the underlying PTY/gRPC channel).
- At
start():
- Spawn the
claude process via TerminalSession.start(command:) with a command built by ClaudeCommandBuilder (D-20) — dialogue flags.
- Subscribe to
terminalSession.rawStdout → forward each Data to parser.ingest(_:).
- Spawn a consumer task that reads
parser.events() and re-emits to self.transcript stream.
- Also watch
terminalSession.events (the existing TerminalEvent stream) for .processExited — on exit, call parser.close().
send(.text(prompt)):
- Wrap prompt as a stream-json user message (
{type: "user", message: {role: "user", content: [{type:"text", text: ...}]}} + \n).
- Write bytes via
terminalSession.write(_:).
interrupt() (MVP — SIGINT only):
- Send
SIGINT to the underlying process (graceful shutdown — CLI emits result with is_error=true).
- If no exit within 2 seconds — escalate to
SIGTERM, then SIGKILL.
- Post-MVP (when
--input-format stream-json is stabilised by Anthropic, Anthropic issue #24594): switch primary path to stream-json ControlRequest ({type: "control_request", request_id: UUID, request: {subtype:"interrupt"}} + \n) written to stdin; keep SIGINT as fallback.
terminate():
terminalSession.terminate(), close parser, finish transcript stream.
State machine
AgentChatStatus:
.idle — before start
.streaming — message is in flight
.thinking — thinking block active
.awaitingApproval — reserved for non-MVP
.error(Error) — parser emitted .error or process exited non-zero
.terminated — after terminate()
Transitions inferred from AgentStreamEvent stream — the adapter updates status as side-effect while re-emitting events.
Acceptance Criteria
Relationships
Description
Implement
ClaudeCodeJSONLSession— the concreteAgentChatSessionthat wraps aTerminalSession(local PTY or remote gRPC), consumes itsrawStdout, runs bytes throughClaudeStreamJSONParser, and exposes the resultingAgentStreamEventstream viatranscript.Spec: Epic #250 §4.4 (ClaudeCodeJSONLSession first implementation of AgentChatSession).
Scope
File
MacApp/Packages/AgentChat/Sources/AgentChat/Adapters/ClaudeCodeJSONLSession.swiftResponsibilities
TerminalSessioninjected at init (the underlying PTY/gRPC channel).start():claudeprocess viaTerminalSession.start(command:)with a command built byClaudeCommandBuilder(D-20) — dialogue flags.terminalSession.rawStdout→ forward eachDatatoparser.ingest(_:).parser.events()and re-emits toself.transcriptstream.terminalSession.events(the existing TerminalEvent stream) for.processExited— on exit, callparser.close().send(.text(prompt)):{type: "user", message: {role: "user", content: [{type:"text", text: ...}]}}+\n).terminalSession.write(_:).interrupt()(MVP — SIGINT only):SIGINTto the underlying process (graceful shutdown — CLI emitsresultwithis_error=true).SIGTERM, thenSIGKILL.--input-format stream-jsonis stabilised by Anthropic, Anthropic issue #24594): switch primary path to stream-json ControlRequest ({type: "control_request", request_id: UUID, request: {subtype:"interrupt"}}+\n) written to stdin; keep SIGINT as fallback.terminate():terminalSession.terminate(), closeparser, finishtranscriptstream.State machine
AgentChatStatus:.idle— before start.streaming— message is in flight.thinking— thinking block active.awaitingApproval— reserved for non-MVP.error(Error)— parser emitted.erroror process exited non-zero.terminated— after terminate()Transitions inferred from
AgentStreamEventstream — the adapter updatesstatusas side-effect while re-emitting events.Acceptance Criteria
ClaudeCodeJSONLSessionimplementsAgentChatSession.send(.text("hello"))writes a valid stream-json user message to the underlying terminal.interrupt()sends SIGINT to underlying process and awaits gracefulresultemission (verified via mock); SIGTERM/SIGKILL escalation tested on hung process.transcriptstream closes cleanly whenterminate()called.resultevent → status transitions to.error(UnexpectedExit).echo-based fake PTY emitting JSONL proves end-to-end bytes → events → transcript pipeline.Relationships