Skip to main content
Multi-turn conversations allow you to send multiple user messages and receive responses, creating an interactive dialog with the AI.

Basic Multi-Turn Example

import { query, type SDKUserMessage } from '@qwen-code/sdk';

async function* conversation(): AsyncIterable<SDKUserMessage> {
  yield {
    type: 'user',
    session_id: 'my-session',
    message: { role: 'user', content: 'Create a file called test.txt' },
    parent_tool_use_id: null,
  };

  // Wait a bit before next message
  await new Promise(resolve => setTimeout(resolve, 1000));

  yield {
    type: 'user',
    session_id: 'my-session',
    message: { role: 'user', content: 'Now read the file back to me' },
    parent_tool_use_id: null,
  };

  yield {
    type: 'user',
    session_id: 'my-session',
    message: { role: 'user', content: 'Delete the file' },
    parent_tool_use_id: null,
  };
}

const result = query({
  prompt: conversation(),
  options: {
    permissionMode: 'auto-edit',
  },
});

for await (const message of result) {
  if (message.type === 'assistant') {
    console.log('\nAssistant:', message.message.content);
  }
}

Interactive CLI-Style Conversation

import { query, type SDKUserMessage } from '@qwen-code/sdk';
import * as readline from 'readline';

function getUserInput(prompt: string): Promise<string> {
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });

  return new Promise((resolve) => {
    rl.question(prompt, (answer) => {
      rl.close();
      resolve(answer);
    });
  });
}

async function* interactiveSession(): AsyncIterable<SDKUserMessage> {
  const sessionId = 'interactive-' + Date.now();

  while (true) {
    const input = await getUserInput('\nYou: ');
    
    if (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') {
      break;
    }

    yield {
      type: 'user',
      session_id: sessionId,
      message: { role: 'user', content: input },
      parent_tool_use_id: null,
    };
  }
}

async function main() {
  console.log('Interactive session started. Type "exit" to quit.\n');

  const result = query({
    prompt: interactiveSession(),
    options: {
      permissionMode: 'default',
      canUseTool: async (toolName, input) => {
        // Ask user for confirmation
        const answer = await getUserInput(
          `\nAllow ${toolName}? (y/n): `
        );
        
        if (answer.toLowerCase() === 'y') {
          return { behavior: 'allow', updatedInput: input };
        }
        
        return { behavior: 'deny', message: 'User denied' };
      },
    },
  });

  for await (const message of result) {
    if (message.type === 'assistant') {
      console.log('\nAssistant:');
      for (const block of message.message.content) {
        if (block.type === 'text') {
          console.log(block.text);
        }
      }
    }
  }

  console.log('\nSession ended.');
}

main().catch(console.error);

Conditional Multi-Turn Based on Results

import { query, type SDKUserMessage, isSDKResultMessage } from '@qwen-code/sdk';

class ConversationManager {
  private sessionId: string;
  private messages: SDKUserMessage[] = [];
  
  constructor() {
    this.sessionId = 'session-' + Date.now();
  }

  async *generateMessages(): AsyncIterable<SDKUserMessage> {
    // First message
    yield {
      type: 'user',
      session_id: this.sessionId,
      message: { role: 'user', content: 'List all TypeScript files' },
      parent_tool_use_id: null,
    };

    // Wait for AI to respond before next message
    await new Promise(resolve => setTimeout(resolve, 2000));

    yield {
      type: 'user',
      session_id: this.sessionId,
      message: { role: 'user', content: 'Now check if any have linting errors' },
      parent_tool_use_id: null,
    };
  }

  async run() {
    const result = query({
      prompt: this.generateMessages(),
      options: {
        permissionMode: 'default',
        allowedTools: ['list_files', 'read_file', 'ShellTool(npx eslint)'],
      },
    });

    for await (const message of result) {
      if (message.type === 'assistant') {
        console.log('\nAssistant:', message.message.content);
      } else if (isSDKResultMessage(message)) {
        console.log('\nConversation completed!');
        console.log('Total turns:', message.num_turns);
      }
    }
  }
}

const manager = new ConversationManager();
await manager.run();

Multi-Turn with State Management

import { query, type SDKUserMessage } from '@qwen-code/sdk';

interface ConversationState {
  tasksCompleted: string[];
  currentTask: string | null;
}

async function* taskBasedConversation(
  tasks: string[]
): AsyncIterable<SDKUserMessage> {
  const sessionId = 'task-session';
  const state: ConversationState = {
    tasksCompleted: [],
    currentTask: null,
  };

  for (const task of tasks) {
    state.currentTask = task;
    
    yield {
      type: 'user',
      session_id: sessionId,
      message: {
        role: 'user',
        content: `Task ${state.tasksCompleted.length + 1}: ${task}`,
      },
      parent_tool_use_id: null,
    };

    // Wait between tasks
    await new Promise(resolve => setTimeout(resolve, 1500));
    
    state.tasksCompleted.push(task);
    console.log(`\nCompleted: ${task}`);
  }

  // Summary task
  yield {
    type: 'user',
    session_id: sessionId,
    message: {
      role: 'user',
      content: `Summarize what you did in all ${state.tasksCompleted.length} tasks`,
    },
    parent_tool_use_id: null,
  };
}

const tasks = [
  'Create a README.md file',
  'Add a description to the README',
  'Create a package.json file',
];

const result = query({
  prompt: taskBasedConversation(tasks),
  options: {
    permissionMode: 'auto-edit',
  },
});

for await (const message of result) {
  if (message.type === 'assistant') {
    console.log('\nAssistant:', message.message.content);
  }
}

Multi-Turn with Dynamic Session ID

import { query, type SDKUserMessage } from '@qwen-code/sdk';
import { randomUUID } from 'crypto';

class MultiTurnSession {
  private sessionId: string;
  
  constructor() {
    this.sessionId = randomUUID();
  }

  async *conversation(
    messages: string[]
  ): AsyncIterable<SDKUserMessage> {
    for (const content of messages) {
      yield {
        type: 'user',
        session_id: this.sessionId,
        message: { role: 'user', content },
        parent_tool_use_id: null,
      };
      
      // Delay between messages
      await new Promise(resolve => setTimeout(resolve, 1000));
    }
  }

  async run(messages: string[]) {
    console.log('Session ID:', this.sessionId);
    
    const result = query({
      prompt: this.conversation(messages),
      options: {
        sessionId: this.sessionId,
        permissionMode: 'yolo',
      },
    });

    for await (const message of result) {
      if (message.type === 'assistant') {
        console.log('\nAssistant:', message.message.content);
      }
    }

    return this.sessionId;
  }
}

const session = new MultiTurnSession();
const sessionId = await session.run([
  'What is the current time?',
  'Create a timestamp.txt file with that time',
  'Read the file back to confirm',
]);

console.log('\nYou can resume this session with ID:', sessionId);

Resuming Previous Sessions

import { query, type SDKUserMessage } from '@qwen-code/sdk';

// First session
async function* firstSession(): AsyncIterable<SDKUserMessage> {
  yield {
    type: 'user',
    session_id: 'persistent-session',
    message: { role: 'user', content: 'Create a counter.txt file with "0"' },
    parent_tool_use_id: null,
  };
}

const result1 = query({
  prompt: firstSession(),
  options: {
    sessionId: 'persistent-session',
    permissionMode: 'yolo',
  },
});

for await (const message of result1) {
  if (message.type === 'result') {
    console.log('First session result:', message.result);
  }
}

// Later: Resume the same session
async function* resumedSession(): AsyncIterable<SDKUserMessage> {
  yield {
    type: 'user',
    session_id: 'persistent-session',
    message: {
      role: 'user',
      content: 'Read counter.txt and increment it by 1',
    },
    parent_tool_use_id: null,
  };
}

const result2 = query({
  prompt: resumedSession(),
  options: {
    resume: 'persistent-session',  // Resume previous session
    permissionMode: 'yolo',
  },
});

for await (const message of result2) {
  if (message.type === 'result') {
    console.log('Resumed session result:', message.result);
  }
}

Error Handling in Multi-Turn

import { query, type SDKUserMessage, isAbortError } from '@qwen-code/sdk';

async function* conversation(): AsyncIterable<SDKUserMessage> {
  const sessionId = 'error-handling';
  
  try {
    yield {
      type: 'user',
      session_id: sessionId,
      message: { role: 'user', content: 'First task' },
      parent_tool_use_id: null,
    };

    await new Promise(resolve => setTimeout(resolve, 1000));

    yield {
      type: 'user',
      session_id: sessionId,
      message: { role: 'user', content: 'Second task' },
      parent_tool_use_id: null,
    };
  } catch (error) {
    console.error('Error in conversation generator:', error);
  }
}

try {
  const result = query({
    prompt: conversation(),
    options: {
      permissionMode: 'default',
    },
  });

  for await (const message of result) {
    if (message.type === 'result' && message.is_error) {
      console.error('Query error:', message.error);
    }
  }
} catch (error) {
  if (isAbortError(error)) {
    console.log('Conversation was aborted');
  } else {
    console.error('Unexpected error:', error);
  }
}

See Also