feat(cli): expand print mode to cover kimi-cli stream-json capabilities#1199
feat(cli): expand print mode to cover kimi-cli stream-json capabilities#1199RealKai42 wants to merge 8 commits into
Conversation
- emit model thinking as its own `{"role":"assistant","type":"thinking"}` JSONL line
- add `--input-format text|stream-json` (multi-turn JSON prompts over stdin)
- add `--final-message-only` and the `--quiet` shorthand
- surface background-task/cron events as `{"type":"notification",...}` lines
- report turn failures as `{"type":"error",...}` JSON and map retryable
provider errors to exit code 75 (keeps stream-json stdout entirely JSON)
- wait for background tasks before exit, gated by background.keepAliveOnExit,
bounded by background.printWaitCeilingS
Adds CLI option/validation tests, prompt-runner behaviour tests, and updates
the kimi-command reference docs (en/zh).
Surface the activity layer the TUI renders on stdout as JSON so the
stream-json output is both complete and entirely JSON:
- live tool output as `{"type":"tool_progress",...}` (moved off stderr in
stream-json mode; text mode keeps the stderr chunk)
- subagent lifecycle as `{"type":"subagent","event":...}`
- `{"type":"warning"|"skill_activated"|"mcp_server_status"|"compaction"|
"goal_update"|"agent_status"|"tool_list_updated",...}`
Activity events flush the pending assistant message first to preserve
ordering, and are emitted only in stream-json mode. Adds prompt-runner
tests and updates the kimi-command reference docs.
🦋 Changeset detectedLatest commit: ff95ddd The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d61fc2d628
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…JSON on failure
- `--quiet` with neither `--prompt` nor `--input-format` now fails validation
("Quiet mode requires --prompt or --input-format.") instead of entering print
mode and crashing on an undefined prompt.
- `PromptFinalJsonWriter.finish()` skips emitting when there is no final text,
matching the text writer — a failed `--final-message-only` turn no longer
prints a spurious `{"role":"assistant","content":""}` before its error line.
Adds a changeset for the print-mode stream-json work.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3bea6f9fa2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const lines = io.stdin ?? defaultStdinLines(process.stdin); | ||
| if (inputFormat === 'stream-json') { | ||
| for await (const command of readUserCommands(lines, stderr)) { | ||
| await runPromptTurn(session, command, outputFormat, finalOnly, stdout, stderr); |
There was a problem hiding this comment.
Keep stream-json notifications alive between stdin turns
When --input-format stream-json is driven interactively, this loop unsubscribes at the end of each runPromptTurn and then waits for the next stdin line. If a background task or cron fires in that idle window, no listener writes the notification, and the exit drain won't recover already-terminated tasks because it lists only activeOnly tasks; the stream-json consumer silently misses events that this mode promises to surface. Keep a run-level notification listener across the whole stdin loop or replay terminal tasks to cover the gap.
Useful? React with 👍 / 👎.
…ge-only mode The exit drain still waits for running background tasks, but no longer emits their `background.task.terminated` JSON in `--output-format stream-json --final-message-only`, preserving the single-final-assistant-message contract.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 46dc04d982
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… for goal summaries
- `tool_progress` JSON now carries `custom_kind`/`custom_data`, so stream-json
clients keep structured `kind: "custom"` payloads (e.g. the MCP OAuth
authorization URL) instead of an opaque `{"kind":"custom"}`.
- The headless `/goal` summary is suppressed on stdout under
`--final-message-only`, matching the single-final-assistant-message contract
(the goal status is still conveyed by the exit code).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 98f4cfd83d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (outputFormat === 'stream-json') { | ||
| outputWriter.writeActivity(toolProgressMessage(event)); |
There was a problem hiding this comment.
Forward subagent tool progress events
When a subagent runs a tool, its tool.progress event is emitted with the child agent id (the TUI handles these in subagent-event-handler.ts), so the main-agent filter above returns before this branch is reached. In --output-format stream-json, live output and custom progress payloads from subagent tools therefore never reach stdout, despite the new activity stream advertising TUI-level live tool output; route child-agent progress here as well or handle it before the main-turn filter.
Useful? React with 👍 / 👎.
Add a `Record<Event['type'], StreamJsonRoute>` coverage table that classifies every protocol event by how print mode's stream-json output treats it. Because it is keyed by the full event union, adding a new event type fails `typecheck` until it is explicitly classified, so print mode can never silently drop a new event. A companion test keeps the notification/activity/tool_progress rows in sync with the mappers in run-prompt.ts (now exported for testing).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fef49beb9d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } else if (event.update.text !== undefined && event.update.text.length > 0) { | ||
| stderr.write( | ||
| event.update.text.endsWith('\n') ? event.update.text : `${event.update.text}\n`, | ||
| ); |
There was a problem hiding this comment.
Gate text tool progress in final-only mode
When --final-message-only or --quiet is used with text output and a tool emits tool.progress text (for example live shell output), this branch still writes every chunk to stderr because it bypasses the final-only writer. That violates the advertised “only the final assistant message”/quiet contract and leaves scripts with progress noise on stderr; suppress this path when finalOnly is true or route it through the writer.
Useful? React with 👍 / 👎.
- text-mode `tool.progress` output is suppressed when `--final-message-only` / `--quiet` is set, matching the minimal-output contract (it bypassed the final-only writer and wrote every chunk to stderr). - add kitchen-sink final-only invariant tests (stream-json and quiet) that feed every in-turn event kind and assert stdout is exactly the final message with a silent stderr — one guard that catches any direct-write leak at once.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ff95ddd82d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| (part as { type?: unknown }).type === 'text' && | ||
| typeof (part as { text?: unknown }).text === 'string' | ||
| ) { | ||
| texts.push((part as { text: string }).text); |
There was a problem hiding this comment.
Preserve media parts in stream-json input
When --input-format stream-json receives a user message whose content array includes image_url or video_url parts, this filter keeps only text parts, so mixed prompts silently lose their media and image-only prompts become empty and are skipped. The SDK prompt input already supports media parts, so programmatic multimodal callers using JSONL get a successful run with the wrong prompt instead of the intended content.
Useful? React with 👍 / 👎.
Summary
Brings
kimi -p(print) mode up to — and slightly beyond — thenon-interactive
stream-jsoncapabilities ofkimi-cli, so other programs candrive Kimi Code and consume its output as pure JSON.
What's included
{"role":"assistant","type":"thinking"}JSONL line, before the answer it produced.--input-format text|stream-json— read prompts from stdin instead of--prompt;stream-jsonreads one JSON user message per line and runs each asa turn (multi-turn).
--final-message-onlyand the--quietshorthand(
--output-format text --final-message-only).{"type":"notification", ...}lines.{"type":"error","code","message","retryable"}on stdout instream-json(stderr in
text), and mapped to exit code75for retryable provider errors(connection/timeout/rate-limit/5xx) or
1otherwise. stdout stays entirely JSON.background tasks before closing, gated by
background.keepAliveOnExitandbounded by
background.printWaitCeilingS.tool_progress(live tool output, moved offstderr in
stream-json),subagent,warning,skill_activated,mcp_server_status,compaction,goal_update,agent_statusandtool_list_updated, so the stream is both complete and entirely JSON.textmode is unchanged (tool progress on stderr, no activity events).
Parity with kimi-cli
stream-jsonnow covers every output type kimi-cli'sJsonPrinteremits(assistant / tool / notification);
plan-displayis the only one not covered andis unreachable since plan mode is disabled in print mode. kimi-code additionally
emits the activity layer above, plus
error, asession.resume_hintmeta lineand a goal summary.
Tests & docs
new path (input/output formats, final-only, quiet, notifications, errors +
exit codes, background-task drain, activity events).
kimicommand reference docs (en/zh).vitest run), typecheck and lint clean.