Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/job-context-tool-execute-node-24.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@livekit/agents": patch
---

fix(voice): propagate job context into tool execute on Node 24

Re-establish `jobContextStorage` inside the per-tool `Task.from()` body in
`performToolExecutions` so `getJobContext()` works from a tool's `execute()`
function on Node 24. Node 24's default `AsyncContextFrame` `AsyncLocalStorage`
implementation does not propagate the job context across the `Task.from()`
boundary the way the legacy `async_hooks` implementation did, which previously
caused `getJobContext()` to throw "no job context found" inside tools (#1255).
20 changes: 15 additions & 5 deletions agents/src/voice/generation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ThrowsPromise } from '@livekit/throws-transformer/throws';
import type { Span } from '@opentelemetry/api';
import { context as otelContext } from '@opentelemetry/api';
import type { ReadableStream, ReadableStreamDefaultReader } from 'stream/web';
import { getJobContext, runWithJobContextAsync } from '../job.js';
import type { Instructions } from '../llm/chat_context.js';
import {
type ChatContext,
Expand Down Expand Up @@ -974,6 +975,13 @@ export function performToolExecutions({
const signal = controller.signal;
const reader = toolCallStream.getReader();

// Capture the job context synchronously so it can be re-established inside
// each toolTask body. Node 24's AsyncContextFrame does not implicitly
// propagate AsyncLocalStorage values across the Task.from() boundary the
// way legacy async_hooks did, so getJobContext() inside a tool's execute()
// would otherwise throw "no job context found" (see #1255).
const currentJobContext = getJobContext(false);

const tasks: Task<void>[] = [];
while (!signal.aborted) {
const { done, value: toolCall } = await reader.read();
Expand Down Expand Up @@ -1135,16 +1143,18 @@ export function performToolExecutions({
});
}

const toolExecution = functionCallStorage.run(
{ functionCall: toolCall, speechHandle },
async () => {
const runToolWithFunctionCallStorage = () =>
functionCallStorage.run({ functionCall: toolCall, speechHandle }, async () => {
return await tool.execute(parsedArgs, {
ctx: new RunContext(session, speechHandle, toolCall),
toolCallId: toolCall.callId,
abortSignal: signal,
});
},
);
});

const toolExecution = currentJobContext
? runWithJobContextAsync(currentJobContext, runToolWithFunctionCallStorage)
: runToolWithFunctionCallStorage();

await tracableToolExecution(toolExecution);
},
Expand Down