Refactor ToolContext to parity class taking a list of Tool | Toolset#1517
Refactor ToolContext to parity class taking a list of Tool | Toolset#1517toubatbrian wants to merge 5 commits into
Conversation
… 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 detectedLatest commit: 1c7c753 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 |
|
|
- 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.
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.
…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.
| get providerTools(): ProviderDefinedTool[] { | ||
| return this._providerTools; | ||
| } |
There was a problem hiding this comment.
🟡 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.
| get providerTools(): ProviderDefinedTool[] { | |
| return this._providerTools; | |
| } | |
| get providerTools(): ProviderDefinedTool[] { | |
| return [...this._providerTools]; | |
| } |
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
Refactor
Agent({ tools })to accept a list ofFunctionTool | ProviderDefinedTool | Toolsetentries instead of the legacyRecord<string, FunctionTool>map, matching the shape of the PythonToolContext.llm.tool({ ... })now requires an explicitname. This is a breaking change with no backward compatibility shim.Key changes
llm.tool({ name, ... })—nameis now required and stored on the returnedFunctionTool.Toolsetbase class — stateful container for a group of tools, withid,tools: readonly Tool[], and overridablesetup()/aclose()lifecycle hooks.ToolContextis a class with Python parity: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()_toolslist + denormalized_functionToolsMap+_providerTools+_toolsets. Tools from aToolsetare recursively flattened into the function/provider collections; duplicate function names referencing different instances throw.Agent({ tools })andagent.updateTools(tools)now accept the same list shape.agents/,plugins/, andexamples/updated. Tests refreshed andToolContext-class coverage added intool_context.test.ts.Usage
Test Plan
pnpm build— all 32 packages buildpnpm 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— greenpnpm lint— clean (only pre-existing warnings remain)pnpm format:check— cleanpnpm testrun all require external API keys / network and pre-date this change.Notes
ToolsetPR (Add base Toolset support #1485) will be rebased on top of this once this lands.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.