This repo try to provide a step-by-step knowledge and coding tutorial to build a coding agent NanaCode based on DeepSeek V4 models and Claude Code architecture.
An coding agent is composed of six components:
- Loop - the main place to continuously process the user queries, send context to models and return responsed
- Task - the working units that our agent relys on to finish its job
- Tools - where we provide definitions, permissions checks and everything related to tools that our agent can leverage, for example, edit a coding file, search materials on internet and .....
- State - a centralized place to store states to initialize our agent and managing runtime configs
- Memory - where to persistent context, skills across multiple user sessions
- Hooks - predefined lifecyle interceptors on each stage to execute customized logics, for example, check the permissions before a tool is called.
In step1/agent.py, we build a minimal Agent class that:
- Accepts a
nameandroleon initialization - Reads a DeepSeek API key from a
deepseek.keysfile in the project root - Maintains
conversation_historyto preserve context across turns - Sends user messages to
deepseek-v4-provia the OpenAI-compatible API with high reasoning effort - Runs a simple REPL loop (
start_loop) — typeexitorquitto stop
Setup:
-
Create a
deepseek.keysfile in the project root containing your DeepSeek API key. -
Install dependencies:
uv sync
-
Run the agent:
uv run step1/agent.py
-
Interact with the agent by typing messages. The agent will respond based on the DeepSeek model's output. Type
exitorquitto end the session.
user: I am Jaho, who are you
DeepSeekAgent (coding assistant): Hi Jaho! I'm DeepSeek, an AI assistant created by the company DeepSeek (深度求索).
I'm here to help you with questions, problem-solving, creative tasks, or just chatting. I'm free to use, and I can handle pretty long conversations (up to 1M tokens of context—think processing entire books at once!). I can also read uploaded files like PDFs, Word docs, Excel sheets, and images, though I can't generate images myself. I have web search capabilities if you turn them on manually, and there's a mobile app with voice input too.
What can I help you with today?
user: what is my name
DeepSeekAgent (coding assistant): Your name is Jaho! You introduced yourself at the start of our conversation. 😊In step2/, we introduce a proper system prompt that gives the agent a clear identity, behavioral guidelines, and awareness of its runtime environment.
New files:
prompt_template.py— definesSYSTEM_PROMPT_TEMPLATE, a detailed prompt covering core principles, task execution, safety rules, tone, and a dynamic# Environmentsection with placeholders.system_prompt.py— provides two functions:get_git_context(): reads the current repo name, branch, short status, and last 5 commits viagitsubprocesses.build_system_prompt(context): fills all placeholders ({{cwd}},{{date}},{{platform}},{{shell}},{{git_context}}, etc.) and returns the final prompt string.
agent.py is updated to call build_system_prompt({}) at initialization and prepend it as the system message in conversation_history.
Run the agent:
uv run step2/agent.py
NanaCode (coding agentß) is ready to receive messages.
user: what's the name of this repo
NanaCode (coding agentß): The repo name is `build-deepseek-coding-agent-from-scratch`.
user: who are you
NanaCode (coding agentß): I'm NanaCode, your CLI-based coding assistant.
user: reply with emoji
NanaCode (coding agentß): 👋
user: hello my friend
NanaCode (coding agentß): Hello, friend! 👋😊In step3/, we give the agent the ability to read, write, edit, search files, run shell commands, and browse the web — the foundation that turns a chatbot into a coding agent.
New files:
-
tools.py— definesTool(a function-calling tool wrapping a name, description, JSON Schema parameters, anexecute_fncallable, and an optionalformat_fnfor display) andToolRegistry(a singleton that registers tools, dispatches tool calls, and exports the OpenAI function-calling schema viato_openai_tools()). Seven default tools are registered, plus two internal helpers:Tool Description read_fileRead a file and return its content with 4-char line numbers write_fileWrite content to a file (creates or overwrites) list_filesList files in a directory (defaults to .)edit_fileReplace old_lineswithnew_linesin a file — usesnormalize_search(simple substring match) for concurrency-safe matching, then returns a unified diff viagenerate_diff_msggrep_searchSearch for a pattern in a file line-by-line run_commandRun a shell command via subprocess.check_outputto capture stdout/stderrweb_searchFetch a URL with urllib+User-Agentheader and parse with BeautifulSoup; falls back to raw HTML on parse failureEach tool's
format_fncontrols how the invocation is displayed (e.g.[tool] edit_file path.py (-3 lines, +5 lines)).Helper functions in
tools.py:normalize_search(source, target)— checks whethertargetis a substring ofsource; returnstargetif found (used byedit_filefor locating the old text before replacement).generate_diff_msg(old_lines, new_lines)— produces adiff -u-style unified diff viadifflib.unified_diffshowing exactly what changed.
-
permission.py— definesPermissionModeenum:DEFAULT,PLAN_ONLY,APPROVAL,ACCEPT_ALL,DONT_ASK, laying the groundwork for tool permission checks. -
system_prompt.py— updated to accept aToolRegistryand inject the tool definitions into the{{deferred_tools}}placeholder so the model knows what tools are available. -
prompt_template.py— added a# Tool usagesection that maps each tool to its conventional counterpart (e.g. "Use read_file instead of cat/head/tail", "Use edit_file (not write_file) for modifying existing files", "Use grep_search instead of grep/rg"). -
agent.py— updated to implement the tool-calling loop:- Sends the conversation + tool definitions to the model (with
thinkingmode enabled andreasoning_effort="high"). - Inspects the response for
tool_calls. - If present, executes each tool via
ToolRegistry.execute_tool()and appends the results astool-role messages back into the conversation. - Loops until the model responds with a plain text answer (no more tool calls).
- Also preserves
reasoning_contentfrom the DeepSeek thinking mode in the conversation history. - Displays a pyfiglet + ASCII-art banner on startup.
- Sends the conversation + tool definitions to the model (with
Run the agent:
uv run step3/agent.pyThe agent can now autonomously chain tool calls. For example:
uv run step3/agent.py
_ _ _ ____ _
_ \'-_,# | \ | | __ _ _ __ __ _ / ___|___ __| | ___
_\'--','`| | \| |/ _` | '_ \ / _` | | / _ \ / _` |/ _ \
\`---` / | |\ | (_| | | | | (_| | |__| (_) | (_| | __/
`----'` |_| \_|\__,_|_| |_|\__,_|\____\___/ \__,_|\___|
Welocome to NanaCode ! I am your coding assistant. How can I help you today?
>> list your tools
[NanaCode] >> Here are the tools I have access to:
| Tool | Description |
|------|-------------|
| `read_file` | Read the content of a file. Requires `file_path`. |
| `write_file` | Write content to a file. Requires `file_path` and `content`. |
| `edit_file` | Replace specific lines in an existing file. Requires `file_path`, `old_lines`, `new_lines`. |
| `list_files` | List files in a directory. `dir_path` is optional (defaults to `.`). |
| `grep_search` | Search for a pattern in a file. Requires `file_path` and `pattern`. |
| `run_command` | Run a shell command. Requires `command`. |
| `web_search` | Visit a URL and parse its content. Requires `url`. |
>> read step3 folder then update README.md accordingly
[tool] list_files step3
[tool] read_file README.md
[tool] read_file step3/tools.py
[tool] read_file step3/agent.py
[tool] read_file step3/permission.py
[tool] read_file step3/system_prompt.py
[tool] read_file step3/prompt_template.py
[tool] read_file step3/__init__.py
[tool] edit_file README.md (-10 lines, +11 lines)
[tool] read_file README.md
[tool] write_file README.md (151 lines)
[NanaCode] >> README.md updated with the following corrections:
1. **`Tool` class description** — now mentions the optional `format_fn` parameter and explains what it controls.
2. **`edit_file` description** — changed "uses `normalize_search` for concurrency-safe matching" to "uses `normalize_search` (simple substring match)" since it currently does not normalize special characters.
3. **`normalize_search` description** — accurately describes the current implementation: checks if `target` is a substring of `source`, returns it if found.
4. **`generate_diff_msg` description** — added mention of `difflib.unified_diff`.
5. **Example output** — fixed to match the actual code: `[tool] run_command $ date` instead of the verbose `[tool] calling ...` / `[tool] result: ...` format. The code only prints the compact `format_fn` output per `agent.py:78`.
>> exit
[NanaCode] >> Shutting down. Goodbye!In step4/, we add a real permission system that intercepts write operations and prompts the user before executing them — giving users control over what the agent can modify.
Key changes from step3:
-
tools.py— introducesOpTypeenum (READ/WRITE). EachToolnow carries anop_typefield (defaultREAD).write_file,edit_file, andrun_commandare markedWRITE.ToolRegistrygainsget_read_tools()andget_write_tools(). A_rel()helper converts absolute paths to relative for compact display informat_fn. -
permission.py— refinesPermissionModeto four modes:Mode Behavior DEFAULTPrompts user for each WRITE operation PLAN_ONLYBlocks all WRITE operations ACCEPT_EDITSAuto-approves edits to files the user has already accepted ACCEPT_ALLAuto-approves everything -
agent.py— implements the permission pipeline:permission_modeproperty (getter/setter) to switch modes at runtime._accepted_files: set— tracks files the user has approved for batch editing._pre_tool_hooks— a list of callables executed before each tool call; any hook returningFalseskips the tool.register_pre_tool_hook(fn)— registers a hook (called in__init__to wire upinspect_permission).inspect_permission(tc)— the core hook:- Non-WRITE tools always pass.
ACCEPT_ALLalways passes;PLAN_ONLYalways denies.ACCEPT_EDITSpasses only for files already in_accepted_files.- Otherwise displays an interactive prompt:
[a] Accept this tool call [b] Accept all edits for this file [c] Accept all edits for this session [d] Deny this tool call
send_message()runs all pre-tool hooks before executing each tool; denied calls append"Tool call denied by user."as the tool result.
Run the agent:
uv run step4/agent.pyExample interaction with permission prompts:
uv run step4/agent.py
_ _ _ ____ _
_ \'-_,# | \ | | __ _ _ __ __ _ / ___|___ __| | ___
_\'--','`| | \| |/ _` | '_ \ / _` | | / _ \ / _` |/ _ \
\`---` / | |\ | (_| | | | | (_| | |__| (_) | (_| | __/
`----'` |_| \_|\__,_|_| |_|\__,_|\____\___/ \__,_|\___|
Welocome to NanaCode ! I am your coding assistant. How can I help you today?
>> search github repo named build-deepseek-coding-agent-from-scratch
[tool] web_search https://github.com/search?q=build-deepseek-coding-agent-from-scratch&type=repositories
[NanaCode] >> GitHub 上搜索到 **1 个匹配仓库**:
| 仓库 | 描述 | 语言 | 更新 |
|------|------|------|------|
| [The-JiahaoJiang/build-deepseek-coding-agent-from-scratch](https://github.com/The-JiahaoJiang/build-deepseek-coding-agent-from-scratch) | A step-by-step knowledge and coding tutorial to build a coding agent based on DeepSeek V4 | Python | 23 小时前 |
这就是你当前工作目录所在的仓库。目前它是唯一一个公开的 `build-deepseek-coding-agent-from-scratch` 仓库,1 star,最近活跃。
>> read step4 folder and update README.md
[tool] list_files step4
[tool] read_file README.md
[tool] read_file step4/agent.py
[tool] read_file step4/tools.py
[tool] read_file step4/system_prompt.py
[tool] read_file step4/prompt_template.py
[tool] read_file step4/permission.py
[tool] read_file step4/__init__.py
[permission] Write operation requested:
[tool] edit_file README.md (-3 lines, +53 lines)
[a] Accept this tool call
[b] Accept all edits for this file (README.md)
[c] Accept all edits for this session
[d] Deny this tool call
Your choice (a/b/c/d): b
[tool] edit_file README.md (-3 lines, +53 lines)
[tool] grep_search README.md pattern='exit'
[tool] read_file README.md
[tool] edit_file README.md (-3 lines, +55 lines)
[NanaCode] >> README.md 已更新,新增了 **### 4. Implement permission system with interactive approval** 章节。主要内容:
- **`OpType` 枚举** — 将工具分为 `READ`/`WRITE` 两类
- **`PermissionMode` 细化** — 四种模式:`DEFAULT`、`PLAN_ONLY`、`ACCEPT_EDITS`、`ACCEPT_ALL`
- **`inspect_permission` 钩子** — 对 WRITE 操作弹出交互式审批对话框(a/b/c/d 四个选项)
- **`_pre_tool_hooks` 管道** — 工具执行前统一走权限检查
- **`_rel()` 辅助函数** — 工具调用显示时使用相对路径
>> exit
[NanaCode] >> Shutting down. Goodbye!5. Add a skill system with slash-command dispatch (This section was updated by NanaCode Skill: /step-updates )
In step5/, we introduce a skill system — reusable, loadable instruction packs stored as markdown files that the agent can pull in on demand via a load_skill tool and slash-command shortcuts.
New files:
skills.py— definesSkill(a dataclass with name, description, triggers, instructions, and file_path; parses YAML-like frontmatter from markdown files viafrom_file()) andSkillRegistry(a singleton that scans a directory forSKILL.mdfiles and provideslist_skills(),get_skill(),load_skill())..skills/step-updates/SKILL.md— the first skill, containing instructions for documenting new tutorial steps.
Key changes from step4:
skills.py—Skill.from_file()reads markdown files with----delimited frontmatter (name, description, triggers).SkillRegistry._scan()recursively findsSKILL.mdfiles and loads them. No external YAML dependency — the parser is a hand-rolled simple parser.tools.py—ToolRegistry.register_skill_tool(skill_registry)registers aload_skilltool (OpType.READ) and stores a reference to theSkillRegistryfor delegation. Newlist_skills()andget_skill_description()methods proxy to the stored registry.system_prompt.py—_build_skills_description()now queriesToolRegistry.list_skills()to populate the{{skills}}placeholder with actual skill names and descriptions (previously "Not available").agent.py— in__init__, scans.skills/and wires the registry into the tool system. Insend_message(), a slash-command parser intercepts messages starting with/skill-name, loads the skill instructions into conversation history as a system message, and forwards the remaining tokens.
Run the agent:
uv run step5/agent.pyExample interaction showing skill loading via slash-command:
❯ uv run ./step5/agent.py
_ _ _ ____ _
_ \'-_,# | \ | | __ _ _ __ __ _ / ___|___ __| | ___
_\'--','`| | \| |/ _` | '_ \ / _` | | / _ \ / _` |/ _ \
\`---` / | |\ | (_| | | | | (_| | |__| (_) | (_| | __/
`----'` |_| \_|\__,_|_| |_|\__,_|\____\___/ \__,_|\___|
Welocome to NanaCode ! I am your coding assistant. How can I help you today?
>> list your skills
[NanaCode] >> My available skills:
- **step-updates** — Find the latest stepN folder, then update README.md and all react-app pages accordingly.
>> /step-updates
[skill] loaded 'step-updates'
[skill] load_skill step-updates
[tool] list_files .
[tool] list_files step5
[tool] list_files step4
[tool] read_file step5/skills.py
[tool] read_file step5/permission.py
[tool] read_file step5/tools.py
[tool] read_file step5/system_prompt.py
[tool] read_file step5/agent.py
[tool] read_file step5/prompt_template.py
[tool] read_file step5/__init__.py
[tool] read_file README.md
[tool] read_file react-app/src/pages/Steps.tsx
[tool] read_file react-app/src/pages/Features.tsx
[tool] read_file react-app/src/pages/Home.tsx
[tool] list_files step5/.skills
[tool] list_files step5/.skills/step-updates
[tool] read_file step5/.skills/step-updates/SKILL.md
[permission] Write operation requested:
[tool] edit_file README.md (-3 lines, +31 lines)
[a] Accept this tool call
[b] Accept all edits for this file (README.md)
[c] Accept all edits for this session
[d] Deny this tool call
Your choice (a/b/c/d): c
[tool] edit_file README.md (-3 lines, +31 lines)
[tool] grep_search README.md pattern='\[NanaCode\] >> Shutting down'
[tool] edit_file README.md (-3 lines, +31 lines)
[tool] grep_search README.md pattern='Goodbye!'
[tool] read_file README.md
[tool] write_file README.md (306 lines)
[tool] edit_file react-app/src/pages/Steps.tsx (-1 lines, +30 lines)
[tool] edit_file react-app/src/pages/Steps.tsx (-1 lines, +1 lines)
[tool] edit_file react-app/src/pages/Steps.tsx (-2 lines, +29 lines)
[tool] edit_file react-app/src/pages/Features.tsx (-1 lines, +6 lines)
[tool] edit_file react-app/src/pages/Features.tsx (-1 lines, +1 lines)
[tool] edit_file react-app/src/pages/Features.tsx (-1 lines, +1 lines)
[tool] edit_file react-app/src/pages/Features.tsx (-1 lines, +1 lines)
[tool] edit_file react-app/src/pages/Features.tsx (-1 lines, +1 lines)
[tool] edit_file react-app/src/pages/Home.tsx (-1 lines, +1 lines)
[tool] edit_file react-app/src/pages/Home.tsx (-1 lines, +1 lines)
[tool] edit_file react-app/src/pages/Home.tsx (-2 lines, +6 lines)
[tool] edit_file react-app/src/pages/Home.tsx (-1 lines, +1 lines)
[tool] edit_file react-app/src/pages/Home.tsx (-1 lines, +1 lines)
[tool] edit_file react-app/src/pages/Home.tsx (-2 lines, +2 lines)
[tool] run_command $ cd /Users/jahojiang/Desktop/code/build-deepseek-coding-agent-from-scratch/react-app && npm run build 2>&1 | tail -20
[NanaCode] >> Build succeeds. Here's a summary of all changes made:
### README.md
- Added `### 5. Add a skill system with slash-command dispatch` section with description of new files (`skills.py`, `.skills/step-updates/SKILL.md`), key changes from step4, run command, and example shell interaction.
### react-app/src/pages/Steps.tsx
- Changed intro from "Three incremental steps" to "Five incremental steps"
- Added `STEP5_CODE` constant (~30 lines showing `SkillRegistry` wiring and slash-command parser)
- Added Step 5 block with tags: `SkillRegistry`, `load_skill`, `SKILL.md`, `frontmatter`, `slash-command`, `system message injection`
### react-app/src/pages/Features.tsx
- Added `load_skill` tool to the tools array (8 tools now, up from 7)
- Updated prompt snippet file reference: `step3/system_prompt.py` → `step5/system_prompt.py`
- Updated placeholder: `{{deferred_tools}}` → `{{tools}}` (matches step5's actual template)
- Updated intro text: "seven tools" → "eight tools"
### react-app/src/pages/Home.tsx
- Updated step count: "4 progressive steps" → "5 progressive steps"
- Added new card: "Skill system"
- Updated terminal bar label: `step3/agent.py` → `step5/agent.py`
- Updated badge: added "Skills"
- Updated subtitle: mentions "permission controls, and a pluggable skill system"
>> exit
[NanaCode] >> Shutting down. Goodbye!