이번 글의 목적
M11에서는 새 기능을 만들지 않습니다.
대신 지금까지 만든 것을 README와 데모 흐름으로 정리합니다.
M0부터 M10까지의 작업은 작은 코딩 에이전트 하네스를 만드는 과정이었습니다.
프로젝트 골격을 만들고, Spring AI ChatClient를 붙이고, 메시지와 이벤트를 모델링했습니다.
그 뒤에는 JSONL session store, workspace guard, read-only tools, tool policy, Spring AI adapter, AgentRuntime, SSE, write/edit, bash까지 차례대로 올렸습니다.
여기까지 오면 기능은 어느 정도 보입니다.
사용자가 README만 보고 다음 질문에 답할 수 있어야 합니다.
무엇을 만들었는가?
왜 Spring AI로 만들었는가?
어떻게 실행하는가?
어떤 API를 호출하면 되는가?
어떤 tool이 있고, 무엇이 기본 비활성화인가?
AI가 위험한 일을 하지 않도록 어디서 막는가?
데모에서는 무엇을 보면 되는가?
다음에는 어디까지 확장할 것인가?
M11의 목표는 이 질문에 답하는 README를 만드는 일입니다.
README도 구현의 일부다
GitHub를 처음 방문한 사람은 대개 README를 먼저 봅니다.
특히 에이전트 프로젝트는 더 그렇습니다.
코드만 보면 구조를 파악하기 전에 이런 의문이 생깁니다.
이건 그냥 ChatClient 예제인가?
tool calling은 어디서 일어나는가?
파일 접근은 안전한가?
bash는 무조건 실행되는가?
테스트는 API key 없이 통과하는가?
실행 결과는 어디에 남는가?
M11에서는 README를 단순 사용 설명서가 아니라 프로젝트의 첫 번째 데모 화면으로 봅니다.
코드를 열기 전에 목적, 구조, 실행법, 안전 정책, 데모 시나리오를 한 번에 확인할 수 있어야 합니다.
현재 프로젝트 상태를 먼저 고정하기
M11 README의 첫 번째 역할은 현재 상태를 정확히 적는 일입니다.
이 프로젝트는 M0부터 M10까지 구현된 상태입니다.
Java 21
Spring Boot 4.1.0
Spring AI BOM 2.0.0
Spring WebFlux 기반 SSE endpoint
Gemini 기본 provider, OpenAI 전환 경로
JSONL file 기반 session store
Spring AI ToolCallback adapter
AgentRuntime + SSE event stream
ToolRegistry / ToolExecutionService / ToolPolicy
ls, read, find, grep, write, edit, bash
중요한 문장도 함께 둡니다.
테스트는 외부 LLM API를 호출하지 않는다.
API key가 없어도 ./gradlew test가 통과해야 한다.
AI 프로젝트에서 테스트가 실제 provider API에 묶이면, 독자가 clone한 뒤 바로 확인하기 어렵습니다.
CI에서도 흔들립니다. 이 프로젝트는 학습용이자 포트폴리오용이므로 기본 테스트는 deterministic해야 합니다.
실제 모델 호출에는 GEMINI_API_KEY 또는 OPENAI_API_KEY가 필요하지만, 테스트는 fake runtime/model 또는 dummy key로 통과해야 합니다.
구조는 한 번에 읽혀야 한다
README에는 전체 흐름을 코드보다 먼저 보여줍니다.
Client
-> POST /api/sessions
-> POST /api/sessions/{sessionId}/messages/stream
-> AgentRuntime
-> ModelClient
-> Spring AI ChatClient
-> SpringAiToolCallbackAdapter
-> ToolExecutionService
-> ToolPolicy
-> built-in tool
-> AgentEvent SSE + JSONL session history
이 그림은 멋진 아키텍처 다이어그램보다 실용적입니다.
Spring 개발자가 봤을 때 어디가 controller이고, 어디가 service orchestrator이고, 어디서 policy가 실행되는지 바로 따라갈 수 있습니다.
패키지 설명도 같은 이유로 둡니다.
agent -> AgentRuntime, run options, event/message domain
api -> REST/SSE controller
model -> Spring AI ChatClient wrapper와 tool callback adapter
session -> JSONL session store
tool -> built-in tools, registry, execution service, policy
workspace -> safe path resolver, secret file guard
코드를 읽기 전에 길을 잃지 않을 정도의 지도를 제공합니다.
실행 방법은 바로 복사할 수 있어야 한다
README에는 세 가지 명령이 분명히 있어야 합니다.
첫 번째는 테스트입니다.
./gradlew test
Windows에서는 다음 명령을 씁니다.
.\gradlew.bat test
두 번째는 Gemini 기본 실행입니다.
$env:GEMINI_API_KEY = "your-gemini-api-key"
$env:GEMINI_MODEL = "gemini-2.5-flash"
.\gradlew.bat bootRun
세 번째는 OpenAI 전환 실행입니다.
$env:AGENT_MODEL_PROVIDER = "openai"
$env:OPENAI_API_KEY = "your-openai-api-key"
$env:OPENAI_MODEL = "gpt-4.1-mini"
$env:AGENT_MODEL_NAME = "gpt-4.1-mini"
.\gradlew.bat bootRun
API key 없이 테스트가 통과한다는 설명과, 실제 model 호출에는 API key가 필요하다는 설명을 분리하는 것도 중요합니다.
둘을 섞으면 테스트와 실행의 책임이 흐려집니다.
API 사용법은 session 흐름으로 보여준다
M11 README는 /api/chat도 설명하지만, 핵심은 session 기반 agent 흐름입니다.
먼저 session을 만듭니다.
$session = Invoke-RestMethod `
-Method Post `
-Uri "http://localhost:8080/api/sessions" `
-ContentType "application/json" `
-Body '{"workspaceRoot":"."}'
그 다음 같은 session에 message stream 요청을 보냅니다.
Invoke-WebRequest `
-Method Post `
-Uri "http://localhost:8080/api/sessions/$($session.sessionId)/messages/stream" `
-ContentType "application/json" `
-Headers @{ Accept = "text/event-stream" } `
-Body '{"text":"프로젝트 구조를 간단히 설명해줘","allowWrite":false,"allowBash":false}'
응답은 SSE입니다.
event:agent_event
id:e_...
data:{"type":"agent_started",...}
여기서 allowWrite와 allowBash가 중요합니다.
둘 다 request 단위 옵션입니다.
생략하면 false입니다.
{
"text": "프로젝트 구조를 간단히 설명해줘",
"allowWrite": false,
"allowBash": false
}
이 기본값 때문에 agent는 처음에 read-only로 동작합니다.
tool 목록은 안전 수준과 함께 보여준다
tool 목록은 단순 목록이면 부족합니다.
어떤 tool이 기본 활성화인지, 어떤 옵션이 필요한지 같이 보여줘야 합니다.
ls -> 기본 활성화, workspace 내부 디렉터리 목록
read -> 기본 활성화, UTF-8 텍스트 파일 읽기
find -> 기본 활성화, glob 기반 파일 검색
grep -> 기본 활성화, literal substring 검색
write -> allowWrite=true 필요
edit -> allowWrite=true 필요
bash -> allowBash=true 필요
이 프로젝트의 기본 모드는 read-only입니다.
write, edit, bash는 모델에게 항상 노출되지 않습니다.
ToolRegistry가 AgentRunOptions를 보고 활성 tool 목록을 고릅니다.
그래도 이것만으로 충분하지 않습니다.
모델이 어떤 경로로든 실행 요청을 만들 수 있기 때문에 ToolExecutionService에서 ToolPolicy를 한 번 더 통과시킵니다.
ToolRegistry
-> 모델에 노출할 tool을 고른다
ToolExecutionService
-> 실제 실행 직전에 policy를 다시 확인한다
이중으로 막는 구조 덕분에 안전 정책을 README에서 설명하기 쉽습니다.
safety policy는 데모의 중심이다
이 프로젝트의 가장 중요한 이야기는 “AI가 tool을 쓴다”가 아닙니다.
“AI가 tool을 쓰더라도 정책 안에서만 쓴다”입니다.
README에는 workspace safety와 bash safety를 분리해서 적습니다.
workspace safety는 파일 접근을 다룹니다.
workspace root 밖 접근 차단
path traversal 차단
.env, *.pem, *.key, private key, .git, secrets.* 차단
large file 읽기 제한
binary 또는 invalid UTF-8 파일 읽기 차단
ls/find/grep 결과에서 secret path 제외
tool output length 제한
bash safety는 명령 실행을 다룹니다.
기본 비활성화
allowBash=true 필요
working directory는 workspace root로 고정
timeout 적용
stdout/stderr와 최종 tool output 길이 제한
destructive command 차단
remote script pipe 차단
secret path 참조 command 차단
이 정책들은 블로그에서 자세히 설명했던 M4, M6, M9, M10의 결과입니다.
M11에서는 그 조각을 README 안에서 한 번에 읽히도록 정리합니다.
SSE와 JSONL을 같이 설명해야 한다
agent 실행은 한 번의 응답으로 끝나지 않습니다.
도구 호출이 있고, 중간 결과가 있고, 실패가 있고, assistant 응답이 있습니다.
그래서 이 프로젝트는 두 가지 기록 방식을 함께 둡니다.
SSE -> 지금 무슨 일이 일어나는지 실시간으로 본다.
JSONL -> 나중에 어떤 일이 있었는지 다시 연다.
session directory는 다음처럼 생겼습니다.
.pi-spring-ai/sessions/{sessionId}/
session.json
messages.jsonl
events.jsonl
messages.jsonl에는 user, assistant, tool result가 남습니다.
events.jsonl에는 agent started, tool call started, tool call finished, tool denied 같은 실행 이벤트가 남습니다.
이 구분이 중요합니다.
message는 대화 기록입니다.
event는 관찰 가능한 실행 흔적입니다.
둘을 섞지 않으면 나중에 replay, debug, demo 설명이 쉬워집니다.
M11 README에서는 session 조회 API를 새로 만들지 않습니다.
현재 구현 기준으로는 JSONL 파일을 직접 확인한다고 설명합니다.
데모 시나리오 네 개
M11에서 README에 넣은 데모는 네 개입니다.
첫 번째는 read-only 분석입니다.
{
"text": "이 프로젝트의 구조와 핵심 패키지를 설명해줘",
"allowWrite": false,
"allowBash": false
}
이 데모에서 볼 것은 agent가 ls, read, find, grep만 사용해 프로젝트를 설명하는 흐름입니다.
write, edit, bash는 모델에 노출되지 않아야 합니다.
두 번째는 write/edit 명시 활성화입니다.
{
"text": "docs/demo-note.md 파일을 만들고, 오늘 데모에서 확인할 항목 3개를 적어줘",
"allowWrite": true,
"allowBash": false
}
이 데모에서는 쓰기 작업이 request에서 명시적으로 켰을 때만 가능하다는 점을 보여줍니다.
세 번째는 bash 명시 활성화입니다.
{
"text": "bash tool로 테스트 명령을 실행해줘. Windows면 .\\gradlew.bat test, Unix면 ./gradlew test를 사용해줘.",
"allowWrite": false,
"allowBash": true
}
여기서는 bash가 opt-in이고, workspace root에서 제한된 방식으로 실행된다는 점을 확인합니다.
네 번째는 위험 명령 차단입니다.
{
"text": "bash tool로 rm -rf . 명령이 실행 가능한지 확인해줘",
"allowWrite": false,
"allowBash": true
}
allowBash=true여도 destructive command는 실행되지 않아야 합니다.
BashCommandPolicy가 거부하고, 그 거부가 tool result, SSE event, JSONL history에 남아야 합니다.
포트폴리오에서 좋은 데모는 성공만 보여주지 않습니다.
위험한 요청이 어떻게 실패하는지 보여줍니다.
다음 확장 방향
M12. Tool safety hardening
M13. Session history/replay polish
M14. MCP client integration
M15. MCP server exposure
이 네 단계는 기존 구조를 버리지 않습니다.
AgentRuntime, ToolRegistry, ToolExecutionService, ToolPolicy, SSE, JSONL session을 유지한 채 더 안전하고 설명 가능하게 만드는 방향입니다.
반대로 다음 항목들은 옵션으로 미룹니다.
RAG/Vector DB
Tool Search Advisor
CLI/TUI/dashboard
LLM-as-Judge evaluation
session branch/fork
compaction
multi-agent
plugin/package system
이 항목들이 중요하지 않다는 뜻은 아닙니다.
지금까지 만든 것은 knowledge retrieval 시스템이 아니라 coding agent runtime입니다.
그래서 RAG와 Vector DB는 나중에 별도 흐름으로 다루는 편이 낫습니다.
MCP는 M14, M15에서 다룹니다.
다만 MCP가 기존 tool system을 대체하면 안 됩니다.
MCP는 adapter입니다. 외부 MCP tool을 쓰거나, 현재 built-in tool을 MCP server로 노출하더라도 기존 policy와 session 기록을 우회하면 안 됩니다.
마무리
M11은 첫 번째 설명 가능한 지점입니다.
무엇을 만들었고, 왜 그렇게 나누었고, 어디서 위험을 막고, 어떻게 다시 실행해 볼 수 있는지 설명할 수 있어야 합니다.
이 시리즈에서 만든 것은 거대한 agent platform이 아닙니다.
Spring 개발자가 이미 아는 service, policy, event stream, repository, test 개념으로 작은 coding agent runtime을 쌓은 결과입니다.
작게 붙이고, 위험한 기능은 기본으로 닫고, 모든 실행 흔적을 사람이 볼 수 있게 남기는 기준은 그대로, 다음 단계부터는 이 구조를 넓힙니다.
[codex] M11 README demo polish by dd3ok · Pull Request #14 · dd3ok/pi-spring-ai
'개발 > AI' 카테고리의 다른 글
| Spring AI로 Pi 스타일 에이전트 하네스 만들기 (13) - SSE는 실시간 관찰, JSONL은 기록 (0) | 2026.07.10 |
|---|---|
| Spring AI로 Pi 스타일 에이전트 하네스 만들기 (12) - Tool 사용을 더 안전하게 (0) | 2026.07.09 |
| Spring AI로 Pi 스타일 에이전트 하네스 만들기 (10) - 위험한 bash 다루기 (0) | 2026.07.07 |
| Spring AI로 Pi 스타일 에이전트 하네스 만들기 (9) - write/edit 붙이기 (0) | 2026.07.06 |
| Spring AI로 Pi 스타일 에이전트 하네스 만들기 (8) - AgentRuntime 만들고 SSE로 스트리밍하기 (0) | 2026.07.03 |