Skip to content

D-9: ClaudeCodeJSONLSession — AgentChatSession adapter over TerminalSession.rawStdout #259

@kirich1409

Description

@kirich1409

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

  1. Own a TerminalSession injected at init (the underlying PTY/gRPC channel).
  2. 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().
  3. 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(_:).
  4. 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.
  5. 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

  • ClaudeCodeJSONLSession implements AgentChatSession.
  • Lifecycle: start → consume → terminate works end-to-end with a mock TerminalSession emitting pre-recorded JSONL.
  • send(.text("hello")) writes a valid stream-json user message to the underlying terminal.
  • interrupt() sends SIGINT to underlying process and awaits graceful result emission (verified via mock); SIGTERM/SIGKILL escalation tested on hung process.
  • transcript stream closes cleanly when terminate() called.
  • Process exit without result event → status transitions to .error(UnexpectedExit).
  • Integration test with live echo-based fake PTY emitting JSONL proves end-to-end bytes → events → transcript pipeline.
  • Swift 6 strict concurrency clean.

Relationships

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions