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.
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