|
| 1 | +# Client Tools |
| 2 | + |
| 3 | +Client tools are tools you declare **in the browser** that the model calls and the browser executes — no server-side implementation. There are three kinds: |
| 4 | + |
| 5 | +| Helper | Kind | What it does | |
| 6 | +|---|---|---| |
| 7 | +| `action()` | function | Runs an async handler in the browser; its resolved return value becomes the tool result sent back to the model. | |
| 8 | +| `view()` | render-only component | The model fills the component's props from the schema; the card renders inline and the call is auto-acknowledged once it mounts. | |
| 9 | +| `ask()` | interactive component | The model fills the component's props; the value the component emits back becomes the tool result (human-in-the-loop). | |
| 10 | + |
| 11 | +Tools are arguments-typed by a [Standard Schema](https://standardschema.dev) (e.g. a Zod object). The catalog is shipped to the model by the adapter; the backend graph binds the client stubs and ends its turn so the browser executes them. |
| 12 | + |
| 13 | +<Callout type="info" title="Adapter-neutral"> |
| 14 | + The same declarations work with `@threadplane/langgraph` and `@threadplane/ag-ui` — only the `provideAgent`/`injectAgent` imports change. |
| 15 | +</Callout> |
| 16 | + |
| 17 | +## Declaring a registry |
| 18 | + |
| 19 | +`tools({...})` collects named tools into a frozen registry. Pass it to `<chat>` via `[clientTools]`: |
| 20 | + |
| 21 | +```typescript |
| 22 | +import { Component } from '@angular/core'; |
| 23 | +import { ChatComponent, tools, action, view, ask } from '@threadplane/chat'; |
| 24 | +import { injectAgent } from '@threadplane/langgraph'; |
| 25 | +import { z } from 'zod/v4'; |
| 26 | +import { WeatherCardComponent } from './weather-card.component'; |
| 27 | +import { ConfirmBookingComponent } from './confirm-booking.component'; |
| 28 | + |
| 29 | +const clientTools = tools({ |
| 30 | + get_weather: action( |
| 31 | + 'Look up the current weather for a location.', |
| 32 | + z.object({ location: z.string() }), |
| 33 | + async ({ location }) => ({ location, temperatureF: 68, conditions: 'Sunny' }), |
| 34 | + ), |
| 35 | + weather_card: view( |
| 36 | + 'Display a weather card for a location.', |
| 37 | + z.object({ location: z.string(), temperatureF: z.number(), conditions: z.string() }), |
| 38 | + WeatherCardComponent, |
| 39 | + ), |
| 40 | + confirm_booking: ask( |
| 41 | + 'Ask the user to confirm a booking before finalizing it.', |
| 42 | + z.object({ summary: z.string() }), |
| 43 | + ConfirmBookingComponent, |
| 44 | + ), |
| 45 | +}); |
| 46 | + |
| 47 | +@Component({ |
| 48 | + selector: 'app-client-tools', |
| 49 | + standalone: true, |
| 50 | + imports: [ChatComponent], |
| 51 | + template: `<chat [agent]="agent" [clientTools]="clientTools" />`, |
| 52 | +}) |
| 53 | +export class ClientToolsComponent { |
| 54 | + protected readonly agent = injectAgent(); |
| 55 | + protected readonly clientTools = clientTools; |
| 56 | +} |
| 57 | +``` |
| 58 | + |
| 59 | +The object keys (`get_weather`, `weather_card`, `confirm_booking`) are the tool names the model sees. `tools()` preserves each tool's precise generic type, so downstream lookups stay typed. |
| 60 | + |
| 61 | +## Typed component props with `ViewProps` |
| 62 | + |
| 63 | +For `view()` and `ask()`, the component's signal inputs are checked against the schema output at compile time — every field the schema produces must be a declared `input()` with an assignable type (the component may declare extra inputs the schema doesn't fill). Derive the input types directly from the schema with `ViewProps<typeof schema>` so the two never drift: |
| 64 | + |
| 65 | +```typescript |
| 66 | +import { Component, input } from '@angular/core'; |
| 67 | +import type { ViewProps } from '@threadplane/chat'; |
| 68 | +import { z } from 'zod/v4'; |
| 69 | + |
| 70 | +export const weatherCardSchema = z.object({ |
| 71 | + location: z.string(), |
| 72 | + temperatureF: z.number(), |
| 73 | + conditions: z.string(), |
| 74 | +}); |
| 75 | + |
| 76 | +// { location: string; temperatureF: number; conditions: string } |
| 77 | +type Inputs = ViewProps<typeof weatherCardSchema>; |
| 78 | + |
| 79 | +@Component({ |
| 80 | + selector: 'app-weather-card', |
| 81 | + standalone: true, |
| 82 | + template: `<div>{{ location() }}: {{ temperatureF() }}°F, {{ conditions() }}</div>`, |
| 83 | +}) |
| 84 | +export class WeatherCardComponent { |
| 85 | + location = input.required<string>(); |
| 86 | + temperatureF = input.required<number>(); |
| 87 | + conditions = input.required<string>(); |
| 88 | +} |
| 89 | +``` |
| 90 | + |
| 91 | +Under `strict: true`, the typed `view`/`ask` overloads report a compile error at the `view(...)`/`ask(...)` call site if the component's inputs diverge from the schema — mismatches become build errors, not silent runtime failures. |
| 92 | + |
| 93 | +## Typed handler args with `ToolArgs` |
| 94 | + |
| 95 | +For `action()`, the handler argument type is inferred from the schema automatically. When you want to name that type — e.g. to write the handler separately — use `ToolArgs<typeof schema>` (an alias of the schema's inferred output): |
| 96 | + |
| 97 | +```typescript |
| 98 | +import { action, type ToolArgs } from '@threadplane/chat'; |
| 99 | +import { z } from 'zod/v4'; |
| 100 | + |
| 101 | +const moveSchema = z.object({ fromDay: z.number(), toDay: z.number() }); |
| 102 | + |
| 103 | +async function moveStop(args: ToolArgs<typeof moveSchema>) { |
| 104 | + // args is { fromDay: number; toDay: number } |
| 105 | + return reorder(args.fromDay, args.toDay); |
| 106 | +} |
| 107 | + |
| 108 | +const move = action('Move a stop to another day.', moveSchema, moveStop); |
| 109 | +``` |
| 110 | + |
| 111 | +## Typed agent state |
| 112 | + |
| 113 | +Tool handlers and components often read agent state. Pair the registry with a typed `AgentRef` so `agent.state()` / `agent.value()` carry your state shape instead of `Record<string, unknown>` — see [Typed state via AgentRef](/docs/langgraph/api/provide-agent#typed-state-via-agentref): |
| 114 | + |
| 115 | +```typescript |
| 116 | +import { createAgentRef } from '@threadplane/chat'; |
| 117 | +import { injectAgent } from '@threadplane/langgraph'; |
| 118 | + |
| 119 | +interface ClientToolsState { messages: unknown[]; client_tools: unknown[]; } |
| 120 | +export const CLIENT_TOOLS = createAgentRef<ClientToolsState>('client-tools'); |
| 121 | + |
| 122 | +// component |
| 123 | +protected readonly agent = injectAgent(CLIENT_TOOLS); // LangGraphAgent<ClientToolsState> |
| 124 | +``` |
| 125 | + |
| 126 | +## API reference |
| 127 | + |
| 128 | +```typescript |
| 129 | +import { |
| 130 | + tools, action, view, ask, |
| 131 | + type ViewProps, type ToolArgs, |
| 132 | + type ClientToolDef, type ClientToolRegistry, |
| 133 | +} from '@threadplane/chat'; |
| 134 | +``` |
| 135 | + |
| 136 | +| Export | Purpose | |
| 137 | +|---|---| |
| 138 | +| `action(description, schema, handler)` | Declare a function tool (handler return → result) | |
| 139 | +| `view(description, schema, component)` | Declare a render-only component tool (auto-acknowledged) | |
| 140 | +| `ask(description, schema, component)` | Declare an interactive component tool (emitted value → result) | |
| 141 | +| `tools(map)` | Freeze a name-keyed registry for `[clientTools]` | |
| 142 | +| `ViewProps<S>` | Component input prop bag inferred from a schema | |
| 143 | +| `ToolArgs<S>` | Handler argument type inferred from a schema | |
| 144 | +| `ClientToolDef` / `ClientToolRegistry` | The tool-definition union and frozen-registry types | |
| 145 | + |
| 146 | +## What's next |
| 147 | + |
| 148 | +<CardGroup cols={3}> |
| 149 | + <Card |
| 150 | + title="Generative UI" |
| 151 | + icon="layout" |
| 152 | + href="/docs/chat/guides/generative-ui" |
| 153 | + > |
| 154 | + Render agent-emitted UI specs with `[views]`, distinct from model-called tools. |
| 155 | + </Card> |
| 156 | + <Card |
| 157 | + title="provideAgent()" |
| 158 | + icon="code" |
| 159 | + href="/docs/langgraph/api/provide-agent" |
| 160 | + > |
| 161 | + Typed agent state via `AgentRef` and client-tuning options. |
| 162 | + </Card> |
| 163 | + <Card |
| 164 | + title="Error Handling" |
| 165 | + icon="alert-triangle" |
| 166 | + href="/docs/chat/guides/error-handling" |
| 167 | + > |
| 168 | + Classified errors and Retry when a tool call or run fails. |
| 169 | + </Card> |
| 170 | +</CardGroup> |
0 commit comments