mirror of
https://github.com/shareAI-lab/learn-claude-code.git
synced 2026-04-28 06:19:32 +00:00
better doc
This commit is contained in:
parent
aea8844bac
commit
665831c774
46 changed files with 1217 additions and 3505 deletions
|
|
@ -1,47 +1,43 @@
|
|||
# s02: Tools
|
||||
# s02: Tool Use
|
||||
|
||||
> A dispatch map routes tool calls to handler functions. The loop stays identical.
|
||||
`s01 > [ s02 ] s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`
|
||||
|
||||
## The Problem
|
||||
> *"The loop didn't change"* -- adding tools means adding handlers, not rewriting the loop.
|
||||
|
||||
With only `bash`, the agent shells out for everything: reading files,
|
||||
writing files, editing files. This works but is fragile. `cat` output
|
||||
gets truncated unpredictably. `sed` replacements fail on special
|
||||
characters. The model wastes tokens constructing shell pipelines when
|
||||
a direct function call would be simpler.
|
||||
## Problem
|
||||
|
||||
More importantly, bash is a security surface. Every bash call can do
|
||||
anything the shell can do. With dedicated tools like `read_file` and
|
||||
`write_file`, you can enforce path sandboxing and block dangerous
|
||||
patterns at the tool level rather than hoping the model avoids them.
|
||||
With only `bash`, the agent shells out for everything. `cat` truncates unpredictably, `sed` fails on special characters, and every bash call is an unconstrained security surface. Dedicated tools like `read_file` and `write_file` let you enforce path sandboxing at the tool level.
|
||||
|
||||
The insight is that adding tools does not require changing the loop.
|
||||
The loop from s01 stays identical. You add entries to the tools array,
|
||||
add handler functions, and wire them together with a dispatch map.
|
||||
The key insight: adding tools does not require changing the loop.
|
||||
|
||||
## The Solution
|
||||
## Solution
|
||||
|
||||
```
|
||||
+----------+ +-------+ +------------------+
|
||||
| User | ---> | LLM | ---> | Tool Dispatch |
|
||||
| prompt | | | | { |
|
||||
+----------+ +---+---+ | bash: run_bash |
|
||||
^ | read: run_read |
|
||||
| | write: run_wr |
|
||||
+----------+ edit: run_edit |
|
||||
tool_result| } |
|
||||
+------------------+
|
||||
+--------+ +-------+ +------------------+
|
||||
| User | ---> | LLM | ---> | Tool Dispatch |
|
||||
| prompt | | | | { |
|
||||
+--------+ +---+---+ | bash: run_bash |
|
||||
^ | read: run_read |
|
||||
| | write: run_wr |
|
||||
+-----------+ edit: run_edit |
|
||||
tool_result | } |
|
||||
+------------------+
|
||||
|
||||
The dispatch map is a dict: {tool_name: handler_function}
|
||||
The dispatch map is a dict: {tool_name: handler_function}.
|
||||
One lookup replaces any if/elif chain.
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. Define handler functions for each tool. Each takes keyword arguments
|
||||
matching the tool's input_schema and returns a string result.
|
||||
1. Each tool gets a handler function. Path sandboxing prevents workspace escape.
|
||||
|
||||
```python
|
||||
def safe_path(p: str) -> Path:
|
||||
path = (WORKDIR / p).resolve()
|
||||
if not path.is_relative_to(WORKDIR):
|
||||
raise ValueError(f"Path escapes workspace: {p}")
|
||||
return path
|
||||
|
||||
def run_read(path: str, limit: int = None) -> str:
|
||||
text = safe_path(path).read_text()
|
||||
lines = text.splitlines()
|
||||
|
|
@ -50,7 +46,7 @@ def run_read(path: str, limit: int = None) -> str:
|
|||
return "\n".join(lines)[:50000]
|
||||
```
|
||||
|
||||
2. Create the dispatch map linking tool names to handlers.
|
||||
2. The dispatch map links tool names to handlers.
|
||||
|
||||
```python
|
||||
TOOL_HANDLERS = {
|
||||
|
|
@ -62,13 +58,14 @@ TOOL_HANDLERS = {
|
|||
}
|
||||
```
|
||||
|
||||
3. In the agent loop, look up the handler by name instead of hardcoding.
|
||||
3. In the loop, look up the handler by name. The loop body itself is unchanged from s01.
|
||||
|
||||
```python
|
||||
for block in response.content:
|
||||
if block.type == "tool_use":
|
||||
handler = TOOL_HANDLERS.get(block.name)
|
||||
output = handler(**block.input)
|
||||
output = handler(**block.input) if handler \
|
||||
else f"Unknown tool: {block.name}"
|
||||
results.append({
|
||||
"type": "tool_result",
|
||||
"tool_use_id": block.id,
|
||||
|
|
@ -76,51 +73,7 @@ for block in response.content:
|
|||
})
|
||||
```
|
||||
|
||||
4. Path sandboxing prevents the model from escaping the workspace.
|
||||
|
||||
```python
|
||||
def safe_path(p: str) -> Path:
|
||||
path = (WORKDIR / p).resolve()
|
||||
if not path.is_relative_to(WORKDIR):
|
||||
raise ValueError(f"Path escapes workspace: {p}")
|
||||
return path
|
||||
```
|
||||
|
||||
## Key Code
|
||||
|
||||
The dispatch pattern (from `agents/s02_tool_use.py`, lines 93-129):
|
||||
|
||||
```python
|
||||
TOOL_HANDLERS = {
|
||||
"bash": lambda **kw: run_bash(kw["command"]),
|
||||
"read_file": lambda **kw: run_read(kw["path"], kw.get("limit")),
|
||||
"write_file": lambda **kw: run_write(kw["path"], kw["content"]),
|
||||
"edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"],
|
||||
kw["new_text"]),
|
||||
}
|
||||
|
||||
def agent_loop(messages: list):
|
||||
while True:
|
||||
response = client.messages.create(
|
||||
model=MODEL, system=SYSTEM, messages=messages,
|
||||
tools=TOOLS, max_tokens=8000,
|
||||
)
|
||||
messages.append({"role": "assistant", "content": response.content})
|
||||
if response.stop_reason != "tool_use":
|
||||
return
|
||||
results = []
|
||||
for block in response.content:
|
||||
if block.type == "tool_use":
|
||||
handler = TOOL_HANDLERS.get(block.name)
|
||||
output = handler(**block.input) if handler \
|
||||
else f"Unknown tool: {block.name}"
|
||||
results.append({
|
||||
"type": "tool_result",
|
||||
"tool_use_id": block.id,
|
||||
"content": output,
|
||||
})
|
||||
messages.append({"role": "user", "content": results})
|
||||
```
|
||||
Add a tool = add a handler + add a schema entry. The loop never changes.
|
||||
|
||||
## What Changed From s01
|
||||
|
||||
|
|
@ -131,10 +84,6 @@ def agent_loop(messages: list):
|
|||
| Path safety | None | `safe_path()` sandbox |
|
||||
| Agent loop | Unchanged | Unchanged |
|
||||
|
||||
## Design Rationale
|
||||
|
||||
The dispatch map scales linearly: add a tool, add a handler, add a schema entry. The loop never changes. Handlers are pure functions, so they test in isolation. Any agent that outgrows a dispatch map has a design problem, not a scaling problem.
|
||||
|
||||
## Try It
|
||||
|
||||
```sh
|
||||
|
|
@ -142,10 +91,7 @@ cd learn-claude-code
|
|||
python agents/s02_tool_use.py
|
||||
```
|
||||
|
||||
Example prompts to try:
|
||||
|
||||
1. `Read the file requirements.txt`
|
||||
2. `Create a file called greet.py with a greet(name) function`
|
||||
3. `Edit greet.py to add a docstring to the function`
|
||||
4. `Read greet.py to verify the edit worked`
|
||||
5. `Run the greet function with bash: python -c "from greet import greet; greet('World')"`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue