Skip to content

Refactor ToolContext to parity class taking a list of Tool | Toolset#1517

Open
toubatbrian wants to merge 5 commits into
1.5.0from
brian/refactor-tool-context-list
Open

Refactor ToolContext to parity class taking a list of Tool | Toolset#1517
toubatbrian wants to merge 5 commits into
1.5.0from
brian/refactor-tool-context-list

Conversation

@toubatbrian
Copy link
Copy Markdown
Contributor

@toubatbrian toubatbrian commented May 15, 2026

Summary

Refactor Agent({ tools }) to accept a list of FunctionTool | ProviderDefinedTool | Toolset entries instead of the legacy Record<string, FunctionTool> map, matching the shape of the Python ToolContext. llm.tool({ ... }) now requires an explicit name. This is a breaking change with no backward compatibility shim.

Key changes

  • llm.tool({ name, ... })name is now required and stored on the returned FunctionTool.
  • Toolset base class — stateful container for a group of tools, with id, tools: readonly Tool[], and overridable setup() / aclose() lifecycle hooks.
  • ToolContext is a class with Python parity:
    • constructor takes readonly (FunctionTool | ProviderDefinedTool | Toolset)[]
    • functionTools (map view, includes tools flattened from toolsets)
    • providerTools, toolsets, tools (original list)
    • flatten(), getFunctionTool(name), updateTools(), copy(), equals()
    • ToolContext.empty()
    • Internal storage mirrors Python: original _tools list + denormalized _functionToolsMap + _providerTools + _toolsets. Tools from a Toolset are recursively flattened into the function/provider collections; duplicate function names referencing different instances throw.
  • Agent({ tools }) and agent.updateTools(tools) now accept the same list shape.
  • All callers across agents/, plugins/, and examples/ updated. Tests refreshed and ToolContext-class coverage added in tool_context.test.ts.

Usage

const getWeather = llm.tool({
  name: 'getWeather',
  description: 'Get the weather for a location.',
  parameters: z.object({ location: z.string() }),
  execute: async ({ location }) => `Sunny in ${location}`,
});

const locationTools = new llm.Toolset({
  id: 'location_tools',
  tools: [getWeather],
});

new voice.Agent({
  instructions: 'You are a helpful assistant.',
  tools: [getWeather, locationTools],
});

Test Plan

  • pnpm build — all 32 packages build
  • pnpm test agents/src/llm/tool_context.test.ts agents/src/llm/tool_context.type.test.ts agents/src/voice/agent.test.ts agents/src/voice/agent_activity.test.ts agents/src/voice/generation_tools.test.ts — green
  • pnpm lint — clean (only pre-existing warnings remain)
  • pnpm format:check — clean
  • Remaining failures in the full pnpm test run all require external API keys / network and pre-date this change.

Notes

  • The standalone Toolset PR (Add base Toolset support  #1485) will be rebased on top of this once this lands.
  • A migration helper toolListFromRecord(map) is exported for any external code still holding the legacy map shape — it returns a list and stamps the map key onto unnamed tools.

… 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.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 15, 2026

🦋 Changeset detected

Latest commit: 1c7c753

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-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-google 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-openai 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

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 15, 2026

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 toubatbrian changed the title Refactor ToolContext to a Python-parity class taking a list of Tool | Toolset Refactor ToolContext to parity class taking a list of Tool | Toolset May 15, 2026
chatgpt-codex-connector[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

Comment thread agents/src/llm/tool_context.ts Outdated
- 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`.
Comment thread agents/src/llm/tool_context.ts Outdated
Comment thread agents/src/inference/llm.ts Outdated
Comment thread agents/src/llm/chat_context.ts Outdated
Comment thread agents/src/voice/testing/run_result.ts Outdated
Comment thread agents/src/voice/agent_activity.ts Outdated
…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.
Comment thread agents/src/llm/tool_context.ts Outdated
Comment thread agents/src/voice/agent.ts Outdated
Comment thread agents/src/voice/amd.ts Outdated
toubatbrian pushed a commit that referenced this pull request May 16, 2026
Every tool-list builder in the plugin layer now iterates `toolCtx.flatten()`
and discriminates on tool type, so function and provider tools contributed
by a Toolset are correctly advertised to the underlying model. The previous
`Object.entries(toolCtx.functionTools)` pattern only picked up function
tools that were registered directly, and would also have missed provider
tools the moment AJS-112 wires them up.

Touched:
- agents/src/inference/llm.ts already migrated in #1517 — included here
  by reference as the reference pattern.
- plugins/openai/src/ws/llm.ts: chat tool builder
- plugins/openai/src/responses/llm.ts: chat tool builder
- plugins/openai/src/realtime/realtime_model.ts: `createToolsUpdateEvent`,
  plus the `retainedTools` post-update filter now preserves Toolsets and
  provider tools instead of dropping them silently.
- plugins/google/src/utils.ts (`toFunctionDeclarations`): used by both
  google/llm.ts and the beta realtime API.
- plugins/google/src/llm.ts: `toolChoice='required'` `allowedFunctionNames`
  list now reads from `functionTools` instead of iterating the class with
  `Object.entries(toolCtx || {})`.
- plugins/mistralai/src/llm.ts: chat tool builder
- plugins/phonic/src/realtime/realtime_model.ts: both `updateTools` and
  `_updateSession` tool-definition builders.

`parallel_tool_calls` guards stay on `functionTools` since the option is
function-tool-specific and toolset-contributed function tools land there
automatically.
@toubatbrian toubatbrian changed the base branch from main to 1.5.0 May 17, 2026 01:31
claude and others added 2 commits May 17, 2026 01:54
…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.
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 13 additional findings in Devin Review.

Open in Devin Review

Comment on lines +253 to +255
get providerTools(): ProviderDefinedTool[] {
return this._providerTools;
}
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.

🟡 providerTools getter exposes internal array without copying, contrary to JSDoc and other getters

The providerTools getter at agents/src/llm/tool_context.ts:254 returns this._providerTools directly (the internal mutable array), while the JSDoc says "A copy of all provider tools". In contrast, the functionTools getter (line 249) creates a new object via Object.fromEntries, and the tools getter (line 270) spreads into a new array. If any caller mutates the returned array (e.g. .push()), it silently corrupts the ToolContext's internal state, which would affect equals() and flatten() until the next updateTools() call.

Suggested change
get providerTools(): ProviderDefinedTool[] {
return this._providerTools;
}
get providerTools(): ProviderDefinedTool[] {
return [...this._providerTools];
}
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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants