Skip to content

feat: structured claude agents in labs, driven through their stream instead of their screen - #76

Merged
sean-brydon merged 1 commit into
mainfrom
claude-driver
Oct 13, 2026
Merged

sean-brydon merged 1 commit into
mainfrom
claude-driver

Conversation

@sean-brydon

Copy link
Copy Markdown
Collaborator

Phase 3 of design/structured-agents/plan.md, behind Labs and off by default: Settings › Labs › Structured Claude agents.

What a structured agent is

claude -p --input-format stream-json --output-format stream-json --include-partial-messages --replay-user-messages --permission-mode <mode> --permission-prompt-tool stdio, run under berthd agent-host (#75) in the session's tmux pane. The session carries @berth_driver=claude, its log folder and its socket. The new Claude driver (internal/box/claudedriver.go) follows the host's log and does everything the terminal path did by screen and keys from the stream instead:

Terminal driver Claude driver
Conversation tail ~/.claude/projects/*.jsonl, guessing which file belongs to the session the host's own log, read by the same Claude parser (internal/transcript/host.go); tool details and older pages read it too
Live reply scrape the styled screen stream_event text deltas
Prompt paste, then Enter {"type":"user",…} line
Interrupt Esc, then scrape for "esc to interrupt" interrupt control request, then wait for result
Mode Shift+Tab until the footer matches set_permission_mode
Permission scrape the numbered menu can_use_tool request; the session's ask.choices drive the same Allow / Always / Deny buttons
Questions arrow keys into Claude's form updatedInput.answers
Turn state berthd hooks events from the stream; berthd's hooks stay quiet (BERTH_DRIVER=structured)

Bypass is berthd allowing every can_use_tool (Claude itself runs in default), so it works where Claude refuses its own bypass, such as when run as root. AskUserQuestion and ExitPlanMode always wait for a person. "Always allow" keeps Claude's suggested rule to the session, not the project's settings file.

Restarts: a driver that starts again reads the log back silently, reconciles the turn ledger (and allows, in bypass, anything asked while it was away), then attaches live. It follows a session from its start and, after a restart, from the first session list.

API / app

  • POST sessions and POST tasks take driver: "claude" and mode, on boxes listing driver.claude. Ask gains choices.
  • Driver gains Record (older pages) and Draft.
  • App: Labs switch plus a Permissions: Bypass / Ask setting (default Bypass). startWork passes the driver for Claude picks. Structured sessions open as conversations. useAsk uses ask.choices when present, so nothing is read off a screen.
  • Docs: changelog, Labs guide and app API.

Not in this PR (planned)

The terminal hand-off (claude --resume in the pane and back), the SQLite conversation index and resume (phase 4), the Codex driver (phase 5) and ACP (phase 6).

Tests

  • TestAStructuredClaudeSessionIsDrivenThroughItsStream: a stand-in claude speaking the spike's recorded stream-json (python3), under this test binary as agent host. It covers the first prompt, turn state with no hooks, the agent's session id, a permission with choices answered by key, bypass, a question answered from the chat, draft plus interrupt, and a fresh driver reading the log back and driving on. It passed 5/5 under -race.
  • TestALiveClaudeSessionWorks (opt-in BERTH_LIVE_CLAUDE=1): real Claude Code (haiku), a reply, then a Bash command allowed by bypass. It passed locally in 4s.
  • TestAStructuredSessionsLogReadsAsItsConversation uses a recorded, anonymised log (internal/transcript/testdata/claude_host_ask.jsonl).
  • The hook stays quiet under BERTH_DRIVER=structured.
  • Full go test -race ./..., go vet ./..., app tsc, typecheck:plugins, check:titles/csp/themes, pnpm test, pnpm build, tsc -p e2e and the docs-site build all pass.

🤖 Generated with Claude Code

…nstead of their screen

With Settings › Labs › Structured Claude agents on, a new Claude Code
agent runs as `claude -p` speaking stream-json under `berthd agent-host`,
and the new Claude driver (internal/box/claudedriver.go) does everything
the terminal path did by screen and keys from the stream:

- the conversation is read from the host's log with the same Claude parser
  (internal/transcript/host.go), so tool details, older pages and questions
  read as before, and the reply streams into the chat as a draft
- prompts, interrupt, permission mode and question answers go in as JSON
  lines and control requests
- each permission comes to the chat with Allow / Always allow / Deny
  (ask.choices). "Bypass" is berthd allowing them, so it also works as root;
  questions and plans always wait for a person
- turn state comes from the stream, and berthd's own hooks stay quiet in
  these sessions (BERTH_DRIVER=structured), so nothing is counted twice
- berthd restarting reads the log back and picks up where it was

Sessions and tasks take driver: "claude" and mode on boxes with the
driver.claude capability. Structured sessions open as conversations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
berth Ready Ready Preview Oct 13, 2026 12:15pm UTC
berth-docs Ready Ready Preview Oct 13, 2026 12:15pm UTC

Request Review

@sean-brydon
sean-brydon merged commit c3212c0 into main Oct 13, 2026
7 of 8 checks passed
@linear-code

linear-code Bot commented Oct 13, 2026

Copy link
Copy Markdown

ENG-6002

sean-brydon added a commit that referenced this pull request Oct 13, 2026
…they do in order

- a structured session started again (resumed, or back from Claude Code's
  terminal) puts its own agent's CLI on PATH, not Claude Code's: on Linux,
  where a login shell resets PATH, a resumed Codex or ACP agent wasn't
  found
- the terminal hand-off's mark is "size path": tmux on Linux shows a
  control character in an option escaped, so the turns typed in the
  terminal weren't read back into the chat
- an answered permission says the agent works again when the answer is read
  back from its log, so it comes before anything the agent says after it;
  said at once, it could land after the turn's end and leave it running
- a driver keeps following a session while a host answers on its socket: a
  host started again listens a moment before its log says so

These are the failures in the Go job since #76, reproduced on Linux.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cursor Bot pushed a commit to edimitchel/berthd that referenced this pull request Oct 13, 2026
…they do in order

- a structured session started again (resumed, or back from Claude Code's
  terminal) puts its own agent's CLI on PATH, not Claude Code's: on Linux,
  where a login shell resets PATH, a resumed Codex or ACP agent wasn't
  found
- the terminal hand-off's mark is "size path": tmux on Linux shows a
  control character in an option escaped, so the turns typed in the
  terminal weren't read back into the chat
- an answered permission says the agent works again when the answer is read
  back from its log, so it comes before anything the agent says after it;
  said at once, it could land after the turn's end and leave it running
- a driver keeps following a session while a host answers on its socket: a
  host started again listens a moment before its log says so

These are the failures in the Go job since cosscom#76, reproduced on Linux.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch was successfully deployed

2 active deployments
Preview – berth-docs — 04e6ec31 Deployed Oct 13, 2026 by vercel[bot]
Preview – berth — 04e6ec31 Deployed Oct 13, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant