Project Setup and the Agent Chat Function

Build the foundational agentChat function that every interface layer — Slack, terminal, API — calls the same way. Covers project structure, streaming input mode, and session ID handling.

4 min read
agent chat function project setup SDK agentChat TypeScript streaming input mode session ID agent

Building something real from the ground up

This guide walks through setting up a full project that you will extend through the rest of this series. Not a minimal example — an actual agent project with a proper folder structure, a reusable agent function, and a test harness to verify everything works before you start connecting it to external services.

By the end, you will have an agent.ts file that exports a working agentChat function, a test.ts file that calls it, and a project structure that scales cleanly when you add Slack, session persistence, and production hardening later.

Project initialization

Start in your terminal. Navigate to where you want to put the project, then:

mkdir slack-ai-agent && cd slack-ai-agent
bun init -y
bun add @anthropic-ai/claude-agent-sdk

Create a src/ directory for your source code. All your agent logic lives here until deployment, when you will add a few root-level files.

mkdir src

The agent function

Create src/agent.ts. This file has one job: define the agentChat function that processes a message and returns a response.

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

const SYSTEM_PROMPT = `You are a helpful assistant.`;

export async function agentChat(
  message: string,
  sessionId?: string
): Promise<{ response: string; sessionId: string }> {
  let response = "";
  let resultSessionId = "";

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

  for await (const msg of query(messages(), {
    model: "claude-haiku-4-5",
    systemPrompt: SYSTEM_PROMPT,
    permissionMode: "bypassPermissions",
    dangerouslyAllowBypassPermissions: true,
    ...(sessionId ? { resume: sessionId } : {}),
  })) {
    if (msg.type === "result") {
      response = msg.subtype === "success"
        ? msg.result
        : "Something went wrong.";
      resultSessionId = msg.session_id;
    }
  }

  return { response, sessionId: resultSessionId };
}

A few things worth noting here:

The async generator wraps the user message in streaming input mode. This is required to unlock hooks, session resumption, and the full feature set of the SDK. Always use the async generator pattern rather than passing a plain string.

The sessionId parameter is optional. When it exists, the resume option is spread into the query. When it does not exist, nothing gets passed and the SDK creates a fresh session. The conditional spread ...(sessionId ? { resume: sessionId } : {}) is the clean way to handle this.

Haiku as the development model. You are not testing answer quality right now — you are testing that the plumbing works. Haiku is fast and cheap. Switch to Sonnet when you care about response quality.

The response format. This function returns both the response text and the session ID. The session ID is what you need later to resume conversations across turns.

The test harness

Create src/test.ts to verify the function works before wiring it to anything else:

import { agentChat } from "./agent";

async function main() {
  console.log("Sending message to agent...");

  const { response, sessionId } = await agentChat("Hello there.");

  console.log("\nResponse:", response);
  console.log("Session ID:", sessionId);
}

main();

Run it:

bun src/test.ts

You should see a response from the agent and a session ID printed to the terminal. The session ID is a UUID that looks something like sess_01AbCdEf.... If you see both, your agent function is working correctly.

> Debugging tip: If you get an error about missing credentials, check that you have a Claude.ai subscription linked to your terminal session or an ANTHROPICAPIKEY set in your environment. The most common setup issue is running the code without authentication configured.

Why this structure matters

The separation between agent.ts and bot.ts (which you will create later for Slack) reflects how production agents actually work. The agent function is pure — it takes a message, returns a response. The bot file handles all the Slack-specific plumbing: receiving events, posting messages, managing threads.

This means you can:

  • Test your agent without running a Slack server
  • Swap out the interface (Slack → Discord → Telegram) without touching agent logic
  • Add custom tools, hooks, and session configuration in one place

The agent SDK configuration options — model, system prompt, permission mode, tool list — all live in agent.ts. The interface layer knows nothing about them.

> For operators building their first deployed agent: Do not skip the test harness. The two minutes it takes to verify agentChat works before connecting it to Slack saves you from debugging Slack authentication issues when the real problem is in your agent function.

---

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