사전 준비, SDK 설치와 인증 상속
10분Agent SDK는 Claude Code CLI를 자식 프로세스로 구동합니다. 그래서 설치만 하면 Chapter 1의 인증과 Chapter 3의 Bedrock 설정을 그대로 상속합니다. 새 자격증명이 필요 없습니다.
node -v # 18 이상, 랩 EC2는 22
mkdir -p ~/agentlab && cd ~/agentlab
npm init -y > /dev/null
npm install @anthropic-ai/claude-agent-sdk zod
claude /status # CLI 인증이 살아 있으면 SDK도 그대로 동작
Bedrock 경로 사용자는 Chapter 3에서 설정한 환경변수 두 줄이 이 셸에 살아 있는지만 확인하세요:
CLAUDE_CODE_USE_BEDROCK=1과
ANTHROPIC_MODEL=global.anthropic.claude-sonnet-4-6.
settingSources: ["user", "project"]처럼 명시해 불러옵니다.
임베딩된 에이전트가 예측 가능해야 하기 때문입니다.
첫 query, CLI의 프로그래매틱 쌍둥이
12분
claude -p로 하던 일을 query() 함수로 합니다.
스트리밍 메시지를 순회하며 Chapter 5에서 본 그 JSON 필드들(session_id, num_turns, total_cost_usd)이
타입 있는 객체로 돌아오는 것을 확인합니다.
cat > hello.mjs << 'HELLOEOF'
// T1: 첫 query, CLI의 프로그래매틱 쌍둥이
import { query } from "@anthropic-ai/claude-agent-sdk";
const q = query({
prompt: "이 디렉토리의 파일 목록을 보고 한 줄 소감을 말해줘",
options: { allowedTools: ["Read", "Glob"], maxTurns: 3 },
});
for await (const m of q) {
if (m.type === "assistant") {
for (const b of m.message.content) if (b.type === "text") process.stdout.write(b.text);
}
if (m.type === "result") {
console.log(`\n--- session=${m.session_id} turns=${m.num_turns} cost=$${m.total_cost_usd}`);
}
}
HELLOEOF
node hello.mjs
package.json과 node_modules뿐인 갓 태어난 프로젝트네요. 시작이 반입니다.
--- session=ab12cd34-... turns=2 cost=$0.0031
allowedTools: ["Read", "Glob"]가 이 에이전트의 손발 전부입니다.
Bash를 주지 않았으니 파일 실행은 불가능합니다. Chapter 5의 --allowed-tools와
같은 개념이 옵션 객체 한 줄이 된 것뿐입니다.
m.message.content 안에 블록 배열로 들어 있습니다.
text 블록만 골라 출력하는 위 패턴을 그대로 쓰세요. 최종 텍스트만 필요하면
result 메시지의 m.result가 가장 간단합니다.
대화 메모리, resume이 기억을 만든다
12분
query는 기본적으로 매번 새 대화입니다. 첫 응답의 session_id를 붙잡아 다음 query의
resume에 넘기면 대화가 이어집니다. Chapter 5의 --resume과 같은 원리를
코드 3줄로 구현합니다.
cat > chat.mjs << 'CHATEOF'
// T2: 대화 메모리, session_id를 resume으로 잇는다
import readline from "node:readline/promises";
import { query } from "@anthropic-ai/claude-agent-sdk";
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
let sessionId;
while (true) {
const line = (await rl.question("나> ")).trim();
if (!line || line === "exit") break;
const q = query({
prompt: line,
options: { resume: sessionId, allowedTools: [], maxTurns: 1 },
});
for await (const m of q) {
if (m.type === "assistant") {
for (const b of m.message.content) if (b.type === "text") process.stdout.write(b.text);
}
if (m.type === "result") { sessionId = m.session_id; process.stdout.write("\n"); }
}
}
rl.close();
CHATEOF
node chat.mjs
나> 내 이름은 우형이야
반갑습니다, 우형님.
나> 내 이름이 뭐라고 했지?
우형님이라고 하셨죠.
나> exit
핵심은 result 메시지에서 sessionId = m.session_id를 갱신해
다음 루프의 resume으로 넘기는 세 줄입니다.
allowedTools: []로 도구를 전부 꺼서 순수 대화만 남겼습니다.
커스텀 도구, 내 함수가 에이전트의 손이 된다
14분
에이전트에게 여러분의 코드가 가진 능력(DB 조회, 사내 API, 이 랩에서는 노트 검색)을 쥐여 줍니다.
tool()로 함수를 정의하고 createSdkMcpServer()로 감싸면
별도 프로세스 없는 in-process MCP 서버가 됩니다.
cat > notes-agent.mjs << 'NOTESEOF'
// T3: 커스텀 도구, 내 함수가 에이전트의 손이 된다 (in-process MCP)
import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
const NOTES = [
"7월 회식은 24일 목요일 19시, 장소는 을지로 골뱅이집",
"배포 원칙: 금요일 오후에는 프로덕션 배포를 하지 않는다",
"신규 입사자 온보딩: 버디가 2주간 매일 30분 페어링",
];
const searchNote = tool(
"search_note",
"사내 노트에서 키워드가 포함된 문장을 찾는다",
{ keyword: z.string().describe("찾을 키워드") },
async ({ keyword }) => ({
content: [{
type: "text",
text: NOTES.filter((n) => n.includes(keyword)).join("\n") || "일치하는 노트 없음",
}],
}),
);
const shelf = createSdkMcpServer({ name: "shelf", version: "1.0.0", tools: [searchNote] });
const q = query({
prompt: process.argv[2] ?? "다음 회식이 언제 어디서인지 노트에서 찾아 알려줘",
options: {
mcpServers: { shelf },
allowedTools: ["mcp__shelf__search_note"],
maxTurns: 5,
systemPrompt: "당신은 사내 노트 사서다. 반드시 search_note 도구로 찾은 내용만 근거로 답한다.",
},
});
for await (const m of q) {
if (m.type === "assistant") {
for (const b of m.message.content) if (b.type === "text") process.stdout.write(b.text);
}
if (m.type === "result") console.log(`\n--- turns=${m.num_turns}`);
}
NOTESEOF
node notes-agent.mjs
node notes-agent.mjs "금요일에 배포해도 돼?"
노트에 따르면 다음 회식은 7월 24일 목요일 19시, 을지로 골뱅이집입니다.
--- turns=3
노트의 배포 원칙상 금요일 오후 프로덕션 배포는 하지 않습니다.
--- turns=3
mcp__서버명__도구명 형식입니다
(mcp__shelf__search_note). zod 스키마가 입력 타입을 강제하고,
systemPrompt의 "도구로 찾은 내용만 근거로" 한 줄이 캡스톤 D에서 배운 grounding 계약의 SDK 버전입니다.
미니 상주 점검원, 프로세스를 넘는 기억
12분session_id를 파일에 저장하면 프로세스가 죽어도 기억이 이어집니다. 상태 파일을 읽는 점검원을 두 번 실행해, 두 번째 실행이 "아까는 OK였는데 지금은 DOWN"이라고 변화를 말하게 만듭니다. 캡스톤 B 메딕의 상주화가 이 원리 위에 섭니다.
echo "service: OK" > status.txt
cat > medic-lite.mjs << 'MEDLITEEOF'
// T4: 미니 상주 점검원, 프로세스를 넘어 기억하는 에이전트
import { query } from "@anthropic-ai/claude-agent-sdk";
import { readFileSync, writeFileSync, existsSync } from "node:fs";
const prev = existsSync(".medic-session")
? readFileSync(".medic-session", "utf8").trim()
: undefined;
const q = query({
prompt: "status.txt를 읽고 서비스 상태를 두 줄로 보고해. DOWN이면 원인 후보와 첫 조치를 제안해. 이전 점검 기록이 있으면 상태 변화를 먼저 언급해.",
options: { resume: prev, allowedTools: ["Read"], maxTurns: 4 },
});
for await (const m of q) {
if (m.type === "assistant") {
for (const b of m.message.content) if (b.type === "text") process.stdout.write(b.text);
}
if (m.type === "result") {
writeFileSync(".medic-session", m.session_id);
console.log(`\n--- 점검 완료, turns=${m.num_turns} cost=$${m.total_cost_usd}`);
}
}
MEDLITEEOF
node medic-lite.mjs
echo "service: DOWN (connection refused)" > status.txt
node medic-lite.mjs
직전 점검에서는 OK였는데 지금은 DOWN으로 바뀌었습니다.
connection refused는 프로세스 다운 또는 포트 미개방이 유력합니다. 먼저 서비스 프로세스 상태를 확인하세요.
--- 점검 완료, turns=2 cost=$0.0027
cron이나 systemd timer에 node medic-lite.mjs 한 줄을 걸면
그대로 상주 점검원입니다. 무인 실행의 안전벨트는 Chapter 5와 동일합니다:
allowedTools 최소화, maxTurns 상한.
마무리, 캡스톤 이식 지도
오늘 배운 네 조각을 각자의 캡스톤에 이식하는 것이 다음 숙제입니다.
| 배운 것 | Task | 캡스톤 이식 아이디어 |
|---|---|---|
| query와 allowedTools | T1 | A 옵스 센터: /ask Lambda를 SDK 서버로 승격, 도구를 가진 질의 패널 |
| resume 대화 메모리 | T2 | C 카페: "아까 주문 취소해줘"가 되는 멀티턴 점원 |
| 커스텀 도구, grounding | T3 | D 사서: 코사인 검색을 search_shelf 도구로 감싼 대화형 사서 |
| 파일 세션, 상주 실행 | T4 | B 메딕: medic.sh를 SDK 상주 프로세스로, 점검 이력을 기억하는 메딕 |