Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/gemini-provider-tools.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@livekit/agents-plugin-google': minor
---

Add Gemini provider tools for Google Search, Google Maps, URL context, File Search, code execution, and Vertex RAG retrieval, and serialize them from `ToolContext` for Google LLM and realtime sessions.
13 changes: 12 additions & 1 deletion .changeset/list-syntax-toolcontext.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,15 @@
'@livekit/agents': minor
---

**BREAKING**: `Agent({ tools })` and `agent.updateTools()` now accept a flat list `(FunctionTool | ProviderDefinedTool | Toolset)[]` instead of a `Record<string, FunctionTool>` map, and `llm.tool({ ... })` requires a `name` field. `ToolContext` is now a Python-parity class with `functionTools` / `providerTools` / `toolsets` accessors, plus `flatten()`, `hasTool(name)`, `getFunctionTool(name)`, `updateTools()`, `copy()`, and `equals()`. To match the Python reference, registering two **different** function-tool instances under the same `name` now throws `duplicate function name: <name>` instead of silently overriding the earlier entry; passing the **same instance** twice is a no-op. `agent.toolCtx` returns a defensive copy so callers can no longer mutate the agent's internal state. `LLM.chat({ toolCtx })` accepts either a `ToolContext` instance or a raw `(FunctionTool | ProviderDefinedTool | Toolset)[]` array (`ToolCtxInput`) and normalizes it internally, so callers don't have to construct a `ToolContext` themselves.
**BREAKING**: `Agent({ tools })` and `agent.updateTools()` now accept a flat list `(FunctionTool | ProviderTool | Toolset)[]` instead of a `Record<string, FunctionTool>` map, and `llm.tool({ ... })` requires a `name` field. `ToolContext` is now a Python-parity class with `functionTools` / `providerTools` / `toolsets` accessors, plus `flatten()`, `hasTool(id)`, `getFunctionTool(id)`, `updateTools()`, `copy()`, and `equals()`. To match the Python reference, registering two **different** function-tool instances under the same `name` now throws `duplicate function name: <name>` instead of silently overriding the earlier entry; passing the **same instance** twice is a no-op. `agent.toolCtx` returns a defensive copy so callers can no longer mutate the agent's internal state. `LLM.chat({ toolCtx })` accepts either a `ToolContext` instance or a raw `(FunctionTool | ProviderTool | Toolset)[]` array (`ToolCtxInput`) and normalizes it internally, so callers don't have to construct a `ToolContext` themselves.

Tools also expose an `id: string` field on the base `Tool` interface (parity with Python's `Tool.id` property): for `FunctionTool` it mirrors `name`, for `ProviderTool` it is the provider tool id. `ToolContext` keys and equality now use `tool.id` consistently.

**BREAKING**: Provider tools are now modeled to match Python's `ProviderTool`:

- `ProviderDefinedTool` is renamed to `ProviderTool`, and `isProviderDefinedTool` is renamed to `isProviderTool`.
- `ProviderTool` is now an **abstract class** (Python parity). Plugins must subclass it (`class WebSearch extends ProviderTool { ... }`) to attach provider-specific fields and serializers; bare `new ProviderTool(...)` is rejected at compile time.
- The `tool({ id })` factory overload is removed; `tool({ ... })` only creates function tools now. Construct provider tools by instantiating a `ProviderTool` subclass.
- The `ToolType` literal for provider tools is renamed from `'provider-defined'` to `'provider'`.

`Toolset` now carries a `TOOLSET_SYMBOL` marker and is detected via a new `isToolset()` guard (consistent with `isFunctionTool` / `isProviderTool`). Existing `instanceof Toolset` checks still work, but symbol-based detection is preferred for cross-realm safety.
5 changes: 3 additions & 2 deletions agents/src/llm/chat_context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
isInstructions,
renderInstructions,
} from './chat_context.js';
import { ToolContext, tool } from './tool_context.js';
import { ProviderTool, ToolContext, tool } from './tool_context.js';

initializeLogger({ pretty: false, level: 'error' });

Expand Down Expand Up @@ -1498,7 +1498,8 @@ describe('ChatContext.copy with toolCtx filter', () => {
});

it('keeps provider-tool calls when the ToolContext holds a matching provider tool id', () => {
const provider = tool({ id: 'code_runner', config: {} });
class CodeRunner extends ProviderTool {}
const provider = new CodeRunner({ id: 'code_runner' });
const ctx = new ChatContext([
FunctionCall.create({ callId: 'p1', name: 'code_runner', args: '{}' }),
FunctionCall.create({ callId: 'p2', name: 'other', args: '{}' }),
Expand Down
5 changes: 3 additions & 2 deletions agents/src/llm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
export {
handoff,
isFunctionTool,
isProviderDefinedTool,
isProviderTool,
isTool,
isToolset,
ProviderTool,
tool,
ToolContext,
ToolError,
Expand All @@ -14,7 +16,6 @@ export {
toToolContext,
type AgentHandoff,
type FunctionTool,
type ProviderDefinedTool,
type Tool,
type ToolCalledEvent,
type ToolChoice,
Expand Down
2 changes: 1 addition & 1 deletion agents/src/llm/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export abstract class LLM extends (EventEmitter as new () => TypedEmitter<LLMCal
chatCtx: ChatContext;
/**
* Tools to advertise to the LLM. Accepts either a `ToolContext` instance or a raw
* `(FunctionTool | ProviderDefinedTool)[]` array — the array form is normalized into a
* `(FunctionTool | ProviderTool)[]` array — the array form is normalized into a
* `ToolContext` internally so callers don't have to construct one themselves.
*/
toolCtx?: ToolCtxInput;
Expand Down
32 changes: 22 additions & 10 deletions agents/src/llm/tool_context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { describe, expect, it } from 'vitest';
import { z } from 'zod';
import * as z3 from 'zod/v3';
import * as z4 from 'zod/v4';
import { ToolContext, type ToolOptions, Toolset, tool } from './tool_context.js';
import { ProviderTool, ToolContext, type ToolOptions, Toolset, tool } from './tool_context.js';
import { createToolOptions, oaiParams } from './utils.js';

describe('Tool Context', () => {
Expand Down Expand Up @@ -448,8 +448,20 @@ describe('tool() name requirement', () => {
});
expect(t.name).toBe('doStuff');
});

it('exposes id mirroring the function tool name', () => {
const t = tool({
name: 'doStuff',
description: 'd',
execute: async () => 'x',
});
expect(t.id).toBe('doStuff');
expect(t.id).toBe(t.name);
});
});

class TestProviderTool extends ProviderTool {}

describe('ToolContext', () => {
const makeFn = (name: string) =>
tool({
Expand Down Expand Up @@ -497,7 +509,7 @@ describe('ToolContext', () => {

it('separates provider tools from function tools', () => {
const fnA = makeFn('a');
const provider = tool({ id: 'code', config: { language: 'python' } });
const provider = new TestProviderTool({ id: 'code' });
const ctx = new ToolContext([fnA, provider]);

expect(ctx.functionTools).toEqual({ a: fnA });
Expand Down Expand Up @@ -537,7 +549,7 @@ describe('ToolContext', () => {

it('equals() is reflexive', () => {
const a = makeFn('a');
const provider = tool({ id: 'code', config: { language: 'python' } });
const provider = new TestProviderTool({ id: 'code' });
const ctx = new ToolContext([a, provider]);
expect(ctx.equals(ctx)).toBe(true);
});
Expand All @@ -547,22 +559,22 @@ describe('ToolContext', () => {
// that hold the same provider-tool identities in different order are still equal so
// realtime-session / preemptive-generation reuse fast paths are not invalidated.
const a = makeFn('a');
const p1 = tool({ id: 'code', config: { language: 'python' } });
const p2 = tool({ id: 'browser', config: {} });
const p1 = new TestProviderTool({ id: 'code' });
const p2 = new TestProviderTool({ id: 'browser' });
expect(new ToolContext([a, p1, p2]).equals(new ToolContext([a, p2, p1]))).toBe(true);
});

it('equals() supports contexts with only provider tools', () => {
const p1 = tool({ id: 'code', config: {} });
const p2 = tool({ id: 'browser', config: {} });
const p1 = new TestProviderTool({ id: 'code' });
const p2 = new TestProviderTool({ id: 'browser' });
expect(new ToolContext([p1, p2]).equals(new ToolContext([p1, p2]))).toBe(true);
const p3 = tool({ id: 'code', config: {} }); // distinct identity, same id
const p3 = new TestProviderTool({ id: 'code' }); // distinct identity, same id
expect(new ToolContext([p1]).equals(new ToolContext([p3]))).toBe(false);
});

it('hasTool() matches function tools by name and provider tools by id', () => {
const a = makeFn('a');
const provider = tool({ id: 'code_runner', config: {} });
const provider = new TestProviderTool({ id: 'code_runner' });
const ctx = new ToolContext([a, provider]);

expect(ctx.hasTool('a')).toBe(true);
Expand All @@ -574,7 +586,7 @@ describe('ToolContext', () => {
// Matches Python's `flatten()`: list(self._fnc_tools_map.values()) + self._provider_tools.
const a = makeFn('a');
const b = makeFn('b');
const provider = tool({ id: 'code', config: {} });
const provider = new TestProviderTool({ id: 'code' });
const ctx = new ToolContext([b, provider, a]);

expect(ctx.flatten()).toEqual([b, a, provider]);
Expand Down
Loading