nanoPyCodeAgent 0.5.1__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/.agents/skills/land-pr/SKILL.md +27 -16
  2. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/AGENTS.md +13 -0
  3. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/PKG-INFO +2 -2
  4. nanopycodeagent-0.7.0/docs/changelogs/0.6.x.md +37 -0
  5. nanopycodeagent-0.7.0/docs/changelogs/0.7.x.md +32 -0
  6. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/docs/dev_notes/en/0.5.x.md +2 -2
  7. nanopycodeagent-0.7.0/docs/dev_notes/en/0.6.x.md +48 -0
  8. nanopycodeagent-0.7.0/docs/dev_notes/en/0.7.x.md +230 -0
  9. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/docs/dev_notes/zh-CN/0.5.x.md +2 -2
  10. nanopycodeagent-0.7.0/docs/dev_notes/zh-CN/0.6.x.md +46 -0
  11. nanopycodeagent-0.7.0/docs/dev_notes/zh-CN/0.7.x.md +230 -0
  12. nanopycodeagent-0.7.0/docs/research/README.md +29 -0
  13. nanopycodeagent-0.7.0/docs/research/en/agent_tools.md +154 -0
  14. nanopycodeagent-0.7.0/docs/research/en/edit_tool.md +412 -0
  15. nanopycodeagent-0.7.0/docs/research/en/read_tool.md +337 -0
  16. nanopycodeagent-0.7.0/docs/research/en/write_tool.md +606 -0
  17. {nanopycodeagent-0.5.1/docs/research → nanopycodeagent-0.7.0/docs/research/zh-CN}/agent_tools.md +1 -1
  18. nanopycodeagent-0.7.0/docs/research/zh-CN/edit_tool.md +412 -0
  19. {nanopycodeagent-0.5.1/docs/research → nanopycodeagent-0.7.0/docs/research/zh-CN}/read_tool.md +1 -1
  20. nanopycodeagent-0.7.0/docs/research/zh-CN/write_tool.md +606 -0
  21. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/src/nanopycodeagent/agent.py +46 -14
  22. nanopycodeagent-0.7.0/src/nanopycodeagent/edit_tool.py +260 -0
  23. nanopycodeagent-0.7.0/src/nanopycodeagent/terminal.py +96 -0
  24. nanopycodeagent-0.7.0/src/nanopycodeagent/write_tool.py +153 -0
  25. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/tests/helpers.py +14 -0
  26. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/tests/test_agent.py +119 -0
  27. nanopycodeagent-0.7.0/tests/test_edit_tool.py +382 -0
  28. nanopycodeagent-0.7.0/tests/test_terminal.py +86 -0
  29. nanopycodeagent-0.7.0/tests/test_write_tool.py +169 -0
  30. nanopycodeagent-0.5.1/docs/research/README.md +0 -14
  31. nanopycodeagent-0.5.1/src/nanopycodeagent/terminal.py +0 -29
  32. nanopycodeagent-0.5.1/tests/test_terminal.py +0 -39
  33. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/.agents/skills/release/SKILL.md +0 -0
  34. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/.agents/skills/release/scripts/verify-release.sh +0 -0
  35. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/.github/workflows/ci.yml +0 -0
  36. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/.github/workflows/release.yml +0 -0
  37. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/.gitignore +0 -0
  38. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/.ignore +0 -0
  39. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/.python-version +0 -0
  40. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/.zed/debug.json +0 -0
  41. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/CLAUDE.md +0 -0
  42. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/LICENSE +0 -0
  43. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/README.md +0 -0
  44. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/README.zh-CN.md +0 -0
  45. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/docs/RELEASING.md +0 -0
  46. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/docs/changelogs/0.1.x.md +0 -0
  47. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/docs/changelogs/0.2.x.md +0 -0
  48. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/docs/changelogs/0.3.x.md +0 -0
  49. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/docs/changelogs/0.4.x.md +0 -0
  50. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/docs/changelogs/0.5.x.md +0 -0
  51. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/docs/changelogs/README.md +0 -0
  52. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/docs/dev_notes/README.md +0 -0
  53. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/docs/dev_notes/en/0.1.x.md +0 -0
  54. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/docs/dev_notes/en/0.2.x.md +0 -0
  55. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/docs/dev_notes/en/0.3.x.md +0 -0
  56. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/docs/dev_notes/en/0.4.x.md +0 -0
  57. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/docs/dev_notes/zh-CN/0.1.x.md +0 -0
  58. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/docs/dev_notes/zh-CN/0.2.x.md +0 -0
  59. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/docs/dev_notes/zh-CN/0.3.x.md +0 -0
  60. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/docs/dev_notes/zh-CN/0.4.x.md +0 -0
  61. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/docs/superpowers/plans/2026-06-21-release-skills.md +0 -0
  62. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/docs/superpowers/specs/2026-06-21-release-skill-design.md +0 -0
  63. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/docs/superpowers/specs/2026-06-29-config-file-support-design.md +0 -0
  64. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/pyproject.toml +0 -0
  65. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/src/nanopycodeagent/__init__.py +0 -0
  66. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/src/nanopycodeagent/bash_tool.py +0 -0
  67. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/src/nanopycodeagent/read_tool.py +0 -0
  68. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/src/nanopycodeagent/settings.py +0 -0
  69. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/tests/conftest.py +0 -0
  70. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/tests/test_bash_tool.py +0 -0
  71. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/tests/test_line_editing.py +0 -0
  72. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/tests/test_read_tool.py +0 -0
  73. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/tests/test_settings.py +0 -0
  74. {nanopycodeagent-0.5.1 → nanopycodeagent-0.7.0}/uv.lock +0 -0
@@ -5,16 +5,18 @@ description: >
5
5
  Makes the PR self-contained: rewrites the PR description from the PR's own
6
6
  contents, appends a changelog [Unreleased] entry when warranted, regenerates the
7
7
  English dev notes from the Chinese source when touched, and keeps the bilingual
8
- README in sync. Then only after explicit maintainer confirmation merges the
9
- PR. Does NOT tag or publish. Triggers on
8
+ README in sync. It also checks bilingual research notes and reminds the maintainer
9
+ when their English version needs refreshing. Then — only after explicit maintainer
10
+ confirmation — merges the PR. Does NOT tag or publish. Triggers on
10
11
  "合并 PR / land PR / 合并这个 PR / merge this PR".
11
12
  ---
12
13
 
13
14
  # Land a PR
14
15
 
15
16
  Merge a feature/bugfix PR into `main`, making the PR **self-contained** first:
16
- its code, changelog entry, dev notes (zh + en), and README (zh + en) all in sync
17
- within the PR. This skill never tags or publishes — that is the `release` skill's job.
17
+ its code, changelog entry, dev notes (zh + en), research-note translation status,
18
+ and README (zh + en) all accounted for within the PR. This skill never tags or
19
+ publishes — that is the `release` skill's job.
18
20
 
19
21
  ## Prerequisites
20
22
 
@@ -31,38 +33,46 @@ file, e.g. `docs/changelogs/0.1.x.md` / `docs/dev_notes/{zh-CN,en}/0.1.x.md`.
31
33
  - `git status --porcelain` — empty (clean tree).
32
34
  - current branch is **not** `main` (`git branch --show-current`).
33
35
 
34
- ### 2. Ensure a PR exists
36
+ ### 2. Check English research notes before PR creation
37
+ If the branch modified any `docs/research/zh-CN/<name>.md`, check the
38
+ corresponding `docs/research/en/<name>.md`. Explicitly remind the maintainer
39
+ that the English version must also be translated or refreshed, and report
40
+ whether it is in sync. Do this even when the English file was already updated.
41
+ If it is missing or stale, surface that clearly before opening or landing the
42
+ PR; do not silently treat the Chinese-only change as complete.
43
+
44
+ ### 3. Ensure a PR exists
35
45
  ```bash
36
46
  gh pr view --json number,url 2>/dev/null || gh pr create --fill
37
47
  ```
38
48
  Note the PR number for later steps.
39
49
 
40
- ### 3. Append a changelog entry (when warranted)
50
+ ### 4. Append a changelog entry (when warranted)
41
51
  Inspect `git diff main...HEAD`. If the changes are worth recording for users, add
42
52
  an entry under `## [Unreleased]` in `<series>` (changelogs) in the right group
43
53
  (Added / Changed / Fixed / Removed). Purely internal/tooling changes may need
44
54
  **no** entry — decide, and surface the decision at the gate.
45
55
 
46
- ### 4. Sync English dev notes (if the Chinese source changed)
56
+ ### 5. Sync English dev notes (if the Chinese source changed)
47
57
  If this PR modified `docs/dev_notes/zh-CN/<series>.md`, regenerate the whole
48
58
  `docs/dev_notes/en/<series>.md` by translating the Chinese source. The English
49
59
  file is generated — do not hand-edit beyond this regeneration.
50
60
 
51
- ### 5. Keep the bilingual README in sync
61
+ ### 6. Keep the bilingual README in sync
52
62
  If this PR changed only one of `README.md` / `README.zh-CN.md`, mirror the change
53
63
  into the other (translate/align) so the pair stays consistent. Both READMEs are
54
64
  hand-written sources — propose the synced change and let the maintainer adjust the
55
65
  wording at the gate; do not blindly overwrite. If both were already changed, skip.
56
66
 
57
- ### 6. Commit and push the sync changes
58
- Commit any changes from steps 35 to the feature branch and push:
67
+ ### 7. Commit and push the sync changes
68
+ Commit any changes from steps 46 to the feature branch and push:
59
69
  ```bash
60
70
  git commit -am "docs: sync changelog/dev-notes/README for this PR"
61
71
  git push
62
72
  ```
63
- (Skip if steps 35 produced no changes.)
73
+ (Skip if steps 46 produced no changes.)
64
74
 
65
- ### 7. Rewrite the PR description from scratch
75
+ ### 8. Rewrite the PR description from scratch
66
76
  Build the description **only** from the PR's actual contents — commits, changed
67
77
  files, `gh pr diff`. **Do not read or extend the old description** (it may be
68
78
  stale). Update via the GitHub API (`gh pr edit` can fail on deprecated Projects
@@ -71,13 +81,14 @@ Classic):
71
81
  gh api repos/{owner}/{repo}/pulls/{number} -X PATCH -f body="..." --silent
72
82
  ```
73
83
 
74
- ### 8. ⛔ Confirmation gate (mandatory)
84
+ ### 9. ⛔ Confirmation gate (mandatory)
75
85
  Present to the maintainer: the rewritten **PR description**, the **changelog
76
86
  entry** added (or "none, because …"), and any **dev notes / README sync** done
77
- (or a README drift warning). **Wait for explicit confirmation** (e.g. "确认 / go").
78
- Do not merge until approved.
87
+ (or a README drift warning), plus the **English research-note sync status** when
88
+ applicable. **Wait for explicit confirmation** (e.g. "确认 / go"). Do not merge
89
+ until approved.
79
90
 
80
- ### 9. Merge
91
+ ### 10. Merge
81
92
  ```bash
82
93
  gh pr merge --merge
83
94
  ```
@@ -37,6 +37,19 @@ The **only** exception is documentation explicitly designated as Chinese:
37
37
  Everything outside that list — including code under `src/`, this file, and all
38
38
  other docs — is English.
39
39
 
40
+ ### Bilingual research notes
41
+
42
+ Research notes are bilingual:
43
+
44
+ - `docs/research/zh-CN/` contains the hand-written Chinese sources of truth.
45
+ - `docs/research/en/` contains English versions generated from the Chinese
46
+ sources; regenerate the whole corresponding file instead of hand-editing it.
47
+
48
+ When a change to a Chinese research source is headed into a pull request, the
49
+ agent MUST remind the user that the corresponding English version also needs to
50
+ be translated or refreshed. Before opening or landing the PR, report whether
51
+ the English version is in sync, even when it has already been updated.
52
+
40
53
  ## Commits & PRs
41
54
 
42
55
  All agents working in this repo MUST follow
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: nanoPyCodeAgent
3
- Version: 0.5.1
3
+ Version: 0.7.0
4
4
  Summary: A nano code agent built from scratch in pure Python.
5
5
  Project-URL: Homepage, https://github.com/minixalpha/nanoPyCodeAgent
6
6
  Project-URL: Repository, https://github.com/minixalpha/nanoPyCodeAgent
@@ -0,0 +1,37 @@
1
+ # Changelog — 0.6.x
2
+
3
+ All notable changes in the **0.6.x** release series are documented here.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.6.0] - 2026-08-13
8
+
9
+ ### Added
10
+ - A `write` tool creates a file or replaces its entire content from a
11
+ structured `{path, content}` call. Content lands on disk as data instead
12
+ of passing through shell quoting and heredocs, which could silently
13
+ corrupt it; missing parent directories are created, directories and
14
+ non-regular files (FIFOs, device files) are refused as targets, and the
15
+ result states whether the file was created or overwritten. The terminal
16
+ echoes the target path with the content folded to its first lines.
17
+ - A spinner animates while waiting for the model's reply or a running bash
18
+ command, so long waits no longer look like a frozen terminal. It draws
19
+ only when stdout is a terminal (and `NO_COLOR` is unset) and erases
20
+ itself once output arrives, leaving no trace in the transcript.
21
+
22
+ ### Changed
23
+ - Echoed tool calls (`[bash]$ ...`, `[read] ...`) now sit on a lighter
24
+ background shade than their output, so the command and what it printed
25
+ read apart at a glance.
26
+
27
+ <!--
28
+ When cutting a release, copy the relevant items from [Unreleased] into a new
29
+ version section above it, e.g.:
30
+
31
+ ## [0.6.0] - YYYY-MM-DD
32
+
33
+ ### Added
34
+ ### Changed
35
+ ### Fixed
36
+ ### Removed
37
+ -->
@@ -0,0 +1,32 @@
1
+ # Changelog — 0.7.x
2
+
3
+ All notable changes in the **0.7.x** release series are documented here.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.7.0] - 2026-08-17
8
+
9
+ ### Added
10
+ - An `edit` tool that replaces an exact stretch of an existing text file,
11
+ so changing a few lines no longer means rewriting the whole file. The
12
+ match is literal — no regex, no whitespace or similarity guessing — and
13
+ must be unique unless `replace_all` is set, so a call that no longer fits
14
+ the file fails with a message saying how to recover instead of editing
15
+ the wrong place. A UTF-8 BOM and a CRLF file's line endings are handled
16
+ for you; creating a file and rewriting one whole stay with `write`.
17
+ - English translations for all research notes, organized alongside their
18
+ hand-written Chinese sources of truth. Development notes now link to the
19
+ matching research language, and the pull-request workflow reports whether
20
+ English research translations are in sync when Chinese sources change.
21
+
22
+ <!--
23
+ When cutting a release, copy the relevant items from [Unreleased] into a new
24
+ version section above it, e.g.:
25
+
26
+ ## [0.7.0] - YYYY-MM-DD
27
+
28
+ ### Added
29
+ ### Changed
30
+ ### Fixed
31
+ ### Removed
32
+ -->
@@ -10,7 +10,7 @@ When the tool starts up without printing its version, I sometimes cannot tell wh
10
10
 
11
11
  ### Built-in tools in common code agents
12
12
 
13
- For a survey of the built-in tools of mainstream code agents, see [Built-in Tools in Common Code Agents](../../research/agent_tools.md) (Chinese).
13
+ For a survey of the built-in tools of mainstream code agents, see [Built-in Tools in Common Code Agents](../../research/en/agent_tools.md).
14
14
 
15
15
  ### Designing the built-in tools
16
16
 
@@ -18,7 +18,7 @@ As the survey shows, beyond the core file/command tools that something like pi o
18
18
 
19
19
  #### The read tool
20
20
 
21
- We also surveyed the [read tool](../../research/read_tool.md) (Chinese): apart from codex, every code agent has a dedicated read tool. pi and opencode are model-agnostic, and you can see it in their read tools — their limits are line counts and file sizes, with no token limit. The other, model-specific code agents all have token limits; presumably because they only support their own vendor's models, where tokens are easier to count?
21
+ We also surveyed the [read tool](../../research/en/read_tool.md): apart from codex, every code agent has a dedicated read tool. pi and opencode are model-agnostic, and you can see it in their read tools — their limits are line counts and file sizes, with no token limit. The other, model-specific code agents all have token limits; presumably because they only support their own vendor's models, where tokens are easier to count?
22
22
 
23
23
  Functionally, read and bash both seem able to satisfy the need to read a file. Even accounting for files that may exceed a size limit, bash can still read them fine. Take codex: in actual testing, whatever the size of the file, it always starts with a `sed -n '1,260p' foo.md`, and then, if it finds there is more, it may follow up with `wc -l foo.md && sed -n '1,260p' foo.md` — reading chunk by chunk, which looks a lot like the `limit` of a dedicated read tool. But I'm not sure whether, for any given model, reading files is better done with a dedicated tool or with `bash`.
24
24
 
@@ -0,0 +1,48 @@
1
+ # Development Notes — 0.6.x
2
+
3
+ > Generated from the Chinese source [`../zh-CN/0.6.x.md`](../zh-CN/0.6.x.md). Do not edit by hand.
4
+
5
+ ## 0.6.0 - 2026.08.13
6
+
7
+ ### Tool output polish
8
+
9
+ Before continuing with the write tool, let's first add some improvements to how the agent presents its output: tool use and tool output now get separate background colors, so it is easier to tell at a glance which part is the tool use and which is the tool use result. Down the road we may want to emit something like an execution trace into a jsonl file; for now this will do.
10
+
11
+ I also tried the animated ASCII indicator pi shows while waiting for the agent's reply: `⠋ Working`. To my surprise, the animated characters turn out to be a few Braille glyphs displayed in rotation — and they are fairly easy to implement, so let's just build it.
12
+
13
+ ### The write tool
14
+
15
+ This version implements the write tool. Today the agent writes files like this:
16
+
17
+ ````text
18
+ You> write a python version hello world to hello.py
19
+
20
+ Agent>
21
+ [bash]$ cat > hello.py << 'EOF'
22
+ print("Hello, World!")
23
+ EOF
24
+ cat hello.py
25
+ python3 hello.py
26
+ print("Hello, World!")
27
+ Hello, World!
28
+
29
+ Agent> I created `hello.py` with the following content:
30
+
31
+ ```python
32
+ print("Hello, World!")
33
+ ```
34
+
35
+ Running it outputs: `Hello, World!`
36
+ ````
37
+
38
+ Based on the revised conclusion of the [write tool research](../../research/en/write_tool.md), the write tool should implement:
39
+
40
+ - Structured `{path, content}` input: the content lands on disk as data, never passing through shell expansion, heredoc delimiters, or quote escaping — eliminating the whole class of failures where a heredoc silently corrupts a file;
41
+ - Create the file when it does not exist, overwrite it whole when it does, and state which of the two happened in the result;
42
+ - Path handling identical to read's: absolute or relative to the working directory, with a leading `~` expanded;
43
+ - Automatically create missing parent directories;
44
+ - Refuse directories and non-regular files (FIFOs, device files) as write targets — opening a FIFO for writing blocks without a timeout and hangs the session, and a device file could be written by mistake; read already performs the same check on its targets, and write follows the same approach;
45
+ - Write the content exactly as given, as UTF-8 text, with no newline or whitespace normalization;
46
+ - Error messages state what to do next, in read's style;
47
+ - The terminal echoes the target path and a content preview folded to the first few lines, consistent with the tool output shading presentation;
48
+ - The tool description limits its purpose to new files and whole-file rewrites (appending and bulk transforms still go through bash) and claims no safety whatsoever: no prior-Read check, no mtime/revision staleness guard, no atomic replace — the semantics are last-writer-wins.
@@ -0,0 +1,230 @@
1
+ # Development Notes — 0.7.x
2
+
3
+ > Generated from the Chinese source [`../zh-CN/0.7.x.md`](../zh-CN/0.7.x.md). Do not edit by hand.
4
+
5
+ ## 0.7.0 - 2026.08.17
6
+
7
+ This version builds the edit tool. It starts from a survey of the edit tools of mainstream code agents, see [edit tool research](../../research/en/edit_tool.md).
8
+
9
+ There are two basic motivations for building an edit tool.
10
+
11
+ First, when changing a few lines of code, sending just the small stretch of old text and its replacement is enough — there is no need to make the model regenerate the entire file. That saves output tokens, and it keeps a full copy of the file from occupying the context for a long time.
12
+
13
+ Second, a whole-file rewrite easily changes places nobody asked it to change, whereas a unique `old_text` match is itself a verifiable precondition — when the old content is no longer there the call simply fails and the model re-reads, instead of overwriting unconditionally.
14
+
15
+ Of course, partial edits can also be made through bash, but at agent runtime it is hard to reliably derive from a dynamic shell snippet which file was changed, what old content it assumed, and what diff it actually produced. A dedicated tool also fails in a structured way (no match / N matches / the file has changed), which is easier to recover from than a chunk of shell stderr.
16
+
17
+ I tested what happens without an edit tool: I first wrote a `quick_sort.py`, then asked for comments to be added. nanoPyCodeAgent 0.6.x called the write tool and rewrote the whole file — confirming that without an edit tool the context grows.
18
+
19
+ Based on the research, here is what this round's edit tool implements.
20
+
21
+ ### The edit tool contract
22
+
23
+ **Input** (`input_schema`, in the same style as `read`/`write`):
24
+
25
+ ```text
26
+ path string required File path, absolute or relative to the agent's working directory; a leading ~ is expanded
27
+ old_text string required The text to replace, character for character as it appears in the file; must not be empty
28
+ new_text string required The replacement text; an empty string means deleting old_text exactly
29
+ replace_all boolean optional Defaults to false; when true, replaces every non-overlapping match
30
+ ```
31
+
32
+ **Output**: like `read`/`write`, it returns a single stretch of plain text plus an `is_error` flag, not a structured object. On success it returns a one-line summary and does not feed the file's content back to the model:
33
+
34
+ ```text
35
+ [edited src/app.py: replaced 1 occurrence at line 42]
36
+ [edited src/app.py: replaced 3 occurrences, first at line 12]
37
+ ```
38
+
39
+ On failure `is_error=true`, and every kind of failure has to state both what happened and what to do next:
40
+
41
+ ```text
42
+ [no match for old_text in src/app.py; read the file again and copy the text exactly — the CRLF retry was already tried]
43
+ [old_text matches 3 times in src/app.py; add surrounding context to make it unique, or set replace_all=true]
44
+ [old_text and new_text are identical: nothing to change]
45
+ [old_text is empty; use write to create a file or replace it whole]
46
+ [file not found: src/app.py]
47
+ [src/app.py is a directory, not a file]
48
+ [src/app.py is not valid UTF-8; edit is text-only]
49
+ [src/app.py is 12000000 bytes, over the 10000000 limit; edit it with bash]
50
+ ```
51
+
52
+ In the terminal the call shows up as `[edit] src/app.py` plus a folded little old/new diff. The full `old_text`/`new_text` is no longer fed back through the tool result — it is already in the tool input of that assistant message.
53
+
54
+ ### Other implementation points for the edit tool
55
+
56
+ - **Field naming follows this project**: use `path` and snake_case, consistent with the existing `read`/`write`. Claude Code's `old_string` and OpenCode's `filePath`/`oldString` are deliberately not copied — a clear tool description is enough to teach the model, and there is no reason to fracture this project's naming just to align with some other product.
57
+ - **Unique match, fail outright when it is not**: by default `old_text` has to occur exactly once in the file; zero matches fails, and multiple matches without `replace_all` also fails and reports the match count. With `replace_all=True` it replaces every non-overlapping match and returns the actual count. Better to make the model retry once more than to guess a location.
58
+ - **Exact only, no fuzzy matching**: no regex, and no fallbacks such as trimming, indentation flexibility, similarity scoring or Unicode normalization. The only input conversions allowed are the BOM and line endings — both directional, both enumerable in scope, and both of which must be stated honestly in the tool description. Unlike OpenCode V1 and Pi, we must not claim "exact" to the model while the real matching boundary is wider.
59
+ - **Draw a clear line against `write`**: it only edits regular files that already exist; creating new files and rewriting whole ones stay with `write`. `old_text` must not be empty, and `old_text == new_text` fails. `new_text=""` means an exact deletion, with no hidden semantics like "and also take the following newline with it."
60
+ - **Strict UTF-8 and BOM handling**: files that are not valid UTF-8, and files containing NUL, are refused — `read` displays bad bytes as replacement characters to make them inspectable, and if edit round-tripped that view it would corrupt the original bytes for good. A UTF-8 BOM is stripped before matching and restored on write; otherwise an `old_text` aimed at the first line would mysteriously fail to match because of one invisible character.
61
+ - **Match `read`'s newline view with one directional CRLF retry**: first do a raw exact pass on `old_text` as given. Only when that finds nothing, and the file contains `\r\n`, and `old_text` contains `\n` but no `\r`, is its LF→CRLF form retried once — with `new_text` converted along with it. The two passes are never unioned: uniqueness and counting both happen in whichever pass actually matched. In a file with mixed line endings, a fragment in the other style simply does not match this time; that is a deliberate fail-closed, and the error message has to say that the CRLF retry was already tried.
62
+ - **File size reuses `MAX_READ_BYTES`, a 10 MB cap**: the implementation is a whole-file read-compute-write, so anything over the cap is refused outright with a suggestion to use bash or a dedicated script instead.
63
+ - **Paths, symlinks and error style align with `read`/`write`**: `~` expansion, the regular-file check and the error format all stay consistent. The first version writes back directly and explicitly offers no mtime check, no CAS and no atomic replace — do not pretend in the copy to have guarantees that do not exist.
64
+ - **The tool description has to teach the model how to choose**: prefer edit for ordinary partial changes, write for new files or whole-file rewrites, bash for bulk mechanical transforms. `old_text` has to be character-for-character identical, two to four lines is usually enough, it must not carry read's line-number prefix, and the model must not splice in `\r` itself.
65
+ - **Test matrix**: unique replacement / deletion / Unicode / no-op; not-found, repeated matches and `replace_all` counting; untouched content keeping its exact bytes under LF, CRLF, no trailing newline and mixed line endings; BOM stripping and restoration; invalid UTF-8 and NUL refusal; missing files, directories, FIFOs, over-cap files; `~` and relative paths, symlink behavior consistent with `write`; the folded terminal display and the `is_error` setting; several edits in one reply taking effect in order.
66
+
67
+ Explicitly out of scope for this version: no enforced prior Read (there is no read revision registry today, and a unique `old_text` is already a local precondition), no `edits[]` batching, no `apply_patch`, no local imitation of a formatter / LSP / history / approval UI, and no per-file queue (tool calls currently execute single-threaded and in order). These wait until parallel calls, approvals and remote filesystems genuinely show up, at which point they all get upgraded together into a mutation core shared by write and edit.
68
+
69
+ ### QA
70
+
71
+ What follows are questions and answers from discussions with the agent: I asked the questions, the agent organized the answers, providing the explanation the material above needs.
72
+
73
+ #### Q: What exactly are the fallbacks mentioned under "exact only, no fuzzy matching"? What do "supporting regex" and "supporting trim" look like as code behavior?
74
+
75
+ First, a baseline: **exact means taking `old_text`'s byte sequence and finding an identical substring in the file**. The four spaces in `" return x\n"`, the `\n` at the end of the line, whether each quote is straight or curly — all of it has to line up. Every item below opens a hole in that baseline, and each one happens *after* exact matching fails, answering the question "should we try again with a relaxed standard?"
76
+
77
+ **1. Regex**
78
+
79
+ "Supporting regex" = treating `old_text` as a pattern instead of a literal:
80
+
81
+ ```python
82
+ content.find(old_text) # literal matching (what we want)
83
+ re.sub(old_text, new_text, content) # regex matching (what we do not want)
84
+ ```
85
+
86
+ The difference is that `. * + ? ( ) [ ] { } | ^ $ \` become **metacharacters**: the model wants to delete `foo(bar)` from the source, and the regex reads it as "foo followed by a capture group bar", actually matching `foobar`; `a.b` matches `axb`. These symbols are extremely dense in code, and the model would have to escape each one to express "I mean this literal text" — which it frequently forgets. Conversely, regex can express bulk patterns like `def \w+\(`, but that is bash's job (`sed`/`perl`). None of the five surveyed projects does regex in edit; this rule keeps sed's mindset out.
87
+
88
+ **2. Trim (comparing after stripping whitespace per line / on both sides)**
89
+
90
+ "Supporting trim" = after exact fails, split both sides into lines, `strip()` the leading and trailing whitespace off each, and compare line by line:
91
+
92
+ ```text
93
+ The line in the file (two trailing spaces): " return x "
94
+ The old_text the model submitted: " return x"
95
+ ```
96
+
97
+ Exact does not match (two trailing spaces missing); with per-line trim on, both sides strip down to `"return x"` and it counts as a hit, then that whole line in the file is replaced. Codex's `seek_sequence` does "ignore trailing whitespace → trim both sides" (`en/edit_tool.md:273`), Pi does per-line trailing-whitespace normalization (`en/edit_tool.md:170`), and OpenCode V1 has a dedicated trim replacer (`en/edit_tool.md:224`).
98
+
99
+ Why they do it: trailing whitespace is especially easy to lose in transit. Claude Code's API message normalization silently strips non-Markdown per-line trailing whitespace from `new_string` (`en/edit_tool.md:208`), so the model never gets a chance to submit accurate trailing whitespace at all.
100
+
101
+ Why we do not: first, the matched range stops being equal to the bytes the model wrote (it thinks it is changing `" return x"`, while what actually gets changed is `" return x "`). Second, **what to write back** becomes another question that has to be settled — keep the file's original trailing whitespace, or erase it per the model's version? Two trailing spaces in Markdown are a hard line break, and erasing them changes the meaning. Once this hole is open, the sentence "the tool changes exactly what the model submitted" is no longer true.
102
+
103
+ **3. Indentation flexibility**
104
+
105
+ "Supporting indentation flexibility" = allowing `old_text` as a whole to be one level less (or more) indented than the file, and after matching, re-indenting `new_text` to the file's original indentation:
106
+
107
+ ```text
108
+ In the file (inside a function, 8-space indent): The old_text the model submitted (flush left):
109
+ if x: if x:
110
+ return 1 return 1
111
+ ```
112
+
113
+ With this level enabled, the tool notices every line is missing the same 8-space prefix, decides it is the same block, and adds the 8 spaces back to each line of `new_text` when replacing. OpenCode V1 has this replacer (`en/edit_tool.md:224`). It addresses the model's habit of rewriting from memory rather than copying, at the cost that in a language like Python — where indentation *is* syntax — getting the level wrong while re-indenting is a silent semantic error, and what the model receives is "success".
114
+
115
+ **4. Similarity (block anchor + Levenshtein)**
116
+
117
+ The widest level of all: use only `old_text`'s first and last lines as anchors to locate the region, and require the lines in between not to be identical but merely "similar". OpenCode V1 finds candidate regions by the first/last lines, computes a Levenshtein edit distance over the middle content to get a similarity score between 0 and 1, and **accepts anything ≥ 0.65**, taking the highest-scoring candidate and keeping the first on a tie (`en/edit_tool.md:224-225`):
118
+
119
+ ```text
120
+ The old_text the model submitted: What is actually in the file:
121
+ def run(x): def run(x):
122
+ y = x + 1 y = x * 2 ← the middle lines all differ
123
+ z = y * 3 z = y - 7
124
+ return z log(z)
125
+ return z
126
+ ```
127
+
128
+ The ends line up, the middle clears the similarity bar, and the tool deletes those 5 lines from the file as a block and swaps in `new_text` — **the old content the model quoted and the content actually deleted are not the same thing**, and the reply it gets is "replaced 1 occurrence". OpenCode V1 has to add an extra guard for this ("reject when the matched span is far larger than `old_text`", `en/edit_tool.md:224`). It can afford this because V1 has a human approval diff as a backstop before anything reaches disk (`en/edit_tool.md:226`); our first version has no approval UI, so there is nothing here to catch what falls through this hole.
129
+
130
+ **5. Unicode normalization**
131
+
132
+ "Supporting" it = folding characters that "look alike but have different code points" into the same thing before comparing:
133
+
134
+ | Category | What the model submitted | What is actually in the file |
135
+ | --- | --- | --- |
136
+ | smart quotes | `"hello"` (U+201C/U+201D) | `"hello"` (U+0022) |
137
+ | dash | `a – b` (en dash) | `a - b` (hyphen) |
138
+ | special spaces | NBSP (U+00A0) | ordinary space (U+0020) |
139
+ | NFKC | `(` fullwidth parenthesis, `fi` ligature | `(`, `fi` |
140
+
141
+ Model output passes through rendering and the tokenizer, which easily turns straight quotes into curly ones, so all five projects implement some version of this level: Grok has optional confusable normalization (`en/edit_tool.md:116`), Pi does NFKC + punctuation + special spaces (`en/edit_tool.md:170`), Claude Code does only quote normalization plus a fixed desanitize set (`en/edit_tool.md:202`), and Codex normalizes punctuation and spaces (`en/edit_tool.md:273`). The crucial difference is what gets written back: Grok maps the matched position back to the original UTF-8 bytes and fails closed when the mapping is not clean; Pi replaces on the normalized text and copies back only the lines it did not touch, so **the lines it did touch incidentally undergo NFKC / punctuation / trailing-whitespace changes**, producing modifications the model never asked for (`en/edit_tool.md:307`).
142
+
143
+ **So why do the BOM and line-ending conversions we kept not count as fuzzy?**
144
+
145
+ | | The five levels above | BOM / line endings |
146
+ | --- | --- | --- |
147
+ | Cause | Guessing the model copied it wrong | Caused by our own `read` view |
148
+ | Direction | Two-way folding, both sides change | One-way conversion, only the `old_text` side changes |
149
+ | Trigger | Heuristic scoring | Can be written as a single if |
150
+ | Matched range | May be larger than the literal the model submitted | Strictly equal |
151
+
152
+ `read` drops the `\r` of CRLF when displaying, so multi-line original text in the model's hands necessarily has LF only — it could not produce `\r` even if it wanted to. That is not the model copying it wrong, it is a mismatch our own view created, so the tool supplies the conversion: the file contains `\r\n`, `old_text` contains `\n` and no `\r`, and the first pass came up empty — only when all three hold does one LF→CRLF retry happen. The BOM is the same story: `read` does not strip U+FEFF, so an invisible character hangs at the start of the first line, and without stripping it the match fails mysteriously. Both can be written into the tool description in one sentence for the model to verify; a similarity threshold of 0.65 cannot, and even if written down the model would have no way to judge which stretch was actually matched.
153
+
154
+ **The trade-off in one sentence**: fuzzy matching raises the first-try success rate, and what it costs is that the range the tool is actually authorized to modify becomes larger than the range the model expressed. Products with a human approval diff can afford that cost; our first version has no approval, no undo and no checkpoint, so one wrong replacement lands straight on disk. So we choose fail closed — the cost of failure is only that the model reads the file once more and retries, and that happens to be what agents are best at.
155
+
156
+ #### Q: What are the mechanisms behind those five items under "explicitly out of scope for this version", and why not do them now?
157
+
158
+ **1. No enforced prior Read (read-before-edit)**
159
+
160
+ "What it looks like if done": the agent keeps a table in the session recording, for each file, **whether it has been read in this session and which version was read** (a timestamp or content hash). Before an edit runs, it consults that table: never read means refuse outright and make the model read first; read but the file's mtime is newer than that read means someone changed it in the meantime, so refuse as well.
161
+
162
+ Claude Code does the heaviest version of this among the five projects: it does not merely suggest reading in the tool description, it enforces session read state at runtime, and a system-injected partial view does not count. Before writing it re-reads the current metadata synchronously to double-check, and deliberately inserts no `await` between the check and the write, squeezing the race window into a single event loop tick (`en/edit_tool.md:200`).
163
+
164
+ Why not now: we do not have that table, and building one means first building a read revision registry — and `read` supports `offset`/`limit` anyway, so what it returns is often just a window onto the file, which makes "has been read" an ambiguous state to begin with. More importantly, a unique `old_text` match **is itself a precondition**: if the old content is gone, the call fails, and that already covers the main scenario of "the file changed, so do not blind-write". Recording mtime only narrows the window; it does not eliminate TOCTOU (between the check and the actual write another process can still cut in), while adding a pile of state to carry (`en/edit_tool.md:367`).
165
+
166
+ When to add it: once there is undo/checkpoint or an approval UI and we need an explicit answer to "which version was the model's judgment based on".
167
+
168
+ **2. No `edits[]` batching**
169
+
170
+ "What it looks like if done": the schema goes from a single old/new to an array, submitting several changes to the same file in one call:
171
+
172
+ ```text
173
+ path: string
174
+ edits: [ {old_text, new_text}, {old_text, new_text}, ... ]
175
+ ```
176
+
177
+ Pi has exactly this shape: every `old_text` matches against **the same original file** (not against the result of the previous item), everything is validated first, overlapping ranges are rejected, and then they are applied in reverse order with a single write to disk (`en/edit_tool.md:149-177`). The benefit is one read, one write, a group of changes that logically all hold or all fail together, and fewer tool round trips.
178
+
179
+ Why not now: our agent loop is single-threaded and sequential — multiple `tool_use` blocks in one assistant reply are executed one at a time in the order the model gave them (`src/nanopycodeagent/agent.py:172-179`), so the model can already fire several edits in one turn, with each seeing the previous one's result. Batching would only save a few I/O operations, in exchange for overlap detection, per-item validation, and the question of how to report partial failure (`en/edit_tool.md:368`).
180
+
181
+ There is one more lesson from Pi: it once supported both a single old/new and `edits[]`, and the model kept mixing the two and producing invalid calls; in the end only the array survived, with the old shape demoted to a runtime input-migration layer (`en/edit_tool.md:161`, `en/edit_tool.md:315`). So if we ever do upgrade, only one public schema can remain — the two cannot coexist.
182
+
183
+ When to add it: when tool round-trip latency, or "many changes to one file", becomes a measured bottleneck.
184
+
185
+ **3. No `apply_patch`**
186
+
187
+ "What it looks like if done": instead of old/new strings, the call carries a whole patch text, expressing additions, deletions, changes and moves across several files at once:
188
+
189
+ ```text
190
+ *** Begin Patch
191
+ *** Update File: src/app.py
192
+ @@ def run():
193
+ - old line
194
+ + new line
195
+ *** Add File: src/new.py
196
+ +content
197
+ *** Delete File: src/old.py
198
+ *** End Patch
199
+ ```
200
+
201
+ Codex has only this one file-modification tool; even creating a file is expressed with `*** Add File`, and no whole-file write is exposed (`en/edit_tool.md:246-267`). It is the most expressive option, with one parser and one permission entry point shared across files.
202
+
203
+ Why not now: it means writing a parser for a patch language, handling error recovery when a hunk fails to locate, facing partial commits where **the first few files write successfully and a later one fails** during a multi-file sequential submission (Codex itself does not roll back either, `en/edit_tool.md:277`), and redefining which set of paths permissions apply to. All of that would significantly inflate the core of a nano agent. Besides, the default model is Claude Sonnet, which is more familiar with the old/new form of Edit (`en/edit_tool.md:369`); OpenCode simply dispatches by model — `apply_patch` for the GPT family, `edit` + `write` for everything else (`en/edit_tool.md:218`).
204
+
205
+ When to add it: when a single call needs to express something atomically across several files (a rename plus updating all references, say), or when adapting to a model whose training distribution is patch-shaped.
206
+
207
+ **4. No local imitation of a formatter / LSP / history / approval UI**
208
+
209
+ These four are an entire lifecycle mature agents hang around "writing to disk". One at a time:
210
+
211
+ - **formatter**: after writing, automatically run something like prettier/black, then **recompute the diff from the formatted result** before returning it to the model. OpenCode V1 does this (`en/edit_tool.md:222`); the cost is that what finally lands on disk may be larger than the diff shown at approval time.
212
+ - **LSP**: after writing, notify the language server to re-analyze and stuff the newly produced diagnostics (type errors, undefined variables) into the tool result, so the model sees on the spot whether it broke something. OpenCode V1 has it, and Claude Code has it when an IDE is connected (`en/edit_tool.md:206`, `en/edit_tool.md:222`).
213
+ - **history / undo**: save a snapshot before every write, supporting rewind to a given step. Grok's `FileWritten` event carries previous/new content precisely to feed the hunk tracker and rewind (`en/edit_tool.md:120`).
214
+ - **approval UI**: put the diff in front of a human before writing, and only write once approved. OpenCode V1/V2's `edit` permission is exactly this (`en/edit_tool.md:222`, `en/edit_tool.md:238`).
215
+
216
+ Why not now: these are **not capabilities of the edit tool but of the whole mutation / exec control plane**. Adding a layer only on edit is bypassed by one `sed -i` in bash — approval, snapshots, path protection, all of it: as long as bash can still write arbitrary files, the defense has a hole (`en/edit_tool.md:370`). A half-installed security boundary is more dangerous than none, because it makes people believe there is one.
217
+
218
+ When to add it: once bash and the file tools land inside the same OS / container / VM filesystem boundary, build it as a whole then, rather than simulating one on edit first.
219
+
220
+ **5. No per-file queue**
221
+
222
+ "What it looks like if done": take an in-process lock keyed by the file's canonical path (the real path with symlinks resolved), serializing the whole "read current content → compute new content → write back" stretch, so two mutations of the same file cannot interleave while different files still run in parallel. Pi uses a mutation queue keyed by `realpath` (`en/edit_tool.md:177`); OpenCode V2 goes further and does `writeIfUnchanged(expectedBytes)` inside that lock — comparing the current bytes against the bytes read at approval time and declaring it stale if they differ (`en/edit_tool.md:238-242`).
223
+
224
+ What it prevents is a **lost update**: two changes run concurrently, A reads the old content, B reads the old content, A writes, B writes, and A's change is silently overwritten by B.
225
+
226
+ Why not now: we have no concurrency at all. Tool calls are dispatched sequentially in a single while loop (`src/nanopycodeagent/agent.py:172-179`), only one edit runs at any moment, and an in-process lock would always be available and never contended (`en/edit_tool.md:333`, `en/edit_tool.md:371`). And to be clear: this kind of lock **can only coordinate the tools that participate in it** — external editors and bash subprocesses can still cut in between the read and the write, so it was never a cross-writer CAS (`en/edit_tool.md:242`).
227
+
228
+ When to add it: the day parallel tool calls actually arrive — at which point the whole read-compute-write stretch goes into a shared queue keyed by canonical path, rather than locking only the write itself.
229
+
230
+ **The shared logic behind these five**: either they address **a problem that does not exist yet** under the current architecture (concurrency, batching round trips), or they **cannot be solved at all** under the current architecture (approval and snapshots are bypassed by bash), or they **would inflate the core for unclear benefit** (a patch parser, a revision registry). The closing line of the research puts it well: first solve the token and accidental-overwrite problems of partial edits with a unique old-text precondition, and wait until the architecture genuinely develops concurrency, approval and remote needs before upgrading the mutation core — rather than simulating safety ahead of time (`en/edit_tool.md:412`).
@@ -8,7 +8,7 @@
8
8
  工具启动时,如果不打印版本号,有时候自己分辨不出来是哪个版本。先开发一下这个功能。
9
9
 
10
10
  ### 常见 code agent 内置工具
11
- 主流 Code Agent 的内置工具调研见 [《常见 Code Agent 内置工具》](../../research/agent_tools.md)。
11
+ 主流 Code Agent 的内置工具调研见 [《常见 Code Agent 内置工具》](../../research/zh-CN/agent_tools.md)。
12
12
 
13
13
  ### 内置工具设计
14
14
 
@@ -16,7 +16,7 @@
16
16
 
17
17
  #### read 工具
18
18
 
19
- 我们对 [read工具](../../research/read_tool.md) 也做了调研,除了 codex 外,其他 code agent 都有专门的 read 工具。pi 和 opencode 是模型无关的,可以看出他们的 read 工具,limit 除了行数,还有文件大小,并没有 token 限制。而其他专用的 code agent,都有 token 限制,大概是只支持自家模型,token 更方便计算?
19
+ 我们对 [read工具](../../research/zh-CN/read_tool.md) 也做了调研,除了 codex 外,其他 code agent 都有专门的 read 工具。pi 和 opencode 是模型无关的,可以看出他们的 read 工具,limit 除了行数,还有文件大小,并没有 token 限制。而其他专用的 code agent,都有 token 限制,大概是只支持自家模型,token 更方便计算?
20
20
 
21
21
  功能上讲,似乎 read 和 bash 都能实现读文件需求。即使考虑文件大小可能超出限制,bash 也能正常读取。例如 codex,实际测试中发现 codex 读文件的操作,不管读什么大小文件,他都会先来个 `sed -n '1,260p' foo.md`,之后如果发现还有内容,可能会 `wc -l foo.md && sed -n '1,260p' foo.md`,相当于一块一块读,和专用 read 工具的 limit 似乎差不多。但不太确定各个模型,在读文件的时候,是用专用工具好,还是用 `bash` 更好。
22
22
 
@@ -0,0 +1,46 @@
1
+ # 开发笔记 — 0.6.x
2
+
3
+ > 本文件为**手写中文源文件**(source of truth);英文版 [`../en/0.6.x.md`](../en/0.6.x.md) 由其生成。
4
+
5
+ ## 0.6.0 - 2026.08.13
6
+ ### 工具输出优化
7
+ 继续开发 write tool 之前,先加一些 Agent 输出方式相关的优化,tool use 和 tool output 背景颜色分开,更容易看出哪里是 tool use ,哪里是 tool use result。后续可能需要输出类似执行轨迹的东西,放 jsonl 里,目前暂时先这样处理。
8
+
9
+ 另外,尝试了一下 pi 里等待 Agent 回复时的 ascii 动态字符: `⠋ Working`,没想到动态字符居然是几个交替显示的盲文,实现起来也比较容易,就先实现吧。
10
+
11
+ ### write 工具
12
+
13
+ 这一版本实现 write 相关工具,当前这个 agent 对文件的写入方式是这样:
14
+
15
+ ````text
16
+ You> write a python version hello world to hello.py
17
+
18
+ Agent>
19
+ [bash]$ cat > hello.py << 'EOF'
20
+ print("Hello, World!")
21
+ EOF
22
+ cat hello.py
23
+ python3 hello.py
24
+ print("Hello, World!")
25
+ Hello, World!
26
+
27
+ Agent> I created `hello.py` with the following content:
28
+
29
+ ```python
30
+ print("Hello, World!")
31
+ ```
32
+
33
+ Running it outputs: `Hello, World!`
34
+ ````
35
+
36
+ 基于 [write 工具调研](../../research/zh-CN/write_tool.md)修正后的结论,write 工具要实现的功能:
37
+
38
+ - 结构化 `{path, content}` 入参:内容作为数据直接落盘,不经过 shell 展开、heredoc delimiter 和引号转义,消除 heredoc 静默写坏文件这一整类失败;
39
+ - 文件不存在则创建,存在则整文件覆盖,并在结果中说明是哪一种;
40
+ - 路径处理与 read 一致:绝对路径或相对工作目录,展开开头的 `~`;
41
+ - 自动创建缺失的父目录;
42
+ - 拒绝目录和非常规文件(FIFO、设备文件)作为写入目标——对 FIFO 的写打开会无超时阻塞挂死会话,设备文件则可能被误写;read 对读取目标已有同样的检查,write 沿用同一做法;
43
+ - 内容按给定原样写入,UTF-8 文本,不做换行或空白归一化;
44
+ - 错误信息按 read 的风格指出下一步该怎么做;
45
+ - 终端回显目标路径和折叠到前几行的内容预览,与 tool output shading 的展示风格一致;
46
+ - 工具描述限定用途为新建文件和整文件重写(追加、批量变换仍走 bash),并且不宣称任何安全性:没有先 Read 检查、没有 mtime/revision 防陈旧、没有原子替换——语义就是 last-writer-wins。