Skip to content

feat(voice): stream tool call status events#1929

Open
rosetta-livekit-bot[bot] wants to merge 1 commit into
mainfrom
port-tool-call-status-events
Open

feat(voice): stream tool call status events#1929
rosetta-livekit-bot[bot] wants to merge 1 commit into
mainfrom
port-tool-call-status-events

Conversation

@rosetta-livekit-bot

@rosetta-livekit-bot rosetta-livekit-bot Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add tool_execution_updated voice events for tool call start/update/end and tool reply lifecycle status
  • emit tool status events from tool execution, RunContext.update(), and generated tool replies
  • wire remote session host/client plumbing for the new event, guarded until the generated protocol package exposes the new protobuf message

Testing

  • pnpm test -- agents/src/voice/generation_tools.test.ts agents/src/voice/remote_session.test.ts
  • pnpm build:agents
  • pnpm --filter @livekit/agents lint (passes with existing warnings)

Notes

  • pnpm api:check --filter=@livekit/agents currently fails before API comparison due to API Extractor not supporting the repo's generated export * as ___ syntax in agents/dist/index.d.ts.

Ported from livekit/agents#6100

Original PR description

Adds a tool_status_updated session event that streams the lifecycle of each function tool call, so frontends and remote sessions can render tool progress (especially for async/long-running tools that use ctx.update()).

The event (ToolStatusUpdatedEvent) carries one of three updates:

  • ToolCallStarted — a tool call was dispatched.
  • ToolCallUpdated — a progress update (running) from ctx.update(), or the call's single terminal entry (done/error/cancelled).
  • ToolReplyUpdated — the deferred reply that voices buffered updates (scheduled then completed/interrupted/skipped).

SessionHost forwards the event to remote sessions over the new AgentSessionEvent.ToolStatusUpdated proto (livekit/protocol#1625), and the new types are exported from livekit.agents. The async_tool_agent example shows publishing the stream to a frontend.

Depends on livekit/protocol#1625 and livekit/python-sdks#716 for the generated bindings.

@changeset-bot

changeset-bot Bot commented Jul 1, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6321578

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

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

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

@rosetta-livekit-bot rosetta-livekit-bot Bot requested a review from longcw July 1, 2026 10:41

@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 2 potential issues.

Open in 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.

🚩 Realtime path does not emit tool_reply_updated events

The pipeline path in _pipelineReplyTaskImpl emits tool_reply_updated events (lines 3096–3122 in agent_activity.ts), but the realtime path in _realtimeGenerationTaskImpl (around line 3617) only emits FunctionToolsExecuted without corresponding tool_reply_updated events. This means consumers listening for ToolExecutionUpdated with tool_reply_updated will not receive events for realtime model tool replies. This may be intentional since the realtime model handles tool replies differently (auto-generation), but it creates an asymmetry between the two paths that consumers should be aware of.

(Refers to lines 3616-3628)

Open in Devin Review

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

Comment on lines +3106 to +3122
toolResponseTask.result.finally(() => {
const status = speechHandle.interrupted
? 'interrupted'
: speechHandle.chatItems.length > initialChatItemCount
? 'completed'
: 'skipped';
this.agentSession.emit(
AgentSessionEventTypes.ToolExecutionUpdated,
createToolExecutionUpdatedEvent({
type: 'tool_reply_updated',
updateIds,
status,
speechId: speechHandle.id,
}),
);
this.onPipelineReplyDone();
});

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.

🔴 Agent cleanup step skipped when an event listener throws, leaving the agent stuck

A critical post-reply cleanup call is placed after an event emission (this.agentSession.emit(...) at agents/src/voice/agent_activity.ts:3112) inside a .finally() block, so if any user-registered listener throws, the cleanup never runs.

Impact: The agent can get stuck in a non-listening state, preventing it from responding to further user input.

Mechanism: emit() can prevent onPipelineReplyDone() from executing

Previously, the .finally() callback was simply () => this.onPipelineReplyDone() (see the unchanged pattern at agents/src/voice/agent_activity.ts:1062 and agents/src/voice/agent_activity.ts:2054). The new code at lines 3106–3122 calls this.agentSession.emit() first, then this.onPipelineReplyDone(). Since AgentSession extends Node.js EventEmitter, emit() propagates listener exceptions synchronously. If any listener for ToolExecutionUpdated throws, onPipelineReplyDone() at line 3121 is never reached.

onPipelineReplyDone() (defined at line 2109) transitions the agent state back to 'listening' and notifies audioRecognition.onEndOfAgentSpeech(). Without it, the agent remains in 'thinking' or 'speaking' and won't process new user turns.

Suggested change
toolResponseTask.result.finally(() => {
const status = speechHandle.interrupted
? 'interrupted'
: speechHandle.chatItems.length > initialChatItemCount
? 'completed'
: 'skipped';
this.agentSession.emit(
AgentSessionEventTypes.ToolExecutionUpdated,
createToolExecutionUpdatedEvent({
type: 'tool_reply_updated',
updateIds,
status,
speechId: speechHandle.id,
}),
);
this.onPipelineReplyDone();
});
toolResponseTask.result.finally(() => {
try {
const status = speechHandle.interrupted
? 'interrupted'
: speechHandle.chatItems.length > initialChatItemCount
? 'completed'
: 'skipped';
this.agentSession.emit(
AgentSessionEventTypes.ToolExecutionUpdated,
createToolExecutionUpdatedEvent({
type: 'tool_reply_updated',
updateIds,
status,
speechId: speechHandle.id,
}),
);
} finally {
this.onPipelineReplyDone();
}
});
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.

0 participants