Skip to content

feat(agents): add Toolset support to ToolContext and AgentActivity#1525

Merged
toubatbrian merged 13 commits into
1.5.0from
brian/toolset
Jun 1, 2026
Merged

feat(agents): add Toolset support to ToolContext and AgentActivity#1525
toubatbrian merged 13 commits into
1.5.0from
brian/toolset

Conversation

@toubatbrian
Copy link
Copy Markdown
Contributor

@toubatbrian toubatbrian commented May 17, 2026

Summary

Adds Python-parity Toolset support: a stateful container that bundles a group of tools behind shared setup() / aclose() lifecycle hooks. Toolset instances can be passed directly into Agent({ tools: [...] }) alongside individual function tools; their tools are flattened into the agent's ToolContext and the runtime drives setup on activity start, close on activity teardown, and a diff on updateTools().

Closes the placeholder TODOs left in tool_context.ts from the previous list-syntax refactor.

Changes

Core (agents/src/llm/tool_context.ts)

  • New Toolset class with id, tools, setup(), aclose() (default no-ops, override in subclass).
  • New ToolCalledEvent / ToolCompletedEvent payload types (exported for plugin authors).
  • ToolContextEntry widened to FunctionTool | ProviderDefinedTool | Toolset.
  • updateTools() recurses into Toolsets and flattens their tools into _functionToolsMap / _providerTools while tracking the Toolset itself in _toolsets.
  • equals() compares _toolsets as an identity set (matches Python's {id(ts) for ts in self._tool_sets} semantics — order-insensitive).
  • Duplicate function-tool names continue to throw duplicate function name: <name>, including when the duplicate is contributed by a Toolset (Python parity).

Activity (agents/src/voice/agent_activity.ts)

  • New setupToolsets() / closeToolsets() driven by _startSession and _closeSessionResources.
  • setup() failures propagate to the caller after rolling back any toolsets that already initialized — the agent fails explicitly rather than running with half-set-up resources.
  • updateTools() now diffs added/removed Toolsets and runs lifecycle in setup → swap toolCtx → close order so a setup failure leaves the activity pointing at the previous (still-valid) ToolContext.
  • The IGNORE_ON_ENTER filter recurses through Toolsets so nested function tools also honor the flag.

Plugin migrations to ToolContext.flatten() + isFunctionTool filter
Toolset-contributed tools are included in _functionToolsMap, but several plugins were also rebuilding tool lists from Object.entries(toolCtx.functionTools), which would silently drop the Toolset references and confuse subsequent updateTools() diffs. Migrated:

  • plugins/google/src/utils.ts
  • plugins/mistralai/src/llm.ts
  • plugins/openai/src/realtime/realtime_model.ts — also fixes a real bug in _handleSessionUpdated() where the post-update retainedTools rebuild dropped Toolsets, causing them to be diffed-as-removed and prematurely aclose()d.
  • plugins/openai/src/responses/llm.ts
  • plugins/openai/src/ws/llm.ts
  • plugins/phonic/src/realtime/realtime_model.ts

plugins/openai/src/llm.ts (chat) and plugins/baseten/src/llm.ts left unchanged: they iterate Object.keys(toolCtx.functionTools) which already picks up flattened Toolset tools, mirroring Python's chat plugin.

Tests

  • agents/src/llm/tool_context.test.ts: new Toolset describe block (id/tools accessors, default no-op lifecycle, subclass override, flatten-into-ToolContext, duplicate-throw parity, identity-set equals() parity).
  • plugins/test/src/llm.ts: new toolset describe block runs against every LLM plugin's shared test suite — verifies the model can call a function tool nested inside a Toolset and that direct tools continue to work alongside.
  • agent_activity.test.ts / agent_activity_handoff.test.ts: existing tests pass (verified the toolset wiring doesn't disturb the fake activity scaffolding).

Example

  • New examples/src/basic_toolsets.ts showing a location_tools Toolset bundling getWeather + lookupTimezone and passed alongside an Agent.

Test plan

  • pnpm test --run agents/src/llm/tool_context.test.ts — 46/46 pass (40 prior + 6 new)
  • pnpm test --run agents/src/voice/agent_activity.test.ts agents/src/voice/agent_activity_handoff.test.ts — 24/24 pass
  • pnpm test --run plugins/openai/src/llm.test.ts plugins/google/src/llm.test.ts plugins/mistralai/src/llm.test.ts — 29/30 pass (1 pre-existing skip), including 2 new toolset cases per plugin
  • pnpm build — all 32 packages green
  • pnpm lint — clean (only pre-existing any warnings in unrelated files)
  • Manual: verify toolset aclose() runs on session close and that setup() rejection rolls back already-set-up toolsets

claude and others added 6 commits May 15, 2026 20:58
… Toolset

`Agent({ tools })` now accepts a list of `FunctionTool | ProviderDefinedTool
| Toolset` entries instead of a `Record<string, FunctionTool>` map, matching
the shape of the Python `ToolContext`. `llm.tool({ ... })` now requires an
explicit `name`. `ToolContext` is exported as a class with the same surface
as the Python one (`functionTools`, `providerTools`, `toolsets`, `flatten()`,
`getFunctionTool()`, `updateTools()`, `copy()`, `equals()`), backed by a
flat `_tools` list plus denormalized name map / provider list / toolset list.
A `Toolset` base class is included as the unit of stateful tool composition
with `setup()` / `aclose()` lifecycle hooks.

This is the foundational refactor; the standalone Toolset PR will rebase on
top of this once it lands.
- Removed `Toolset` from this PR — it lives in the stacked Toolset PR
  on top instead. `ToolContext` only handles `FunctionTool` and
  `ProviderDefinedTool` here.
- `updateTools()` now uses later-wins semantics for duplicate function
  names instead of throwing.
- Fixed every remaining call site that iterated `ToolContext` like a
  record (`Object.keys/entries(toolCtx)`): inference LLM, OpenAI chat /
  WS / responses, MistralAI, Baseten, and AgentActivity's initial-tools
  snapshot all now go through `.functionTools`.
…rray UX

- `updateTools()`: dropped redundant name-empty check (the `tool()` factory
  already enforces a non-empty name at construction).
- `ToolContext.hasTool(name)`: new helper that matches function tools by
  `name` and provider tools by `id`, so chat-context filtering remains
  correct when provider tool call items are added later.
- `ChatContext.copy({ toolCtx })`: switched the filter predicate from
  `getFunctionTool(name)` to `hasTool(name)` for that future-proofing.
- `AgentActivity` initial-tools snapshot: include both function tool names
  and provider tool ids in the `AgentConfigUpdate.toolsAdded` payload.
- Inference LLM tool builder: iterates `toolCtx.flatten()` and discriminates
  on tool type instead of only `functionTools`; provider tools are skipped
  until AJS-112 wires them up properly.
- `ToolCtxInput` + `toToolContext()` helpers: LLM `chat({ toolCtx })` and
  `LLMStream` now accept either a `ToolContext` or a raw
  `(FunctionTool | ProviderDefinedTool)[]` array. Callers can write
  `toolCtx: [tool1, tool2]` directly; `run_result.ts` updated to use it.
…n amd typing

- Removed `toolListFromRecord` — `tool()` requires `name` so the legacy
  `Record<string, FunctionTool>` shape never gets produced in practice.
- `Agent.toolCtx` now returns `this._toolCtx.copy()` so external callers
  can't mutate the agent's internal state.
- `amd.ts` imports `ToolContextEntry` properly at the top instead of
  using the inline `import('../llm/index.js').ToolContextEntry` syntax.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 17, 2026

🦋 Changeset detected

Latest commit: 4c3794f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 31 packages
Name Type
@livekit/agents Major
@livekit/agents-plugin-google Major
@livekit/agents-plugin-openai Major
@livekit/agents-plugin-anam Major
@livekit/agents-plugin-assemblyai Major
@livekit/agents-plugin-baseten Major
@livekit/agents-plugin-bey Major
@livekit/agents-plugin-cartesia Major
@livekit/agents-plugin-cerebras Major
@livekit/agents-plugin-deepgram Major
@livekit/agents-plugin-elevenlabs Major
@livekit/agents-plugin-fishaudio Major
@livekit/agents-plugin-hedra Major
@livekit/agents-plugin-hume Major
@livekit/agents-plugin-inworld Major
@livekit/agents-plugin-lemonslice Major
@livekit/agents-plugin-liveavatar Major
@livekit/agents-plugin-livekit Major
@livekit/agents-plugin-minimax Major
@livekit/agents-plugin-mistral Major
@livekit/agents-plugin-mistralai Major
@livekit/agents-plugin-neuphonic Major
@livekit/agents-plugin-phonic Major
@livekit/agents-plugin-resemble Major
@livekit/agents-plugin-rime Major
@livekit/agents-plugin-runway Major
@livekit/agents-plugin-sarvam Major
@livekit/agents-plugin-silero Major
@livekit/agents-plugins-test Major
@livekit/agents-plugin-trugen Major
@livekit/agents-plugin-xai Major

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

@toubatbrian toubatbrian requested a review from a team May 17, 2026 22:55
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 5 additional findings.

Open in Devin Review

Base automatically changed from brian/refactor-tool-context-list to 1.5.0 May 28, 2026 21:44
Co-authored-by: rosetta-livekit-bot[bot] <282703043+rosetta-livekit-bot[bot]@users.noreply.github.com>
Co-authored-by: u9g <jason.lernerman@livekit.io>
@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ toubatbrian
❌ claude
You have signed the CLA already but the status is still pending? Let us recheck it.

toubatbrian and others added 2 commits June 1, 2026 10:12
Co-authored-by: rosetta-livekit-bot[bot] <282703043+rosetta-livekit-bot[bot]@users.noreply.github.com>
Co-authored-by: u9g <jason.lernerman@livekit.io>
@toubatbrian toubatbrian merged commit 6b85bfe into 1.5.0 Jun 1, 2026
0 of 2 checks passed
@toubatbrian toubatbrian deleted the brian/toolset branch June 1, 2026 17:23
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 15 additional findings in Devin Review.

Open in Devin Review

Comment on lines +204 to +206
if (tools.length > 0 && providerTools.length > 0) {
throw new Error('Gemini does not support mixing function tools and provider tools');
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Google toToolsConfig incorrectly throws when function tools coexist with provider tools or geminiTools

The new toToolsConfig helper throws "Gemini does not support mixing function tools and provider tools" whenever function declarations and provider-side tools exist simultaneously (plugins/google/src/utils.ts:204-205). This is a regression: the old realtime code (buildConnectConfig) merged functionDeclarations and geminiTools (e.g. { codeExecution: {} }) into a single types.Tool entry, which worked. The Gemini API accepts multiple Tool entries in the array — one for function declarations and separate ones for googleSearch, codeExecution, etc. — so rejecting the combination outright is incorrect.

This breaks two paths:

  1. Realtime sessions — any user passing geminiTools (e.g. { codeExecution: {} }) alongside agents that have function tools will now get a runtime error instead of the previously-working merged behavior.
  2. New GeminiTool provider tools — the newly-introduced GoogleSearch, ToolCodeExecution, etc. cannot be used together with function tools, which is a core use case for provider tools.

The onlySingleType flag used by the non-realtime LLM path (plugins/google/src/llm.ts:361) also cannot help since the throw fires before that check.

Prompt for agents
The error at plugins/google/src/utils.ts:204-206 incorrectly prevents valid Gemini API configurations where function declarations coexist with other tool types (googleSearch, codeExecution, etc.).

The Gemini API accepts a Tool[] array where function declarations and other tool types are separate entries. The fix should remove the throw and instead allow both types to coexist as separate entries in the returned array.

However, care is needed for the onlySingleType=true path (used by the non-realtime LLM at plugins/google/src/llm.ts:361). When onlySingleType is true and both function tools and provider tools exist, the current early-return at line 208 would silently drop provider tools. The fix should either:
1. Remove onlySingleType entirely and always merge both types into the array (function declarations in one entry, provider tools in separate entries), OR
2. When onlySingleType is true and both types exist, merge them into a single types.Tool object (like the old realtime code did with spread), OR
3. Keep onlySingleType but ensure provider tools are not silently dropped — perhaps by merging them into the same Tool entry that holds functionDeclarations.

The old realtime code used spread to merge: { functionDeclarations: [...], ...geminiTools } which put everything in one Tool. The new architecture with separate GeminiTool subclasses (each producing their own toToolConfig()) naturally maps to separate Tool[] entries. Both approaches are valid per the Gemini API.

Affected call sites: plugins/google/src/llm.ts:358-362 (LLM path) and plugins/google/src/beta/realtime/realtime_api.ts:1416-1420 (realtime path).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants