Files
fusion/docs/grok-cli-contract.md
gsxdsm 1fc615d0da FN-7724: bridge Grok CLI tool_use NDJSON events in GrokRuntimeAdapter
Bridges Grok CLI tool execution events (tool_use start/result) from the NDJSON stream into the runtime adapter's onToolStart/onToolEnd callbacks, alongside existing text bridging.

- GrokRuntimeAdapter.promptWithFallback now parses and bridges tool_use NDJSON events into onToolStart/onToolEnd callbacks
- Tool name/args/result pass through unchanged (no Grok→pi tool-name mapping, since the verified contract doesn't pin a vocabulary)
- step_finish/error remain non-terminal per-step events, not bridged to any callback; only subprocess close/error finalizes (unchanged from FN-7722)
- Extended stream-parser.ts to recognize tool_use event shapes
- Added new types for tool event payloads in types.ts
- Updated docs/grok-cli-contract.md and plugin README to document tool event bridging
- Added changeset for @runfusion/fusion (minor)
- Added/extended tests in runtime-adapter.test.ts and stream-parser.test.ts (fixture-based, no live binary)

Files changed:
 .changeset/fn-7724-grok-cli-tool-bridging.md       |   7 ++
 docs/grok-cli-contract.md                          |  16 ++-
 plugins/fusion-plugin-grok-runtime/README.md       |  12 +++
 .../src/__tests__/runtime-adapter.test.ts          | 120 +++++++++++++++++++++
 .../src/__tests__/stream-parser.test.ts            |  33 ++++++
 .../src/runtime-adapter.ts                         |  83 +++++++++++---
 .../src/stream-parser.ts                           |  10 ++
 plugins/fusion-plugin-grok-runtime/src/types.ts    |  23 ++++
 8 files changed, 287 insertions(+), 17 deletions(-)

Fusion-Task-Id: FN-7724

Fusion-Task-Lineage: 73abbf2a-6dcd-44fb-86be-71d4788c92d2

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-09 19:58:05 -07:00

14 KiB

Grok CLI Contract (FN-7722)

Date: 2026-07-09

Research method

  • fn_web_fetch against the canonical upstream repository (https://github.com/superagent-ai/grok-cli), specifically:
    • README.md (headless-mode overview, feature summary).
    • src/index.ts (commander CLI argument parsing — the exact flag spellings and headless dispatch).
    • src/headless/output.ts (the actual NDJSON event emitter — the authoritative schema source, not just docs prose).
    • src/headless/output.test.ts (fixture-level confirmation of the emitted JSONL shapes, used as ground truth for this plugin's own fixture tests).
  • No live grok binary was invoked; no field name or flag spelling in this document is guessed — every claim below traces to one of the four files above. Raw captured research (queries + verbatim schema) is preserved as this task's research task document (fn_task_document_read key research on FN-7722).

Confirmed non-interactive invocation

grok --prompt "<text>" --format json
# short flags:
grok -p "<text>" --format json
  • -p, --prompt <prompt> — run a single prompt headlessly, then exit.
  • --format <format> — headless output format, text (default) or json; invalid values are rejected by commander's InvalidArgumentError (parseHeadlessOutputFormat/isHeadlessOutputFormat in src/index.ts).
  • Useful companion flags confirmed in the same program.option(...) chain: -d, --directory <dir> (cwd), -m, --model <model>, -s, --session <id> (resume a saved session, or latest), -k, --api-key <key> (inline key).
  • --format json output is newline-delimited JSON (NDJSON/JSONL) — one JSON object per line — not a single JSON document. This is directly confirmed by createHeadlessJsonlEmitter()'s jsonLine() helper in src/headless/output.ts, which appends \n after each JSON.stringify.

Verified NDJSON event schema (verbatim)

Source: HeadlessJsonEvent union type in src/headless/output.ts.

type HeadlessJsonEvent =
  | { type: "step_start"; sessionID?: string; stepNumber: number; timestamp: number }
  | { type: "text"; sessionID?: string; stepNumber: number; text: string; timestamp: number }
  | {
      type: "tool_use";
      sessionID?: string;
      stepNumber: number;
      timestamp: number;
      toolCall: ToolCall;
      toolResult: ToolResult;
      timing?: { startedAt?: number; finishedAt?: number; durationMs?: number };
    }
  | {
      type: "step_finish";
      sessionID?: string;
      stepNumber: number;
      timestamp: number;
      finishReason: string;
      usage: { inputTokens?: number; outputTokens?: number; totalTokens?: number; costUsdTicks?: number };
    }
  | { type: "error"; sessionID?: string; message: string; timestamp: number };

Notes:

  • sessionID appears on every event type when a session id is available (agent.getSessionId()); it is simply absent from the JSON object otherwise (not null).
  • text events are per-step, buffered assistant content — one text event per step carrying the accumulated text for that step, flushed either right before a tool-triggering step_finish or inline with a tool-less step_finish.
  • No thinking/reasoning NDJSON event exists. The underlying StreamChunk union used internally does carry a "reasoning" chunk type, but createHeadlessJsonlEmitter().consumeChunk() explicitly no-ops on it (case "reasoning": break; in src/headless/output.ts) — reasoning content is never surfaced through --format json. This is a confirmed absence, not upstream-pending-verification: the Grok streaming adapter therefore drives onText only; there is no onThinking signal to bridge for this CLI path today.
  • There is no explicit terminal done/result event type. A prompt run can contain multiple step_start/step_finish pairs (multi-round tool use); the authoritative "the run is over" signal is the headless process's stdout stream ending (readline close) / subprocess exit, mirroring how the Droid CLI adapter treats subprocess close as terminal. error events ({ type: "error", message, timestamp }) can also appear inline without necessarily ending the process.
  • A fatal, pre-JSON failure (e.g. missing API key) is not a JSON line at all: src/index.ts's requireApiKey() writes a plain console.error(...) line to stderr and calls process.exit(1) before any NDJSON is emitted. Consumers must therefore also treat a non-zero exit with no JSON output as a distinct failure mode from a well-formed error event.

Auth / readiness

  • The grok CLI owns authentication end-to-end for CLI-routed execution. runHeadless() in src/index.ts is only reached via requireApiKey(config.apiKey), which resolves the key from (in order via resolveConfig/getApiKey()): -k/--api-key flag, GROK_API_KEY env var, project .env, or ~/.grok/user-settings.json's apiKey field. If none resolve, the CLI itself exits 1 with an actionable error — Fusion does not need to pass, see, or validate a key for this path to work, as long as the operator's grok install already has one configured by any of those methods.
  • Auth implication for this task: because CLI-routed model selections let the grok binary own both auth and inference, the direct-endpoint GROK_API_KEY Fusion-visibility requirement established by FN-7711 (built-in xai/openai-completions provider) and FN-7714 (hydrating GROK_API_KEY from ~/.grok/user-settings.json when the env var is unset) becomes unnecessary for CLI-routed selections specifically. It remains necessary and unchanged for the direct xAI OpenAI-compatible path, which stays the default (see "What stays unchanged" below).
  • This mirrors FN-7716's separate finding that Grok CLI readiness (probe/ auth-status surfacing) does not require Fusion to see a key either — that surface (probe.ts, register-auth-routes.ts, GrokCliProviderCard.tsx) is out of scope for this task and is not modified here.

Wiring (resolved — FN-7725)

Decision: option (a) — formalize, document, and test the existing agent Runtime-mode picker path. Do NOT add a new settings toggle (option (b)).

Trigger: an agent's runtimeConfig.runtimeHint === "grok", set today via the dashboard's agent Runtime Source → Runtime picker (NewAgentDialog.tsx / AgentDetailView.tsx), which is populated from GET /api/plugins/runtimes (already generic — surfaces every registered plugin runtime, including the bundled Grok Runtime plugin's runtimeId: "grok", with no Grok-specific code required).

Exact seam: packages/engine/src/agent-session-helpers.ts's extractRuntimeHint(runtimeConfig) reads that hint from the assigned agent's runtimeConfig and threads it, as runtimeHint, into packages/engine/src/runtime-resolution.ts's resolveRuntime() — which is totally runtime-agnostic: when the hint matches a registered plugin runtimeId, resolvePluginRuntime() calls that plugin's runtime.factory (the Grok plugin's factory returns new GrokRuntimeAdapter(), plugins/fusion-plugin-grok-runtime/src/index.ts) and the resolved adapter becomes the session's runtime. This same generic chain already carries "hermes" and "droid" runtime hints end-to-end (see hermes-runtime-integration.test.ts, droid-runtime-e2e.test.ts) — Grok's plugin registration alone was sufficient for the chain to reach it; no engine or dashboard code changed for this task, because the generic Runtime-mode picker → extractRuntimeHint → resolveRuntime → resolvePluginRuntime → plugin factory chain was already correct and exercised for other plugin runtimes. FN-7725 formalizes this as the decided Grok wiring, adds packages/engine/src/__tests__/grok-runtime-routing.test.ts proving the chain specifically resolves GrokRuntimeAdapter (id "grok") and drives its onText streaming seam via a faked spawn (see runtime-adapter.ts's injectable spawn option; no live grok binary), and records the decision here plus in plugins/fusion-plugin-grok-runtime/README.md.

Why option (a), not (b): option (b) (an opt-in "prefer CLI runtime" setting deriving runtimeHint: "grok" from a grok-cli/* model selection) would add a new Settings field, defaulting/resolution logic, and a SettingsModal UI toggle (desktop + mobile) — net-new surface area for a path that, on inspection, was already fully wired generically by the existing Runtime-mode picker. Per the Decision guidance's preference for "the smaller, additive change," formalizing + testing + documenting the already-working path is lower risk and closes the actual gap (an exercised path, not just an implemented adapter) without adding new user-facing config surface.

Known limitation (by design, unchanged by this task): Runtime-mode is
model-agnostic — NewAgentDialog.tsx/AgentDetailView.tsx clear the model
field when Runtime mode is selected (`model: runtimeMode === "runtime" ? ""
...), so GrokRuntimeAdapter.createSession()never receives adefaultModelIdfrom this path and always falls back to"grok/default". A specific grok-cli/*` model choice is therefore not preserved when routing via Runtime-mode. Preserving model selection through the CLI runtime would require option (b) (or an equivalent); it is filed as a follow-up task only if genuinely warranted (see Follow-ups below), not implemented here.

Why the direct xAI endpoint stays default: nothing in this task changes what a grok-cli/* model selection does — it continues to route through the direct xAI OpenAI-compatible endpoint (FN-7711/FN-7714, packages/core/src/grok-provider.ts, packages/engine/src/pi.ts), which this task does not touch. The CLI-routed path is reached only by the separate, explicit agent Runtime-mode choice — an opt-in, additive, fully-reversible path (nothing sets the hint unless an operator explicitly picks Runtime mode for that agent).

Decision

Route Grok execution through the CLI: YES, as a scoped, additive GrokRuntimeAdapter implementation.

Rationale:

  • The non-interactive contract is fully pinned to primary source code (src/index.ts CLI parsing + src/headless/output.ts emitter + src/headless/output.test.ts fixtures), not just README prose — this clears the External Integration Evidence bar and the "testable from fixture lines without a live binary" bar from the task mission — the parser can be fixture-tested exactly like the Droid plugin's stream-parser.ts, with no live-binary dependency in tests.
  • The event schema is simple (step_start / text / tool_use / step_finish / error) and text-only for this scoped adapter (no thinking event exists to bridge), so the implementation stays narrow: a resilient NDJSON line parser plus an onText bridge, deliberately leaving tool-call/break-early bridging as a documented follow-up (the Droid adapter's much larger provider.ts is the effort ceiling, not the target shape).
  • It is fully reversible: the adapter is only reachable via runtimeHint === "grok", which nothing sets today, so landing it carries no behavioral change to any exercised path.

What stays unchanged

  • The direct xAI OpenAI-compatible streaming path (base URL https://api.x.ai/v1, api type openai-completions, GROK_API_KEY sourced per FN-7711/FN-7714) remains the default, exercised Grok execution path. This task does not touch packages/core/src/grok-provider.ts or packages/engine/src/pi.ts.
  • FN-7716's probe/auth-readiness surface (probe.ts, register-auth-routes.ts, GrokCliProviderCard.tsx) is untouched by this task.
  • End-to-end routing was out of scope for FN-7722 and is resolved by FN-7725 (see "Wiring" above): decision option (a), formalizing the existing agent Runtime-mode picker path. No settings toggle was added.

Follow-ups filed from this task

See the task's fn_task_create calls (linked from FN-7722) for:

  1. End-to-end routing wiring — closed by FN-7725 (see "Wiring" above): the agent Runtime-mode picker path was formalized, documented, and covered by packages/engine/src/__tests__/grok-runtime-routing.test.ts.
  2. Full tool-call bridging for tool_use NDJSON events — closed by FN-7724: GrokRuntimeAdapter now bridges tool_use into onToolStart/onToolEnd (no Grok→pi tool-name mapping was added — the verified schema does not pin grok-cli's tool-name vocabulary, so names/args pass through unchanged). Break-early on step_finish was deliberately NOT adopted: this doc's own "Verified NDJSON event schema" notes above establish step_finish is a per-step boundary (a run can contain multiple step_start/step_finish pairs), not the run terminal — the adapter's terminal signal remains subprocess close/error, unchanged from FN-7722. See plugins/fusion-plugin-grok-runtime/README.md's "Tool execution bridging (FN-7724)" section.
  3. (Filed by FN-7725, if warranted) Preserving a specific grok-cli/* model selection when routing through the CLI runtime (Runtime-mode is currently model-agnostic — see "Known limitation" in "Wiring" above). This is the deferred option (b) shape; only file it if a genuine operator need surfaces, per the task's Decision guidance.