feat(exec): add --output-format json so --exec can surface its session ID - #272
Draft
bruno-rpassos wants to merge 1 commit into
Draft
feat(exec): add --output-format json so --exec can surface its session ID#272bruno-rpassos wants to merge 1 commit into
bruno-rpassos wants to merge 1 commit into
Conversation
`--exec` accepts `--resume <sessionId>`, but exec mode never writes the
session ID anywhere a caller can read it — only `session.assistantReply`
reaches stdout. A script that runs `deepcode --exec` therefore has no way
to obtain the ID it needs to resume, so `--resume` is unreachable from the
non-interactive path and every exec run is effectively single-shot.
This adds an opt-in `--output-format json` flag that emits newline-delimited
JSON events instead of the plain assistant reply. The shape follows Claude
Code's `--output-format stream-json` convention — one object per line, each
with a `type` discriminator and `session_id` — so existing headless agent
tooling can consume Deep Code without a bespoke parser:
{"type":"system","subtype":"init","session_id":"...","cwd":...,"model":...}
{"type":"assistant","session_id":"...","message":{...}}
{"type":"result","subtype":"success","is_error":false,"session_id":"...",...}
Exactly one `init` event is emitted first and one `result` event last. A
fresh session mints its ID inside handleUserPrompt, so `init` is emitted as
soon as the first streamed message reports it — before the turn completes,
which is what lets a caller capture the ID for a later `--resume`.
The default remains `text`, so existing behaviour and exit codes are
unchanged, and stderr diagnostics are left exactly as they were.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
--execalready accepts--resume <sessionId>, but exec mode never writes the session ID anywhere a caller can read it.exec-runner.tscallsmanager.getActiveSessionId()internally, yet onlysession.assistantReplyreaches stdout.So a script that runs
deepcode --exechas no way to learn the ID it would need to pass back to--resume. The flag is reachable from the interactive/resumepicker, but unreachable from the non-interactive path — which makes every--execrun effectively single-shot unless the caller scrapes~/.deepcode's session store.This PR closes that gap rather than adding a convenience: it makes an already-shipped flag usable from the mode it was clearly meant for.
What this adds
An opt-in
--output-format jsonflag for--execthat emits newline-delimited JSON events instead of the plain assistant reply.The caller can now read
session_idoff the first line and feed it straight back:$ deepcode --exec --resume 782eb448-… --prompt "and again" --output-format jsonWhy this shape
Rather than invent a new schema, the events follow Claude Code's
--output-format stream-jsonconvention: one JSON object per line, each carrying atypediscriminator andsession_id, with asystem/initevent first and aresultevent last. Tooling that already drives headless coding agents can then consume Deep Code without a bespoke parser.Fields Deep Code has no equivalent for are omitted rather than faked — there is no
total_cost_usd, andusagepasses through Deep Code's ownModelUsageverbatim. Message events use Deep Code's own roles (assistant,user,tool,system), since forcing them into Claude's role set would misrepresent them.Guaranteed invariants
initevent first, exactly oneresultevent last.session_id.handleUserPrompt, soinitis emitted the moment the first streamed message reports it — before the turn completes. That is what lets a caller capture the ID even if the run later fails or is interrupted.permission_required,input_required,interrupted,error) all emit aresultevent, so a consumer always gets a terminating event and never has to infer failure from a closed pipe.Compatibility
text; existing behaviour, stdout content, and exit codes are unchanged.stderrdiagnostics are untouched — JSON mode adds structured output on stdout without removing the human-readable message.--output-formatis rejected outside--exec.Implementation notes
Deliberately small and additive — it sits on the
deps.writeStdoutLineseam that already existed, and does not restructure the exec path:packages/cli/src/exec-json-output.tsExecJsonEmitter+ event types (~170 lines)packages/cli/src/exec-runner.tspackages/cli/src/cli-args.ts--output-formatflag and its validationpackages/cli/src/cli.tsxpackages/cli/src/tests/*Serialization is defensive: tool payloads arrive as
unknown, so a value that cannot be stringified falls back to a reduced payload rather than throwing and taking the run down.Testing
init, resume/fork attribution, each error subtype, and that text mode is byte-for-byte unchanged.npm run check(typecheck + lint + format) passes; full suite is green (312 tests in the CLI package).initand successfully resuming with it, producing a single session containing both turns.Branched from
bea340f.Open questions for the maintainer
--json,--output-format stream-json) or reshape the events if you'd prefer something different — I'd rather match your preference now than have consumers build on the wrong schema.--execat all, so I only updated the--helpexamples and epilog. Glad to add a README section covering headless mode and this flag if you want one.onAssistantMessagealso carries tool messages, sotype: "tool"events appear in the stream. Let me know if you'd rather filter those out by default or gate them behind a verbosity flag.