Skip to content

feat(voice): manage GPT-Live client delegation through async tools - #7258

Open
chasef07 wants to merge 1 commit into
livekit:mainfrom
chasef07:codex/client-delegation-owner
Open

feat(voice): manage GPT-Live client delegation through async tools#7258
chasef07 wants to merge 1 commit into
livekit:mainfrom
chasef07:codex/client-delegation-owner

Conversation

@chasef07

Copy link
Copy Markdown

Summary

GPT-Live client delegation currently bypasses the framework's tool lifecycle: the example creates independent backend tasks, rebuilds their context, and manually sends results. A corrected request can therefore be followed by an obsolete answer.

Add the reusable ClientDelegation SDK toolset, backed by AsyncToolset and the existing _ToolExecutor. GPT-Live dispatches managed requests as internal lk_agents_delegate calls, preserving task registration, cancellation, draining, and lifecycle events. The example now consumes this SDK capability instead of owning a controller or a mandatory routing LLM.

  • Retain backend conversation and tool outcomes. An application-selected task key advances a task's revision; distinct keys preserve independent work. Applications still decide request meaning and own business operation IDs and reconciliation.
  • Check revision, toolset ownership, and connection identity before queuing and sending each progress/result chunk. Preserve already-running tool outcomes even when a parent is cancelled or its answer becomes obsolete.
  • Support quiet progress, speakable chunks, final results, and failures through the shared lifecycle. Keep the existing low-level manual client-delegation path available.

This follows the async-tool direction of draft #6602, reviewed at 197cbaa, and uses its builtin delegation tool name. The client-specific interface is proposed for review alongside that work; this does not copy its broader AgentSession(delegation_llm=...) API. Adjacent fixes in #7230, #7234, #7229, #7239, #7231, and #7238 are outside this diff.

Validation

  • 228 deterministic tests passed across test_gpt_live_client_delegation.py, test_tools.py, test_duplex_adapter.py, and test_session_host.py.
  • The 19 delegation tests include the original stale-answer reproduction and exercise the real AgentSession, shared executor, and GPT-Live send/receive loops with an in-memory WebSocket: corrections, independent work, queued and streamed output races, cancellation-resistant results, tool outcomes, reconnect, handoff, shutdown, failures, and session isolation.
  • Focused Ruff lint/format and strict mypy checks passed for changed SDK/plugin modules, the example, and tests. The example CLI help/import smoke check passed; git diff --check passed.
  • Full make check stops at 15 pre-existing formatting failures in README code blocks. Each was reproduced from unchanged origin/main; the full-repository lint/type stages were not reached. These unrelated files are excluded from the PR.

Limits

  • No live provider, audio/SIP, or latency benchmark was run. This does not establish a latency improvement or implement continuous prefill.
  • Managed output uses a conservative 500 UTF-8 byte limit per coherent chunk to stay within the service's token limit without a tokenizer dependency. Oversized chunks fail visibly; they are not silently truncated.
  • Revision changes suppress delivery and prevent new obsolete tool execution. They do not undo external actions or provide business-level exactly-once execution. A handoff that opens a new voice connection does not replay results associated with the old connection.

@chasef07
chasef07 marked this pull request as ready for review September 13, 2026 06:19
@chasef07
chasef07 requested a review from a team as a code owner September 13, 2026 06:19
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 13, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
πŸ“ Code Review βœ… Completed 2026-09-13T06:24:19.736054Z 4f1f73b Draft marked ready
ℹ️ 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" or "@codex security review".

Codex reacts with πŸ‘€ while any review is running, comments if it has suggestions, and reacts with πŸ‘ once all reviews finish with no findings.

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

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.

Devin Review found 5 potential issues.

Devin Review

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.

🟨 Backend exceptions leak into telemetry

When delegated work raises an exception containing customer data, ToolCallEnded.message publishes str(output) unmarked. Event consumers can persist sensitive exception text without redaction.

(Refers to this code)

Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Comment on lines +228 to +231
self._select_task = select_task or (lambda request: utils.shortuuid("task_"))
self._states: dict[str, _TaskState] = {}
self._requests: dict[str, DelegationContext] = {}
self._seen: set[tuple[str, str]] = set()

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.

πŸ”΄ Delegation history grows quadratically

With the default selector, _states retains a full conversation snapshot for every completed delegation. Neither _states nor _seen removes completed entries. Long-running sessions can exhaust worker memory.

Learn more

The default selector creates a unique task key for every transport request. Each new _TaskState then imports the request's entire chat context, while _states retains every state for the session lifetime. The retained data therefore grows with the sum of all conversation snapshots rather than the conversation itself. _seen also retains every connection/request key, including keys from disconnected connections.

Example: After 1,000 independent delegations, the toolset keeps 1,000 task states. Later states each contain nearly the full 1,000-request conversation, producing roughly quadratic retained history.

Recommended fix: Remove completed one-request states when no execution can still reference them, and expire _seen entries when their connection ends. Preserve states only for task keys intentionally reused by select_task; this may require explicit task lifetime tracking or a bounded retention policy.

Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Comment on lines +117 to +120
call = llm.FunctionCall(
call_id=call_id,
name=name,
arguments=json.dumps(arguments),

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.

🟑 Concurrent tasks collide on call IDs

When independent tasks reuse a backend call_id, execute_tool forwards both IDs into the shared executor. _ToolExecutor rejects the second call while the first remains registered. The second delegation fails instead of running its tool.

Learn more

A backend model's call ID correlates a call only within that model interaction or retained task history. ClientDelegation can run several independent task histories concurrently, but all child calls share one _ToolExecutor. That executor indexes running work globally by FunctionCall.call_id, so equal IDs from different tasks conflict even though their histories are independent.

Example: Task order and task weather both produce call_id="call_1" before either tool finishes. The order tool registers first. The weather tool then receives Task already running for call_id: call_1 and its delegation returns a failure.

Recommended fix: Give executor registrations a globally unique internal call ID, namespaced by task or delegation revision. Keep the backend's original call ID when constructing the model-facing retained FunctionCall and FunctionCallOutput, with an explicit mapping between model and executor IDs.

Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Comment on lines +284 to +289
extra={
"delegation_id": request.id,
"task_id": task_id,
"revision": state.revision,
"connection_id": request.connection_id,
},

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.

🟨 Delegation metadata bypasses PII redaction

FunctionCall.extra stores request, task, and connection identifiers under unmarked keys. Lifecycle telemetry can export customer-derived identifiers without redaction.

Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Comment on lines +150 to +157
if result.fnc_call_out is not None:
self._state.history.insert(result.fnc_call_out)
child.session.emit(
"function_tools_executed",
FunctionToolsExecutedEvent(
function_calls=[call], function_call_outputs=[result.fnc_call_out]
),
)

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.

🟨 Delegated tool outputs bypass redaction

FunctionToolsExecutedEvent publishes delegated tool results in ordinary output fields. Customer and business data can reach telemetry consumers without a PII marker.

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.

1 participant