---
product: "AG Studio"
title: "Client Tool Runner"
description: "Learn how to answer each turn yourself and have AG Studio execute the tool calls you leave unresolved."
framework: javascript
version: "3.0.0"
related:
    - title: "Harness Overview"
      url: "https://www.ag-grid.com/studio/archive/3.0.0/javascript/ai-harness/"
    - title: "Built-in Harness"
      url: "https://www.ag-grid.com/studio/archive/3.0.0/javascript/ai-builtin-harness/"
    - title: "Direct LLM Runner"
      url: "https://www.ag-grid.com/studio/archive/3.0.0/javascript/ai-direct-llm-runner/"
    - title: "Custom Runner"
      url: "https://www.ag-grid.com/studio/archive/3.0.0/javascript/ai-custom-runner/"
    - title: "Custom Harness"
      url: "https://www.ag-grid.com/studio/archive/3.0.0/javascript/ai-custom-harness/"
llms: "https://www.ag-grid.com/studio/archive/3.0.0/llms.txt"
---

# Client Tool Runner

A client tool runner answers one turn at a time, through a function that takes a run's input and yields events.

Studio brackets the rounds as one run, executes any tool call your answer left unresolved, appends the results, and asks you again, until nothing is outstanding.

Use it when the turns come from somewhere that cannot reach into the page: a server of yours, or an SDK client with no way to execute a tool here. If your loop already resolves its own tool calls, use a [Custom Runner](https://www.ag-grid.com/studio/archive/3.0.0/javascript/ai-custom-runner/) instead.

The example on [Tool Components](https://www.ag-grid.com/studio/archive/3.0.0/javascript/ai-tool-components/) is a working client tool runner: a scripted loop that calls a tool of its own, with no LLM involved.

## The Shape

```ts
clientToolRunner({
    id: 'analyst',
    description: 'Builds dashboards from a natural-language request.',
    schema: (s) => s.undefined(),
    tools: () => [studio.viewSchema(), studio.addWidget()],
    run: async function* (input, ctx) {
        // yield events until the run is done
    },
});
```

`run` receives the run input - the thread and run ids, and the message history - plus a context resolved for this run. `ctx.params` are the forwarded delegation parameters, `ctx.instructions()` and `ctx.tools()` re-resolve per request, and `ctx.signal` aborts if the user cancels. It returns an async iterable of events.

`tools` lists the client tools your loop may call back for. Studio executes those on your behalf when your events ask it to, and never executes anything you have not listed.

## The Events

At minimum a run emits some text:

```ts
run: async function* () {
    const messageId = 'msg-1';
    yield { type: 'TEXT_MESSAGE_START', messageId, role: 'assistant' };
    yield { type: 'TEXT_MESSAGE_CONTENT', messageId, delta: 'Working on it' };
    yield { type: 'TEXT_MESSAGE_END', messageId };
},
```

> **Note**
>
> Emit content events only. `clientToolRunner` brackets the whole set of rounds as one run, so it emits `RUN_STARTED` and `RUN_FINISHED` itself and drops any you yield. A [Custom Runner](https://www.ag-grid.com/studio/archive/3.0.0/javascript/ai-custom-runner/) emits its own run boundaries, because nothing else will.

To have Studio run a tool for you, emit a tool call:

```ts
yield { type: 'TOOL_CALL_START', toolCallId: 'call-1', toolCallName: 'add_widget' };
yield { type: 'TOOL_CALL_ARGS', toolCallId: 'call-1', delta: JSON.stringify(args) };
yield { type: 'TOOL_CALL_END', toolCallId: 'call-1' };
```

Studio executes the named tool if you listed it, and feeds a `TOOL_CALL_RESULT` back into the conversation. Text streams into the panel as it arrives, so emit `TEXT_MESSAGE_CONTENT` in small deltas rather than one block at the end.

The full event vocabulary is the [AG-UI protocol](https://docs.ag-ui.com). Studio's `AgAiEvent` declarations mirror the AG-UI wire shapes and depend on no `@ag-ui/*` package, so there is nothing to install: if you already use an AG-UI SDK, its events are structurally compatible and drive `run` directly. See [AG-UI Compatibility](https://www.ag-grid.com/studio/archive/3.0.0/javascript/ai-agents/#ag-ui-compatibility) for the limits of that alignment.

### Rounds

`run` is called once per **round**. A round that asks for a client tool is not the end of the run: Studio executes the tool, appends the result to the messages, and calls `run` again. The run ends on a round that asks for no client tool.

An agent that emits tool calls unconditionally therefore never terminates. It is re-entered until it reaches `maxTurns` on its definition. Branch on what is already in the history:

```ts
run: async function* (input) {
    const toolsHaveRun = input.messages.some((message) => message.role === 'tool');

    if (toolsHaveRun) {
        // The results are in: answer and finish.
        yield* say(summarise(input.messages));
    } else {
        yield* say('Looking that up.');
        yield* callTool('view_schema', {});
    }
},
```

A model-driven loop needs no such branch, because the model stops asking for tools once it has what it needs. A scripted one has to decide for itself.

## Cancellation

Honour `ctx.signal`. When a user cancels a run, Studio aborts it and stops reading your stream. Abort anything you have in flight, such as a model request or a fetch:

```ts
run: async function* (input, { signal }) {
    const response = await fetch(url, { signal });
    // ...
},
```

## Errors

Emit a `RUN_ERROR` event rather than throwing, so the panel can show the failure and the thread stays usable:

```ts
try {
    // ...
} catch (error) {
    yield { type: 'RUN_ERROR', message: error instanceof Error ? error.message : String(error) };
}
```

## Delegation

A client tool runner can be a delegate target like any other. Give it a `description` and, if it takes parameters, a `schema`. The parameters arrive on the run input's forwarded props, and a delegated run finishes by calling `complete_task`, which Studio appends automatically for a child run. See [Rosters and Delegation](https://www.ag-grid.com/studio/archive/3.0.0/javascript/ai-agents/#rosters-and-delegation).

## Next

- [Custom Runner](https://www.ag-grid.com/studio/archive/3.0.0/javascript/ai-custom-runner/) - when your loop runs its own tools
- [Direct LLM Runner](https://www.ag-grid.com/studio/archive/3.0.0/javascript/ai-direct-llm-runner/) - when Studio should drive the model too
- [Agent Overview](https://www.ag-grid.com/studio/archive/3.0.0/javascript/ai-agents/) - what an agent is, and the three runners
