TrendShift 딥다이브 · 2026-07-03

huggingface/tau 딥다이브
— "읽어서 배우는" 미니멀 코딩 에이전트

tau는 터미널에서 파일을 읽고 고치고 명령을 실행하는 진짜 쓸 수 있는 코딩 에이전트이면서, 동시에 코딩 에이전트가 내부적으로 어떻게 만들어지는지 소스 코드로 직접 보여주는 교육용 프로젝트다. Claude Code나 opencode처럼 "쓰기 위한" 도구들과 달리, tau는 처음부터 "읽히기 위해" 설계됐다 — 3계층(tau_ai → tau_agent → tau_coding)으로 깔끔히 분리되고, 30개에 가까운 dev-notes/ 단계별 빌드 일지가 "왜 이렇게 만들었는지"까지 남아 있다. (저장소: huggingface/tau · MIT · ★333 · PyPI tau-ai · earendil-works/pi에서 영감을 받음)
목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

이 저장소가 대체 무엇인가.

핵심 메시지

"작지만 완결된 코딩 에이전트" — 실행도 되고, 통째로 읽고 이해할 수도 있는 두 마리 토끼.

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_coding → tau_agent → tau_ai

tau_ai는 여러 모델 프로바이더(OpenAI, Anthropic 등)의 서로 다른 API를 tau만의 프로바이더 중립 스트림으로 번역한다. tau_agent는 "이식 가능한 두뇌"를 담고 있다 — 메시지, 툴, 이벤트, 루프, 하니스, 세션 원시 타입까지 전부 CLI나 UI를 몰라도 되는 순수 로직이다. tau_coding은 그 두뇌를 실제 코딩 앱으로 감싼다 — CLI, TUI, 파일/셸 툴, 프로바이더 설정, 프로젝트 지침, 스킬, 디스크에 저장되는 세션까지.

용어
Pi 스타일 아키텍처
tau는 earendil-works/pi(비공개·유료 코딩 에이전트로 알려진 프로젝트)의 최소주의 하니스 설계에서 영감을 받았다고 명시한다(pyproject.toml 설명: "A Python implementation of a minimalist Pi-style coding-agent harness"). 즉 tau는 "Pi가 어떤 아이디어로 만들어졌을지"를 오픈소스·읽을 수 있는 형태로 재구현해 보여주는 프로젝트이기도 하다.

2왜 주목받는가

"교육용 코딩 에이전트"라는 드문 포지셔닝 · 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 조직이 교육용 프로젝트를 직접 호스팅
"코딩 에이전트가 어떻게 동작하는지"를 가르치는 데 Hugging Face 브랜드가 붙었다

Hugging Face는 원래 모델·데이터셋 허브로 유명하지만, tau처럼 "AI 시스템 자체의 내부 구조를 가르치는" 교육 프로젝트를 조직 산하에 두는 것은 커뮤니티에 강한 신호를 준다 — "에이전트를 잘 쓰는 법"뿐 아니라 "에이전트를 어떻게 만드는지"도 오픈 교육 자산으로 삼겠다는 방향성이다. README 첫 줄의 "a working example of how coding agents are built"이라는 문구가 이 프로젝트의 정체성을 정확히 요약한다.

주목 포인트 — "이벤트가 계약이다"라는 설계 철학을 코드로 증명
필로소피 섹션의 6개 원칙이 실제로 모든 소스 파일에서 지켜진다

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.pyTuiEventAdapter.apply()가 오직 이벤트 타입만으로 화면 상태를 갱신한다 — 선언이 코드로 그대로 지켜지는 드문 사례다.

기대치 조정 — "교육용"이 "장난감"을 의미하지는 않는다
Python 3.14+ 요구, 활발한 실사용 병행 개발

tau는 최소주의를 표방하지만 실제로는 30개의 pytest 테스트 파일, mypy strict 모드, ruff 린트, Textual 기반 완성형 TUI(tui/app.py 3,803줄)까지 갖춘 진지한 프로젝트다. 다만 pyproject.tomlrequires-python = ">=3.14"가 보여주듯 매우 최신 Python 버전을 요구하며, 클론해도 원격 저장소가 스쿼시/재작성을 거쳐 얕은 클론에는 커밋이 1개만 보일 수 있다(실제 GitHub 이력은 PR #213까지 이어지는 활발한 프로젝트). "며칠 안에 완성된 장난감"이 아니라 단계별로 25개 이상의 phase를 거쳐 구축된 시스템이라는 점을 염두에 두자.

3기술 스택 전체 지도

Python 3.14+, Textual TUI, Rich, uv, 프로바이더 5종, PyPI 배포 — 각각 상세.

pyproject.toml을 열어 보면 tau의 런타임 의존성이 놀랍도록 적다 — 딱 7개뿐이다. 이는 우연이 아니라 "작은 레이어가 마법을 이긴다"는 철학의 직접적 결과다.

레이어기술 / 버전역할
언어Python ≥ 3.142026년 기준 최신급 버전 요구. match 문, PEP 695 type 별칭 등 최신 문법 적극 사용
비동기 유틸anyio ≥ 4.0asyncio/trio 양쪽을 추상화한 비동기 프리미티브(락, 취소 등)
HTTP 클라이언트httpx ≥ 0.27모든 모델 프로바이더(OpenAI/Anthropic/OpenRouter 등)로의 스트리밍 요청
버전 파싱packaging ≥ 24.0update_check.py의 PyPI 최신 버전 비교
스키마 검증pydantic ≥ 2.0메시지·이벤트·툴콜·세션 엔트리 전체를 타입 안전 모델로 정의(extra="forbid" 엄격 모드)
터미널 렌더링rich ≥ 13.0print 모드의 마크다운·구문강조·표 렌더링
TUI 프레임워크textual ≥ 1.0완전한 인터랙티브 터미널 앱(tui/app.py, 3,803줄) — 위젯·오토컴플리트·마크다운 위젯 포함
CLI 프레임워크typer ≥ 0.12서브커맨드·옵션 파싱, tau 진입점 정의
프로바이더 — OpenAI 호환tau_ai/openai_compatible.pyOpenAI, OpenRouter, Hugging Face 추론, 커스텀 로컬 엔드포인트를 하나의 어댑터로 처리
프로바이더 — Anthropictau_ai/anthropic.pyClaude 계열 전용 스트리밍 어댑터(341줄)
프로바이더 — Codex 구독tau_ai/openai_codex.pyChatGPT/Codex 구독 OAuth 인증 기반 어댑터(773줄, 가장 큰 프로바이더 파일)
패키지 관리/빌드uv + hatchlinguv tool install tau-ai 한 줄 설치, PEP 517 빌드 백엔드
배포PyPI tau-aiproject.scriptstau = "tau_coding.cli:app"로 콘솔 스크립트 등록
테스트/린트/타입pytest + ruff + mypy(strict)30개 테스트 파일, ruff 규칙(E/F/I/UP/B/SIM), mypy strict 전체 패키지 적용
문서 사이트Astro + Starlight (+ bun)website/ — twotimespi.dev에 배포되는 별도 정적 문서 사이트
용어
프로바이더 중립(Provider-neutral)
OpenAI·Anthropic·OpenRouter는 각각 완전히 다른 API 스키마와 스트리밍 프로토콜을 쓴다. "프로바이더 중립"이란 이 차이를 tau_ai 계층 안에 완전히 가두고, 그 위의 tau_agent·tau_coding공통의 ProviderEvent 타입만 알면 되도록 만드는 설계다. 새 프로바이더를 추가하고 싶으면 ModelProvider 프로토콜(stream_response() 메서드 하나)만 구현하면 되고, 나머지 코드는 한 줄도 안 바뀐다.
용어
Textual (TUI 프레임워크)
터미널 안에서 마우스 클릭·스크롤·다중 패널이 가능한 리치한 UI를 만들게 해주는 파이썬 프레임워크다. 웹의 CSS와 비슷한 스타일링, 위젯 트리, 반응형 레이아웃을 터미널 안에 그대로 구현한다. tau의 ADR 0001은 "핵심 에이전트 하니스는 Textual을 몰라야 한다"를 명시적으로 못박는데, 실제로 tau_agent 패키지 어디에도 textual import가 없다 — 오직 tau_coding/tui/ 아래에서만 등장한다.

4아키텍처 심화 분석

tau_ai→tau_agent→tau_coding 3계층 · 이벤트 스트림 계약 · AgentHarness vs CodingSession vs TUI 경계 · 툴=타입드 함수 · JSONL 세션.

tau의 아키텍처를 이해하는 가장 좋은 방법은 "요청 하나가 어디를 거쳐 화면까지 도달하는가"를 실제 소스 코드로 따라가 보는 것이다. 이 절에서는 tau_agent/harness.py·loop.py·events.py·tools.pytau_coding/tui/adapter.py를 근거로 그 경로를 그려본다.

사용자 입력 ("이 레포 설명해줘") │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ tau_coding — 코딩 에이전트 애플리케이션 │ │ │ │ CLI(cli.py) 또는 TUI(tui/app.py) │ │ │ │ │ ▼ │ │ CodingSession (session.py, 1873줄) ─ AgentHarness를 감싼 래퍼 │ │ │ · 슬래시 커맨드 처리(commands.py) │ │ │ · 세션 JSONL 영속화 · 프로젝트 지침/스킬 로딩 │ │ │ · 컨텍스트 압축(context_window.py) │ └────────┼──────────────────────────────────────────────────────┘ │ harness.prompt(content) 호출 ▼ ┌──────────────────────────────────────────────────────────────┐ │ tau_agent — 이식 가능한 두뇌 (CLI/TUI/Textual/Rich 모름) │ │ │ │ AgentHarness (harness.py) ─ 트랜스크립트 소유 + 큐 관리 │ │ │ │ │ ▼ │ │ run_agent_loop() (loop.py) ─ 순수 함수, 상태 없음 │ │ │ │ │ │ ▼ provider.stream_response() ▼ tool.execute() │ └────────┼──────────────────────────────┼──────────────────────┘ │ │ ▼ ▼ ┌───────────────────────┐ ┌───────────────────────────────┐ │ tau_ai — 프로바이더 층 │ │ AgentTool (read/write/edit/bash)│ │ OpenAI·Anthropic·Codex │ │ 스키마 + 비동기 실행기 │ │ → ProviderEvent 스트림 │ └───────────────────────────────┘ └───────────────────────┘ │ ▼ (AgentEvent 스트림으로 변환되어 위로 흐름) ┌──────────────────────────────────────────────────────────────┐ │ 프론트엔드 — TuiEventAdapter.apply() / FinalTextRenderer 등 │ │ 이벤트 타입만 보고 화면 상태(TuiState)를 갱신 │ └──────────────────────────────────────────────────────────────┘

① 핵심 경계 — "두뇌 vs 코딩 환경 vs 프론트엔드"

README가 명시하는 문장을 그대로 옮기면: "AgentHarness = reusable brain, CodingSession = coding-agent environment, TUI = one possible frontend". 실제 코드에서 이 경계가 어떻게 지켜지는지 보면 — tau_agent/harness.pyAgentHarness 클래스는 메시지 리스트, 이벤트 리스너, 취소 토큰, 스티어링/팔로업 큐만 알면 된다. 이 클래스 안에는 파일 경로도, 터미널 렌더링도, 세션 JSONL 저장 로직도 전혀 없다. 반면 tau_coding/session.pyCodingSession같은 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.pyTuiEventAdapter.apply()가 정확히 보여준다. 이 메서드는 isinstance(event, ...) 체인으로 각 이벤트 타입을 분기해 TuiState(순수 데이터 클래스)를 갱신할 뿐, Textual 위젯을 직접 건드리지 않는다. 반대편에는 tau_coding/rendering/ 아래 FinalTextRenderer·JsonEventRenderer·TranscriptRenderer가 있는데, 이들은 같은 AgentEvent 스트림을 완전히 다른 방식(순수 텍스트/JSON/트랜스크립트)으로 소비한다 — "이벤트가 계약"이라는 말이 실제로 여러 프론트엔드가 같은 계약을 놓고 자유롭게 구현을 갈아 끼울 수 있다는 뜻임을 코드로 확인할 수 있다.

③ 프로바이더 경계 — ProviderEvent → AgentEvent 번역

tau_ai/events.pytau_agent/events.py별개의 이벤트 계층을 정의한다(ProviderResponseStartEvent, ProviderTextDeltaEvent, ProviderToolCallEvent 등). tau_agent/loop.pyrun_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.pyAgentTool은 놀랍도록 단순한 @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 딕셔너리로 이름만 보고 디스패치하므로, 툴 종류가 늘어나도 루프 코드는 단 한 줄도 바뀌지 않는다.

⑤ JSONL 세션 — append-only 트리 구조 + 재생(replay)

tau의 세션 저장 방식은 "히스토리를 통째로 덮어쓰는" 대신 추가만 하는(append-only) JSONL 파일이다. tau_agent/session/entries.py는 9가지 SessionEntry 타입(MessageEntry, ModelChangeEntry, CompactionEntry, BranchSummaryEntry, LeafEntry 등)을 정의하고, 각 엔트리는 idparent_id를 가진다 — 즉 세션 파일 하나가 사실은 트리(tree) 구조다. session/tree.pypath_to_entry()는 임의의 리프(leaf) id에서 부모를 따라 올라가며 루트까지의 경로를 복원한다.

JSONL 파일 (append-only, 시간순 기록) {"type":"session_info", "id":"a1", "parent_id":null, ...} {"type":"message", "id":"a2", "parent_id":"a1", "message":{"role":"user",...}} {"type":"message", "id":"a3", "parent_id":"a2", "message":{"role":"assistant",...}} {"type":"leaf", "id":"a4", "parent_id":"a3", "entry_id":"a3"} │ ▼ SessionState.from_entries(entries, leaf_id="a3") path_to_entry(): a3 → a2 → a1 → 역순 정렬 → [a1, a2, a3] │ ▼ 타입별로 재생(replay) SessionState(messages=(...), model=..., active_leaf_id="a3", ...)

이 구조 덕분에 세션 분기(branching)가 자연스럽게 가능하다 — 같은 parent_id를 가리키는 서로 다른 leaf_id 두 개는 "같은 지점에서 갈라진 두 개의 대화 흐름"이 된다. 파일은 절대 잘리거나 덮어써지지 않고, "지금 어느 리프에 있는지"만 LeafEntry로 추적한다. CompactionEntry는 이 append-only 원칙을 지키면서도 "요약으로 대체"를 구현하는 영리한 방법을 보여준다 — 원본 엔트리는 그대로 남기고, 재생(replay) 시점에만 replaces_entry_ids에 나열된 옛 메시지들을 요약 하나로 치환한다(session/memory.py_apply_compaction()).

용어
Append-only(추가 전용) 로그
데이터를 수정하거나 삭제하지 않고 오직 끝에 새 항목을 추가만 하는 저장 방식이다. 데이터베이스의 WAL(Write-Ahead Log), 이벤트 소싱(Event Sourcing) 패턴과 같은 계열이다. 장점은 "무슨 일이 있었는지"에 대한 완전한 이력이 항상 보존된다는 것 — tau에서는 압축(compaction)이 일어나도 원본 대화 내용이 파일에서 절대 사라지지 않고, 필요하면 언제든 원본으로 되돌아갈 수 있다.

5디렉토리 구조 해부

src/의 3패키지 실제 구성 + dev-notes/website/tests 역할.

tau/ ├── src/ │ ├── tau_ai/ # 프로바이더 층 (1,872줄, 8개 파일) │ │ ├── provider.py # ModelProvider 프로토콜 — stream_response() 계약 │ │ ├── events.py # ProviderEvent 유니언 (7종) │ │ ├── anthropic.py # AnthropicProvider (341줄) │ │ ├── openai_compatible.py # OpenAI/OpenRouter/HF/커스텀 엔드포인트 (369줄) │ │ ├── openai_codex.py # OpenAICodexProvider — ChatGPT 구독 OAuth (773줄 ★최대) │ │ ├── fake.py # FakeProvider — 테스트/실습용 가짜 프로바이더 │ │ ├── retry.py # 지수 백오프 재시도 로직 │ │ └── env.py # 환경변수 기반 설정 헬퍼 │ │ │ ├── tau_agent/ # 이식 가능한 두뇌 (1,337줄, 8개 파일) │ │ ├── messages.py # UserMessage/AssistantMessage/ToolResultMessage │ │ ├── tools.py # AgentTool, ToolCall, AgentToolResult │ │ ├── events.py # AgentEvent 유니언 (14종) │ │ ├── loop.py # run_agent_loop() — 순수 함수 루프 (274줄) │ │ ├── harness.py # AgentHarness, AgentHarnessConfig (297줄) │ │ ├── types.py # JSONValue/JSONObject/JSONPrimitive 별칭 │ │ └── session/ # append-only 세션 트리 (5개 파일) │ │ ├── entries.py # SessionEntry 9종 (message/compaction/leaf 등) │ │ ├── jsonl.py # JSONL 직렬화/역직렬화 │ │ ├── tree.py # path_to_entry() — 트리 순회 │ │ ├── memory.py # SessionState.from_entries() — 재생(replay) │ │ └── storage.py # JsonlSessionStorage — 디스크 append │ │ │ └── tau_coding/ # 코딩 앱 (16,418줄, 34개 파일 — 최대 패키지) │ ├── cli.py # typer 앱, tau 콘솔 스크립트 진입점 (539줄) │ ├── session.py # CodingSession — AgentHarness 래퍼 (1,873줄 ★최대) │ ├── commands.py # CommandRegistry — /login /model /compact 등 17개 (806줄) │ ├── tools.py # create_coding_tools() — read/write/edit/bash (1,055줄) │ ├── skills.py # 마크다운 스킬 로딩/확장 (/skill:name) │ ├── prompt_templates.py # 프롬프트 템플릿 확장 │ ├── context.py # AGENTS.md/.tau//.agents/ 프로젝트 지침 발견 │ ├── context_window.py # 컨텍스트 사용량 추정 + 압축 프롬프트 (283줄) │ ├── provider_config.py # 프로바이더 설정 저장/로드 (1,160줄) │ ├── provider_catalog.py # 내장 프로바이더 카탈로그(모델·컨텍스트창 메타데이터) │ ├── provider_runtime.py # ProviderConfig → 실제 ModelProvider 인스턴스화 │ ├── oauth.py # OpenAI Codex 구독 OAuth 플로우 (501줄) │ ├── session_manager.py # ~/.tau/sessions/ 세션 목록/이력 관리 │ ├── session_export.py # /export — HTML/JSONL 내보내기 (664줄) │ ├── branch_summary.py # 세션 분기 요약(모델 호출로 요약 생성) │ ├── system_prompt.py # 시스템 프롬프트 조립 │ ├── thinking.py # 사고 수준(off/low/medium/high/xhigh) 관리 │ ├── rendering/ # print 모드 렌더러 3종(text/json/transcript) │ └── tui/ # Textual TUI (5개 파일, 6,552줄) │ ├── app.py # TauApp — 메인 앱 (3,803줄 ★전체 최대) │ ├── widgets.py # 커스텀 위젯(채팅 버블·툴콜 카드 등, 1,373줄) │ ├── autocomplete.py # 슬래시 커맨드/파일경로 자동완성 (509줄) │ ├── state.py # TuiState — 순수 데이터 클래스 (376줄) │ └── adapter.py # TuiEventAdapter — 이벤트→상태 번역 (98줄) │ ├── dev-notes/ # "어떻게 만들어졌는가" 빌드 일지 │ ├── adr/ # 아키텍처 결정 기록 2건(Textual 채택, 툴 문서화 방침) │ ├── architecture/ # phase-1 ~ phase-25 단계별 문서 (30개 이상 파일) │ └── design/ # 초기 설계 스케치(로드맵·아키텍처·루프·툴·세션) │ ├── website/ # Astro + Starlight 공식 문서 (twotimespi.dev) │ └── src/content/docs/ # quickstart/concepts/internals/reference 20개 문서 │ ├── tests/ # pytest 30개 파일 — 하니스/루프/세션/TUI/CLI 전 계층 커버 ├── AGENTS.md # 프로젝트 자체의 에이전트 개발 지침(계층 규칙 명시) ├── pyproject.toml # hatchling 빌드 + 7개 런타임 의존성 + tau 스크립트 └── LICENSE # MIT (Copyright 2026 Alejandro AO)
한눈에 읽는 법

패키지 크기 순서(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부터 순서대로 읽으면 저자가 실제로 어떤 순서로 이 코드를 작성했는지도 그대로 따라갈 수 있다.

6학습 포인트 (기술별)

이 저장소에서 실제로 무엇을 배울 수 있는가 + 각 주제 실습 아이디어.

학습 1 · 에이전트 루프 — 상태 없는(stateless) 순수 함수로 짜는 법

"무한 while 루프 하나로 도구 호출·재시도·취소를 전부 처리한다"

tau_agent/loop.pyrun_agent_loop()은 클래스도 아니고 상태를 담는 객체도 없는 단일 비동기 제너레이터 함수다. 매개변수로 프로바이더·모델·시스템 프롬프트·메시지 리스트·툴 리스트를 전부 받아, max_turns까지 턴을 반복하며 이벤트를 yield한다. "왜 클래스로 안 만들었을까?"를 AgentHarness(상태를 갖는 클래스, harness.py)와 비교해보면, 순수 로직(loop.py)과 상태 관리(harness.py)를 의도적으로 분리했다는 게 보인다.

실습: tau_ai.FakeProvider를 사용해 run_agent_loop()을 직접 호출해 보고, 툴 호출이 없는 응답 → 대화가 바로 끝나는 경로와 툴 호출이 있는 응답 → _execute_tool_calls()가 실행되는 경로 두 가지를 각각 이벤트 리스트로 출력해 비교.

학습 2 · 툴 콜 아키텍처 — 스키마 + 비동기 실행기

"툴을 추가하는 데 에이전트 루프를 한 줄도 안 고쳐도 되는 이유"

tau_agent/tools.pyAgentTool dataclass와 tau_coding/tools.pycreate_read_tool_definition() 등 실제 팩토리 함수를 나란히 읽어보자. ToolExecutor 프로토콜이 "인자를 받아 AgentToolResult를 반환하는 비동기 콜러블"이라는 최소 계약만 강제하기 때문에, 파일시스템 툴이든 API 호출 툴이든 같은 방식으로 등록할 수 있다.

실습: tau의 4개 내장 툴(read/write/edit/bash) 외에 list_files라는 5번째 툴을 직접 설계해 보기 — input_schema(JSON 스키마), executor(비동기 함수), description을 채워 AgentTool 인스턴스를 만들고 create_coding_tools()가 반환하는 리스트에 추가.

학습 3 · 이벤트 기반 아키텍처 — 프론트엔드 독립성

"같은 이벤트 스트림을 텍스트로도, JSON으로도, TUI로도 그린다"

tau_coding/rendering/base.pyEventRenderer 프로토콜(render(event) + finish() 단 2개 메서드)과 tui/adapter.pyTuiEventAdapter.apply()를 비교해서 읽으면, "이벤트 소비자"가 얼마나 얇게 구현될 수 있는지 체감할 수 있다. FinalTextRenderer·JsonEventRenderer·TranscriptRenderer 세 렌더러의 render() 구현을 나란히 열어 같은 MessageDeltaEvent를 각각 어떻게 다르게 처리하는지 대조해 보자.

실습: 4번째 렌더러 MarkdownFileRenderer를 직접 만들어, 이벤트 스트림을 그대로 .md 파일로 받아쓰기(append)하는 최소 구현을 작성.

학습 4 · 컨텍스트 압축(compaction) — 토큰 예산 관리

"대화가 길어지면 오래된 메시지를 요약으로 접는다"

tau_coding/context_window.pyestimate_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()을 직접 호출해 본 뒤, 압축 전/후 메시지 개수와 내용이 어떻게 달라지는지 확인.

학습 5 · 세션 트리 & JSONL 영속화

"대화 이력을 삭제 없이, 분기까지 가능하게 저장하는 법"

tau_agent/session/entries.py의 9가지 SessionEntry 타입과 pydantic의 discriminator="type" 패턴(태그된 유니언)을 살펴보고, session/tree.pypath_to_entry()가 어떻게 순환(cycle) 감지까지 포함한 안전한 트리 순회를 구현하는지 읽어보자.

실습: JsonlSessionStorage를 사용해 메시지 3개를 저장한 뒤, 중간 지점에서 parent_id를 다르게 지정한 4번째 엔트리를 추가로 append해 "분기"를 인위적으로 만들고, 두 개의 서로 다른 leaf_idSessionState.from_entries()를 호출해 서로 다른 대화 흐름이 복원되는지 확인.

학습 6 · 프로바이더 어댑터 패턴

"프로토콜 하나(ModelProvider)로 완전히 다른 API 3개를 통일한다"

tau_ai/provider.pyModelProvider Protocol(stream_response() 메서드 하나)과, 이를 구현하는 AnthropicProvider·OpenAICompatibleProvider·OpenAICodexProvider 세 클래스를 비교해 보자. 특히 openai_codex.py(773줄, 가장 큰 프로바이더 파일)가 ChatGPT 구독 OAuth 인증까지 처리하는 걸 보면, "같은 인터페이스 뒤에 완전히 다른 복잡도의 구현이 숨을 수 있다"는 어댑터 패턴의 진가를 확인할 수 있다.

실습: tau_ai.FakeProvider의 소스(41줄, 가장 짧은 프로바이더)를 그대로 참고해, 고정된 문자열 응답을 스트리밍처럼 한 글자씩 나눠 ProviderTextDeltaEvent로 내보내는 나만의 미니 프로바이더를 작성.

7하드웨어 / 시스템 요구사항

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 — 상업적 사용·수정·재배포 모두 자유
가장 쉬운 시작 방법
API 키 하나만 있으면 5분 안에 실행 가능

다른 두 딥다이브 대상(대규모 GPU 학습 인프라)과 달리, tau는 노트북에서 바로 실행할 수 있는 몇 안 되는 "무거운 시스템" 학습 자료다. OpenAI든 Anthropic이든 API 키 하나만 있으면 uv tool install tau-aitau/login 세 단계로 전체 아키텍처를 실제로 체험하며 배울 수 있다. 로컬 모델(Ollama 등 OpenAI 호환 엔드포인트)을 쓰면 API 키 없이도 실습이 가능하다.

8직접 해볼 수 있는 실습 과제

난이도별 5단계 — 설치·사용부터 새 프로바이더 붙이기까지.

과제 1 · 설치하고 첫 대화 나눠보기 난이도 ★☆☆☆☆

가장 먼저 할 일이다. uv로 tau를 설치하고, 아무 프로젝트 폴더에서 실행해 /login으로 프로바이더를 연결한 뒤, "이 프로젝트 설명해줘" 같은 간단한 요청을 던져본다. 응답이 스트리밍되는 동안 터미널에 무엇이 표시되는지, 툴 호출(파일 읽기)이 어떻게 시각화되는지 관찰한다.

# 설치 & 실행
uv tool install tau-ai
cd my-project
tau

# TUI 안에서:
/login openai
explain what this project does

과제 2 · JSONL 세션 파일을 직접 열어 구조 파악하기 난이도 ★★☆☆☆

대화를 몇 차례 나눈 뒤 ~/.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

과제 3 · 새 코딩 툴 추가하기 (예: word_count) 난이도 ★★★☆☆

tau_coding/tools.pycreate_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")로 반환
    ...

과제 4 · 커스텀 이벤트 렌더러(프론트엔드) 만들기 난이도 ★★★★☆

tau_coding/rendering/plain.py(FinalTextRenderer)의 구조를 참고해, AgentEvent 스트림을 받아 이모지가 포함된 한 줄 로그(예: 툴 실행 시작 = 🔧, 에러 = ❌, 메시지 완료 = ✅)로 출력하는 나만의 EventRenderer를 구현한다. EventRenderer 프로토콜(render() + finish())만 지키면 되므로 기존 코드를 전혀 건드릴 필요가 없다는 걸 체감하는 게 목표.

과제 5 · 새 모델 프로바이더 어댑터 붙이기 난이도 ★★★★★

tau_ai/fake.py(가장 단순한 참고 구현, 41줄)와 tau_ai/openai_compatible.py를 함께 읽고, Google Gemini나 Mistral처럼 아직 지원하지 않는 프로바이더용 ModelProvider 구현체를 처음부터 작성해 본다. stream_response() 하나만 구현해 ProviderTextDeltaEvent·ProviderResponseEndEvent를 올바르게 내보내면, tau_agent/loop.py는 전혀 수정하지 않고도 새 프로바이더가 tau 전체와 즉시 호환된다는 걸 직접 확인한다.

9관련 기술 심화 학습 로드맵

에이전트 루프 기초 → 이벤트 아키텍처 → 세션/영속화 → 프로바이더 통합 — 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주차프로바이더 통합 & TUItau_ai/ 3개 프로바이더 비교, Textual 공식 튜토리얼 병행. Adapter 패턴 일반론 정리. 과제 5(새 프로바이더) 도전
학습 순서 팁

tau의 dev-notes/architecture/phase-1부터 phase-25까지가 사실상 이미 완성된 학습 로드맵이다. 위 5주 계획이 부담스럽다면, 매일 phase 문서 1~2개씩 읽고 해당 phase가 언급하는 소스 파일을 열어보는 것만으로도 충분한 커리큘럼이 된다. 순서상 phase 문서는 "왜"를 설명하고 소스 코드는 "어떻게"를 보여주므로, 항상 문서를 먼저 읽고 코드를 나중에 보는 순서를 권장한다 — README의 철학 그대로 "Documentation follows implementation"이지만, 학습자 입장에서는 거꾸로 읽는 게 이해가 빠르다.

10핵심 키워드 사전

이 문서와 저장소에 나온 주요 용어 정리.

용어
Agent Harness (에이전트 하니스)
모델 프로바이더·시스템 프롬프트·툴 목록·메시지 트랜스크립트를 하나로 묶어 관리하며, 프롬프트를 실행할 때마다 에이전트 루프를 구동하는 재사용 가능한 "두뇌" 객체. tau에서는 AgentHarness 클래스가 이 역할이며, CLI·TUI·자체 프론트엔드 어디서든 동일하게 재사용된다.
용어
Event Stream (이벤트 스트림) / "Events are the contract"
에이전트 내부에서 일어나는 모든 일(턴 시작, 텍스트 조각, 툴 실행, 에러 등)을 타입이 정해진 이벤트 객체로 순서대로 흘려보내는 방식. tau의 핵심 철학 중 하나로, 프로바이더·렌더러·TUI·커스텀 프론트엔드가 모두 이 이벤트 타입 집합에만 합의하면 서로 독립적으로 구현될 수 있다.
용어
Tool Schema (툴 스키마)
모델에게 "이런 이름의 함수를 이런 인자로 호출할 수 있다"고 알려주는 JSON 형식의 명세. tau의 AgentTool.input_schema가 이에 해당하며, 모델은 이 스키마를 보고 언제 어떤 인자로 툴을 호출할지 스스로 판단한다.
용어
TUI (Text-based User Interface, 텍스트 기반 사용자 인터페이스)
그래픽 없이 터미널 화면 안에서 마우스·키보드로 조작 가능한 인터랙티브 UI. tau는 Textual 프레임워크로 채팅 버블·툴콜 카드·자동완성을 갖춘 완전한 TUI를 구현하지만, 이는 "여러 프론트엔드 중 하나"일 뿐 핵심 로직에 필수는 아니다.
용어
Compaction (컨텍스트 압축)
대화가 길어져 모델의 컨텍스트 창(토큰 한도)에 다 담기 어려워질 때, 오래된 메시지들을 구조화된 요약 하나로 압축해 활성 컨텍스트 크기를 줄이는 기법. tau는 수동(/compact)과 자동(임계값 초과 시) 압축을 모두 지원하며, 원본 JSONL 기록은 절대 삭제하지 않는다.
용어
Provider-neutral (프로바이더 중립)
OpenAI·Anthropic 등 서로 다른 모델 API의 차이를 하나의 공통 인터페이스 뒤로 완전히 숨기는 설계 원칙. tau에서는 ModelProvider 프로토콜과 ProviderEvent 타입이 이 역할을 하며, 덕분에 상위 계층은 어떤 모델을 쓰는지 몰라도 된다.
용어
JSONL Session (JSONL 세션)
대화 이력을 한 줄에 JSON 객체 하나씩(JSON Lines 형식) 추가만 하며 저장하는 방식. tau는 ~/.tau/sessions/에 이 형식으로 세션을 영구 저장하며, 각 줄이 트리 구조의 노드(부모 id 포함)라서 이어하기(resume)와 분기(branching)가 모두 가능하다.
용어
Append-only (추가 전용)
기존 데이터를 수정·삭제하지 않고 새 항목만 끝에 덧붙이는 저장 방식. tau의 세션 파일이 이 원칙을 따르며, 압축(compaction)이 일어나도 원본 메시지는 파일에서 사라지지 않고 "재생 시점에만" 요약으로 치환된다.
용어
Agent Loop (에이전트 루프)
모델에게 메시지를 보내고, 응답에 툴 호출이 있으면 실행한 뒤 결과를 다시 모델에 보내는 과정을 (툴 호출이 없어질 때까지) 반복하는 핵심 제어 흐름. tau의 run_agent_loop()가 이 루프를 상태 없는 순수 비동기 제너레이터로 구현한다.
용어
Steering / Follow-up Message (스티어링 / 팔로업 메시지)
에이전트가 실행 중일 때 사용자가 추가로 입력한 메시지를 즉시 끼워넣지 않고 큐에 쌓아두는 두 가지 방식. "스티어링"은 현재 턴/툴 배치가 끝나는 즉시 주입되고, "팔로업"은 에이전트가 멈추려는 시점에만 주입된다 — tau가 실행 중에도 사용자 입력을 놓치지 않도록 하는 장치.
용어
ADR (Architecture Decision Record, 아키텍처 결정 기록)
"왜 이 기술/설계를 선택했는가"를 배경·결정·결과 형식으로 남기는 짧은 문서. tau의 dev-notes/adr/에는 "왜 Textual을 TUI로 선택했는가", "왜 툴 문서를 손으로 쓰는가" 같은 실제 결정 기록이 남아 있다.
용어
Pi (earendil-works/pi)
tau가 설계 철학의 영감을 받았다고 명시하는 프로젝트. "AgentHarness = 두뇌, AgentSession = 코딩 환경, TUI = 프론트엔드 중 하나"라는 계층 분리 아이디어의 원류로 언급된다. tau는 이 아이디어를 오픈소스·교육용으로 재구현한 프로젝트로 볼 수 있다.

11참고 링크

공식 저장소 · 문서 · 패키지 · 관련 프로젝트.

구분링크
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)