dev-notes/ 단계별 빌드 일지가 "왜 이렇게 만들었는지"까지 남아 있다.
(저장소: huggingface/tau · MIT · ★333 · PyPI tau-ai · earendil-works/pi에서 영감을 받음)
이 저장소가 대체 무엇인가.
README 저장소 설명은 이렇게 못박는다: "A small, readable terminal coding agent — and a working example of how coding agents are built." tau는 alejandro-ao가 만들고 Hugging Face 조직 아래로 옮겨진 저장소로, 여러분이 터미널에서 "이 레포 설명해줘", "테스트 추가해줘", "이 에러 스택트레이스 고쳐줘" 같은 요청을 하면 파일을 읽고, 코드를 고치고, 명령을 실행하고, 그 과정을 세션 파일에 영구 저장하는 진짜 코딩 에이전트다.
비유하면: 세상에는 "잘 만들어진 시계"와 "뒷면이 유리로 되어 톱니바퀴가 다 보이는 시계"가 있다. 대부분의 코딩 에이전트(Claude Code, opencode 등)는 전자다 — 잘 작동하지만 내부 구현은 클로즈드거나 매우 크고 복잡해서 처음부터 끝까지 읽어내기 어렵다. tau는 후자를 목표로 한다. 실제로 시간을 알려주는(=코딩을 도와주는) 시계이면서, 톱니바퀴 하나하나(=harness.py, loop.py, events.py 같은 파일들)가 훤히 들여다보이도록 설계되어 있다.
tau의 코드는 3개의 계층으로 명확히 나뉜다. README가 그린 그림 그대로:
tau_ai는 여러 모델 프로바이더(OpenAI, Anthropic 등)의 서로 다른 API를 tau만의 프로바이더 중립 스트림으로 번역한다. tau_agent는 "이식 가능한 두뇌"를 담고 있다 — 메시지, 툴, 이벤트, 루프, 하니스, 세션 원시 타입까지 전부 CLI나 UI를 몰라도 되는 순수 로직이다. tau_coding은 그 두뇌를 실제 코딩 앱으로 감싼다 — CLI, TUI, 파일/셸 툴, 프로바이더 설정, 프로젝트 지침, 스킬, 디스크에 저장되는 세션까지.
pyproject.toml 설명: "A Python implementation of a minimalist Pi-style coding-agent harness"). 즉 tau는 "Pi가 어떤 아이디어로 만들어졌을지"를 오픈소스·읽을 수 있는 형태로 재구현해 보여주는 프로젝트이기도 하다."교육용 코딩 에이전트"라는 드문 포지셔닝 · Hugging Face의 후원 · Claude Code/opencode/pi 대비 위치.
2026년 코딩 에이전트 생태계는 이미 붐비다 못해 포화 상태다. Claude Code, opencode, Codex CLI, Aider, Cursor CLI 등 실전에서 쓰는 도구는 넘쳐난다. 그런데 tau가 TrendShift에서 주목받는 이유는 "실전에서 더 잘 쓰기 위한" 경쟁이 아니라 "어떻게 만들어지는지 배우기 위한" 경쟁에 참가했기 때문이다. 이 포지셔닝은 실제로 드물다.
| 비교 축 | Claude Code / opencode 등 | tau의 접근 |
|---|---|---|
| 주 목적 | 실전 생산성 — 최대한 많은 기능·프로바이더·통합 | 읽기 쉬움이 최우선 — 기능보다 "한 사람이 하루 만에 전체를 이해할 수 있는가" |
| 코드 규모 | 수만~수십만 줄, 다수의 팀이 유지 | 핵심 3패키지 약 19,600줄(tau_ai 1,872 + tau_agent 1,337 + tau_coding 16,418) — 한 사람이 통독 가능한 크기 |
| 문서화 | 사용자 가이드 위주 | "왜 이렇게 만들었는지" 단계별 빌드 일지 — dev-notes/architecture/phase-1~25 + ADR 2건 |
| 아키텍처 노출 | 내부 구현은 대체로 블랙박스 | "AgentHarness = 두뇌, CodingSession = 코딩 환경, TUI = 프론트엔드 중 하나"라는 경계를 README에서부터 선언 |
| 영감의 원천 | 독자적 설계 | earendil-works/pi(비공개 코딩 에이전트)의 설계 철학을 공개적으로 재구현 |
Hugging Face는 원래 모델·데이터셋 허브로 유명하지만, tau처럼 "AI 시스템 자체의 내부 구조를 가르치는" 교육 프로젝트를 조직 산하에 두는 것은 커뮤니티에 강한 신호를 준다 — "에이전트를 잘 쓰는 법"뿐 아니라 "에이전트를 어떻게 만드는지"도 오픈 교육 자산으로 삼겠다는 방향성이다. README 첫 줄의 "a working example of how coding agents are built"이라는 문구가 이 프로젝트의 정체성을 정확히 요약한다.
README의 Philosophy 섹션은 "Small layers beat magic", "Events are the contract", "The core stays portable", "Tools are ordinary typed functions", "Sessions are durable and inspectable", "Documentation follows implementation" 6개 원칙을 선언한다. 실제로 tau_agent/harness.py를 열어보면 Textual·Rich·CLI를 단 한 줄도 import하지 않고, tau_coding/tui/adapter.py의 TuiEventAdapter.apply()가 오직 이벤트 타입만으로 화면 상태를 갱신한다 — 선언이 코드로 그대로 지켜지는 드문 사례다.
tau는 최소주의를 표방하지만 실제로는 30개의 pytest 테스트 파일, mypy strict 모드, ruff 린트, Textual 기반 완성형 TUI(tui/app.py 3,803줄)까지 갖춘 진지한 프로젝트다. 다만 pyproject.toml의 requires-python = ">=3.14"가 보여주듯 매우 최신 Python 버전을 요구하며, 클론해도 원격 저장소가 스쿼시/재작성을 거쳐 얕은 클론에는 커밋이 1개만 보일 수 있다(실제 GitHub 이력은 PR #213까지 이어지는 활발한 프로젝트). "며칠 안에 완성된 장난감"이 아니라 단계별로 25개 이상의 phase를 거쳐 구축된 시스템이라는 점을 염두에 두자.
Python 3.14+, Textual TUI, Rich, uv, 프로바이더 5종, PyPI 배포 — 각각 상세.
pyproject.toml을 열어 보면 tau의 런타임 의존성이 놀랍도록 적다 — 딱 7개뿐이다. 이는 우연이 아니라 "작은 레이어가 마법을 이긴다"는 철학의 직접적 결과다.
| 레이어 | 기술 / 버전 | 역할 |
|---|---|---|
| 언어 | Python ≥ 3.14 | 2026년 기준 최신급 버전 요구. match 문, PEP 695 type 별칭 등 최신 문법 적극 사용 |
| 비동기 유틸 | anyio ≥ 4.0 | asyncio/trio 양쪽을 추상화한 비동기 프리미티브(락, 취소 등) |
| HTTP 클라이언트 | httpx ≥ 0.27 | 모든 모델 프로바이더(OpenAI/Anthropic/OpenRouter 등)로의 스트리밍 요청 |
| 버전 파싱 | packaging ≥ 24.0 | update_check.py의 PyPI 최신 버전 비교 |
| 스키마 검증 | pydantic ≥ 2.0 | 메시지·이벤트·툴콜·세션 엔트리 전체를 타입 안전 모델로 정의(extra="forbid" 엄격 모드) |
| 터미널 렌더링 | rich ≥ 13.0 | print 모드의 마크다운·구문강조·표 렌더링 |
| TUI 프레임워크 | textual ≥ 1.0 | 완전한 인터랙티브 터미널 앱(tui/app.py, 3,803줄) — 위젯·오토컴플리트·마크다운 위젯 포함 |
| CLI 프레임워크 | typer ≥ 0.12 | 서브커맨드·옵션 파싱, tau 진입점 정의 |
| 프로바이더 — OpenAI 호환 | tau_ai/openai_compatible.py | OpenAI, OpenRouter, Hugging Face 추론, 커스텀 로컬 엔드포인트를 하나의 어댑터로 처리 |
| 프로바이더 — Anthropic | tau_ai/anthropic.py | Claude 계열 전용 스트리밍 어댑터(341줄) |
| 프로바이더 — Codex 구독 | tau_ai/openai_codex.py | ChatGPT/Codex 구독 OAuth 인증 기반 어댑터(773줄, 가장 큰 프로바이더 파일) |
| 패키지 관리/빌드 | uv + hatchling | uv tool install tau-ai 한 줄 설치, PEP 517 빌드 백엔드 |
| 배포 | PyPI tau-ai | project.scripts가 tau = "tau_coding.cli:app"로 콘솔 스크립트 등록 |
| 테스트/린트/타입 | pytest + ruff + mypy(strict) | 30개 테스트 파일, ruff 규칙(E/F/I/UP/B/SIM), mypy strict 전체 패키지 적용 |
| 문서 사이트 | Astro + Starlight (+ bun) | website/ — twotimespi.dev에 배포되는 별도 정적 문서 사이트 |
tau_ai 계층 안에 완전히 가두고, 그 위의 tau_agent·tau_coding은 공통의 ProviderEvent 타입만 알면 되도록 만드는 설계다. 새 프로바이더를 추가하고 싶으면 ModelProvider 프로토콜(stream_response() 메서드 하나)만 구현하면 되고, 나머지 코드는 한 줄도 안 바뀐다.tau_agent 패키지 어디에도 textual import가 없다 — 오직 tau_coding/tui/ 아래에서만 등장한다.tau_ai→tau_agent→tau_coding 3계층 · 이벤트 스트림 계약 · AgentHarness vs CodingSession vs TUI 경계 · 툴=타입드 함수 · JSONL 세션.
tau의 아키텍처를 이해하는 가장 좋은 방법은 "요청 하나가 어디를 거쳐 화면까지 도달하는가"를 실제 소스 코드로 따라가 보는 것이다. 이 절에서는 tau_agent/harness.py·loop.py·events.py·tools.py와 tau_coding/tui/adapter.py를 근거로 그 경로를 그려본다.
README가 명시하는 문장을 그대로 옮기면: "AgentHarness = reusable brain, CodingSession = coding-agent environment, TUI = one possible frontend". 실제 코드에서 이 경계가 어떻게 지켜지는지 보면 — tau_agent/harness.py의 AgentHarness 클래스는 메시지 리스트, 이벤트 리스너, 취소 토큰, 스티어링/팔로업 큐만 알면 된다. 이 클래스 안에는 파일 경로도, 터미널 렌더링도, 세션 JSONL 저장 로직도 전혀 없다. 반면 tau_coding/session.py의 CodingSession은 같은 AgentHarness 인스턴스를 소유하면서 그 위에 프로젝트 지침 로딩, 스킬 확장, 슬래시 커맨드, JSONL 세션 저장, 자동 압축까지 코딩 앱에 필요한 모든 걸 덧붙인다.
AgentHarness는 "질문에 답하고 도구를 쓸 줄 아는 뇌"만 있는 사람이라고 생각하면 된다. 이 사람은 사무실이 어디 있는지, 회사 규정이 뭔지 전혀 모른다. CodingSession은 이 사람에게 사무실 열쇠, 회사 규정집(AGENTS.md), 업무 이력 서류철(JSONL 세션)을 쥐어주고 "이제 이 회사 직원으로 일해줘"라고 배치한 것이다. TUI는 이 직원과 대화하는 창구 하나일 뿐 — 전화로 대화하든(CLI print 모드), 화상회의로 대화하든(TUI), 이메일로 대화하든(JSON 출력 모드) 직원(두뇌) 자체는 똑같다.
tau_agent/events.py는 PEP 695 type 별칭으로 AgentEvent를 14가지 pydantic 모델의 합집합(union)으로 정의한다 — AgentStartEvent, TurnStartEvent, MessageDeltaEvent, ToolExecutionStartEvent, ToolExecutionEndEvent, ErrorEvent 등. 모든 이벤트가 model_config = ConfigDict(extra="forbid")를 쓴다는 점이 중요한데, 이는 "정의되지 않은 필드가 몰래 섞여 들어가는 것"을 pydantic이 즉시 막아준다는 뜻이다 — 이벤트 스키마가 사실상의 API 계약이 된다.
# tau_agent/events.py 실제 union 정의 (PEP 695 type 별칭)
type AgentEvent = (
AgentStartEvent | AgentEndEvent
| TurnStartEvent | TurnEndEvent
| QueueUpdateEvent | RetryEvent
| MessageStartEvent | MessageDeltaEvent | ThinkingDeltaEvent | MessageEndEvent
| ToolExecutionStartEvent | ToolExecutionUpdateEvent | ToolExecutionEndEvent
| ErrorEvent
)
이 계약이 실전에서 어떻게 소비되는지는 tau_coding/tui/adapter.py의 TuiEventAdapter.apply()가 정확히 보여준다. 이 메서드는 isinstance(event, ...) 체인으로 각 이벤트 타입을 분기해 TuiState(순수 데이터 클래스)를 갱신할 뿐, Textual 위젯을 직접 건드리지 않는다. 반대편에는 tau_coding/rendering/ 아래 FinalTextRenderer·JsonEventRenderer·TranscriptRenderer가 있는데, 이들은 같은 AgentEvent 스트림을 완전히 다른 방식(순수 텍스트/JSON/트랜스크립트)으로 소비한다 — "이벤트가 계약"이라는 말이 실제로 여러 프론트엔드가 같은 계약을 놓고 자유롭게 구현을 갈아 끼울 수 있다는 뜻임을 코드로 확인할 수 있다.
tau_ai/events.py는 tau_agent/events.py와 별개의 이벤트 계층을 정의한다(ProviderResponseStartEvent, ProviderTextDeltaEvent, ProviderToolCallEvent 등). tau_agent/loop.py의 run_agent_loop()가 바로 이 두 계층 사이의 번역자다 — provider.stream_response()가 내보내는 ProviderEvent를 하나씩 받아, isinstance 분기로 대응하는 AgentEvent로 변환해 yield한다. 왜 프로바이더 전용 이벤트와 에이전트 전용 이벤트를 분리했을까 — 프로바이더가 늘어나도(OpenAI, Anthropic, Codex, 커스텀 엔드포인트) 그 위의 에이전트/UI 계층 코드는 한 글자도 안 바뀌게 하기 위해서다.
// tau_agent/loop.py 핵심 번역 로직 (실제 코드 발췌)
async for provider_event in provider.stream_response(...):
if isinstance(provider_event, ProviderResponseStartEvent):
yield MessageStartEvent()
elif isinstance(provider_event, ProviderTextDeltaEvent):
yield MessageDeltaEvent(delta=provider_event.delta)
elif isinstance(provider_event, ProviderResponseEndEvent):
assistant_message = provider_event.message
messages.append(assistant_message) // 트랜스크립트는 loop가 직접 갱신
yield MessageEndEvent(message=assistant_message)
elif isinstance(provider_event, ProviderErrorEvent):
yield ErrorEvent(message=provider_event.message, recoverable=False)
tau_agent/tools.py의 AgentTool은 놀랍도록 단순한 @dataclass(frozen=True, slots=True)다 — name, description, input_schema(JSON 스키마), executor(비동기 콜러블) 4개 필드가 전부다. "툴은 그냥 평범한 타입드 함수"라는 철학이 그대로 타입 시그니처가 됐다. 실제 구현은 tau_coding/tools.py(1,055줄)의 create_coding_tools()가 read·write·edit·bash 4종을 생성하며, 각 팩토리 함수는 실행기 클로저 안에서 cwd 기준 경로 해석, 출력 바이트/라인 트렁케이션, 동시 쓰기 방지용 파일별 asyncio.Lock까지 처리한다.
새 툴(예: grep, web_fetch)을 추가하려면 에이전트 루프나 이벤트 시스템을 전혀 건드릴 필요 없이, AgentTool(name=..., description=..., input_schema=..., executor=...) 인스턴스 하나만 만들어 tools 리스트에 넣으면 끝난다. run_agent_loop()는 tool_by_name 딕셔너리로 이름만 보고 디스패치하므로, 툴 종류가 늘어나도 루프 코드는 단 한 줄도 바뀌지 않는다.
tau의 세션 저장 방식은 "히스토리를 통째로 덮어쓰는" 대신 추가만 하는(append-only) JSONL 파일이다. tau_agent/session/entries.py는 9가지 SessionEntry 타입(MessageEntry, ModelChangeEntry, CompactionEntry, BranchSummaryEntry, LeafEntry 등)을 정의하고, 각 엔트리는 id와 parent_id를 가진다 — 즉 세션 파일 하나가 사실은 트리(tree) 구조다. session/tree.py의 path_to_entry()는 임의의 리프(leaf) id에서 부모를 따라 올라가며 루트까지의 경로를 복원한다.
이 구조 덕분에 세션 분기(branching)가 자연스럽게 가능하다 — 같은 parent_id를 가리키는 서로 다른 leaf_id 두 개는 "같은 지점에서 갈라진 두 개의 대화 흐름"이 된다. 파일은 절대 잘리거나 덮어써지지 않고, "지금 어느 리프에 있는지"만 LeafEntry로 추적한다. CompactionEntry는 이 append-only 원칙을 지키면서도 "요약으로 대체"를 구현하는 영리한 방법을 보여준다 — 원본 엔트리는 그대로 남기고, 재생(replay) 시점에만 replaces_entry_ids에 나열된 옛 메시지들을 요약 하나로 치환한다(session/memory.py의 _apply_compaction()).
src/의 3패키지 실제 구성 + dev-notes/website/tests 역할.
패키지 크기 순서(tau_ai 1,872줄 → tau_agent 1,337줄 → tau_coding 16,418줄)가 역설적으로 "두뇌는 작을수록 좋다"는 설계를 보여준다. 가장 이식 가능해야 할 tau_agent가 가장 작고, 로컬 파일시스템·터미널·프로바이더 인증처럼 "지저분한 현실"을 다뤄야 하는 tau_coding이 가장 크다 — 이는 실패가 아니라 의도된 결과다. 처음 읽는다면 tau_agent/messages.py(가장 작고 근본적인 타입) → tau_agent/loop.py(핵심 루프) → tau_agent/harness.py(상태 관리) → tau_coding/session.py(실전 래퍼) → tau_coding/tui/adapter.py(이벤트 소비) 순서로 "두뇌에서 바깥으로" 읽어나가는 게 가장 자연스럽다. dev-notes/architecture/phase-1-core-types-and-events.md부터 순서대로 읽으면 저자가 실제로 어떤 순서로 이 코드를 작성했는지도 그대로 따라갈 수 있다.
이 저장소에서 실제로 무엇을 배울 수 있는가 + 각 주제 실습 아이디어.
tau_agent/loop.py의 run_agent_loop()은 클래스도 아니고 상태를 담는 객체도 없는 단일 비동기 제너레이터 함수다. 매개변수로 프로바이더·모델·시스템 프롬프트·메시지 리스트·툴 리스트를 전부 받아, max_turns까지 턴을 반복하며 이벤트를 yield한다. "왜 클래스로 안 만들었을까?"를 AgentHarness(상태를 갖는 클래스, harness.py)와 비교해보면, 순수 로직(loop.py)과 상태 관리(harness.py)를 의도적으로 분리했다는 게 보인다.
실습: tau_ai.FakeProvider를 사용해 run_agent_loop()을 직접 호출해 보고, 툴 호출이 없는 응답 → 대화가 바로 끝나는 경로와 툴 호출이 있는 응답 → _execute_tool_calls()가 실행되는 경로 두 가지를 각각 이벤트 리스트로 출력해 비교.
tau_agent/tools.py의 AgentTool dataclass와 tau_coding/tools.py의 create_read_tool_definition() 등 실제 팩토리 함수를 나란히 읽어보자. ToolExecutor 프로토콜이 "인자를 받아 AgentToolResult를 반환하는 비동기 콜러블"이라는 최소 계약만 강제하기 때문에, 파일시스템 툴이든 API 호출 툴이든 같은 방식으로 등록할 수 있다.
실습: tau의 4개 내장 툴(read/write/edit/bash) 외에 list_files라는 5번째 툴을 직접 설계해 보기 — input_schema(JSON 스키마), executor(비동기 함수), description을 채워 AgentTool 인스턴스를 만들고 create_coding_tools()가 반환하는 리스트에 추가.
tau_coding/rendering/base.py의 EventRenderer 프로토콜(render(event) + finish() 단 2개 메서드)과 tui/adapter.py의 TuiEventAdapter.apply()를 비교해서 읽으면, "이벤트 소비자"가 얼마나 얇게 구현될 수 있는지 체감할 수 있다. FinalTextRenderer·JsonEventRenderer·TranscriptRenderer 세 렌더러의 render() 구현을 나란히 열어 같은 MessageDeltaEvent를 각각 어떻게 다르게 처리하는지 대조해 보자.
실습: 4번째 렌더러 MarkdownFileRenderer를 직접 만들어, 이벤트 스트림을 그대로 .md 파일로 받아쓰기(append)하는 최소 구현을 작성.
tau_coding/context_window.py의 estimate_context_usage()가 문자 수 기반(CHARS_PER_TOKEN = 4)의 근사 토큰 계산을 어떻게 하는지, 그리고 SUMMARIZATION_PROMPT가 요구하는 "Goal / Progress / Key Decisions / Next Steps" 구조화된 요약 포맷을 살펴보자. tau_agent/session/memory.py의 _apply_compaction()이 원본 JSONL 엔트리는 그대로 두고 재생(replay) 시에만 압축을 적용한다는 점이 이 설계의 핵심이다.
실습: 임의의 AgentMessage 10개짜리 리스트를 만들고, CompactionEntry(replaces_entry_ids=[...])를 하나 추가해 _apply_compaction()을 직접 호출해 본 뒤, 압축 전/후 메시지 개수와 내용이 어떻게 달라지는지 확인.
tau_agent/session/entries.py의 9가지 SessionEntry 타입과 pydantic의 discriminator="type" 패턴(태그된 유니언)을 살펴보고, session/tree.py의 path_to_entry()가 어떻게 순환(cycle) 감지까지 포함한 안전한 트리 순회를 구현하는지 읽어보자.
실습: JsonlSessionStorage를 사용해 메시지 3개를 저장한 뒤, 중간 지점에서 parent_id를 다르게 지정한 4번째 엔트리를 추가로 append해 "분기"를 인위적으로 만들고, 두 개의 서로 다른 leaf_id로 SessionState.from_entries()를 호출해 서로 다른 대화 흐름이 복원되는지 확인.
tau_ai/provider.py의 ModelProvider Protocol(stream_response() 메서드 하나)과, 이를 구현하는 AnthropicProvider·OpenAICompatibleProvider·OpenAICodexProvider 세 클래스를 비교해 보자. 특히 openai_codex.py(773줄, 가장 큰 프로바이더 파일)가 ChatGPT 구독 OAuth 인증까지 처리하는 걸 보면, "같은 인터페이스 뒤에 완전히 다른 복잡도의 구현이 숨을 수 있다"는 어댑터 패턴의 진가를 확인할 수 있다.
실습: tau_ai.FakeProvider의 소스(41줄, 가장 짧은 프로바이더)를 그대로 참고해, 고정된 문자열 응답을 스트리밍처럼 한 글자씩 나눠 ProviderTextDeltaEvent로 내보내는 나만의 미니 프로바이더를 작성.
GPU 불필요 · 로컬 실행 중심 · 모델 프로바이더 API 키만 있으면 됨.
tau는 GPU도, 특별한 인프라도 필요 없는 순수 CPU·터미널 애플리케이션이다. 실제로 무거운 계산(LLM 추론)은 전부 원격 프로바이더 API가 담당하고, tau 자신은 파일 읽기/쓰기, 셸 명령 실행, HTTP 요청 정도의 가벼운 작업만 로컬에서 수행한다.
| 항목 | 요구사항 |
|---|---|
| Python | ≥ 3.14 — 2026년 기준으로도 최신급, 구버전 Python으로는 설치 자체가 안 됨 |
| 패키지 관리자 | uv 권장 (uv tool install tau-ai) — 없다면 curl -LsSf https://astral.sh/uv/install.sh | sh로 설치 |
| 운영체제 | 터미널/셸이 있는 환경이면 무관 (macOS/Linux/WSL) — Textual 자체는 크로스플랫폼 |
| GPU | 불필요 — 모델 추론은 전부 원격 프로바이더 API 호출로 처리 |
| 모델 프로바이더 | OpenAI, Anthropic, OpenAI Codex 구독, OpenRouter, Hugging Face 추론, 커스텀 OpenAI 호환 엔드포인트(로컬 모델 포함) 중 최소 1개의 API 키/인증 |
| 디스크 | 세션 JSONL 파일은 ~/.tau/sessions/에 누적 저장 — 텍스트 기반이라 용량 부담 적음 |
| 개발 시(선택) | 문서 사이트 빌드에는 bun 필요(website/ Astro 프로젝트) |
| 라이선스 | MIT — 상업적 사용·수정·재배포 모두 자유 |
다른 두 딥다이브 대상(대규모 GPU 학습 인프라)과 달리, tau는 노트북에서 바로 실행할 수 있는 몇 안 되는 "무거운 시스템" 학습 자료다. OpenAI든 Anthropic이든 API 키 하나만 있으면 uv tool install tau-ai → tau → /login 세 단계로 전체 아키텍처를 실제로 체험하며 배울 수 있다. 로컬 모델(Ollama 등 OpenAI 호환 엔드포인트)을 쓰면 API 키 없이도 실습이 가능하다.
난이도별 5단계 — 설치·사용부터 새 프로바이더 붙이기까지.
가장 먼저 할 일이다. uv로 tau를 설치하고, 아무 프로젝트 폴더에서 실행해 /login으로 프로바이더를 연결한 뒤, "이 프로젝트 설명해줘" 같은 간단한 요청을 던져본다. 응답이 스트리밍되는 동안 터미널에 무엇이 표시되는지, 툴 호출(파일 읽기)이 어떻게 시각화되는지 관찰한다.
# 설치 & 실행
uv tool install tau-ai
cd my-project
tau
# TUI 안에서:
/login openai
explain what this project does
대화를 몇 차례 나눈 뒤 ~/.tau/sessions/ 아래 생성된 .jsonl 파일을 텍스트 에디터로 열어본다. 각 줄이 {"type": "message", "id": ..., "parent_id": ...} 형태의 독립된 JSON 객체임을 확인하고, message·leaf·session_info 타입이 각각 몇 개씩 있는지 세어본다. /tree 슬래시 커맨드로 tau가 보여주는 트리 구조와 파일 내용을 대조.
# 세션 파일 위치 확인 & 내용 보기
ls ~/.tau/sessions/
cat ~/.tau/sessions/<session-id>.jsonl | python3 -m json.tool --json-lines
grep -o '"type":"[a-z_]*"' ~/.tau/sessions/<session-id>.jsonl | sort | uniq -c
tau_coding/tools.py의 create_read_tool_definition() 구조를 그대로 참고해, 파일 경로를 입력받아 단어 수를 세어 반환하는 word_count 툴을 직접 작성한다. ToolDefinition(스키마 + 실행기)을 만들고 create_coding_tools()가 반환하는 리스트에 추가한 뒤, 실제로 tau를 재빌드해 "이 파일 단어 수 세어줘"라고 요청해 새 툴이 호출되는지 확인.
// 직접 구현해 볼 시그니처 (tau_coding/tools.py 패턴 참고)
def create_word_count_tool_definition(*, cwd: Path | None = None) -> ToolDefinition:
# input_schema: {"path": str}
# executor: 파일을 읽어 단어 수를 세고 AgentToolResult(ok=True, content=f"{n} words")로 반환
...
tau_coding/rendering/plain.py(FinalTextRenderer)의 구조를 참고해, AgentEvent 스트림을 받아 이모지가 포함된 한 줄 로그(예: 툴 실행 시작 = 🔧, 에러 = ❌, 메시지 완료 = ✅)로 출력하는 나만의 EventRenderer를 구현한다. EventRenderer 프로토콜(render() + finish())만 지키면 되므로 기존 코드를 전혀 건드릴 필요가 없다는 걸 체감하는 게 목표.
tau_ai/fake.py(가장 단순한 참고 구현, 41줄)와 tau_ai/openai_compatible.py를 함께 읽고, Google Gemini나 Mistral처럼 아직 지원하지 않는 프로바이더용 ModelProvider 구현체를 처음부터 작성해 본다. stream_response() 하나만 구현해 ProviderTextDeltaEvent·ProviderResponseEndEvent를 올바르게 내보내면, tau_agent/loop.py는 전혀 수정하지 않고도 새 프로바이더가 tau 전체와 즉시 호환된다는 걸 직접 확인한다.
에이전트 루프 기초 → 이벤트 아키텍처 → 세션/영속화 → 프로바이더 통합 — 5주 계획.
| 주차 | 주제 | 할 일 |
|---|---|---|
| 1주차 | 에이전트 루프 & 메시지 모델 | tau_agent/messages.py·loop.py 정독. "왜 루프가 상태 없는 순수 함수인가" 이해. 과제 1(설치·첫 대화) 완주 |
| 2주차 | 툴 콜 & 스키마 설계 | tau_agent/tools.py·tau_coding/tools.py 비교 정독. JSON 스키마로 함수를 기술하는 법(OpenAI function calling 개념) 함께 학습. 과제 3(새 툴 추가) 완주 |
| 3주차 | 이벤트 기반 아키텍처 | tau_agent/events.py·tui/adapter.py·rendering/ 3종 렌더러 비교. Observer 패턴·pub-sub 아키텍처 일반론과 연결. 과제 4(커스텀 렌더러) 완주 |
| 4주차 | 세션 영속화 & 압축 | session/entries.py·tree.py·memory.py 정독. Event Sourcing 패턴 개념 학습 + context_window.py의 압축 프롬프트 분석. 과제 2(JSONL 구조 파악) 완주 |
| 5주차 | 프로바이더 통합 & TUI | tau_ai/ 3개 프로바이더 비교, Textual 공식 튜토리얼 병행. Adapter 패턴 일반론 정리. 과제 5(새 프로바이더) 도전 |
tau의 dev-notes/architecture/phase-1부터 phase-25까지가 사실상 이미 완성된 학습 로드맵이다. 위 5주 계획이 부담스럽다면, 매일 phase 문서 1~2개씩 읽고 해당 phase가 언급하는 소스 파일을 열어보는 것만으로도 충분한 커리큘럼이 된다. 순서상 phase 문서는 "왜"를 설명하고 소스 코드는 "어떻게"를 보여주므로, 항상 문서를 먼저 읽고 코드를 나중에 보는 순서를 권장한다 — README의 철학 그대로 "Documentation follows implementation"이지만, 학습자 입장에서는 거꾸로 읽는 게 이해가 빠르다.
이 문서와 저장소에 나온 주요 용어 정리.
AgentHarness 클래스가 이 역할이며, CLI·TUI·자체 프론트엔드 어디서든 동일하게 재사용된다.AgentTool.input_schema가 이에 해당하며, 모델은 이 스키마를 보고 언제 어떤 인자로 툴을 호출할지 스스로 판단한다./compact)과 자동(임계값 초과 시) 압축을 모두 지원하며, 원본 JSONL 기록은 절대 삭제하지 않는다.ModelProvider 프로토콜과 ProviderEvent 타입이 이 역할을 하며, 덕분에 상위 계층은 어떤 모델을 쓰는지 몰라도 된다.~/.tau/sessions/에 이 형식으로 세션을 영구 저장하며, 각 줄이 트리 구조의 노드(부모 id 포함)라서 이어하기(resume)와 분기(branching)가 모두 가능하다.run_agent_loop()가 이 루프를 상태 없는 순수 비동기 제너레이터로 구현한다.dev-notes/adr/에는 "왜 Textual을 TUI로 선택했는가", "왜 툴 문서를 손으로 쓰는가" 같은 실제 결정 기록이 남아 있다.공식 저장소 · 문서 · 패키지 · 관련 프로젝트.
| 구분 | 링크 |
|---|---|
| GitHub 저장소 | github.com/huggingface/tau |
| 공식 문서 | twotimespi.dev |
| 퀵스타트 | twotimespi.dev/quickstart |
| 아키텍처 개요 | twotimespi.dev/internals/architecture |
| 에이전트 루프 & 이벤트 문서 | twotimespi.dev/internals/agent-loop |
| PyPI 패키지 | pypi.org/project/tau-ai |
| 로드맵(GitHub 이슈 #1) | github.com/alejandro-ao/tau/issues/1 |
| 영감을 받은 프로젝트 | github.com/earendil-works/pi |
| 라이선스 | MIT (상업적 이용 가능, Copyright 2026 Alejandro AO) |