Multi-turn Conversations and the Interactive Agent

Build an interactive terminal agent with a conversation loop, session memory across turns, and a thinking indicator. The session ID pattern that makes follow-up questions work.

4 min read
multi-turn agent interactive agent terminal session resumption loop conversation memory SDK readline agent

Building a real interactive agent

Every example up to this point has been a one-shot agent: send a message, get a response, done. That covers a lot of automation use cases, but it is not a conversation. It does not let you follow up, ask clarifying questions, or build on previous answers.

This guide builds an interactive terminal agent from scratch — with a conversation loop, session memory across turns, and a thinking indicator so the experience feels responsive. The patterns here are the same ones you will wire into Slack, Discord, or any other interface later.

Reading input from the terminal

Node.js has a built-in module called readline that handles terminal input. Bun runs Node, so readline works directly:

import * as readline from "readline";
import { query } from "@anthropic-ai/claude-agent-sdk";

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

function ask(prompt: string): Promise<string> {
  return new Promise((resolve) => {
    rl.question(prompt, resolve);
  });
}

ask takes a prompt string, displays it, and returns whatever the user types. This is the building block for the conversation loop.

The conversation loop

async function main() {
  console.log("Chat with Claude. Type 'quit' to exit.");

  let sessionId: string | undefined;

  while (true) {
    const input = await ask("You: ");

    if (input.toLowerCase() === "quit") {
      break;
    }

    async function* messages() {
      yield { role: "user" as const, content: input };
    }

    process.stdout.write("\nClaude: ");

    for await (const message of query(messages(), {
      model: "claude-sonnet-4-5",
      systemPrompt: "You are a helpful assistant.",
      permissionMode: "bypassPermissions",
      dangerouslyAllowBypassPermissions: true,
      ...(sessionId ? { resume: sessionId } : {}),
    })) {
      if (message.type === "assistant") {
        for (const block of message.message.content) {
          if (block.type === "text") {
            process.stdout.write(block.text);
          }
        }
      }
      if (message.type === "result") {
        sessionId = message.session_id;
        console.log("\n");
      }
    }
  }

  rl.close();
}

main();

The session ID is the memory. At the end of each turn, save the session ID from the result message. On the next turn, pass it via resume. The agent picks up exactly where the conversation left off. Without this, every message starts a fresh session with no memory of previous turns.

The conditional spread ...(sessionId ? { resume: sessionId } : {}) handles both cases cleanly: new session when there is no ID, resumed session when there is one.

Adding a thinking indicator

Five seconds of silence while waiting for a response is a bad experience. A simple animated indicator tells the user the agent is working:

let dotCount = 0;
const thinking = setInterval(() => {
  dotCount = (dotCount % 3) + 1;
  process.stdout.write(`\rClaude is thinking${".".repeat(dotCount)}   `);
}, 400);

let firstChunk = true;

// Inside the for await loop, in the assistant message handler:
if (message.type === "assistant") {
  for (const block of message.message.content) {
    if (block.type === "text" && firstChunk) {
      clearInterval(thinking);
      process.stdout.write("\r\x1B[2K"); // clear the thinking line
      firstChunk = false;
    }
    if (block.type === "text") {
      process.stdout.write(block.text);
    }
  }
}

The \r carriage return overwrites the current line. When the first text chunk arrives, clearInterval stops the animation and the ANSI escape sequence clears the line before the response prints.

What this teaches about interface-independent agents

Most of the code above has nothing to do with the Agent SDK. The readline setup, the conversation loop, the thinking indicator — that is all interface glue. The SDK part is five lines: the query call with its options.

This is the real insight: the Agent SDK handles the intelligence layer. You handle the interface layer. When you move from a terminal agent to a Slack agent, you replace readline with Slack Bolt's event handlers. The query call stays almost identical.

> Challenge for operators: Before moving to the next guide, try adding a skill to this agent. Give it a system prompt that makes it useful for a specific client workflow you run — weekly reporting prep, client research, meeting synthesis. The terminal interface is a good testing environment before you deploy to a real interface.

---

Author: FractionalSkill

Keep Going

Ready to Start Building?

Pick the next step that matches where you are right now.

Tutorial
Claude Code Basics

Start with the terminal basics. A hands-on, step-by-step guide to your first 10 minutes with Claude Code.

Start the Tutorial
Guide
AI-Powered Workflows

Automate your client work. Learn how to connect AI tools into workflows that handle repetitive tasks for you.

Read the Guide
Community
Join the Community

Connect with other fractional leaders building with AI. Share workflows, get feedback, and learn from operators who are ahead of you.

Apply to Join