์ฒดํฌํฌ์ธํ ์ผ๋ก ํ์ผ ๋ณ๊ฒฝ ์ฌํญ ๋๋๋ฆฌ๊ธฐ
์์ด์ ํธ ์ธ์ ์ค ํ์ผ ๋ณ๊ฒฝ ์ฌํญ์ ์ถ์ ํ๊ณ ํ์ผ์ ์ด์ ์ ๋ชจ๋ ์ํ๋ก ๋ณต์ํฉ๋๋ค
ํ์ผ ์ฒดํฌํฌ์ธํ ์ ์์ด์ ํธ ์ธ์ ์ค Write, Edit, NotebookEdit ๋๊ตฌ๋ฅผ ํตํด ์ํ๋ ํ์ผ ์์ ์ฌํญ์ ์ถ์ ํ์ฌ ํ์ผ์ ์ด์ ์ ๋ชจ๋ ์ํ๋ก ๋๋๋ฆด ์ ์๊ฒ ํฉ๋๋ค. ์ง์ ์๋ํด๋ณด๊ณ ์ถ์ผ์ ๊ฐ์? ๋ํํ ์์ ๋ก ์ด๋ํ์ธ์.
์ฒดํฌํฌ์ธํ ์ ์ฌ์ฉํ๋ฉด ๋ค์์ ์ํํ ์ ์์ต๋๋ค:
- ์์น ์๋ ๋ณ๊ฒฝ ์ฌํญ ์คํ ์ทจ์ - ํ์ผ์ ์๋ ค์ง ์ํธํ ์ํ๋ก ๋ณต์
- ๋์ ํ์ - ์ฒดํฌํฌ์ธํธ๋ก ๋ณต์ํ ํ ๋ค๋ฅธ ์ ๊ทผ ๋ฐฉ์ ์๋
- ์ค๋ฅ ๋ณต๊ตฌ - ์์ด์ ํธ๊ฐ ์๋ชป๋ ์์ ์ ์ํํ์ ๋
Write, Edit, NotebookEdit ๋๊ตฌ๋ฅผ ํตํด ์ํ๋ ๋ณ๊ฒฝ ์ฌํญ๋ง ์ถ์ ๋ฉ๋๋ค. Bash ๋ช
๋ น์ด(์: echo > file.txt ๋๋ sed -i)๋ฅผ ํตํด ์ํ๋ ๋ณ๊ฒฝ ์ฌํญ์ ์ฒดํฌํฌ์ธํธ ์์คํ
์์ ์บก์ฒ๋์ง ์์ต๋๋ค.
์ฒดํฌํฌ์ธํ ์๋ ๋ฐฉ์
ํ์ผ ์ฒดํฌํฌ์ธํ ์ ํ์ฑํํ๋ฉด SDK๋ Write, Edit ๋๋ NotebookEdit ๋๊ตฌ๋ฅผ ํตํด ํ์ผ์ ์์ ํ๊ธฐ ์ ์ ํ์ผ์ ๋ฐฑ์ ์ ์์ฑํฉ๋๋ค. ์๋ต ์คํธ๋ฆผ์ ์ฌ์ฉ์ ๋ฉ์์ง์๋ ๋ณต์ ์ง์ ์ผ๋ก ์ฌ์ฉํ ์ ์๋ ์ฒดํฌํฌ์ธํธ UUID๊ฐ ํฌํจ๋ฉ๋๋ค.
์ฒดํฌํฌ์ธํธ๋ ์์ด์ ํธ๊ฐ ํ์ผ์ ์์ ํ๋ ๋ฐ ์ฌ์ฉํ๋ ๋ค์์ ๊ธฐ๋ณธ ์ ๊ณต ๋๊ตฌ์ ํจ๊ป ์๋ํฉ๋๋ค:
| ๋๊ตฌ | ์ค๋ช |
|---|---|
| Write | ์ ํ์ผ์ ์์ฑํ๊ฑฐ๋ ๊ธฐ์กด ํ์ผ์ ์ ์ฝํ ์ธ ๋ก ๋ฎ์ด์๋๋ค |
| Edit | ๊ธฐ์กด ํ์ผ์ ํน์ ๋ถ๋ถ์ ๋ํ ๋์ ํธ์ง์ ์ํํฉ๋๋ค |
| NotebookEdit | Jupyter ๋
ธํธ๋ถ(.ipynb ํ์ผ)์ ์
์ ์์ ํฉ๋๋ค |
ํ์ผ ๋๋๋ฆฌ๊ธฐ๋ ๋์คํฌ์ ํ์ผ์ ์ด์ ์ํ๋ก ๋ณต์ํฉ๋๋ค. ๋ํ ์์ฒด๋ฅผ ๋๋๋ฆฌ์ง๋ ์์ต๋๋ค. rewindFiles()(TypeScript) ๋๋ rewind_files()(Python)๋ฅผ ํธ์ถํ ํ์๋ ๋ํ ๊ธฐ๋ก๊ณผ ์ปจํ
์คํธ๋ ๊ทธ๋๋ก ์ ์ง๋ฉ๋๋ค.
์ฒดํฌํฌ์ธํธ ์์คํ ์ ๋ค์์ ์ถ์ ํฉ๋๋ค:
- ์ธ์ ์ค์ ์์ฑ๋ ํ์ผ
- ์ธ์ ์ค์ ์์ ๋ ํ์ผ
- ์์ ๋ ํ์ผ์ ์๋ณธ ์ฝํ ์ธ
์ฒดํฌํฌ์ธํธ๋ก ๋๋๋ฆฌ๋ฉด ์์ฑ๋ ํ์ผ์ ์ญ์ ๋๊ณ ์์ ๋ ํ์ผ์ ํด๋น ์์ ์ ์ฝํ ์ธ ๋ก ๋ณต์๋ฉ๋๋ค.
์ฒดํฌํฌ์ธํ ๊ตฌํ
ํ์ผ ์ฒดํฌํฌ์ธํ
์ ์ฌ์ฉํ๋ ค๋ฉด ์ต์
์์ ํ์ฑํํ๊ณ , ์๋ต ์คํธ๋ฆผ์์ ์ฒดํฌํฌ์ธํธ UUID๋ฅผ ์บก์ฒํ ๋ค์, ๋ณต์์ด ํ์ํ ๋ rewindFiles()(TypeScript) ๋๋ rewind_files()(Python)๋ฅผ ํธ์ถํฉ๋๋ค.
๋ค์ ์์ ๋ ์ ์ฒด ํ๋ฆ์ ๋ณด์ฌ์ค๋๋ค: ์ฒดํฌํฌ์ธํ ์ ํ์ฑํํ๊ณ , ์๋ต ์คํธ๋ฆผ์์ ์ฒดํฌํฌ์ธํธ UUID์ ์ธ์ ID๋ฅผ ์บก์ฒํ ๋ค์, ๋์ค์ ์ธ์ ์ ์ฌ๊ฐํ์ฌ ํ์ผ์ ๋๋๋ฆฝ๋๋ค. ๊ฐ ๋จ๊ณ๋ ์๋์์ ์์ธํ ์ค๋ช ๋ฉ๋๋ค.
import asyncio
from claude_agent_sdk import (
ClaudeSDKClient,
ClaudeAgentOptions,
UserMessage,
ResultMessage,
)
async def main():
# Step 1: Enable checkpointing
options = ClaudeAgentOptions(
enable_file_checkpointing=True,
permission_mode="acceptEdits", # Auto-accept file edits without prompting
extra_args={
"replay-user-messages": None
}, # Required to receive checkpoint UUIDs in the response stream
)
checkpoint_id = None
session_id = None
# Run the query and capture checkpoint UUID and session ID
async with ClaudeSDKClient(options) as client:
await client.query("Refactor the authentication module")
# Step 2: Capture checkpoint UUID from the first user message
async for message in client.receive_response():
if isinstance(message, UserMessage) and message.uuid and not checkpoint_id:
checkpoint_id = message.uuid
if isinstance(message, ResultMessage) and not session_id:
session_id = message.session_id
# Step 3: Later, rewind by resuming the session with an empty prompt
if checkpoint_id and session_id:
async with ClaudeSDKClient(
ClaudeAgentOptions(enable_file_checkpointing=True, resume=session_id)
) as client:
await client.query("") # Empty prompt to open the connection
async for message in client.receive_response():
await client.rewind_files(checkpoint_id)
break
print(f"Rewound to checkpoint: {checkpoint_id}")
asyncio.run(main())
import { query } from "@anthropic-ai/claude-agent-sdk";
async function main() {
// Step 1: Enable checkpointing
const opts = {
enableFileCheckpointing: true,
permissionMode: "acceptEdits" as const, // Auto-accept file edits without prompting
extraArgs: { "replay-user-messages": null } // Required to receive checkpoint UUIDs in the response stream
};
const response = query({
prompt: "Refactor the authentication module",
options: opts
});
let checkpointId: string | undefined;
let sessionId: string | undefined;
// Step 2: Capture checkpoint UUID from the first user message
for await (const message of response) {
if (message.type === "user" && message.uuid && !checkpointId) {
checkpointId = message.uuid;
}
if ("session_id" in message && !sessionId) {
sessionId = message.session_id;
}
}
// Step 3: Later, rewind by resuming the session with an empty prompt
if (checkpointId && sessionId) {
const rewindQuery = query({
prompt: "", // Empty prompt to open the connection
options: { ...opts, resume: sessionId }
});
for await (const msg of rewindQuery) {
await rewindQuery.rewindFiles(checkpointId);
break;
}
console.log(`Rewound to checkpoint: ${checkpointId}`);
}
}
main();
์ฒดํฌํฌ์ธํ ํ์ฑํ
์ฒดํฌํฌ์ธํ ์ ํ์ฑํํ๊ณ ์ฒดํฌํฌ์ธํธ UUID๋ฅผ ์์ ํ๋๋ก SDK ์ต์ ์ ๊ตฌ์ฑํฉ๋๋ค:
| ์ต์ | Python | TypeScript | ์ค๋ช |
|---|---|---|---|
| ์ฒดํฌํฌ์ธํ ํ์ฑํ | enable_file_checkpointing=True |
enableFileCheckpointing: true |
๋๋๋ฆฌ๊ธฐ ์ํด ํ์ผ ๋ณ๊ฒฝ ์ฌํญ์ ์ถ์ ํฉ๋๋ค |
| ์ฒดํฌํฌ์ธํธ UUID ์์ | extra_args={"replay-user-messages": None} |
extraArgs: { 'replay-user-messages': null } |
์คํธ๋ฆผ์์ ์ฌ์ฉ์ ๋ฉ์์ง UUID๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฐ ํ์ํฉ๋๋ค |
options = ClaudeAgentOptions(
enable_file_checkpointing=True,
permission_mode="acceptEdits",
extra_args={"replay-user-messages": None},
)
async with ClaudeSDKClient(options) as client:
await client.query("Refactor the authentication module")
const response = query({
prompt: "Refactor the authentication module",
options: {
enableFileCheckpointing: true,
permissionMode: "acceptEdits" as const,
extraArgs: { "replay-user-messages": null }
}
});
์ฒดํฌํฌ์ธํธ UUID ๋ฐ ์ธ์ ID ์บก์ฒ
replay-user-messages ์ต์
์ด ์ค์ ๋๋ฉด(์์ ํ์๋จ), ์๋ต ์คํธ๋ฆผ์ ๊ฐ ์ฌ์ฉ์ ๋ฉ์์ง์๋ ์ฒดํฌํฌ์ธํธ๋ก ์ฌ์ฉ๋๋ UUID๊ฐ ์์ต๋๋ค.
๋๋ถ๋ถ์ ์ฌ์ฉ ์ฌ๋ก์์ ์ฒซ ๋ฒ์งธ ์ฌ์ฉ์ ๋ฉ์์ง UUID(message.uuid)๋ฅผ ์บก์ฒํฉ๋๋ค. ์ด๋ก ๋๋๋ฆฌ๋ฉด ๋ชจ๋ ํ์ผ์ด ์๋ณธ ์ํ๋ก ๋ณต์๋ฉ๋๋ค. ์ฌ๋ฌ ์ฒดํฌํฌ์ธํธ๋ฅผ ์ ์ฅํ๊ณ ์ค๊ฐ ์ํ๋ก ๋๋๋ฆฌ๋ ค๋ฉด ์ฌ๋ฌ ๋ณต์ ์ง์ ์ ์ฐธ์กฐํ์ธ์.
์ธ์
ID(message.session_id)๋ฅผ ์บก์ฒํ๋ ๊ฒ์ ์ ํ ์ฌํญ์
๋๋ค. ์คํธ๋ฆผ์ด ์๋ฃ๋ ํ ๋์ค์ ๋๋๋ฆฌ๋ ค๋ ๊ฒฝ์ฐ์๋ง ํ์ํฉ๋๋ค. ์ฒดํฌํฌ์ธํธ ์ ์ ์ํํ ์์
์์ ์ฒ๋ผ ๋ฉ์์ง๋ฅผ ์ฒ๋ฆฌํ๋ ๋์ rewindFiles()๋ฅผ ์ฆ์ ํธ์ถํ๋ ๊ฒฝ์ฐ ์ธ์
ID ์บก์ฒ๋ฅผ ๊ฑด๋๋ธ ์ ์์ต๋๋ค.
checkpoint_id = None
session_id = None
async for message in client.receive_response():
# Update checkpoint on each user message (keeps the latest)
if isinstance(message, UserMessage) and message.uuid:
checkpoint_id = message.uuid
# Capture session ID from the result message
if isinstance(message, ResultMessage):
session_id = message.session_id
let checkpointId: string | undefined;
let sessionId: string | undefined;
for await (const message of response) {
// Update checkpoint on each user message (keeps the latest)
if (message.type === "user" && message.uuid) {
checkpointId = message.uuid;
}
// Capture session ID from any message that has it
if ("session_id" in message) {
sessionId = message.session_id;
}
}
ํ์ผ ๋๋๋ฆฌ๊ธฐ
์คํธ๋ฆผ์ด ์๋ฃ๋ ํ ๋๋๋ฆฌ๋ ค๋ฉด ๋น ํ๋กฌํํธ๋ก ์ธ์
์ ์ฌ๊ฐํ๊ณ ์ฒดํฌํฌ์ธํธ UUID์ ํจ๊ป rewind_files()(Python) ๋๋ rewindFiles()(TypeScript)๋ฅผ ํธ์ถํฉ๋๋ค. ์คํธ๋ฆผ ์ค์๋ ๋๋๋ฆด ์ ์์ต๋๋ค. ์ฒดํฌํฌ์ธํธ ์ ์ ์ํํ ์์
์์ ํด๋น ํจํด์ ์ฐธ์กฐํ์ธ์.
async with ClaudeSDKClient(
ClaudeAgentOptions(enable_file_checkpointing=True, resume=session_id)
) as client:
await client.query("") # Empty prompt to open the connection
async for message in client.receive_response():
await client.rewind_files(checkpoint_id)
break
const rewindQuery = query({
prompt: "", // Empty prompt to open the connection
options: { ...opts, resume: sessionId }
});
for await (const msg of rewindQuery) {
await rewindQuery.rewindFiles(checkpointId);
break;
}
์ธ์ ID์ ์ฒดํฌํฌ์ธํธ ID๋ฅผ ์บก์ฒํ ๊ฒฝ์ฐ CLI์์๋ ๋๋๋ฆด ์ ์์ต๋๋ค:
claude -p --resume <session-id> --rewind-files <checkpoint-uuid>
์ผ๋ฐ์ ์ธ ํจํด
์ด๋ฌํ ํจํด์ ์ฌ์ฉ ์ฌ๋ก์ ๋ฐ๋ผ ์ฒดํฌํฌ์ธํธ UUID๋ฅผ ์บก์ฒํ๊ณ ์ฌ์ฉํ๋ ๋ค์ํ ๋ฐฉ๋ฒ์ ๋ณด์ฌ์ค๋๋ค.
์ํํ ์์ ์ ์ ์ฒดํฌํฌ์ธํธ
์ด ํจํด์ ๊ฐ์ฅ ์ต๊ทผ์ ์ฒดํฌํฌ์ธํธ UUID๋ง ์ ์งํ๋ฉฐ, ๊ฐ ์์ด์ ํธ ํด ์ ์ ์ ๋ฐ์ดํธํฉ๋๋ค. ์ฒ๋ฆฌ ์ค์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ฉด ๋ง์ง๋ง ์์ ํ ์ํ๋ก ์ฆ์ ๋๋๋ฆฌ๊ณ ๋ฃจํ๋ฅผ ๋ฒ์ด๋ ์ ์์ต๋๋ค.
import asyncio
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, UserMessage
async def main():
options = ClaudeAgentOptions(
enable_file_checkpointing=True,
permission_mode="acceptEdits",
extra_args={"replay-user-messages": None},
)
safe_checkpoint = None
async with ClaudeSDKClient(options) as client:
await client.query("Refactor the authentication module")
async for message in client.receive_response():
# Update checkpoint before each agent turn starts
# This overwrites the previous checkpoint. Only keep the latest
if isinstance(message, UserMessage) and message.uuid:
safe_checkpoint = message.uuid
# Decide when to revert based on your own logic
# For example: error detection, validation failure, or user input
if your_revert_condition and safe_checkpoint:
await client.rewind_files(safe_checkpoint)
# Exit the loop after rewinding, files are restored
break
asyncio.run(main())
import { query } from "@anthropic-ai/claude-agent-sdk";
async function main() {
const response = query({
prompt: "Refactor the authentication module",
options: {
enableFileCheckpointing: true,
permissionMode: "acceptEdits" as const,
extraArgs: { "replay-user-messages": null }
}
});
let safeCheckpoint: string | undefined;
for await (const message of response) {
// Update checkpoint before each agent turn starts
// This overwrites the previous checkpoint. Only keep the latest
if (message.type === "user" && message.uuid) {
safeCheckpoint = message.uuid;
}
// Decide when to revert based on your own logic
// For example: error detection, validation failure, or user input
if (yourRevertCondition && safeCheckpoint) {
await response.rewindFiles(safeCheckpoint);
// Exit the loop after rewinding, files are restored
break;
}
}
}
main();
์ฌ๋ฌ ๋ณต์ ์ง์
Claude๊ฐ ์ฌ๋ฌ ํด์ ๊ฑธ์ณ ๋ณ๊ฒฝ์ ์ํํ๋ ๊ฒฝ์ฐ, ๋ชจ๋ ๋ฐฉ์์ผ๋ก ๋๋๋ฆฌ๊ธฐ๋ณด๋ค๋ ํน์ ์ง์ ์ผ๋ก ๋๋๋ฆฌ๊ณ ์ถ์ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด, Claude๊ฐ ์ฒซ ๋ฒ์งธ ํด์์ ํ์ผ์ ๋ฆฌํฉํ ๋งํ๊ณ ๋ ๋ฒ์งธ ํด์์ ํ ์คํธ๋ฅผ ์ถ๊ฐํ๋ ๊ฒฝ์ฐ, ๋ฆฌํฉํ ๋ง์ ์ ์งํ๋ ํ ์คํธ๋ ์คํ ์ทจ์ํ๊ณ ์ถ์ ์ ์์ต๋๋ค.
์ด ํจํด์ ๋ชจ๋ ์ฒดํฌํฌ์ธํธ UUID๋ฅผ ๋ฉํ๋ฐ์ดํฐ์ ํจ๊ป ๋ฐฐ์ด์ ์ ์ฅํฉ๋๋ค. ์ธ์ ์ด ์๋ฃ๋ ํ ์ด์ ์ ๋ชจ๋ ์ฒดํฌํฌ์ธํธ๋ก ๋๋๋ฆด ์ ์์ต๋๋ค:
import asyncio
from dataclasses import dataclass
from datetime import datetime
from claude_agent_sdk import (
ClaudeSDKClient,
ClaudeAgentOptions,
UserMessage,
ResultMessage,
)
# Store checkpoint metadata for better tracking
@dataclass
class Checkpoint:
id: str
description: str
timestamp: datetime
async def main():
options = ClaudeAgentOptions(
enable_file_checkpointing=True,
permission_mode="acceptEdits",
extra_args={"replay-user-messages": None},
)
checkpoints = []
session_id = None
async with ClaudeSDKClient(options) as client:
await client.query("Refactor the authentication module")
async for message in client.receive_response():
if isinstance(message, UserMessage) and message.uuid:
checkpoints.append(
Checkpoint(
id=message.uuid,
description=f"After turn {len(checkpoints) + 1}",
timestamp=datetime.now(),
)
)
if isinstance(message, ResultMessage) and not session_id:
session_id = message.session_id
# Later: rewind to any checkpoint by resuming the session
if checkpoints and session_id:
target = checkpoints[0] # Pick any checkpoint
async with ClaudeSDKClient(
ClaudeAgentOptions(enable_file_checkpointing=True, resume=session_id)
) as client:
await client.query("") # Empty prompt to open the connection
async for message in client.receive_response():
await client.rewind_files(target.id)
break
print(f"Rewound to: {target.description}")
asyncio.run(main())
import { query } from "@anthropic-ai/claude-agent-sdk";
// Store checkpoint metadata for better tracking
interface Checkpoint {
id: string;
description: string;
timestamp: Date;
}
async function main() {
const opts = {
enableFileCheckpointing: true,
permissionMode: "acceptEdits" as const,
extraArgs: { "replay-user-messages": null }
};
const response = query({
prompt: "Refactor the authentication module",
options: opts
});
const checkpoints: Checkpoint[] = [];
let sessionId: string | undefined;
for await (const message of response) {
if (message.type === "user" && message.uuid) {
checkpoints.push({
id: message.uuid,
description: `After turn ${checkpoints.length + 1}`,
timestamp: new Date()
});
}
if ("session_id" in message && !sessionId) {
sessionId = message.session_id;
}
}
// Later: rewind to any checkpoint by resuming the session
if (checkpoints.length > 0 && sessionId) {
const target = checkpoints[0]; // Pick any checkpoint
const rewindQuery = query({
prompt: "", // Empty prompt to open the connection
options: { ...opts, resume: sessionId }
});
for await (const msg of rewindQuery) {
await rewindQuery.rewindFiles(target.id);
break;
}
console.log(`Rewound to: ${target.description}`);
}
}
main();
์ง์ ์๋ํด๋ณด๊ธฐ
์ด ์์ ํ ์์ ๋ ์์ ์ ํธ๋ฆฌํฐ ํ์ผ์ ์์ฑํ๊ณ , ์์ด์ ํธ๊ฐ ๋ฌธ์ ์ฃผ์์ ์ถ๊ฐํ๋๋ก ํ๋ฉฐ, ๋ณ๊ฒฝ ์ฌํญ์ ํ์ํ ๋ค์, ๋๋๋ฆฌ๊ณ ์ถ์์ง ๋ฌป์ต๋๋ค.
์์ํ๊ธฐ ์ ์ Claude Agent SDK๊ฐ ์ค์น๋์ด ์๋์ง ํ์ธํ์ธ์.
ํ ์คํธ ํ์ผ ์์ฑ
utils.py(Python) ๋๋ utils.ts(TypeScript)๋ผ๋ ์ ํ์ผ์ ์์ฑํ๊ณ ๋ค์ ์ฝ๋๋ฅผ ๋ถ์ฌ๋ฃ์ต๋๋ค:
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def multiply(a, b):
return a * b
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
export function add(a: number, b: number): number {
return a + b;
}
export function subtract(a: number, b: number): number {
return a - b;
}
export function multiply(a: number, b: number): number {
return a * b;
}
export function divide(a: number, b: number): number {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}
๋ํํ ์์ ์คํ
์ ํธ๋ฆฌํฐ ํ์ผ๊ณผ ๊ฐ์ ๋๋ ํ ๋ฆฌ์ try_checkpointing.py(Python) ๋๋ try_checkpointing.ts(TypeScript)๋ผ๋ ์ ํ์ผ์ ์์ฑํ๊ณ ๋ค์ ์ฝ๋๋ฅผ ๋ถ์ฌ๋ฃ์ต๋๋ค.
์ด ์คํฌ๋ฆฝํธ๋ Claude์๊ฒ ์ ํธ๋ฆฌํฐ ํ์ผ์ ๋ฌธ์ ์ฃผ์์ ์ถ๊ฐํ๋๋ก ์์ฒญํ ๋ค์ ์๋ณธ์ ๋ณต์ํ๊ธฐ ์ํด ๋๋๋ฆฌ๊ณ ์ถ์์ง ์ ํํ ์ ์๋ ์ต์ ์ ์ ๊ณตํฉ๋๋ค.
import asyncio
from claude_agent_sdk import (
ClaudeSDKClient,
ClaudeAgentOptions,
UserMessage,
ResultMessage,
)
async def main():
# Configure the SDK with checkpointing enabled
# - enable_file_checkpointing: Track file changes for rewinding
# - permission_mode: Auto-accept file edits without prompting
# - extra_args: Required to receive user message UUIDs in the stream
options = ClaudeAgentOptions(
enable_file_checkpointing=True,
permission_mode="acceptEdits",
extra_args={"replay-user-messages": None},
)
checkpoint_id = None # Store the user message UUID for rewinding
session_id = None # Store the session ID for resuming
print("Running agent to add doc comments to utils.py...\n")
# Run the agent and capture checkpoint data from the response stream
async with ClaudeSDKClient(options) as client:
await client.query("Add doc comments to utils.py")
async for message in client.receive_response():
# Capture the first user message UUID - this is our restore point
if isinstance(message, UserMessage) and message.uuid and not checkpoint_id:
checkpoint_id = message.uuid
# Capture the session ID so we can resume later
if isinstance(message, ResultMessage):
session_id = message.session_id
print("Done! Open utils.py to see the added doc comments.\n")
# Ask the user if they want to rewind the changes
if checkpoint_id and session_id:
response = input("Rewind to remove the doc comments? (y/n): ")
if response.lower() == "y":
# Resume the session with an empty prompt, then rewind
async with ClaudeSDKClient(
ClaudeAgentOptions(enable_file_checkpointing=True, resume=session_id)
) as client:
await client.query("") # Empty prompt opens the connection
async for message in client.receive_response():
await client.rewind_files(checkpoint_id) # Restore files
break
print(
"\nโ File restored! Open utils.py to verify the doc comments are gone."
)
else:
print("\nKept the modified file.")
asyncio.run(main())
import { query } from "@anthropic-ai/claude-agent-sdk";
import * as readline from "readline";
async function main() {
// Configure the SDK with checkpointing enabled
// - enableFileCheckpointing: Track file changes for rewinding
// - permissionMode: Auto-accept file edits without prompting
// - extraArgs: Required to receive user message UUIDs in the stream
const opts = {
enableFileCheckpointing: true,
permissionMode: "acceptEdits" as const,
extraArgs: { "replay-user-messages": null }
};
let sessionId: string | undefined; // Store the session ID for resuming
let checkpointId: string | undefined; // Store the user message UUID for rewinding
console.log("Running agent to add doc comments to utils.ts...\n");
// Run the agent and capture checkpoint data from the response stream
const response = query({
prompt: "Add doc comments to utils.ts",
options: opts
});
for await (const message of response) {
// Capture the first user message UUID - this is our restore point
if (message.type === "user" && message.uuid && !checkpointId) {
checkpointId = message.uuid;
}
// Capture the session ID so we can resume later
if ("session_id" in message) {
sessionId = message.session_id;
}
}
console.log("Done! Open utils.ts to see the added doc comments.\n");
// Ask the user if they want to rewind the changes
if (checkpointId && sessionId) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const answer = await new Promise<string>((resolve) => {
rl.question("Rewind to remove the doc comments? (y/n): ", resolve);
});
rl.close();
if (answer.toLowerCase() === "y") {
// Resume the session with an empty prompt, then rewind
const rewindQuery = query({
prompt: "", // Empty prompt opens the connection
options: { ...opts, resume: sessionId }
});
for await (const msg of rewindQuery) {
await rewindQuery.rewindFiles(checkpointId); // Restore files
break;
}
console.log("\nโ File restored! Open utils.ts to verify the doc comments are gone.");
} else {
console.log("\nKept the modified file.");
}
}
}
main();
์ด ์์ ๋ ์์ ํ ์ฒดํฌํฌ์ธํ ์ํฌํ๋ก์ฐ๋ฅผ ๋ณด์ฌ์ค๋๋ค:
- ์ฒดํฌํฌ์ธํ
ํ์ฑํ:
enable_file_checkpointing=True๋ฐpermission_mode="acceptEdits"๋ก SDK๋ฅผ ๊ตฌ์ฑํ์ฌ ํ์ผ ํธ์ง์ ์๋์ผ๋ก ์น์ธํฉ๋๋ค - ์ฒดํฌํฌ์ธํธ ๋ฐ์ดํฐ ์บก์ฒ: ์์ด์ ํธ๊ฐ ์คํ๋๋ ๋์ ์ฒซ ๋ฒ์งธ ์ฌ์ฉ์ ๋ฉ์์ง UUID(๋ณต์ ์ง์ ) ๋ฐ ์ธ์ ID๋ฅผ ์ ์ฅํฉ๋๋ค
- ๋๋๋ฆฌ๊ธฐ ํ๋กฌํํธ: ์์ด์ ํธ๊ฐ ์๋ฃ๋ ํ ์ ํธ๋ฆฌํฐ ํ์ผ์ ํ์ธํ์ฌ ๋ฌธ์ ์ฃผ์์ ๋ณด๊ณ ๋ณ๊ฒฝ ์ฌํญ์ ์คํ ์ทจ์ํ ์ง ๊ฒฐ์ ํฉ๋๋ค
- ์ฌ๊ฐ ๋ฐ ๋๋๋ฆฌ๊ธฐ: ์์ธ ๊ฒฝ์ฐ ๋น ํ๋กฌํํธ๋ก ์ธ์
์ ์ฌ๊ฐํ๊ณ
rewind_files()๋ฅผ ํธ์ถํ์ฌ ์๋ณธ ํ์ผ์ ๋ณต์ํฉ๋๋ค
์์ ์คํ
์ ํธ๋ฆฌํฐ ํ์ผ๊ณผ ๊ฐ์ ๋๋ ํ ๋ฆฌ์์ ์คํฌ๋ฆฝํธ๋ฅผ ์คํํฉ๋๋ค.
์คํฌ๋ฆฝํธ๋ฅผ ์คํํ๊ธฐ ์ ์ IDE ๋๋ ํธ์ง๊ธฐ์์ ์ ํธ๋ฆฌํฐ ํ์ผ(utils.py ๋๋ utils.ts)์ ์ฝ๋๋ค. ์์ด์ ํธ๊ฐ ๋ฌธ์ ์ฃผ์์ ์ถ๊ฐํ ๋ ํ์ผ์ด ์ค์๊ฐ์ผ๋ก ์
๋ฐ์ดํธ๋๋ ๊ฒ์ ๋ณผ ์ ์์ผ๋ฉฐ, ๋๋๋ฆฌ๊ธฐ๋ฅผ ์ ํํ๋ฉด ์๋ณธ์ผ๋ก ๋๋์๊ฐ๋๋ค.
python try_checkpointing.py
npx tsx try_checkpointing.ts
์์ด์ ํธ๊ฐ ๋ฌธ์ ์ฃผ์์ ์ถ๊ฐํ ๋ค์ ๋๋๋ฆฌ๊ณ ์ถ์์ง ๋ฌป๋ ํ๋กฌํํธ๊ฐ ํ์๋ฉ๋๋ค. ์๋ฅผ ์ ํํ๋ฉด ํ์ผ์ด ์๋ณธ ์ํ๋ก ๋ณต์๋ฉ๋๋ค.
์ ํ ์ฌํญ
ํ์ผ ์ฒดํฌํฌ์ธํ ์๋ ๋ค์๊ณผ ๊ฐ์ ์ ํ ์ฌํญ์ด ์์ต๋๋ค:
| ์ ํ ์ฌํญ | ์ค๋ช |
|---|---|
| Write/Edit/NotebookEdit ๋๊ตฌ๋ง | Bash ๋ช ๋ น์ด๋ฅผ ํตํด ์ํ๋ ๋ณ๊ฒฝ ์ฌํญ์ ์ถ์ ๋์ง ์์ต๋๋ค |
| ๋์ผํ ์ธ์ | ์ฒดํฌํฌ์ธํธ๋ ์ด๋ฅผ ์์ฑํ ์ธ์ ์ ์ฐ๊ฒฐ๋ฉ๋๋ค |
| ํ์ผ ์ฝํ ์ธ ๋ง | ๋๋ ํ ๋ฆฌ ์์ฑ, ์ด๋ ๋๋ ์ญ์ ๋ ๋๋๋ฆฌ๊ธฐ๋ก ์คํ ์ทจ์๋์ง ์์ต๋๋ค |
| ๋ก์ปฌ ํ์ผ | ์๊ฒฉ ๋๋ ๋คํธ์ํฌ ํ์ผ์ ์ถ์ ๋์ง ์์ต๋๋ค |
๋ฌธ์ ํด๊ฒฐ
์ฒดํฌํฌ์ธํ ์ต์ ์ด ์ธ์๋์ง ์์
enableFileCheckpointing ๋๋ rewindFiles()๋ฅผ ์ฌ์ฉํ ์ ์๋ ๊ฒฝ์ฐ ์ด์ SDK ๋ฒ์ ์ ์ฌ์ฉ ์ค์ผ ์ ์์ต๋๋ค.
ํด๊ฒฐ์ฑ : ์ต์ SDK ๋ฒ์ ์ผ๋ก ์ ๋ฐ์ดํธํฉ๋๋ค:
- Python:
pip install --upgrade claude-agent-sdk - TypeScript:
npm install @anthropic-ai/claude-agent-sdk@latest
์ฌ์ฉ์ ๋ฉ์์ง์ UUID๊ฐ ์์
message.uuid๊ฐ undefined์ด๊ฑฐ๋ ๋๋ฝ๋ ๊ฒฝ์ฐ ์ฒดํฌํฌ์ธํธ UUID๋ฅผ ์์ ํ์ง ๋ชปํ๊ณ ์์ต๋๋ค.
์์ธ: replay-user-messages ์ต์
์ด ์ค์ ๋์ง ์์์ต๋๋ค.
ํด๊ฒฐ์ฑ
: ์ต์
์ extra_args={"replay-user-messages": None}(Python) ๋๋ extraArgs: { 'replay-user-messages': null }(TypeScript)์ ์ถ๊ฐํฉ๋๋ค.
"No file checkpoint found for message" ์ค๋ฅ
์ด ์ค๋ฅ๋ ์ง์ ๋ ์ฌ์ฉ์ ๋ฉ์์ง UUID์ ๋ํ ์ฒดํฌํฌ์ธํธ ๋ฐ์ดํฐ๊ฐ ์์ ๋ ๋ฐ์ํฉ๋๋ค.
์ผ๋ฐ์ ์ธ ์์ธ:
- ์๋ณธ ์ธ์
์์ ํ์ผ ์ฒดํฌํฌ์ธํ
์ด ํ์ฑํ๋์ง ์์์ต๋๋ค(
enable_file_checkpointing๋๋enableFileCheckpointing์ดtrue๋ก ์ค์ ๋์ง ์์) - ์ฌ๊ฐ ๋ฐ ๋๋๋ฆฌ๊ธฐ๋ฅผ ์๋ํ๊ธฐ ์ ์ ์ธ์ ์ด ์ ๋๋ก ์๋ฃ๋์ง ์์์ต๋๋ค
ํด๊ฒฐ์ฑ
: ์๋ณธ ์ธ์
์์ enable_file_checkpointing=True(Python) ๋๋ enableFileCheckpointing: true(TypeScript)๊ฐ ์ค์ ๋์๋์ง ํ์ธํ ๋ค์, ์์ ์ ํ์๋ ํจํด์ ์ฌ์ฉํฉ๋๋ค: ์ฒซ ๋ฒ์งธ ์ฌ์ฉ์ ๋ฉ์์ง UUID๋ฅผ ์บก์ฒํ๊ณ , ์ธ์
์ ์์ ํ ์๋ฃํ ๋ค์, ๋น ํ๋กฌํํธ๋ก ์ฌ๊ฐํ๊ณ rewindFiles()๋ฅผ ํ ๋ฒ ํธ์ถํฉ๋๋ค.
"ProcessTransport is not ready for writing" ์ค๋ฅ
์ด ์ค๋ฅ๋ ์๋ต์ ๋ฐ๋ณตํ๋ ๊ฒ์ ์๋ฃํ ํ rewindFiles() ๋๋ rewind_files()๋ฅผ ํธ์ถํ ๋ ๋ฐ์ํฉ๋๋ค. ๋ฃจํ๊ฐ ์๋ฃ๋๋ฉด CLI ํ๋ก์ธ์ค์ ๋ํ ์ฐ๊ฒฐ์ด ๋ซํ๋๋ค.
ํด๊ฒฐ์ฑ : ๋น ํ๋กฌํํธ๋ก ์ธ์ ์ ์ฌ๊ฐํ ๋ค์ ์ ์ฟผ๋ฆฌ์์ ๋๋๋ฆฌ๊ธฐ๋ฅผ ํธ์ถํฉ๋๋ค:
# Resume session with empty prompt, then rewind
async with ClaudeSDKClient(
ClaudeAgentOptions(enable_file_checkpointing=True, resume=session_id)
) as client:
await client.query("")
async for message in client.receive_response():
await client.rewind_files(checkpoint_id)
break
// Resume session with empty prompt, then rewind
const rewindQuery = query({
prompt: "",
options: { ...opts, resume: sessionId }
});
for await (const msg of rewindQuery) {
await rewindQuery.rewindFiles(checkpointId);
break;
}
๋ค์ ๋จ๊ณ
- ์ธ์ : ์ธ์ ์ ์ฌ๊ฐํ๋ ๋ฐฉ๋ฒ์ ์์๋ด ๋๋ค. ์ด๋ ์คํธ๋ฆผ์ด ์๋ฃ๋ ํ ๋๋๋ฆฌ๊ธฐ์ ํ์ํฉ๋๋ค. ์ธ์ ID, ๋ํ ์ฌ๊ฐ ๋ฐ ์ธ์ ํฌํน์ ๋ค๋ฃน๋๋ค.
- ๊ถํ: Claude๊ฐ ์ฌ์ฉํ ์ ์๋ ๋๊ตฌ์ ํ์ผ ์์ ์ด ์น์ธ๋๋ ๋ฐฉ์์ ๊ตฌ์ฑํฉ๋๋ค. ํธ์ง์ด ๋ฐ์ํ๋ ์๊ธฐ๋ฅผ ๋ ๋ง์ด ์ ์ดํ๋ ค๋ ๊ฒฝ์ฐ ์ ์ฉํฉ๋๋ค.
- TypeScript SDK ์ฐธ์กฐ:
query()๋ฐrewindFiles()๋ฉ์๋์ ๋ชจ๋ ์ต์ ์ ํฌํจํ ์์ ํ API ์ฐธ์กฐ์ ๋๋ค. - Python SDK ์ฐธ์กฐ:
ClaudeAgentOptions๋ฐrewind_files()๋ฉ์๋์ ๋ชจ๋ ์ต์ ์ ํฌํจํ ์์ ํ API ์ฐธ์กฐ์ ๋๋ค.