Files
fusion/plugins/fusion-plugin-grok-runtime/src/stream-parser.ts
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

65 lines
2.3 KiB
TypeScript

import type { GrokNdjsonEvent } from "./types.js";
/*
FNXC:GrokCli 2026-07-09-00:00:
FN-7722: `grok --prompt <text> --format json` emits newline-delimited JSON
(one JSON object per line) per the verified upstream contract captured in
docs/grok-cli-contract.md (source: src/headless/output.ts's
`createHeadlessJsonlEmitter` / `HeadlessJsonEvent`). This parser mirrors the
Droid plugin's `stream-parser.ts` shape and resilience contract: it never
throws. Debug noise, empty lines, and malformed/unrecognized JSON all return
null so the streaming pipeline can safely skip them and continue.
*/
/*
FNXC:GrokCli 2026-07-09-00:10:
FN-7724: confirmed at execution time — FN-7722 already typed `tool_use` /
`step_finish` / `error` into the `GrokNdjsonEvent` union (types.ts) and this
parser already accepts them via KNOWN_EVENT_TYPES below, so no parser change
was needed to "surface" them; only runtime-adapter.ts's bridge (previously
intentionally dropping tool_use/step_finish/error, see FN-7722 comment
above) needed extending. See docs/grok-cli-contract.md for the verified
schema this parser accepts unmodified.
*/
const KNOWN_EVENT_TYPES = new Set(["step_start", "text", "tool_use", "step_finish", "error"]);
/**
* Parse a single NDJSON line from `grok --prompt --format json` stdout into a
* typed event, or null when the line should be skipped (empty, non-JSON
* debug noise, malformed JSON, or a JSON object whose `type` isn't one of
* the five verified event types).
*/
export function parseLine(line: string): GrokNdjsonEvent | null {
const trimmed = line.trim();
// Skip empty lines
if (!trimmed) {
return null;
}
// Skip non-JSON lines (e.g. any stray debug/log output not part of the JSONL stream)
if (!trimmed.startsWith("{")) {
return null;
}
let parsed: unknown;
try {
parsed = JSON.parse(trimmed);
} catch {
console.error("Failed to parse Grok CLI NDJSON line:", trimmed);
return null;
}
// Validate that the parsed result is a non-null object (not array, not primitive)
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
return null;
}
const candidate = parsed as { type?: unknown };
if (typeof candidate.type !== "string" || !KNOWN_EVENT_TYPES.has(candidate.type)) {
return null;
}
return parsed as GrokNdjsonEvent;
}