feat: add --stream-json streaming NDJSON output for headless mode - #69
feat: add --stream-json streaming NDJSON output for headless mode#69tornado404 wants to merge 1 commit into
Conversation
The bundled ZCode runtime (vendor/zcode.cjs) already emits live session
events internally via runtime.subscribeEvents, but the headless --prompt
--json path ignores them and prints a single summary object at turn end.
Integrations like Multica therefore see no tool calls, thinking, or
incremental text while the agent works.
This adds a --stream-json flag that surfaces the live event stream as
qwen-compatible NDJSON on stdout (one JSON object per line):
system → assistant/thinking → assistant/text → tool_use →
user/tool_result → ... → result
The implementation is a post-sync patch (scripts/patch-runtime-stream.ts)
that injects an onEvent callback into runPrompt's submitPrompt call and
maps runtime events (model_streaming, tool_call_*, turn_complete) to the
qwen schema {type, message:{content:[...]}}. The patch anchors on stable
substrings in the minified bundle, fails loudly on upstream changes, and
is idempotent. It runs automatically as part of sync/sync:local/sync:locked.
Verification: scripts/test-stream.ts runs a prompt end-to-end and asserts
the NDJSON shape (system/assistant/result events, non-zero usage).
Launcher change: --stream-json added to runtimeBooleanOptions so the flag
is forwarded to the runtime instead of being flagged invalid.
|
@kingsword09 作者你好,我希望你能评审并采纳这个流式输出功能,配合使用 我发起的PR multica ,可以在多智能体工作流multica调用zcode desktop 享受1.5倍token的权益 |
|
感谢PR。针对这个方案,我又借助 GPT-5.6-Sol 做了一轮交叉审查,并对官方 ZCode runtime 进行了实际协议测试。最终结论主要基于 runtime 的真实行为:现有的 zcode app-server 已经可以提供 Multica 所需的完整流式能力。包括:
因此,对于 Multica 集成,不需要在压缩后的 zcode.cjs 中注入 --stream-json。更合适的方案是参考 Multica 的 zcode app-server Codex 和 ZCode 的具体协议不同,所以不能直接复用 codexClient,但可以复用它的整体架构,包括长期进程管理、 建议 Multica PR 保留 runtime discovery、migration 和 UI 等改动,但将 zcode --prompt --stream-json 基于这个结论,当前 PR 中对 runtime bundle 的注入方案暂时不合并。原因不是流式需求不合理,而是官方 runtime For the Multica integration, the preferred approach is therefore to drive the native app-server: session/create or session/resume This is similar to Multica's Codex backend, although ZCode needs its own protocol adapter because the Please keep the discovery, migration, and UI changes in the Multica PR, but replace the zcode --prompt Given that the runtime already provides this capability, I don't plan to merge the current minified- |
Background
zcode-cli's headless mode (zcode --prompt <text> --json) runs a whole turn and prints a single JSON summary object at the end. While the bundled runtime emits rich live session events internally (assistant text deltas, reasoning, tool calls, tool results, usage), the--promptpath ignores them —runPromptcallssubmitPrompt({abortSignal})without passing anonEventcallback, so the event stream is never surfaced.This means integrations that drive zcode headlessly (e.g. Multica) see no activity while the agent works — no thinking, no tool calls, no incremental text — until the entire turn finishes. The inactivity watchdog sees a single message at the end, which is indistinguishable from a hang.
What this PR does
Adds a
--stream-jsonflag that surfaces the live event stream as qwen-compatible NDJSON on stdout (one JSON object per line), so integrations can observe the full agent workflow in real time.Event mapping (runtime native → NDJSON)
turn_started{"type":"system","subtype":"init","session_id":"..."}model_streaming(text_delta){"type":"assistant","message":{"content":[{"type":"text","text":"..."}]}}model_streaming(reasoning_delta){"type":"assistant","message":{"content":[{"type":"thinking","thinking":"..."}]}}tool_call_scheduled{"type":"assistant","message":{"content":[{"type":"tool_use","id":"...","name":"Bash","input":{...}}]}}tool_call_result/tool_call_error{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"...","content":"..."}]}}turn_complete{"type":"result","subtype":"success","is_error":false,"result":"...","usage":{...}}turn_failed{"type":"result","subtype":"error_during_execution","is_error":true,"error":{"message":"..."}}The schema intentionally matches qwen-code's
--output-format stream-jsonso downstream consumers can reuse a single streaming parser.Example output
{"type":"system","subtype":"init","session_id":"sess_abc"} {"type":"assistant","session_id":"sess_abc","message":{"content":[{"type":"thinking","thinking":"Let me check"}]}} {"type":"assistant","session_id":"sess_abc","message":{"content":[{"type":"tool_use","id":"call_1","name":"Bash","input":{"command":"ls"}}]}} {"type":"user","session_id":"sess_abc","message":{"content":[{"type":"tool_result","tool_use_id":"call_1","content":"file.txt"}]}} {"type":"assistant","session_id":"sess_abc","message":{"content":[{"type":"text","text":"Found file.txt"}]}} {"type":"result","subtype":"success","session_id":"sess_abc","is_error":false,"result":"Found file.txt","usage":{"input_tokens":100,"output_tokens":5,"cache_read_input_tokens":80}}Implementation
The patch lives in
scripts/patch-runtime-stream.ts— a post-sync patch applied automatically aftersync-runtime.tsextractsvendor/zcode.cjsfrom ZCode Desktop. Sincevendor/zcode.cjsis a minified upstream artifact (regenerated on every sync), editing it directly would be overwritten; the patch script keeps the change reproducible across syncs.The patch makes 4 anchored string replacements in the bundle:
"stream-json"boolean option (strict mode would otherwise reject it)lva) — mapstream-jsonargv value too.streamJson(camelCase) for runPromptrunPromptsubmitPrompt call — injectonEvent: o.streamJson ? __zcodeStreamEmit : undefinedresultNDJSON line carries response + usage)The
__zcodeStreamEmithelper (injected at module scope after"use strict") subscribes via the runtime's existingruntime.subscribeEvents({onSessionEvent})mechanism — the same one the TUI uses for live rendering.Properties
__zcodeStreamEmit)--stream-jsonis NOT set, behavior is identical to before (existing--prompt --jsonunaffected)Other changes
src/launcher.ts: add--stream-jsontoruntimeBooleanOptionsso the launcher forwards it instead of flagging it invalidpackage.json: wire the patch intosync/sync:local/sync:locked; addcheck:streamscriptscripts/test-stream.ts: end-to-end validation (runs a prompt with--stream-json, asserts NDJSON shape, non-zero usage)Verification
Tested with tool-calling prompts (34 events: thinking → tool_use → tool_result → text → result) — all event types map correctly.
Backward compatibility
--stream-jsonis additive. Existing--promptand--prompt --jsonbehavior is unchanged. TUI mode andapp-serverare untouched.Security analysis
scripts/patch-runtime-stream.ts,scripts/test-stream.ts,src/launcher.ts(one line),package.json,.gitignore. Scanned for API keys, tokens, secrets — none present.vendor/zcode.cjs), not on user data or network-fetched content.npm package
Published as
zcode-cli-stream(@3.7.5-11):npm install -g zcode-cli-stream@latest zcode --prompt "hello" --stream-jsonForked from
kingsword09/zcode-cliwith full attribution. MIT-licensed. This upstream PR remains open for consideration.AI Disclosure
AI tool used: ZCode (GLM-5.2)
Approach: The patch anchors and event-schema mapping were derived by probing the bundled runtime with a temporary
onEventdump, capturing real event payloads from both a simple prompt and a tool-calling prompt, then writing the qwen-compatible mapping against the observed shapes. The streaming backend consumer (Multica side) reuses the existing qwen streaming parser.