-
Notifications
You must be signed in to change notification settings - Fork 309
fix(voice): preserve toolsets during LLM tool sync #1752
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 1.5.0
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@livekit/agents': patch | ||
| --- | ||
|
|
||
| Preserve toolset grouping when syncing LLM-mutated tools during voice generation. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -439,13 +439,23 @@ export class ToolContext<UserData = UnknownUserData> { | |
| } | ||
|
|
||
| updateTools(tools: readonly ToolContextEntry<UserData>[]): void { | ||
| this._updateTools(tools); | ||
| } | ||
|
|
||
| private _updateTools( | ||
| tools: readonly ToolContextEntry<UserData>[], | ||
| exclude: readonly Tool[] = [], | ||
| ): void { | ||
| this._tools = [...tools]; | ||
| this._functionToolsMap = new Map(); | ||
| this._providerTools = []; | ||
| this._toolsets = []; | ||
| const excludedTools = new Set(exclude); | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any tool shape | ||
| const addTool = (tool: any): void => { | ||
| if (excludedTools.has(tool)) return; | ||
|
|
||
| if (isToolset(tool)) { | ||
| for (const inner of tool.tools) { | ||
| addTool(inner); | ||
|
|
@@ -479,6 +489,28 @@ export class ToolContext<UserData = UnknownUserData> { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Apply in-place edits of a flattened tool list while preserving Toolset grouping. | ||
| * | ||
| * Added tools become top-level entries; removed Toolset members stay owned by their toolset | ||
| * for lifecycle purposes, but are excluded from this context's callable/provider lookups. | ||
| * | ||
| * @internal | ||
| */ | ||
| _syncFlattened( | ||
| tools: readonly Tool[], | ||
| structuredTools: readonly ToolContextEntry<UserData>[] = this._tools, | ||
| ): void { | ||
| const current = new ToolContext<UserData>(structuredTools).flatten(); | ||
| const currentTools = new Set(current); | ||
| const nextTools = new Set(tools); | ||
| const added = tools.filter((tool) => !currentTools.has(tool)); | ||
| const removed = current.filter((tool) => !nextTools.has(tool)); | ||
|
|
||
| const structured = structuredTools.filter((tool) => !removed.some((r) => tool === r)); | ||
| this._updateTools([...structured, ...(added as ToolContextEntry<UserData>[])], removed); | ||
| } | ||
|
Comment on lines
+500
to
+512
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚩 No test coverage for _syncFlattened The new Was this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
| copy(): ToolContext<UserData> { | ||
| return new ToolContext<UserData>([...this._tools]); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚩 _tools inconsistency with _functionToolsMap after exclusion-based sync
After
_syncFlattenedexcludes Toolset members (the removal scenario),_toolsstill contains Toolsets whose.toolsarrays include the excluded members, but_functionToolsMapdoes not. This meansnew ToolContext(toolCtx.tools)would re-include excluded tools andnew ToolContext(toolCtx.tools).equals(toolCtx)would returnfalse. This inconsistency is explicitly documented in the_syncFlatteneddocstring ("removed Toolset members stay owned by their toolset for lifecycle purposes, but are excluded from this context's callable/provider lookups"). However, it could interact withonToolsetToolsChangedatagents/src/voice/agent_activity.ts:4210— that method createsnew ToolContext(current.tools)and checks.equals(current), so it would detect a difference and callupdateTools(current.tools), potentially re-adding tools that were intentionally excluded. This is only triggered when a dynamic Toolset pushes new tools, which is unlikely to happen during active LLM inference, but worth being aware of.Was this helpful? React with 👍 or 👎 to provide feedback.