Skip to content

getJobContext() fails inside tool execute functions on Node 24 (AsyncContextFrame context loss) #1255

@gidiupgidi

Description

@gidiupgidi

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

  1. Use Node 24 (without --no-async-context-frame)
  2. Define a tool that calls getJobContext() inside its execute function
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions