```class MyTask extends voice.AgentTask<string&...
# ask-ai
a
Copy code
class MyTask extends voice.AgentTask<string> {
    constructor() {
        super({instructions: 'Testing AgentTask...', allowInterruptions: true});
    }

    override async onEnter(): Promise<void> {
        this.session.say('Hello from MyTask!', { allowInterruptions: true });
        this.complete('Task completed successfully.');
    }
}

export class MyAgent extends voice.Agent {
    constructor() {
        super({
            instructions: 'You are a helpful assistant.',
            tools: {
                testTool: llm.tool({
                    description: 'Call when user request to execute to test this tool.',
                    parameters: {},
                    execute: async (_, { ctx }) => {
                        console.log('Executing test tool...');
                        await ctx.waitForPlayout();
                        return new MyTask().run();
                    }
                }),
                hangupTool: llm.tool({
                    description: 'Hang up the call when the user requests to end the call.',
                    parameters: {},
                    execute: async (_, { ctx }) => {
                        console.log('Executing hangup tool, ending call...');
                        await ctx.waitForPlayout();
                        const roomServiceClient = getRoomServiceClient();
                        await roomServiceClient.deleteRoom(getJobContext().room?.name!);
                    }
                })
            }
        });
    }
}
There seems to be a bug in the JS SDK when the first tool is executed and returns, it won't execute any other tools anymore
t
Thanks for reaching out, Ahron. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I’m sorry you’re hitting this. I’m responding to your report that when the first tool in your Agent runs and returns, subsequent tools no longer execute. I don’t have a confirmed bug report for this specific behavior, so I’ll need a bit more information to reproduce and diagnose. Could you please provide the following? • SDK and package versions ◦ The Node/JS SDK package name and exact version(s) you’re using (for example,
@livekit/agents
or similar and the version number). • Reproduction steps ◦ Exact steps you take to trigger the first tool and then the subsequent tool (including what user input or event causes each tool to be invoked). ◦ Whether this happens every time or only sometimes. • Minimal repro ◦ A small self-contained snippet or repository that reproduces the issue (the snippet you posted is helpful; if you can reduce it to the smallest possible example that demonstrates the problem, that’s ideal). • Runtime environment ◦ Node.js version, OS, whether running locally or in a cloud/container environment. • Logs and errors ◦ Any console logs, stack traces, or warnings shown when the first tool returns and subsequent tools fail to run. ◦ If you can enable debug-level logging (or show the output of existing logs) from the Agent runtime, include that. • Agent/tool details ◦ How are tools being selected/invoked? (e.g., are tools invoked by user utterances parsed by the LLM, or invoked programmatically?) ◦ Are you seeing any state left behind after the first tool (e.g., tasks left running, session state not updated, promises unresolved)? • Any differences that change behavior ◦ Do things change if you remove
await ctx.waitForPlayout()
calls or change
allowInterruptions
? Do synchronous vs. asynchronous tool returns matter? What I can try next once you provide that: • Reproduce with your versions and minimal example. • Check whether
waitForPlayout()
or
AgentTask.run()
is leaving the agent in a state that prevents further tool dispatches. • Walk through likely causes: ◦ tool selection/state machine not advancing after a task completes, ◦ unresolved promises or unhandled rejections, ◦ the Agent being marked finished after the first tool return, ◦ or event routing issues between speech/playout and the tool dispatcher. • Suggest targeted workarounds you can try immediately (for example, returning a resolved Promise instead of calling
run()
directly, or ensuring task completion happens after
waitForPlayout()
finishes), and share a patch if we identify a fix. If you share the version, logs, and a minimal repro, I’ll dig in and propose concrete next steps or a code fix. Sources: Agents and handoffs | LiveKit Documentation | Agent speech and audio | LiveKit Documentation | Tasks and task groups | LiveKit Documentation | Agents (Node.js) | livekit/livekit_composite | DeepWiki
a
Can you provide me test units to test this code?
t
Thanks — I can help with test units. I’m responding to your request for unit tests to exercise the Agent/tool flow in your posted code (the bug where the first tool runs and then subsequent tools stop executing). Two quick clarifications before I provide tests: • By “test units” do you want plain unit tests (no real network/RTC) that mock Room/LLM behavior, or end-to-end integration tests that create a real Room and run audio playout? • Which test runner do you prefer (Vitest or Jest)? Example below uses Vitest; I can convert to Jest if you prefer. Below is a self-contained Vitest + TypeScript example that: • Mocks the Agent runtime and Room interactions, • Exercises the two tools (
testTool
and
hangupTool
) in sequence, • Asserts that both tool execute handlers are called and that the Room delete is invoked when hangupTool runs. Setup notes: • This assumes your Agent and tools are exported from a module (adjust imports accordingly). • The tests mock
ctx.waitForPlayout()
and
getRoomServiceClient().deleteRoom(...)
. • Replace or adapt mocks for your actual SDK objects if names differ. 1) Install test deps • vitest and any typings: npm install -D vitest @types/node 2) Test file (tests/agent-tools.spec.ts)
Copy code
import { describe, it, beforeEach, afterEach, vi, expect } from 'vitest';

// --- Replace these imports with your actual module paths ---
import { MyAgent } from '../src/my-agent'; // file that exports MyAgent
import * as agentRuntime from '@livekit/agents'; // if needed for types/mocks

// --- Mocks ---
// Mock context passed into tool execute functions
function makeCtxMock() {
  return {
    waitForPlayout: vi.fn().mockResolvedValue(undefined),
  };
}

// Mock getRoomServiceClient and getJobContext used in hangupTool
const mockDeleteRoom = vi.fn().mockResolvedValue(undefined);
vi.mock('../src/roomService', () => ({ // adjust path to where getRoomServiceClient is implemented
  getRoomServiceClient: () => ({ deleteRoom: mockDeleteRoom }),
  getJobContext: () => ({ room: { name: 'test-room' } }),
}));

describe('MyAgent tools', () => {
  let agent: MyAgent;
  let ctx: ReturnType<typeof makeCtxMock>;

  beforeEach(() => {
    // instantiate your agent
    agent = new MyAgent();
    ctx = makeCtxMock();
    mockDeleteRoom.mockClear();
    ctx.waitForPlayout.mockClear();
  });

  it('executes testTool then hangupTool (both handlers called)', async () => {
    // Find tools from agent config. This depends on how your Agent exposes tools.
    // If agent stores tools under `agent.tools`, use that; otherwise adapt accordingly.
    // For this example we assume `agent.tools` is a record of tool definitions.
    const tools = (agent as any).tools;
    expect(tools).toBeDefined();

    // Call testTool.execute
    const testTool = tools.testTool;
    expect(typeof testTool.execute).toBe('function');

    // Simulate LLM invoking the tool: execute returns whatever MyTask.run() returns.
    // We mock MyTask.run() if necessary to avoid heavy runtime behavior.
    // If your MyTask is internal and side-effect-free, call directly. Otherwise, stub it:
    const runSpy = vi.spyOn(require('../src/my-task'), 'MyTask').mockImplementation(() => {
      return {
        run: vi.fn().mockResolvedValue('Task completed successfully.'),
      };
    });

    const testRes = await testTool.execute({}, { ctx });
    expect(ctx.waitForPlayout).toHaveBeenCalled();
    expect(testRes).toBe('Task completed successfully.');
    runSpy.mockRestore();

    // Now call hangupTool.execute
    const hangupTool = tools.hangupTool;
    expect(typeof hangupTool.execute).toBe('function');

    const hangupRes = await hangupTool.execute({}, { ctx });
    expect(ctx.waitForPlayout).toHaveBeenCalledTimes(2); // once per tool if both call it
    expect(mockDeleteRoom).toHaveBeenCalledWith('test-room');
  });

  it('handles testTool run resolving and allows subsequent tool executions', async () => {
    // This test is similar but explicitly checks that a returned Task does not block further tools
    const tools = (agent as any).tools;

    // Let testTool.execute return a Promise that resolves after a short delay
    const originalTestExecute = tools.testTool.execute;
    const delay = (ms: number) => new Promise(r => setTimeout(r, ms));

    const slowRunSpy = vi.spyOn(require('../src/my-task'), 'MyTask').mockImplementation(() => {
      return {
        run: vi.fn().mockImplementation(async () => {
          await delay(20); // emulate async work
          return 'Task done';
        }),
      };
    });

    // Start testTool but don't await immediately to simulate overlapping operations
    const testPromise = tools.testTool.execute({}, { ctx });

    // While testTool is still pending, invoke hangupTool
    const hangupPromise = tools.hangupTool.execute({}, { ctx });

    const [testValue, hangupValue] = await Promise.all([testPromise, hangupPromise]);

    expect(testValue).toBe('Task done');
    expect(hangupValue).toBeUndefined(); // hangupTool.execute may return void
    expect(mockDeleteRoom).toHaveBeenCalled();
    slowRunSpy.mockRestore();
  });
});
3) Notes on adapting the tests • If your MyAgent stores tools differently, replace
(agent as any).tools
with the correct accessor. • If
MyTask
is defined inline in the same file, mock the constructor or the
run
method accordingly (use vi.spyOn or replace the module with vi.mock). • If
getRoomServiceClient
and
getJobContext
live in a different module, mock that module path instead. • If your environment requires real LiveKit Room objects, you can mock the RoomService client to avoid network calls. 4) Debugging the original bug while testing • The second test intentionally starts
testTool.execute
without awaiting before invoking
hangupTool.execute
to mimic real concurrency. If running these tests shows the hangupTool never runs, capture logs and stack traces to see where execution stops (e.g., an unhandled rejection or an Agent state change). • Try removing or toggling
ctx.waitForPlayout()
calls in the code under test to see if
waitForPlayout()
is causing the agent to stall. If you want: • I can convert these Vitest tests to Jest. • I can produce a minimal runnable repo (package.json, tsconfig, test file) you can clone and run. • I can adapt mocks to your exact module layout if you share the file paths where
MyAgent
,
MyTask
,
getRoomServiceClient
, and
getJobContext
live. Which of those would you like next? Sources: Tasks and task groups | LiveKit Documentation | Testing and evaluation | LiveKit Documentation | Agents (Node.js) | livekit/livekit_composite | DeepWiki