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>
This commit is contained in:
7
.changeset/fn-7724-grok-cli-tool-bridging.md
Normal file
7
.changeset/fn-7724-grok-cli-tool-bridging.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Grok CLI runtime now bridges tool execution events (name/args/result) from the NDJSON stream, not just text.
|
||||||
|
category: feature
|
||||||
|
dev: GrokRuntimeAdapter.promptWithFallback now bridges tool_use NDJSON events into onToolStart/onToolEnd; tool name/args/result pass through unchanged (no Grok→pi name mapping — the verified docs/grok-cli-contract.md schema does not pin a tool-name vocabulary). step_finish/error remain non-terminal per-step events and are not bridged to a callback; only subprocess close/error finalizes, unchanged from FN-7722. Fixture-tested (no live binary). End-to-end runtimeHint="grok" routing remains FN-7725's scope; the direct xAI path (FN-7711/7714) is unchanged.
|
||||||
@@ -253,10 +253,18 @@ 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"
|
1. ~~End-to-end routing wiring~~ — **closed by FN-7725** (see "Wiring"
|
||||||
above): the agent Runtime-mode picker path was formalized, documented,
|
above): the agent Runtime-mode picker path was formalized, documented,
|
||||||
and covered by `packages/engine/src/__tests__/grok-runtime-routing.test.ts`.
|
and covered by `packages/engine/src/__tests__/grok-runtime-routing.test.ts`.
|
||||||
2. Full tool-call/break-early bridging for `tool_use` NDJSON events, if a
|
2. ~~Full tool-call bridging for `tool_use` NDJSON events~~ — **closed by
|
||||||
future need for Grok-CLI-driven tool execution arises (out of scope for
|
FN-7724**: `GrokRuntimeAdapter` now bridges `tool_use` into
|
||||||
the scoped text/no-thinking adapter landed here; tracked separately as
|
`onToolStart`/`onToolEnd` (no Grok→pi tool-name mapping was added — the
|
||||||
FN-7724).
|
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
|
3. (Filed by FN-7725, if warranted) Preserving a specific `grok-cli/*` model
|
||||||
selection when routing through the CLI runtime (Runtime-mode is currently
|
selection when routing through the CLI runtime (Runtime-mode is currently
|
||||||
model-agnostic — see "Known limitation" in "Wiring" above). This is the
|
model-agnostic — see "Known limitation" in "Wiring" above). This is the
|
||||||
|
|||||||
@@ -59,6 +59,18 @@ grok --prompt "<text>" --format json
|
|||||||
- The adapter parses that stream (`src/stream-parser.ts`) and drives
|
- The adapter parses that stream (`src/stream-parser.ts`) and drives
|
||||||
`onText` as `text` events arrive. There is no `thinking`/`reasoning` event
|
`onText` as `text` events arrive. There is no `thinking`/`reasoning` event
|
||||||
in the verified schema, so `onThinking` is never invoked for this path.
|
in the verified schema, so `onThinking` is never invoked for this path.
|
||||||
|
- **Tool execution bridging (FN-7724):** each verified `tool_use` event
|
||||||
|
(`toolCall`/`toolResult`/`timing`) additionally drives `onToolStart(toolName,
|
||||||
|
args)` / `onToolEnd(toolName, isError, result)`, mirroring the Droid
|
||||||
|
plugin's `DroidCallbacks` shape. `toolName`/`args` are
|
||||||
|
`toolCall.function.name` / parsed `toolCall.function.arguments`;
|
||||||
|
`isError` derives from `toolResult.success === false`. No Grok→pi
|
||||||
|
tool-name/arg translation is applied — the verified contract does not pin
|
||||||
|
grok-cli's specific tool-name vocabulary (unlike Droid's Claude-shaped
|
||||||
|
names), so names/args pass through unchanged. `step_finish` is a per-step
|
||||||
|
boundary (a run can contain multiple), not the run terminal, so it does
|
||||||
|
not finalize the adapter's promise; only subprocess `close`/`error` does,
|
||||||
|
unchanged from FN-7722.
|
||||||
- **Auth implication:** because the `grok` binary resolves its own
|
- **Auth implication:** because the `grok` binary resolves its own
|
||||||
credentials for this path (env var, project `.env`, `grok -k`, or
|
credentials for this path (env var, project `.env`, `grok -k`, or
|
||||||
`~/.grok/user-settings.json`), a CLI-routed selection needs **no
|
`~/.grok/user-settings.json`), a CLI-routed selection needs **no
|
||||||
|
|||||||
@@ -85,6 +85,126 @@ describe("GrokRuntimeAdapter", () => {
|
|||||||
await expect(promise).resolves.toBeUndefined();
|
await expect(promise).resolves.toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FNXC:GrokCli 2026-07-09-00:10: FN-7724 — tool_use bridging coverage.
|
||||||
|
it("bridges tool_use events into onToolStart/onToolEnd in order with translated args", async () => {
|
||||||
|
const { proc, stdout } = makeFakeProc();
|
||||||
|
const spawn = vi.fn().mockReturnValue(proc);
|
||||||
|
const adapter = new GrokRuntimeAdapter({ spawn });
|
||||||
|
|
||||||
|
const onToolStart = vi.fn();
|
||||||
|
const onToolEnd = vi.fn();
|
||||||
|
const { session } = await adapter.createSession({ onToolStart, onToolEnd });
|
||||||
|
|
||||||
|
const promise = adapter.promptWithFallback(session, "list files");
|
||||||
|
|
||||||
|
stdout.write(`${JSON.stringify({ type: "step_start", stepNumber: 1, timestamp: 1 })}\n`);
|
||||||
|
stdout.write(
|
||||||
|
`${JSON.stringify({
|
||||||
|
type: "tool_use",
|
||||||
|
stepNumber: 1,
|
||||||
|
timestamp: 2,
|
||||||
|
toolCall: { id: "tc-1", type: "function", function: { name: "bash", arguments: '{"command":"ls"}' } },
|
||||||
|
toolResult: { success: true, output: "a.ts\nb.ts" },
|
||||||
|
timing: { startedAt: 1, finishedAt: 2, durationMs: 1 },
|
||||||
|
})}\n`,
|
||||||
|
);
|
||||||
|
stdout.write(
|
||||||
|
`${JSON.stringify({
|
||||||
|
type: "step_finish",
|
||||||
|
stepNumber: 1,
|
||||||
|
timestamp: 3,
|
||||||
|
finishReason: "tool_calls",
|
||||||
|
usage: {},
|
||||||
|
})}\n`,
|
||||||
|
);
|
||||||
|
proc.emit("close", 0, null);
|
||||||
|
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
expect(onToolStart).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onToolStart).toHaveBeenCalledWith("bash", { command: "ls" });
|
||||||
|
expect(onToolEnd).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onToolEnd).toHaveBeenCalledWith("bash", false, { success: true, output: "a.ts\nb.ts" });
|
||||||
|
// onToolStart must fire before onToolEnd for the same tool call.
|
||||||
|
expect(onToolStart.mock.invocationCallOrder[0]).toBeLessThan(onToolEnd.mock.invocationCallOrder[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("marks onToolEnd as an error when toolResult.success is false", async () => {
|
||||||
|
const { proc, stdout } = makeFakeProc();
|
||||||
|
const spawn = vi.fn().mockReturnValue(proc);
|
||||||
|
const adapter = new GrokRuntimeAdapter({ spawn });
|
||||||
|
const onToolStart = vi.fn();
|
||||||
|
const onToolEnd = vi.fn();
|
||||||
|
const { session } = await adapter.createSession({ onToolStart, onToolEnd });
|
||||||
|
|
||||||
|
const promise = adapter.promptWithFallback(session, "read missing file");
|
||||||
|
stdout.write(
|
||||||
|
`${JSON.stringify({
|
||||||
|
type: "tool_use",
|
||||||
|
stepNumber: 1,
|
||||||
|
timestamp: 2,
|
||||||
|
toolCall: { id: "tc-2", type: "function", function: { name: "read_file", arguments: '{"path":"x"}' } },
|
||||||
|
toolResult: { success: false, output: "ENOENT" },
|
||||||
|
})}\n`,
|
||||||
|
);
|
||||||
|
proc.emit("close", 0, null);
|
||||||
|
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
expect(onToolEnd).toHaveBeenCalledWith("read_file", true, { success: false, output: "ENOENT" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles malformed tool_use arguments without throwing, passing the raw string through", async () => {
|
||||||
|
const { proc, stdout } = makeFakeProc();
|
||||||
|
const spawn = vi.fn().mockReturnValue(proc);
|
||||||
|
const adapter = new GrokRuntimeAdapter({ spawn });
|
||||||
|
const onToolStart = vi.fn();
|
||||||
|
const { session } = await adapter.createSession({ onToolStart });
|
||||||
|
|
||||||
|
const promise = adapter.promptWithFallback(session, "hi");
|
||||||
|
stdout.write(
|
||||||
|
`${JSON.stringify({
|
||||||
|
type: "tool_use",
|
||||||
|
stepNumber: 1,
|
||||||
|
timestamp: 2,
|
||||||
|
toolCall: { id: "tc-3", type: "function", function: { name: "bash", arguments: "not-json" } },
|
||||||
|
toolResult: { success: true },
|
||||||
|
})}\n`,
|
||||||
|
);
|
||||||
|
proc.emit("close", 0, null);
|
||||||
|
|
||||||
|
await expect(promise).resolves.toBeUndefined();
|
||||||
|
expect(onToolStart).toHaveBeenCalledWith("bash", "not-json");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not finalize on step_finish alone (per-step, not run-terminal); only close/error finalizes", async () => {
|
||||||
|
const { proc, stdout } = makeFakeProc();
|
||||||
|
const spawn = vi.fn().mockReturnValue(proc);
|
||||||
|
const adapter = new GrokRuntimeAdapter({ spawn });
|
||||||
|
const onText = vi.fn();
|
||||||
|
const { session } = await adapter.createSession({ onText });
|
||||||
|
|
||||||
|
const promise = adapter.promptWithFallback(session, "multi-round");
|
||||||
|
let resolved = false;
|
||||||
|
void promise.then(() => {
|
||||||
|
resolved = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
stdout.write(
|
||||||
|
`${JSON.stringify({ type: "step_finish", stepNumber: 1, timestamp: 1, finishReason: "tool_calls", usage: {} })}\n`,
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(resolved).toBe(false);
|
||||||
|
|
||||||
|
stdout.write(`${JSON.stringify({ type: "text", stepNumber: 2, text: "done", timestamp: 2 })}\n`);
|
||||||
|
proc.emit("close", 0, null);
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
expect(resolved).toBe(true);
|
||||||
|
expect(onText).toHaveBeenCalledWith("done");
|
||||||
|
});
|
||||||
|
|
||||||
it("never invokes onThinking: the verified grok-cli NDJSON schema has no thinking/reasoning event", async () => {
|
it("never invokes onThinking: the verified grok-cli NDJSON schema has no thinking/reasoning event", async () => {
|
||||||
const { proc, stdout } = makeFakeProc();
|
const { proc, stdout } = makeFakeProc();
|
||||||
const spawn = vi.fn().mockReturnValue(proc);
|
const spawn = vi.fn().mockReturnValue(proc);
|
||||||
|
|||||||
@@ -76,4 +76,37 @@ describe("parseLine (Grok CLI NDJSON)", () => {
|
|||||||
it("skips a JSON array (not an object)", () => {
|
it("skips a JSON array (not an object)", () => {
|
||||||
expect(parseLine(JSON.stringify([{ type: "text" }]))).toBeNull();
|
expect(parseLine(JSON.stringify([{ type: "text" }]))).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FNXC:GrokCli 2026-07-09-00:10: FN-7724 — additional tool_use/step_finish/
|
||||||
|
// error coverage for the runtime-adapter bridge (Step 3). The parser itself
|
||||||
|
// needed no change (see stream-parser.ts's FN-7724 comment); these prove
|
||||||
|
// the full toolCall/toolResult/timing shape round-trips and malformed tool
|
||||||
|
// lines are still skipped without throwing.
|
||||||
|
it("parses a tool_use event with full toolCall/toolResult/timing fields", () => {
|
||||||
|
const line = JSON.stringify({
|
||||||
|
type: "tool_use",
|
||||||
|
sessionID: "sess-2",
|
||||||
|
stepNumber: 2,
|
||||||
|
timestamp: 300,
|
||||||
|
toolCall: { id: "tc-2", type: "function", function: { name: "read_file", arguments: '{"path":"a.ts"}' } },
|
||||||
|
toolResult: { success: false, output: "ENOENT" },
|
||||||
|
timing: { startedAt: 280, finishedAt: 300, durationMs: 20 },
|
||||||
|
});
|
||||||
|
const parsed = parseLine(line);
|
||||||
|
expect(parsed).toEqual({
|
||||||
|
type: "tool_use",
|
||||||
|
sessionID: "sess-2",
|
||||||
|
stepNumber: 2,
|
||||||
|
timestamp: 300,
|
||||||
|
toolCall: { id: "tc-2", type: "function", function: { name: "read_file", arguments: '{"path":"a.ts"}' } },
|
||||||
|
toolResult: { success: false, output: "ENOENT" },
|
||||||
|
timing: { startedAt: 280, finishedAt: 300, durationMs: 20 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips a malformed tool_use line (broken JSON) without throwing", () => {
|
||||||
|
const line = '{"type":"tool_use","toolCall":{"function":{"name":';
|
||||||
|
expect(() => parseLine(line)).not.toThrow();
|
||||||
|
expect(parseLine(line)).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,15 +12,26 @@ prose: src/index.ts's CLI parsing + src/headless/output.ts's
|
|||||||
`createHeadlessJsonlEmitter` + its fixture tests). Contract captured in
|
`createHeadlessJsonlEmitter` + its fixture tests). Contract captured in
|
||||||
docs/grok-cli-contract.md. This adapter spawns that command via the
|
docs/grok-cli-contract.md. This adapter spawns that command via the
|
||||||
`cli-stream` seam, parses NDJSON via `stream-parser.parseLine`, and drives
|
`cli-stream` seam, parses NDJSON via `stream-parser.parseLine`, and drives
|
||||||
`onText` as `text` events arrive. Scoped deliberately narrow, mirroring the
|
`onText` as `text` events arrive.
|
||||||
Droid plugin's parser+text-bridge pattern but NOT its full tool-call/
|
|
||||||
break-early machinery: the verified schema has no thinking/reasoning event
|
FNXC:GrokCli 2026-07-09-00:10:
|
||||||
(onThinking is therefore never invoked here — kept only for AgentRuntime
|
FN-7724: extends the above with `tool_use` (and terminal `step_finish`/
|
||||||
interface parity), and tool_use bridging is filed as a follow-up (see
|
`error`) bridging, per docs/grok-cli-contract.md's verified NDJSON schema.
|
||||||
docs/grok-cli-contract.md "Follow-ups"). This adapter is only reached when
|
`onToolStart`/`onToolEnd` fire from each `tool_use` event's
|
||||||
an agent's `runtimeConfig.runtimeHint === "grok"`, which nothing in the
|
`toolCall`/`toolResult`, mirroring the Droid plugin's `DroidCallbacks`
|
||||||
product sets today (recorded as the wiring gap in the contract doc) — this
|
shape. No Grok→pi tool-name/arg mapping is applied: the verified contract
|
||||||
task lands the adapter without wiring an end-to-end exercised path.
|
does not pin grok-cli's specific tool-name vocabulary (unlike Droid's
|
||||||
|
Claude-shaped names), so `toolCall.function.name`/parsed `.arguments` pass
|
||||||
|
through unchanged (decision recorded in the FN-7724 `research` task
|
||||||
|
document). `onThinking` is still never invoked — the verified schema has no
|
||||||
|
thinking/reasoning event (confirmed absence, not a gap). The terminal
|
||||||
|
lifecycle is UNCHANGED from FN-7722: the doc states `step_finish` is a
|
||||||
|
per-step boundary (multiple can occur per run for multi-round tool use), so
|
||||||
|
it does NOT finalize the promise here; only subprocess `close`/`error`
|
||||||
|
does, same `streamEnded`-guarded (via the existing `settled` flag)
|
||||||
|
resolve-never-reject lifecycle as before. This adapter is only reached when
|
||||||
|
an agent's `runtimeConfig.runtimeHint === "grok"` (wired end-to-end by
|
||||||
|
FN-7725).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,6 +52,25 @@ const FIRST_LINE_TIMEOUT_MS = 60_000;
|
|||||||
*/
|
*/
|
||||||
const INACTIVITY_TIMEOUT_MS = 30 * 60_000;
|
const INACTIVITY_TIMEOUT_MS = 30 * 60_000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FNXC:GrokCli 2026-07-09-00:10:
|
||||||
|
* FN-7724: `toolCall.function.arguments` is a JSON-encoded string per the
|
||||||
|
* verified `ToolCall` shape (docs/grok-cli-contract.md / types.ts's
|
||||||
|
* `GrokToolCallLike`). Parse it defensively — malformed/missing arguments
|
||||||
|
* must never throw inside the NDJSON read loop; fall back to the raw string
|
||||||
|
* (or undefined) so callers still see something rather than losing the
|
||||||
|
* event, mirroring the Droid event-bridge's empty-args guard.
|
||||||
|
*/
|
||||||
|
function parseToolArguments(raw: string | undefined): unknown {
|
||||||
|
if (raw === undefined) return undefined;
|
||||||
|
if (raw === "") return {};
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface GrokRuntimeAdapterOptions {
|
export interface GrokRuntimeAdapterOptions {
|
||||||
/** Binary name/path to invoke. Defaults to "grok" (PATH resolution). */
|
/** Binary name/path to invoke. Defaults to "grok" (PATH resolution). */
|
||||||
binary?: string;
|
binary?: string;
|
||||||
@@ -59,7 +89,16 @@ export class GrokRuntimeAdapter implements AgentRuntime {
|
|||||||
this.spawnFn = options?.spawn ?? spawnGrokStream;
|
this.spawnFn = options?.spawn ?? spawnGrokStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createSession(options: { defaultModelId?: string; systemPrompt?: string; onText?: (text: string) => void; onThinking?: (text: string) => void } = {}): Promise<AgentSessionResult> {
|
async createSession(
|
||||||
|
options: {
|
||||||
|
defaultModelId?: string;
|
||||||
|
systemPrompt?: string;
|
||||||
|
onText?: (text: string) => void;
|
||||||
|
onThinking?: (text: string) => void;
|
||||||
|
onToolStart?: (toolName: string, args?: unknown) => void;
|
||||||
|
onToolEnd?: (toolName: string, isError: boolean, result?: unknown) => void;
|
||||||
|
} = {},
|
||||||
|
): Promise<AgentSessionResult> {
|
||||||
const model = options.defaultModelId ?? "grok/default";
|
const model = options.defaultModelId ?? "grok/default";
|
||||||
const session: GrokSession = {
|
const session: GrokSession = {
|
||||||
model,
|
model,
|
||||||
@@ -70,6 +109,8 @@ export class GrokRuntimeAdapter implements AgentRuntime {
|
|||||||
callbacks: {
|
callbacks: {
|
||||||
onText: options.onText,
|
onText: options.onText,
|
||||||
onThinking: options.onThinking,
|
onThinking: options.onThinking,
|
||||||
|
onToolStart: options.onToolStart,
|
||||||
|
onToolEnd: options.onToolEnd,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return { session, sessionFile: undefined };
|
return { session, sessionFile: undefined };
|
||||||
@@ -133,10 +174,26 @@ export class GrokRuntimeAdapter implements AgentRuntime {
|
|||||||
|
|
||||||
if (event.type === "text") {
|
if (event.type === "text") {
|
||||||
grokSession.callbacks.onText?.(event.text);
|
grokSession.callbacks.onText?.(event.text);
|
||||||
|
} else if (event.type === "tool_use") {
|
||||||
|
// FNXC:GrokCli 2026-07-09-00:10: FN-7724 — bridge the verified
|
||||||
|
// tool_use event. toolCall.function.name/arguments and
|
||||||
|
// toolResult.success/output are the verified fields
|
||||||
|
// (docs/grok-cli-contract.md); pass-through, no name/arg mapping
|
||||||
|
// (see FN-7724 research task document for the decision).
|
||||||
|
const toolName = event.toolCall?.function?.name ?? event.toolCall?.type ?? "unknown";
|
||||||
|
const args = parseToolArguments(event.toolCall?.function?.arguments);
|
||||||
|
grokSession.callbacks.onToolStart?.(toolName, args);
|
||||||
|
const isError = event.toolResult?.success === false;
|
||||||
|
grokSession.callbacks.onToolEnd?.(toolName, isError, event.toolResult);
|
||||||
}
|
}
|
||||||
// step_start / tool_use / step_finish / error: intentionally not
|
// step_start / step_finish / error: step_finish is a per-step
|
||||||
// bridged by this scoped adapter (text-only). tool_use bridging is
|
// boundary (not run-terminal, per docs/grok-cli-contract.md — a run
|
||||||
// a follow-up (docs/grok-cli-contract.md).
|
// can have multiple step_start/step_finish pairs for multi-round
|
||||||
|
// tool use), so it is intentionally NOT bridged into a callback or
|
||||||
|
// treated as the finalize signal; only subprocess close/error
|
||||||
|
// finalizes (see finish() below). `error` events carry no dedicated
|
||||||
|
// callback in this scoped adapter (mirrors FN-7722: they can appear
|
||||||
|
// inline without ending the process, per the verified contract).
|
||||||
});
|
});
|
||||||
|
|
||||||
proc.on("error", () => {
|
proc.on("error", () => {
|
||||||
|
|||||||
@@ -11,6 +11,16 @@ throws. Debug noise, empty lines, and malformed/unrecognized JSON all return
|
|||||||
null so the streaming pipeline can safely skip them and continue.
|
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"]);
|
const KNOWN_EVENT_TYPES = new Set(["step_start", "text", "tool_use", "step_finish", "error"]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -82,6 +82,25 @@ export interface GrokCallbacks {
|
|||||||
* event to bridge (see docs/grok-cli-contract.md).
|
* event to bridge (see docs/grok-cli-contract.md).
|
||||||
*/
|
*/
|
||||||
onThinking?: (text: string) => void;
|
onThinking?: (text: string) => void;
|
||||||
|
/**
|
||||||
|
* FNXC:GrokCli 2026-07-09-00:10:
|
||||||
|
* FN-7724: bridged from the verified `tool_use` NDJSON event's
|
||||||
|
* `toolCall.function.name` / parsed `toolCall.function.arguments`.
|
||||||
|
* Mirrors the Droid plugin's `DroidCallbacks.onToolStart` signature. No
|
||||||
|
* Grok→pi tool-name mapping is applied — the verified contract
|
||||||
|
* (docs/grok-cli-contract.md) does not pin grok-cli's specific tool-name
|
||||||
|
* vocabulary, so names/args pass through unchanged (see FN-7724 research
|
||||||
|
* task document for the decision).
|
||||||
|
*/
|
||||||
|
onToolStart?: (toolName: string, args?: unknown) => void;
|
||||||
|
/**
|
||||||
|
* FNXC:GrokCli 2026-07-09-00:10:
|
||||||
|
* FN-7724: bridged from the same `tool_use` event's `toolResult` field —
|
||||||
|
* `isError` derives from the verified `toolResult.success === false`,
|
||||||
|
* `result` is the full `toolResult` object (includes `output` plus any
|
||||||
|
* other verified/unverified passthrough fields).
|
||||||
|
*/
|
||||||
|
onToolEnd?: (toolName: string, isError: boolean, result?: unknown) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GrokSession {
|
export interface GrokSession {
|
||||||
@@ -101,6 +120,10 @@ export interface AgentRuntimeOptions {
|
|||||||
defaultModelId?: string;
|
defaultModelId?: string;
|
||||||
onText?: (text: string) => void;
|
onText?: (text: string) => void;
|
||||||
onThinking?: (text: string) => void;
|
onThinking?: (text: string) => void;
|
||||||
|
/** FNXC:GrokCli 2026-07-09-00:10: FN-7724 — additive, mirrors GrokCallbacks.onToolStart. */
|
||||||
|
onToolStart?: (toolName: string, args?: unknown) => void;
|
||||||
|
/** FNXC:GrokCli 2026-07-09-00:10: FN-7724 — additive, mirrors GrokCallbacks.onToolEnd. */
|
||||||
|
onToolEnd?: (toolName: string, isError: boolean, result?: unknown) => void;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user