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"
|
||||
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/break-early bridging for `tool_use` NDJSON events, if a
|
||||
future need for Grok-CLI-driven tool execution arises (out of scope for
|
||||
the scoped text/no-thinking adapter landed here; tracked separately as
|
||||
FN-7724).
|
||||
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
|
||||
|
||||
@@ -59,6 +59,18 @@ grok --prompt "<text>" --format json
|
||||
- The adapter parses that stream (`src/stream-parser.ts`) and drives
|
||||
`onText` as `text` events arrive. There is no `thinking`/`reasoning` event
|
||||
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
|
||||
credentials for this path (env var, project `.env`, `grok -k`, or
|
||||
`~/.grok/user-settings.json`), a CLI-routed selection needs **no
|
||||
|
||||
@@ -85,6 +85,126 @@ describe("GrokRuntimeAdapter", () => {
|
||||
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 () => {
|
||||
const { proc, stdout } = makeFakeProc();
|
||||
const spawn = vi.fn().mockReturnValue(proc);
|
||||
|
||||
@@ -76,4 +76,37 @@ describe("parseLine (Grok CLI NDJSON)", () => {
|
||||
it("skips a JSON array (not an object)", () => {
|
||||
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
|
||||
docs/grok-cli-contract.md. This adapter spawns that command via the
|
||||
`cli-stream` seam, parses NDJSON via `stream-parser.parseLine`, and drives
|
||||
`onText` as `text` events arrive. Scoped deliberately narrow, mirroring the
|
||||
Droid plugin's parser+text-bridge pattern but NOT its full tool-call/
|
||||
break-early machinery: the verified schema has no thinking/reasoning event
|
||||
(onThinking is therefore never invoked here — kept only for AgentRuntime
|
||||
interface parity), and tool_use bridging is filed as a follow-up (see
|
||||
docs/grok-cli-contract.md "Follow-ups"). This adapter is only reached when
|
||||
an agent's `runtimeConfig.runtimeHint === "grok"`, which nothing in the
|
||||
product sets today (recorded as the wiring gap in the contract doc) — this
|
||||
task lands the adapter without wiring an end-to-end exercised path.
|
||||
`onText` as `text` events arrive.
|
||||
|
||||
FNXC:GrokCli 2026-07-09-00:10:
|
||||
FN-7724: extends the above with `tool_use` (and terminal `step_finish`/
|
||||
`error`) bridging, per docs/grok-cli-contract.md's verified NDJSON schema.
|
||||
`onToolStart`/`onToolEnd` fire from each `tool_use` event's
|
||||
`toolCall`/`toolResult`, mirroring the Droid plugin's `DroidCallbacks`
|
||||
shape. No Grok→pi tool-name/arg mapping is applied: the verified contract
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
/** Binary name/path to invoke. Defaults to "grok" (PATH resolution). */
|
||||
binary?: string;
|
||||
@@ -59,7 +89,16 @@ export class GrokRuntimeAdapter implements AgentRuntime {
|
||||
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 session: GrokSession = {
|
||||
model,
|
||||
@@ -70,6 +109,8 @@ export class GrokRuntimeAdapter implements AgentRuntime {
|
||||
callbacks: {
|
||||
onText: options.onText,
|
||||
onThinking: options.onThinking,
|
||||
onToolStart: options.onToolStart,
|
||||
onToolEnd: options.onToolEnd,
|
||||
},
|
||||
};
|
||||
return { session, sessionFile: undefined };
|
||||
@@ -133,10 +174,26 @@ export class GrokRuntimeAdapter implements AgentRuntime {
|
||||
|
||||
if (event.type === "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
|
||||
// bridged by this scoped adapter (text-only). tool_use bridging is
|
||||
// a follow-up (docs/grok-cli-contract.md).
|
||||
// step_start / step_finish / error: step_finish is a per-step
|
||||
// boundary (not run-terminal, per docs/grok-cli-contract.md — a run
|
||||
// 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", () => {
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
/*
|
||||
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"]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -82,6 +82,25 @@ export interface GrokCallbacks {
|
||||
* event to bridge (see docs/grok-cli-contract.md).
|
||||
*/
|
||||
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 {
|
||||
@@ -101,6 +120,10 @@ export interface AgentRuntimeOptions {
|
||||
defaultModelId?: string;
|
||||
onText?: (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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user