Align tool context surface with Python (Tool.id, ProviderTool abstract class, Toolset symbol)#1576
Align tool context surface with Python (Tool.id, ProviderTool abstract class, Toolset symbol)#1576toubatbrian wants to merge 2 commits into
Conversation
Brings the public tool surface in line with `livekit-agents` Python:
- Tool gains an `id: string` field on the base interface. `FunctionTool.id`
mirrors `name`; for `ProviderTool` it's the provider tool id. `ToolContext`
keys and equality use `tool.id` consistently
(matches Python's `_fnc_tools_map[tool.info.name]`).
- `ProviderDefinedTool` → `ProviderTool`, converted from an interface to
an `abstract class`. Plugins now subclass it
(`class WebSearch extends ProviderTool { ... }`) to attach
provider-specific fields and serializers, mirroring Python's
`OpenAITool(ProviderTool, ABC)` / `WebSearch(OpenAITool)` chain.
The unused `config: Record<string, unknown>` field is dropped, and the
`tool({ id })` factory overload is removed — bare instantiation of the
abstract base is now a compile error.
- `ToolType` literal `'provider-defined'` → `'provider'`.
- `Toolset` now carries a `TOOLSET_SYMBOL` marker; added `isToolset()`
guard alongside `isFunctionTool` / `isProviderTool`. Replaced the one
remaining `instanceof Toolset` check in `agent_activity` for
cross-realm safety (consistent with how the other tool kinds are
detected).
- Plugin TODO comments in google / mistralai / openai / phonic updated
from "provider-defined tools" → "provider tools".
Tests, types, and changeset updated. All 312 LLM tests pass and the 31
publishable packages build clean.
🦋 Changeset detectedLatest commit: d05b896 The changes in this PR will be included in the next version bump. This PR includes changesets to release 31 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4b883bfb56
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| description: tool.description, | ||
| parameters, | ||
| execute: tool.execute, | ||
| flags: tool.flags ?? ToolFlag.NONE, |
There was a problem hiding this comment.
Validate execute before constructing function tool
The new tool() implementation always returns a function tool once name passes validation, but it no longer checks that execute is present and callable. In JavaScript consumers (or any-typed TS call sites), tool({ name, description }) now succeeds and stores execute: undefined, which only fails later when the tool is invoked instead of failing fast during registration. The previous behavior rejected non-function/non-provider shapes early, so this is a regression in runtime safety.
Useful? React with 👍 / 👎.
Co-authored-by: u9g <jason.lernerman@livekit.io> Co-authored-by: rosetta-livekit-bot[bot] <282703043+rosetta-livekit-bot[bot]@users.noreply.github.com>
| if (tools.length > 0 && providerTools.length > 0) { | ||
| throw new Error('Gemini does not support mixing function tools and provider tools'); | ||
| } | ||
|
|
||
| if (onlySingleType && tools.length > 0) { | ||
| return tools; |
There was a problem hiding this comment.
🔴 toToolsConfig error check fires before onlySingleType guard, breaking LLM path when both function tools and geminiTools are configured
The error check at plugins/google/src/utils.ts:204 (if (tools.length > 0 && providerTools.length > 0) throw ...) runs before the onlySingleType early-return at line 208. This means when the Google LLM calls toToolsConfig({ ..., onlySingleType: true }) (plugins/google/src/llm.ts:358-362), and the user has both function tools and geminiTools (e.g. Google Search), the function throws instead of gracefully returning only the function tools.
Additionally, the same error causes a regression in the realtime session path (plugins/google/src/beta/realtime/realtime_api.ts:1416-1420), which does NOT set onlySingleType. The old realtime code merged functionDeclarations and geminiTools into a single types.Tool object ({ functionDeclarations: [...], ...geminiTools }), which the Gemini Live API supports. The new code separates them and throws an error, breaking users who combine function tools with Google Search or other Gemini-provided tools.
Old realtime code that worked with mixed tools
tools: [
{
functionDeclarations: this.geminiDeclarations,
...this.options.geminiTools, // e.g. { googleSearch: {} }
},
]Prompt for agents
The error check on line 204 that throws when both function tools and provider tools exist is (a) too early -- it should run AFTER the onlySingleType early-return at line 208 so the LLM path can gracefully return just function tools, and (b) too strict for the realtime path -- the old realtime code merged functionDeclarations and geminiTools into a single types.Tool object which the Gemini API supports.
To fix:
1. Move the onlySingleType check (line 208-209) BEFORE the error check (line 204-206).
2. For the realtime path (onlySingleType=false), instead of throwing, merge functionDeclarations and provider tool configs into a single types.Tool object like the old code did. This means combining { functionDeclarations: [...] } with e.g. { googleSearch: {} } into one object: { functionDeclarations: [...], googleSearch: {} }.
Affected files: plugins/google/src/utils.ts (toToolsConfig function, lines 163-215).
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
Closes the remaining parity gaps between this repo's
agents/src/llm/tool_context.tsand Python'slivekit-agents/livekit/agents/llm/tool_context.py. Three changes, all behind the same changeset:1.
Tool.idon the base interfacePython's abstract
Tooldeclaresid: str. JS now does the same. For function toolsid === name; for provider tools it's the provider tool id.ToolContextkeys the function-tool map and runs equality ontool.id, mirroring Python's_fnc_tools_map[tool.info.name].2.
ProviderToolis now an abstract classPython's
ProviderToolis a class plugins extend (OpenAITool(ProviderTool, ABC)→WebSearch(OpenAITool), with each leaf adding fields and aconfigserializer). The JS shape didn't support this —ProviderDefinedToolwas an interface carrying an unusedconfig: Record<string, unknown>, and thetool({ id, config })factory hid the construction path.Now:
ProviderDefinedTool→ProviderTool,isProviderDefinedTool→isProviderTool.abstract class ProviderTool implements Toolwithreadonlysymbol fields so subclasses inherit the marker.configfield and thetool({ id })factory overload.ToolTypeliteral'provider-defined'→'provider'.Plugin authoring follows Python 1:1:
new WebSearch('safe')passesisProviderTooland lands inToolContext._providerTools. The TODO to actually wire each plugin'sconfiggetter through its LLM serializer is tracked asAJS-112and unchanged by this PR.3. Symbol-based detection for all three tool kinds
FunctionToolandProviderToolalready had symbol markers.Toolsetwas the odd one out (instanceof Toolset). AddedTOOLSET_SYMBOL+ anisToolset()guard, and swapped the oneinstanceof Toolsetinagent_activity.ts.BREAKING
See
.changeset/list-syntax-toolcontext.mdfor the rendered release note. TL;DR for callers:ProviderDefinedTool/isProviderDefinedToolwithProviderTool/isProviderTool.tool({ id, config: {} })withnew MyProviderTool(...)(aProviderToolsubclass).tool.type === 'provider-defined'withtool.type === 'provider'.Test plan
pnpm test agents/src/llm— 312/312 pass (2 pre-existing skips)pnpm build:agentsandpnpm build:plugins— all 31 publishable packages build cleanProviderToolis rejected for direct instantiation (@ts-expect-error) and a subclass worksToolContextparity tests (function tools / provider tools / toolsets / equals / flatten / hasTool) updated to usenew ProviderToolsubclasses