Describe the bug
On Node 24, getJobContext() throws "no job context found" when called inside a tool's execute function. This breaks every tool that needs job context (room name, room SID, etc). The workaround is --no-async-context-frame, but the root cause is a missing context wrapper in the SDK.
Version
@livekit/agents 1.2.4, Node 24.10.0
Root cause
Node 24 enables AsyncContextFrame as the default AsyncLocalStorage implementation. This changes how context propagates across async boundaries (see nodejs/node#58204). The old async_hooks-based implementation allowed implicit propagation that the new one doesn't.
In src/voice/generation.ts (around line 1065), tool execution creates a Task that wraps three AsyncLocalStorage contexts:
- ✅
agentActivityStorage — wrapped in agent_activity.ts:1100
- ✅
speechHandleStorage — wrapped in agent_activity.ts:1109
- ✅
functionCallStorage — wrapped in generation.ts:1077
- ❌
jobContextStorage — not wrapped
With the old async_hooks implementation, jobContextStorage context propagated implicitly through the Task.from() boundary. With AsyncContextFrame, it doesn't, so getJobContext() returns undefined inside tool execute functions.
Steps to reproduce
- Use Node 24 (without
--no-async-context-frame)
- Define a tool that calls
getJobContext() inside its execute function
- Run a voice agent that triggers the tool
const myTool = {
name: "myTool",
execute: async (params, { ctx }) => {
const jobContext = getJobContext(); // throws "no job context found"
const roomName = jobContext.room.name;
// ...
},
};
Expected behaviour
getJobContext() should return the job context inside tool execute functions, regardless of the AsyncLocalStorage implementation.
Workaround
NODE_OPTIONS=--no-async-context-frame
This reverts Node 24 to the legacy async_hooks-based AsyncLocalStorage, which propagates context correctly.
Suggested fix
Wrap the tool execution task in generation.ts with jobContextStorage.run(), consistent with how the other three stores are already wrapped:
// generation.ts ~line 1065
const toolTask = Task.from(
async () => {
const toolExecution = jobContextStorage.run(currentJobContext, () =>
functionCallStorage.run({ functionCall: toolCall }, async () => {
return await tool.execute(parsedArgs, {
ctx: new RunContext(session, speechHandle, toolCall),
toolCallId: toolCall.callId,
abortSignal: signal,
});
})
);
await tracableToolExecution(toolExecution);
},
controller,
`performToolExecution:${toolCall.name}`,
);
Environment
- macOS Darwin 25.4.0 (arm64)
- Node 24.10.0
@livekit/agents 1.2.4
Describe the bug
On Node 24,
getJobContext()throws "no job context found" when called inside a tool'sexecutefunction. This breaks every tool that needs job context (room name, room SID, etc). The workaround is--no-async-context-frame, but the root cause is a missing context wrapper in the SDK.Version
@livekit/agents1.2.4, Node 24.10.0Root cause
Node 24 enables
AsyncContextFrameas the defaultAsyncLocalStorageimplementation. This changes how context propagates across async boundaries (see nodejs/node#58204). The oldasync_hooks-based implementation allowed implicit propagation that the new one doesn't.In
src/voice/generation.ts(around line 1065), tool execution creates aTaskthat wraps three AsyncLocalStorage contexts:agentActivityStorage— wrapped inagent_activity.ts:1100speechHandleStorage— wrapped inagent_activity.ts:1109functionCallStorage— wrapped ingeneration.ts:1077jobContextStorage— not wrappedWith the old
async_hooksimplementation,jobContextStoragecontext propagated implicitly through theTask.from()boundary. WithAsyncContextFrame, it doesn't, sogetJobContext()returnsundefinedinside tool execute functions.Steps to reproduce
--no-async-context-frame)getJobContext()inside itsexecutefunctionExpected behaviour
getJobContext()should return the job context inside tool execute functions, regardless of theAsyncLocalStorageimplementation.Workaround
This reverts Node 24 to the legacy
async_hooks-basedAsyncLocalStorage, which propagates context correctly.Suggested fix
Wrap the tool execution task in
generation.tswithjobContextStorage.run(), consistent with how the other three stores are already wrapped:Environment
@livekit/agents1.2.4