corecoder 0.9.0__tar.gz → 0.10.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 (77) hide show
  1. {corecoder-0.9.0 → corecoder-0.10.0}/PKG-INFO +18 -9
  2. {corecoder-0.9.0 → corecoder-0.10.0}/README.md +16 -7
  3. {corecoder-0.9.0 → corecoder-0.10.0}/README_CN.md +16 -7
  4. {corecoder-0.9.0 → corecoder-0.10.0}/article/00-index.md +1 -1
  5. {corecoder-0.9.0 → corecoder-0.10.0}/article/00-index_EN.md +1 -1
  6. {corecoder-0.9.0 → corecoder-0.10.0}/article/05-parallel-and-subagents.md +1 -1
  7. {corecoder-0.9.0 → corecoder-0.10.0}/article/05-parallel-and-subagents_EN.md +1 -1
  8. {corecoder-0.9.0 → corecoder-0.10.0}/corecoder/__init__.py +1 -1
  9. {corecoder-0.9.0 → corecoder-0.10.0}/corecoder/tools/agent.py +87 -6
  10. {corecoder-0.9.0 → corecoder-0.10.0}/examples/multi_agent_demo.py +16 -0
  11. {corecoder-0.9.0 → corecoder-0.10.0}/pyproject.toml +2 -2
  12. {corecoder-0.9.0 → corecoder-0.10.0}/tests/test_multi_agent.py +86 -0
  13. {corecoder-0.9.0 → corecoder-0.10.0}/.github/workflows/ci.yml +0 -0
  14. {corecoder-0.9.0 → corecoder-0.10.0}/.github/workflows/publish.yml +0 -0
  15. {corecoder-0.9.0 → corecoder-0.10.0}/.gitignore +0 -0
  16. {corecoder-0.9.0 → corecoder-0.10.0}/LICENSE +0 -0
  17. {corecoder-0.9.0 → corecoder-0.10.0}/article/01-the-loop.md +0 -0
  18. {corecoder-0.9.0 → corecoder-0.10.0}/article/01-the-loop_EN.md +0 -0
  19. {corecoder-0.9.0 → corecoder-0.10.0}/article/02-tools.md +0 -0
  20. {corecoder-0.9.0 → corecoder-0.10.0}/article/02-tools_EN.md +0 -0
  21. {corecoder-0.9.0 → corecoder-0.10.0}/article/03-llm-and-cost.md +0 -0
  22. {corecoder-0.9.0 → corecoder-0.10.0}/article/03-llm-and-cost_EN.md +0 -0
  23. {corecoder-0.9.0 → corecoder-0.10.0}/article/04-context.md +0 -0
  24. {corecoder-0.9.0 → corecoder-0.10.0}/article/04-context_EN.md +0 -0
  25. {corecoder-0.9.0 → corecoder-0.10.0}/article/06-session-and-cli.md +0 -0
  26. {corecoder-0.9.0 → corecoder-0.10.0}/article/06-session-and-cli_EN.md +0 -0
  27. {corecoder-0.9.0 → corecoder-0.10.0}/article/07-build-your-own.md +0 -0
  28. {corecoder-0.9.0 → corecoder-0.10.0}/article/07-build-your-own_EN.md +0 -0
  29. {corecoder-0.9.0 → corecoder-0.10.0}/article/08-extensibility.md +0 -0
  30. {corecoder-0.9.0 → corecoder-0.10.0}/article/08-extensibility_EN.md +0 -0
  31. {corecoder-0.9.0 → corecoder-0.10.0}/article/claude-code-source-analysis.md +0 -0
  32. {corecoder-0.9.0 → corecoder-0.10.0}/assets/demo-plan-hooks.gif +0 -0
  33. {corecoder-0.9.0 → corecoder-0.10.0}/assets/demo.png +0 -0
  34. {corecoder-0.9.0 → corecoder-0.10.0}/assets/demo_en.png +0 -0
  35. {corecoder-0.9.0 → corecoder-0.10.0}/corecoder/__main__.py +0 -0
  36. {corecoder-0.9.0 → corecoder-0.10.0}/corecoder/agent.py +0 -0
  37. {corecoder-0.9.0 → corecoder-0.10.0}/corecoder/checkpoints.py +0 -0
  38. {corecoder-0.9.0 → corecoder-0.10.0}/corecoder/cli.py +0 -0
  39. {corecoder-0.9.0 → corecoder-0.10.0}/corecoder/config.py +0 -0
  40. {corecoder-0.9.0 → corecoder-0.10.0}/corecoder/context.py +0 -0
  41. {corecoder-0.9.0 → corecoder-0.10.0}/corecoder/demo.py +0 -0
  42. {corecoder-0.9.0 → corecoder-0.10.0}/corecoder/hooks.py +0 -0
  43. {corecoder-0.9.0 → corecoder-0.10.0}/corecoder/llm.py +0 -0
  44. {corecoder-0.9.0 → corecoder-0.10.0}/corecoder/mcp.py +0 -0
  45. {corecoder-0.9.0 → corecoder-0.10.0}/corecoder/permissions.py +0 -0
  46. {corecoder-0.9.0 → corecoder-0.10.0}/corecoder/prompt.py +0 -0
  47. {corecoder-0.9.0 → corecoder-0.10.0}/corecoder/session.py +0 -0
  48. {corecoder-0.9.0 → corecoder-0.10.0}/corecoder/shell.py +0 -0
  49. {corecoder-0.9.0 → corecoder-0.10.0}/corecoder/tools/__init__.py +0 -0
  50. {corecoder-0.9.0 → corecoder-0.10.0}/corecoder/tools/base.py +0 -0
  51. {corecoder-0.9.0 → corecoder-0.10.0}/corecoder/tools/bash.py +0 -0
  52. {corecoder-0.9.0 → corecoder-0.10.0}/corecoder/tools/edit.py +0 -0
  53. {corecoder-0.9.0 → corecoder-0.10.0}/corecoder/tools/glob_tool.py +0 -0
  54. {corecoder-0.9.0 → corecoder-0.10.0}/corecoder/tools/grep.py +0 -0
  55. {corecoder-0.9.0 → corecoder-0.10.0}/corecoder/tools/read.py +0 -0
  56. {corecoder-0.9.0 → corecoder-0.10.0}/corecoder/tools/todo.py +0 -0
  57. {corecoder-0.9.0 → corecoder-0.10.0}/corecoder/tools/write.py +0 -0
  58. {corecoder-0.9.0 → corecoder-0.10.0}/examples/mcp_filesystem.json +0 -0
  59. {corecoder-0.9.0 → corecoder-0.10.0}/examples/mcp_filesystem.md +0 -0
  60. {corecoder-0.9.0 → corecoder-0.10.0}/examples/plan_hooks_demo.py +0 -0
  61. {corecoder-0.9.0 → corecoder-0.10.0}/tests/__init__.py +0 -0
  62. {corecoder-0.9.0 → corecoder-0.10.0}/tests/conftest.py +0 -0
  63. {corecoder-0.9.0 → corecoder-0.10.0}/tests/test_checkpoints.py +0 -0
  64. {corecoder-0.9.0 → corecoder-0.10.0}/tests/test_core.py +0 -0
  65. {corecoder-0.9.0 → corecoder-0.10.0}/tests/test_demo.py +0 -0
  66. {corecoder-0.9.0 → corecoder-0.10.0}/tests/test_hooks.py +0 -0
  67. {corecoder-0.9.0 → corecoder-0.10.0}/tests/test_litellm.py +0 -0
  68. {corecoder-0.9.0 → corecoder-0.10.0}/tests/test_live_smoke.py +0 -0
  69. {corecoder-0.9.0 → corecoder-0.10.0}/tests/test_mcp.py +0 -0
  70. {corecoder-0.9.0 → corecoder-0.10.0}/tests/test_permissions.py +0 -0
  71. {corecoder-0.9.0 → corecoder-0.10.0}/tests/test_plan_mode.py +0 -0
  72. {corecoder-0.9.0 → corecoder-0.10.0}/tests/test_repl.py +0 -0
  73. {corecoder-0.9.0 → corecoder-0.10.0}/tests/test_safety_matrix.py +0 -0
  74. {corecoder-0.9.0 → corecoder-0.10.0}/tests/test_session.py +0 -0
  75. {corecoder-0.9.0 → corecoder-0.10.0}/tests/test_shell.py +0 -0
  76. {corecoder-0.9.0 → corecoder-0.10.0}/tests/test_slash_commands.py +0 -0
  77. {corecoder-0.9.0 → corecoder-0.10.0}/tests/test_tools.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: corecoder
3
- Version: 0.9.0
4
- Summary: Minimal AI coding agent (2,908 lines of Python) inspired by Claude Code. Works with any LLM. (formerly NanoCoder)
3
+ Version: 0.10.0
4
+ Summary: Minimal AI coding agent (2,989 lines of Python) inspired by Claude Code. Works with any LLM. (formerly NanoCoder)
5
5
  Project-URL: Homepage, https://github.com/he-yufeng/CoreCoder
6
6
  Project-URL: Repository, https://github.com/he-yufeng/CoreCoder
7
7
  Project-URL: Issues, https://github.com/he-yufeng/CoreCoder/issues
@@ -37,7 +37,7 @@ Description-Content-Type: text/markdown
37
37
 
38
38
  # CoreCoder
39
39
 
40
- **The nanoGPT of coding agents. A 1.4k-line engine inside 2,908 readable lines of pure Python: understand how a coding agent actually works, then fork your own.**
40
+ **The nanoGPT of coding agents. A 1.4k-line engine inside 2,989 readable lines of pure Python: understand how a coding agent actually works, then fork your own.**
41
41
 
42
42
  *learn from it · fork it · ship something better*
43
43
 
@@ -47,7 +47,7 @@ Description-Content-Type: text/markdown
47
47
  [![Python](https://img.shields.io/badge/python-3.10+-blue)](https://python.org)
48
48
  [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
49
49
  [![Tests](https://github.com/he-yufeng/CoreCoder/actions/workflows/ci.yml/badge.svg)](https://github.com/he-yufeng/CoreCoder/actions)
50
- [![engine](https://img.shields.io/badge/engine-1405_LoC-blue)](article/00-index_EN.md)
50
+ [![engine](https://img.shields.io/badge/engine-1483_LoC-blue)](article/00-index_EN.md)
51
51
  [![essays](https://img.shields.io/badge/source--reading-8_bilingual-orange)](article/00-index_EN.md)
52
52
 
53
53
  </div>
@@ -60,7 +60,7 @@ Description-Content-Type: text/markdown
60
60
 
61
61
  | | CoreCoder | Claude Code | aider | nanoGPT |
62
62
  |---|---|---|---|---|
63
- | Lines of code | ~1,405 engine / 2,908 total | hundreds of thousands (closed) | tens of thousands of Python | ~600 (two files) |
63
+ | Lines of code | ~1,405 engine / 2,989 total | hundreds of thousands (closed) | tens of thousands of Python | ~600 (two files) |
64
64
  | Time to read it all | one afternoon | can't (closed) | a few days of slogging | one afternoon |
65
65
  | Breakpoint, change, rerun? | yes, every line | no | yes, but there's a lot | yes |
66
66
  | What it's for | understand one, then fork your own | production coding assistant | terminal pair-programming | minimal GPT for teaching |
@@ -71,9 +71,9 @@ The nanoGPT column is there as a reference point: minimal, readable, but it teac
71
71
 
72
72
  I've always felt coding agents get talked about as if they were arcane. Strip a tool like Claude Code or Cursor all the way down and the core is a `while` loop wrapped around a large model, plus seven or eight tools that let it actually do things. The hard part was never the loop; it's everything the loop has to cope with once it meets the real world. CoreCoder is the minimal version that writes that core out honestly.
73
73
 
74
- The engine (loop, model interface, context, tools, sessions) is 1,405 lines once you drop blank lines and comments. Counting the outer CLI, config and packaging too, the whole package is 25 files: 2,908 physical lines, 2,364 net, every one short enough to read in a single sitting. The growth since the original 1,161-line snapshot went into visible features: plan mode, hooks and checkpoints, each documented below.
74
+ The engine (loop, model interface, context, tools, sessions) is 1,405 lines once you drop blank lines and comments. Counting the outer CLI, config and packaging too, the whole package is 25 files: 2,989 physical lines, 2,442 net, every one short enough to read in a single sitting. The growth since the original 1,161-line snapshot went into visible features: plan mode, hooks and checkpoints, each documented below.
75
75
 
76
- And it really runs: reads and writes files, executes shell, spawns sub-agents, compacts context in three tiers, and tells you the tokens and dollars a run burned whenever you ask. Anything that would mutate your disk or run a command stops for your consent first. The suite has 242 offline tests and an opt-in live smoke test. But the point of it running isn't to become your daily driver. It runs so the walkthrough can't lie: a reference that shows how an agent works has to actually work.
76
+ And it really runs: reads and writes files, executes shell, spawns sub-agents, compacts context in three tiers, and tells you the tokens and dollars a run burned whenever you ask. Anything that would mutate your disk or run a command stops for your consent first. The suite has 246 offline tests and an opt-in live smoke test. But the point of it running isn't to become your daily driver. It runs so the walkthrough can't lie: a reference that shows how an agent works has to actually work.
77
77
 
78
78
  The code came out of a public teardown: open analyses have already exposed a lot of the load-bearing architecture inside production agents like Claude Code. I took the most essential layer and rewrote it honestly, in as little code as I could. So reading CoreCoder is roughly like reading a runnable, annotated take on how that kind of agent works, except it's only a minimal reimplementation, sitting right there on your machine for you to take apart and change.
79
79
 
@@ -126,7 +126,7 @@ Laid out flat, the whole project is this big. Skim it before you clone and you'l
126
126
  corecoder/
127
127
  ├── agent.py agent loop + parallel tool exec 240 lines ← start here
128
128
  ├── llm.py streaming client + retry + cost 364 lines
129
- ├── context.py three-tier context compaction 220 lines
129
+ ├── context.py three-tier context compaction 224 lines
130
130
  ├── session.py save / resume + path-traversal guard 97 lines
131
131
  ├── permissions.py consent for mutating tools 81 lines
132
132
  ├── hooks.py Pre/PostToolUse shell hooks 87 lines
@@ -145,7 +145,7 @@ corecoder/
145
145
  ├── read.py file read 56 lines
146
146
  ├── write.py file write 46 lines
147
147
  ├── todo.py agent-maintained task checklist 79 lines
148
- ├── agent.py sub-agent teams + isolation 143 lines
148
+ ├── agent.py sub-agent teams + isolation 224 lines
149
149
  └── base.py tool base class 32 lines
150
150
  examples/
151
151
  ├── plan_hooks_demo.py offline plan mode + hooks demo (no API key)
@@ -197,6 +197,15 @@ report = agent_tool.execute(tasks=[
197
197
 
198
198
  One to eight workers run up to four at a time, each through the normal agent loop. Reports come back in input order, not completion order, and one worker's exception does not cancel its siblings. Shared token accounting and interactive permission prompts are serialized, so concurrency doesn't lose cost or interleave consent. There is no scheduler, shared message bus, recursive spawning, or second agent loop. `examples/multi_agent_demo.py` runs the researcher / implementer / reviewer pattern offline with scripted findings; no API key and no model-quality claim.
199
199
 
200
+ Dependencies are just as small: set `depends_on` on a worker, and it starts only after every named dependency has finished successfully. Each dependent worker receives those upstream reports in its own prompt, while a failed dependency skips only the affected branch. Independent work in the same call still runs concurrently, and the returned report keeps the original input order:
201
+
202
+ ```python
203
+ {"name": "implementer", "task": "Fix using the research", "depends_on": ["researcher"]},
204
+ {"name": "reviewer", "task": "Review the fix", "depends_on": ["implementer"]},
205
+ ```
206
+
207
+ Names must be unique, references must exist, and cycles are rejected before any worker starts. It is still one blocking tool call, not a background orchestration service.
208
+
200
209
  Every one of these *whys* is traced down to the actual lines of code in the series below.
201
210
 
202
211
  ## The source-reading series · 8 bilingual essays
@@ -2,7 +2,7 @@
2
2
 
3
3
  # CoreCoder
4
4
 
5
- **The nanoGPT of coding agents. A 1.4k-line engine inside 2,908 readable lines of pure Python: understand how a coding agent actually works, then fork your own.**
5
+ **The nanoGPT of coding agents. A 1.4k-line engine inside 2,989 readable lines of pure Python: understand how a coding agent actually works, then fork your own.**
6
6
 
7
7
  *learn from it · fork it · ship something better*
8
8
 
@@ -12,7 +12,7 @@
12
12
  [![Python](https://img.shields.io/badge/python-3.10+-blue)](https://python.org)
13
13
  [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
14
14
  [![Tests](https://github.com/he-yufeng/CoreCoder/actions/workflows/ci.yml/badge.svg)](https://github.com/he-yufeng/CoreCoder/actions)
15
- [![engine](https://img.shields.io/badge/engine-1405_LoC-blue)](article/00-index_EN.md)
15
+ [![engine](https://img.shields.io/badge/engine-1483_LoC-blue)](article/00-index_EN.md)
16
16
  [![essays](https://img.shields.io/badge/source--reading-8_bilingual-orange)](article/00-index_EN.md)
17
17
 
18
18
  </div>
@@ -25,7 +25,7 @@
25
25
 
26
26
  | | CoreCoder | Claude Code | aider | nanoGPT |
27
27
  |---|---|---|---|---|
28
- | Lines of code | ~1,405 engine / 2,908 total | hundreds of thousands (closed) | tens of thousands of Python | ~600 (two files) |
28
+ | Lines of code | ~1,405 engine / 2,989 total | hundreds of thousands (closed) | tens of thousands of Python | ~600 (two files) |
29
29
  | Time to read it all | one afternoon | can't (closed) | a few days of slogging | one afternoon |
30
30
  | Breakpoint, change, rerun? | yes, every line | no | yes, but there's a lot | yes |
31
31
  | What it's for | understand one, then fork your own | production coding assistant | terminal pair-programming | minimal GPT for teaching |
@@ -36,9 +36,9 @@ The nanoGPT column is there as a reference point: minimal, readable, but it teac
36
36
 
37
37
  I've always felt coding agents get talked about as if they were arcane. Strip a tool like Claude Code or Cursor all the way down and the core is a `while` loop wrapped around a large model, plus seven or eight tools that let it actually do things. The hard part was never the loop; it's everything the loop has to cope with once it meets the real world. CoreCoder is the minimal version that writes that core out honestly.
38
38
 
39
- The engine (loop, model interface, context, tools, sessions) is 1,405 lines once you drop blank lines and comments. Counting the outer CLI, config and packaging too, the whole package is 25 files: 2,908 physical lines, 2,364 net, every one short enough to read in a single sitting. The growth since the original 1,161-line snapshot went into visible features: plan mode, hooks and checkpoints, each documented below.
39
+ The engine (loop, model interface, context, tools, sessions) is 1,405 lines once you drop blank lines and comments. Counting the outer CLI, config and packaging too, the whole package is 25 files: 2,989 physical lines, 2,442 net, every one short enough to read in a single sitting. The growth since the original 1,161-line snapshot went into visible features: plan mode, hooks and checkpoints, each documented below.
40
40
 
41
- And it really runs: reads and writes files, executes shell, spawns sub-agents, compacts context in three tiers, and tells you the tokens and dollars a run burned whenever you ask. Anything that would mutate your disk or run a command stops for your consent first. The suite has 242 offline tests and an opt-in live smoke test. But the point of it running isn't to become your daily driver. It runs so the walkthrough can't lie: a reference that shows how an agent works has to actually work.
41
+ And it really runs: reads and writes files, executes shell, spawns sub-agents, compacts context in three tiers, and tells you the tokens and dollars a run burned whenever you ask. Anything that would mutate your disk or run a command stops for your consent first. The suite has 246 offline tests and an opt-in live smoke test. But the point of it running isn't to become your daily driver. It runs so the walkthrough can't lie: a reference that shows how an agent works has to actually work.
42
42
 
43
43
  The code came out of a public teardown: open analyses have already exposed a lot of the load-bearing architecture inside production agents like Claude Code. I took the most essential layer and rewrote it honestly, in as little code as I could. So reading CoreCoder is roughly like reading a runnable, annotated take on how that kind of agent works, except it's only a minimal reimplementation, sitting right there on your machine for you to take apart and change.
44
44
 
@@ -91,7 +91,7 @@ Laid out flat, the whole project is this big. Skim it before you clone and you'l
91
91
  corecoder/
92
92
  ├── agent.py agent loop + parallel tool exec 240 lines ← start here
93
93
  ├── llm.py streaming client + retry + cost 364 lines
94
- ├── context.py three-tier context compaction 220 lines
94
+ ├── context.py three-tier context compaction 224 lines
95
95
  ├── session.py save / resume + path-traversal guard 97 lines
96
96
  ├── permissions.py consent for mutating tools 81 lines
97
97
  ├── hooks.py Pre/PostToolUse shell hooks 87 lines
@@ -110,7 +110,7 @@ corecoder/
110
110
  ├── read.py file read 56 lines
111
111
  ├── write.py file write 46 lines
112
112
  ├── todo.py agent-maintained task checklist 79 lines
113
- ├── agent.py sub-agent teams + isolation 143 lines
113
+ ├── agent.py sub-agent teams + isolation 224 lines
114
114
  └── base.py tool base class 32 lines
115
115
  examples/
116
116
  ├── plan_hooks_demo.py offline plan mode + hooks demo (no API key)
@@ -162,6 +162,15 @@ report = agent_tool.execute(tasks=[
162
162
 
163
163
  One to eight workers run up to four at a time, each through the normal agent loop. Reports come back in input order, not completion order, and one worker's exception does not cancel its siblings. Shared token accounting and interactive permission prompts are serialized, so concurrency doesn't lose cost or interleave consent. There is no scheduler, shared message bus, recursive spawning, or second agent loop. `examples/multi_agent_demo.py` runs the researcher / implementer / reviewer pattern offline with scripted findings; no API key and no model-quality claim.
164
164
 
165
+ Dependencies are just as small: set `depends_on` on a worker, and it starts only after every named dependency has finished successfully. Each dependent worker receives those upstream reports in its own prompt, while a failed dependency skips only the affected branch. Independent work in the same call still runs concurrently, and the returned report keeps the original input order:
166
+
167
+ ```python
168
+ {"name": "implementer", "task": "Fix using the research", "depends_on": ["researcher"]},
169
+ {"name": "reviewer", "task": "Review the fix", "depends_on": ["implementer"]},
170
+ ```
171
+
172
+ Names must be unique, references must exist, and cycles are rejected before any worker starts. It is still one blocking tool call, not a background orchestration service.
173
+
165
174
  Every one of these *whys* is traced down to the actual lines of code in the series below.
166
175
 
167
176
  ## The source-reading series · 8 bilingual essays
@@ -2,7 +2,7 @@
2
2
 
3
3
  # CoreCoder
4
4
 
5
- **编程 agent 里的 nanoGPT。1.4k 行引擎、整包 2908 行纯 Python 全部一口气可读,读懂一个 coding agent 到底怎么运作,再 fork 出你自己的。**
5
+ **编程 agent 里的 nanoGPT。1.4k 行引擎、整包 2989 行纯 Python 全部一口气可读,读懂一个 coding agent 到底怎么运作,再 fork 出你自己的。**
6
6
 
7
7
  *learn from it · fork it · ship something better*
8
8
 
@@ -12,7 +12,7 @@
12
12
  [![Python](https://img.shields.io/badge/python-3.10+-blue)](https://python.org)
13
13
  [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
14
14
  [![Tests](https://github.com/he-yufeng/CoreCoder/actions/workflows/ci.yml/badge.svg)](https://github.com/he-yufeng/CoreCoder/actions)
15
- [![engine](https://img.shields.io/badge/engine-1405_LoC-blue)](article/)
15
+ [![engine](https://img.shields.io/badge/engine-1483_LoC-blue)](article/)
16
16
  [![源码导读](https://img.shields.io/badge/源码导读-8篇双语-orange)](article/)
17
17
 
18
18
  </div>
@@ -25,7 +25,7 @@
25
25
 
26
26
  | | CoreCoder | Claude Code | aider | nanoGPT |
27
27
  |---|---|---|---|---|
28
- | 代码量 | 引擎约 1405 行 / 整包 2908 行 | 几十万行(闭源) | 数万行 Python | 约 600 行(两个文件) |
28
+ | 代码量 | 引擎约 1483 行 / 整包 2989 行 | 几十万行(闭源) | 数万行 Python | 约 600 行(两个文件) |
29
29
  | 读完要多久 | 一个下午 | 读不了(闭源) | 得啃几天 | 一个下午 |
30
30
  | 能不能下断点改了再跑 | 能,每一行 | 不能 | 能,但量大 | 能 |
31
31
  | 定位 | 读懂并 fork 出你自己的 agent | 生产级编程助手 | 终端结对编程 | 教学用最小 GPT |
@@ -36,9 +36,9 @@ nanoGPT 那一列是拿来对照的:它最小、可读,但教的是训一个
36
36
 
37
37
  我一直觉得 coding agent 被讲得太玄了。把 Claude Code、Cursor 这类工具扒到底,核心是一个 while 循环套着一个大模型,外加七八个让它能真正动手的工具。难的从来不是这个循环,而是循环跑进真实世界以后要兜的那些底。CoreCoder 就是把这个核心老老实实写出来的最小版本。
38
38
 
39
- 引擎部分(循环、模型接口、上下文、工具、会话)去掉空行和注释是 1405 行。连最外层的 CLI、配置、打包一起算,整个包 25 个文件、物理 2908 行、净 2364 行,每个文件都短到能一口气读完。自 1161 行快照之后的增长都花在了看得见的功能上:plan mode、hooks、checkpoints,下文各有交代。
39
+ 引擎部分(循环、模型接口、上下文、工具、会话)去掉空行和注释是 1483 行。连最外层的 CLI、配置、打包一起算,整个包 25 个文件、物理 2989 行、净 2442 行,每个文件都短到能一口气读完。自 1161 行快照之后的增长都花在了看得见的功能上:plan mode、hooks、checkpoints,下文各有交代。
40
40
 
41
- 它真能跑:读写文件、执行 shell、派子 agent、分三层压上下文,还能随时把这趟烧掉的 token 和美元数报给你。任何要动你磁盘、要跑命令的调用,都会先停下来等你点头。现在有 242 个离线测试,另有按需开启的真实 API 冒烟。但能跑不是为了劝你拿去日用,而是为了让这份「注释」不撒谎:一个解释 agent 怎么运作的范例,自己得真能运作。
41
+ 它真能跑:读写文件、执行 shell、派子 agent、分三层压上下文,还能随时把这趟烧掉的 token 和美元数报给你。任何要动你磁盘、要跑命令的调用,都会先停下来等你点头。现在有 246 个离线测试,另有按需开启的真实 API 冒烟。但能跑不是为了劝你拿去日用,而是为了让这份「注释」不撒谎:一个解释 agent 怎么运作的范例,自己得真能运作。
42
42
 
43
43
  代码来自一次公开拆解。公开的源码分析里,Claude Code 这类生产级 agent 暴露出不少关键架构,我挑出最核心的一层,用尽量少的代码诚实地复写了一遍。所以读 CoreCoder,约等于读一份基于公开源码分析的「可运行注释版」:讲的是这类 agent 的核心思路,而它本身只是最小复写,就摆在你机器上,随你拆、随你改。
44
44
 
@@ -91,7 +91,7 @@ corecoder -p "给 parse_config() 加错误处理" # 一次性模式,干完
91
91
  corecoder/
92
92
  ├── agent.py agent 主循环 + 并行工具执行 240 行 ← 从这里开始读
93
93
  ├── llm.py 流式客户端 + 重试 + 成本统计 364 行
94
- ├── context.py 三层上下文压缩 220 行
94
+ ├── context.py 三层上下文压缩 224 行
95
95
  ├── session.py 会话存盘 / 续聊 + 路径穿越防护 97 行
96
96
  ├── permissions.py 改动类工具的用户授权 81 行
97
97
  ├── hooks.py 工具调用前后的用户 shell 钩子 87 行
@@ -110,7 +110,7 @@ corecoder/
110
110
  ├── read.py 文件读取 56 行
111
111
  ├── write.py 文件写入 46 行
112
112
  ├── todo.py agent 自维护的任务清单 79 行
113
- ├── agent.py 子 agent 团队与隔离 143 行
113
+ ├── agent.py 子 agent 团队与隔离 224 行
114
114
  └── base.py 工具基类 32 行
115
115
  examples/
116
116
  ├── plan_hooks_demo.py 离线 plan mode + hooks 演示(免 API key)
@@ -162,6 +162,15 @@ report = agent_tool.execute(tasks=[
162
162
 
163
163
  1 到 8 个 worker、最多 4 个并发,每个仍走原来的 agent 循环。报告按输入顺序返回,不按完成顺序;单个 worker 出错不会取消其他 worker。共享的 token 记账和交互式授权提示都串行化,并发不会漏账,也不会把确认问题交错打印。没有调度器、共享消息总线、递归派生或第二个 agent 循环。`examples/multi_agent_demo.py` 用脚本化结论离线演示 researcher / implementer / reviewer,不需要 API key,也不拿它冒充真实模型质量验证。
164
164
 
165
+ 依赖关系也只有一行:给 worker 写上 `depends_on`,它会等所有指定依赖成功完成后再启动,并在自己的 prompt 里收到这些上游报告。某个依赖失败时,只跳过受影响的下游分支,同一次调用里的独立任务照常并发,最终报告仍按输入顺序返回:
166
+
167
+ ```python
168
+ {"name": "implementer", "task": "结合研究结论提出修复", "depends_on": ["researcher"]},
169
+ {"name": "reviewer", "task": "审查这个修复", "depends_on": ["implementer"]},
170
+ ```
171
+
172
+ worker 名不能重复,依赖必须指向已有名字,循环依赖会在任何 worker 启动前直接拒绝。它依然是一次阻塞式工具调用,不是常驻的编排服务。
173
+
165
174
  每一个「为什么」,下面的文章系列都拆到了具体代码行。
166
175
 
167
176
  ## 配套源码导读 · 八篇双语
@@ -8,7 +8,7 @@
8
8
 
9
9
  因为 Claude Code 本体太大了。它的代码量在几十万行的量级,光一个负责跑 shell 命令的 BashTool 就上千行,真要逐行读完,多数人会在第三个文件就关掉编辑器。可 agent 的核心其实没那么复杂,复杂的是工程化之后那些为真实世界兜底的边角:模型半路被打断怎么办、上下文塞满了怎么办、十个工具调用同时回来怎么办、provider 偶尔抽风返回 500 又怎么办。这些东西摊在几十万行里,你很难一眼看到骨架。
10
10
 
11
- CoreCoder 做的事,是把这套骨架压到一千四百行出头的纯 Python。准确说,agent 引擎(循环、模型接口、上下文、工具、会话)去掉空行和注释是 1405 行;连最外层的 CLI 终端、配置和打包都算上,整个包去掉空行和注释 2364 行,物理代码 2908 行。每个文件都短到能一口气读完,跑起来又确实是个能改代码、能跑命令、能自己开子任务、能压缩上下文、能算钱的 agent。它不是玩具。它是把生产级 agent 的每个设计决策,拣出最核心的那版,用最少的代码诚实地写一遍。
11
+ CoreCoder 做的事,是把这套骨架压到一千四百行出头的纯 Python。准确说,agent 引擎(循环、模型接口、上下文、工具、会话)去掉空行和注释是 1483 行;连最外层的 CLI 终端、配置和打包都算上,整个包去掉空行和注释 2442 行,物理代码 2989 行。每个文件都短到能一口气读完,跑起来又确实是个能改代码、能跑命令、能自己开子任务、能压缩上下文、能算钱的 agent。它不是玩具。它是把生产级 agent 的每个设计决策,拣出最核心的那版,用最少的代码诚实地写一遍。
12
12
 
13
13
  读它,约等于读一份 Claude Code 的「可运行注释版」。区别在于,CoreCoder 的每一行你都能在自己机器上断点、改、跑、看它出什么效果。本系列里我引用的每一个行数、每一段代码,都是从仓库里现读现核的,不是凭印象。这点我后面会反复较真,因为编码 agent 这个领域,太多文章在凭感觉编数字。
14
14
 
@@ -8,7 +8,7 @@ First, use CoreCoder, an open source project whose core runs to just over fourte
8
8
 
9
9
  Because Claude Code itself is too big. Its codebase runs into the hundreds of thousands of lines, and the BashTool alone, the part that runs shell commands, is over a thousand. Read it line by line and most people close the editor by the third file. Yet the core of an agent isn't really that complicated. What's complicated is everything engineered around it to survive the real world: what to do when the model gets interrupted mid-stream, when the context window fills up, when ten tool calls come back at once, when a provider hiccups and returns a 500. Spread across hundreds of thousands of lines, that skeleton is hard to see.
10
10
 
11
- What CoreCoder does is squeeze the skeleton down to just over fourteen hundred lines of pure Python. To be precise, the agent engine (the loop, model interface, context, tools, session) is 1,405 lines once you drop blank lines and comments; add the outermost CLI terminal, config, and packaging and the whole package is 2,364 lines without blanks and comments, 2,908 physical. Every file is short enough to read in one sitting, and what runs is genuinely an agent: it edits code, runs commands, spins up its own subtasks, compresses context, tracks cost. It is not a toy. It takes every design decision a production agent makes, picks the most essential version of each, and writes it out honestly in the least code it can.
11
+ What CoreCoder does is squeeze the skeleton down to just over fourteen hundred lines of pure Python. To be precise, the agent engine (the loop, model interface, context, tools, session) is 1,483 lines once you drop blank lines and comments; add the outermost CLI terminal, config, and packaging and the whole package is 2,442 lines without blanks and comments, 2,989 physical. Every file is short enough to read in one sitting, and what runs is genuinely an agent: it edits code, runs commands, spins up its own subtasks, compresses context, tracks cost. It is not a toy. It takes every design decision a production agent makes, picks the most essential version of each, and writes it out honestly in the least code it can.
12
12
 
13
13
  Reading it is close to reading a runnable, annotated edition of Claude Code. The difference is that every line of CoreCoder is something you can breakpoint, change, run, and watch on your own machine. Every line count and every snippet I quote in this series is read straight out of the repository, not recalled from memory. I'll keep being pedantic about that, because in the coding-agent space far too many write-ups just make the numbers up.
14
14
 
@@ -73,7 +73,7 @@ _local.cwd = running
73
73
 
74
74
  ## 子 agent:给自己开一个分身
75
75
 
76
- `agent` 工具(`corecoder/tools/agent.py`,143 行)解决的是另一个问题。有些子任务很重,比如「把这个陌生代码库摸一遍,告诉我认证是怎么实现的」。这种活如果让主 agent 自己干,它得读一大堆文件、跑一堆搜索,这些中间过程全堆进主对话的窗口,等它摸清楚了,窗口也被探索垃圾塞得差不多了,真正的任务反而没空间了。
76
+ `agent` 工具(`corecoder/tools/agent.py`,224 行)解决的是另一个问题。有些子任务很重,比如「把这个陌生代码库摸一遍,告诉我认证是怎么实现的」。这种活如果让主 agent 自己干,它得读一大堆文件、跑一堆搜索,这些中间过程全堆进主对话的窗口,等它摸清楚了,窗口也被探索垃圾塞得差不多了,真正的任务反而没空间了。
77
77
 
78
78
  子 agent 的思路是:派一个有独立上下文的分身去干这件重活,它在自己的窗口里折腾,干完只把一个精简结论交回来。主 agent 的窗口始终干净,只多了一句「认证是这么实现的」。
79
79
 
@@ -73,7 +73,7 @@ I pull this passage out on its own because it's a particularly good teaching poi
73
73
 
74
74
  ## Sub-agents: spawning a clone of yourself
75
75
 
76
- The `agent` tool (`corecoder/tools/agent.py`, 143 lines) solves a different problem. Some subtasks are heavy, say "go over this unfamiliar codebase and tell me how authentication is implemented." If the main agent does this itself, it has to read a pile of files and run a bunch of searches, and all that intermediate process piles into the main conversation's window; by the time it's figured things out, the window is nearly stuffed with exploration garbage and the real task has no room left.
76
+ The `agent` tool (`corecoder/tools/agent.py`, 224 lines) solves a different problem. Some subtasks are heavy, say "go over this unfamiliar codebase and tell me how authentication is implemented." If the main agent does this itself, it has to read a pile of files and run a bunch of searches, and all that intermediate process piles into the main conversation's window; by the time it's figured things out, the window is nearly stuffed with exploration garbage and the real task has no room left.
77
77
 
78
78
  The sub-agent's idea is: dispatch a clone with its own independent context to do this heavy work, let it churn in its own window, and hand back only a distilled conclusion when done. The main agent's window stays clean throughout, with just one extra line, "authentication is implemented this way."
79
79
 
@@ -1,6 +1,6 @@
1
1
  """CoreCoder - Minimal AI coding agent inspired by Claude Code's architecture."""
2
2
 
3
- __version__ = "0.9.0"
3
+ __version__ = "0.10.0"
4
4
 
5
5
  from corecoder.agent import Agent
6
6
  from corecoder.config import Config
@@ -48,6 +48,11 @@ class AgentTool(Tool):
48
48
  "type": "string",
49
49
  "description": "What this worker should accomplish",
50
50
  },
51
+ "depends_on": {
52
+ "type": "array",
53
+ "description": "Worker names that must finish successfully before this worker starts",
54
+ "items": {"type": "string"},
55
+ },
51
56
  },
52
57
  "required": ["task"],
53
58
  },
@@ -86,7 +91,40 @@ class AgentTool(Tool):
86
91
  hooks=parent.hooks,
87
92
  )
88
93
 
89
- def _run_one(self, item: dict, cwd: str | None) -> str:
94
+ def _dependency_waves(self, work: list[dict]) -> list[list[dict]]:
95
+ by_name = {}
96
+ for item in work:
97
+ name = item["name"]
98
+ if name in by_name:
99
+ raise ValueError(f"duplicate worker name {name!r}")
100
+ by_name[name] = item
101
+ for item in work:
102
+ dependencies = item.get("depends_on", [])
103
+ if not isinstance(dependencies, list) or any(
104
+ not isinstance(dependency, str) or not dependency.strip() for dependency in dependencies
105
+ ):
106
+ raise ValueError(f"worker {item['name']!r} has an invalid depends_on list")
107
+ item["depends_on"] = [dependency.strip() for dependency in dependencies]
108
+ for dependency in item["depends_on"]:
109
+ if dependency == item["name"]:
110
+ raise ValueError(f"worker {item['name']!r} depends on itself")
111
+ if dependency not in by_name:
112
+ raise ValueError(f"worker {item['name']!r} depends on unknown worker {dependency!r}")
113
+
114
+ pending = work[:]
115
+ completed = set()
116
+ waves = []
117
+ while pending:
118
+ ready = [item for item in pending if all(name in completed for name in item["depends_on"])]
119
+ if not ready:
120
+ raise ValueError("worker dependency cycle detected")
121
+ waves.append(ready)
122
+ completed.update(item["name"] for item in ready)
123
+ ready_names = {item["name"] for item in ready}
124
+ pending = [item for item in pending if item["name"] not in ready_names]
125
+ return waves
126
+
127
+ def _run_one(self, item: dict, cwd: str | None, dependency_reports: dict | None = None) -> str:
90
128
  # Each worker gets the session cwd without leaking its own cd back.
91
129
  from .bash import get_tracked_cwd, set_tracked_cwd
92
130
 
@@ -95,6 +133,10 @@ class AgentTool(Tool):
95
133
  set_tracked_cwd(cwd)
96
134
  name = item.get("name")
97
135
  prompt = item["task"] if name is None else f"You are {name}.\n\n{item['task']}"
136
+ if dependency_reports:
137
+ prompt += "\n\nCompleted upstream reports:\n" + "\n\n".join(
138
+ f"### {dependency}\n{report}" for dependency, report in dependency_reports.items()
139
+ )
98
140
  result = self._new_subagent().chat(prompt)
99
141
  if len(result) > 5000:
100
142
  result = result[:4500] + "\n... (sub-agent output truncated)"
@@ -125,7 +167,13 @@ class AgentTool(Tool):
125
167
  return f"Error: task {i} needs a non-empty 'task' string"
126
168
  if name is not None and (not isinstance(name, str) or not name.strip()):
127
169
  return f"Error: task {i} needs a non-empty 'name' when provided"
128
- work.append({"task": prompt, "name": name.strip() if name else f"agent-{i}"})
170
+ work.append(
171
+ {
172
+ "task": prompt,
173
+ "name": name.strip() if name else f"agent-{i}",
174
+ "depends_on": item.get("depends_on", []),
175
+ }
176
+ )
129
177
 
130
178
  from .bash import get_tracked_cwd
131
179
 
@@ -136,8 +184,41 @@ class AgentTool(Tool):
136
184
  return result
137
185
  return f"[Sub-agent completed]\n{result}"
138
186
 
139
- # pool.map returns in task order even when workers finish out of order.
140
- with concurrent.futures.ThreadPoolExecutor(max_workers=min(4, len(work))) as pool:
141
- results = list(pool.map(lambda item: self._run_one(item, cwd), work))
142
- reports = [f"### {item['name']}\n{result}" for item, result in zip(work, results)]
187
+ try:
188
+ waves = self._dependency_waves(work)
189
+ except ValueError as e:
190
+ return f"Error: {e}"
191
+
192
+ results = {}
193
+ for wave in waves:
194
+ ready = []
195
+ for item in wave:
196
+ failed_dependency = next(
197
+ (
198
+ dependency
199
+ for dependency in item["depends_on"]
200
+ if results[dependency].startswith(("Sub-agent error:", "Sub-agent skipped:"))
201
+ ),
202
+ None,
203
+ )
204
+ if failed_dependency is not None:
205
+ results[item["name"]] = f"Sub-agent skipped: dependency {failed_dependency!r} failed"
206
+ else:
207
+ ready.append(item)
208
+ if not ready:
209
+ continue
210
+ with concurrent.futures.ThreadPoolExecutor(max_workers=min(4, len(ready))) as pool:
211
+ completed = list(
212
+ pool.map(
213
+ lambda item: self._run_one(
214
+ item,
215
+ cwd,
216
+ {dependency: results[dependency] for dependency in item["depends_on"]},
217
+ ),
218
+ ready,
219
+ )
220
+ )
221
+ results.update({item["name"]: result for item, result in zip(ready, completed)})
222
+
223
+ reports = [f"### {item['name']}\n{results[item['name']]}" for item in work]
143
224
  return f"[Sub-agent team: {len(work)} reports]\n\n" + "\n\n".join(reports)
@@ -48,6 +48,22 @@ def run_demo() -> int:
48
48
  ]
49
49
  )
50
50
  print(result)
51
+ dependent = tool.execute(
52
+ tasks=[
53
+ {"name": "researcher", "task": "Find semantic risks in sorted(set(records))."},
54
+ {
55
+ "name": "implementer",
56
+ "task": "Propose an order-preserving deduplication using the research.",
57
+ "depends_on": ["researcher"],
58
+ },
59
+ {
60
+ "name": "reviewer",
61
+ "task": "Review the research and proposed fix for unhashable records.",
62
+ "depends_on": ["researcher", "implementer"],
63
+ },
64
+ ]
65
+ )
66
+ print("\n[Dependent pipeline]\n" + dependent)
51
67
  print(f"\nThree workers reported in {time.monotonic() - started:.2f}s (offline scripted findings).")
52
68
  return 0
53
69
 
@@ -5,8 +5,8 @@ build-backend = "hatchling.build"
5
5
 
6
6
  [project]
7
7
  name = "corecoder"
8
- version = "0.9.0"
9
- description = "Minimal AI coding agent (2,908 lines of Python) inspired by Claude Code. Works with any LLM. (formerly NanoCoder)"
8
+ version = "0.10.0"
9
+ description = "Minimal AI coding agent (2,989 lines of Python) inspired by Claude Code. Works with any LLM. (formerly NanoCoder)"
10
10
  readme = "README.md"
11
11
  license = "MIT"
12
12
  requires-python = ">=3.10"
@@ -28,6 +28,8 @@ def test_multi_agent_schema_accepts_named_tasks():
28
28
  assert tasks["maxItems"] == 8
29
29
  assert tasks["items"]["required"] == ["task"]
30
30
  assert "name" in tasks["items"]["properties"]
31
+ assert tasks["items"]["properties"]["depends_on"]["type"] == "array"
32
+ assert tasks["items"]["properties"]["depends_on"]["items"] == {"type": "string"}
31
33
  assert schema["anyOf"] == [{"required": ["task"]}, {"required": ["tasks"]}]
32
34
 
33
35
 
@@ -38,6 +40,7 @@ def test_offline_multi_agent_demo(capsys):
38
40
  assert "### implementer" in output
39
41
  assert "### reviewer" in output
40
42
  assert "unhashable records" in output
43
+ assert "Dependent pipeline" in output
41
44
 
42
45
 
43
46
  def test_single_sub_agent_keeps_its_existing_contract(monkeypatch):
@@ -121,6 +124,89 @@ def test_multi_agent_failure_does_not_cancel_siblings(monkeypatch):
121
124
  assert "Sub-agent error: synthetic failure" in result
122
125
 
123
126
 
127
+ def test_multi_agent_dependencies_share_results_in_dependency_order(monkeypatch):
128
+ _, tool, _ = parent_agent()
129
+ events = []
130
+ prompts = {}
131
+
132
+ def fake_chat(self, text, **kwargs):
133
+ role = text.split("\n", 1)[0].removeprefix("You are ").removesuffix(".")
134
+ prompts[role] = text
135
+ events.append(("start", role))
136
+ if role == "implementer":
137
+ assert ("end", "researcher") in events
138
+ assert "researcher report" in text
139
+ elif role == "reviewer":
140
+ assert ("end", "implementer") in events
141
+ assert "implementer report" in text
142
+ events.append(("end", role))
143
+ return f"{role} report"
144
+
145
+ monkeypatch.setattr(Agent, "chat", fake_chat)
146
+ result = tool.execute(
147
+ tasks=[
148
+ {"name": "reviewer", "task": "Review both outputs", "depends_on": ["implementer"]},
149
+ {"name": "implementer", "task": "Use the research", "depends_on": ["researcher"]},
150
+ {"name": "researcher", "task": "Inspect first"},
151
+ ]
152
+ )
153
+
154
+ assert events.index(("end", "researcher")) < events.index(("start", "implementer"))
155
+ assert events.index(("end", "implementer")) < events.index(("start", "reviewer"))
156
+ assert result.index("### reviewer") < result.index("### implementer") < result.index("### researcher")
157
+ assert "researcher report" in prompts["implementer"]
158
+ assert "implementer report" in prompts["reviewer"]
159
+
160
+
161
+ def test_multi_agent_failed_dependency_skips_downstream_but_not_independent_work(monkeypatch):
162
+ _, tool, _ = parent_agent()
163
+ started = []
164
+
165
+ def fake_chat(self, text, **kwargs):
166
+ role = text.split("\n", 1)[0].removeprefix("You are ").removesuffix(".")
167
+ started.append(role)
168
+ if role == "researcher":
169
+ raise RuntimeError("research failed")
170
+ return f"{role} report"
171
+
172
+ monkeypatch.setattr(Agent, "chat", fake_chat)
173
+ result = tool.execute(
174
+ tasks=[
175
+ {"name": "researcher", "task": "fail"},
176
+ {"name": "implementer", "task": "must not start", "depends_on": ["researcher"]},
177
+ {"name": "reviewer", "task": "independent review"},
178
+ ]
179
+ )
180
+
181
+ assert started == ["researcher", "reviewer"]
182
+ assert "Sub-agent skipped: dependency 'researcher' failed" in result
183
+ assert "reviewer report" in result
184
+
185
+
186
+ def test_multi_agent_rejects_invalid_dependencies_without_spawning(monkeypatch):
187
+ _, tool, _ = parent_agent()
188
+
189
+ def unexpected_chat(self, text, **kwargs):
190
+ raise AssertionError("invalid work must not spawn")
191
+
192
+ monkeypatch.setattr(Agent, "chat", unexpected_chat)
193
+ invalid_calls = [
194
+ {"tasks": [{"name": "same", "task": "one"}, {"name": "same", "task": "two"}]},
195
+ {"tasks": [{"name": "one", "task": "one", "depends_on": ["missing"]}]},
196
+ {"tasks": [{"name": "one", "task": "one", "depends_on": ["one"]}]},
197
+ {
198
+ "tasks": [
199
+ {"name": "one", "task": "one", "depends_on": ["two"]},
200
+ {"name": "two", "task": "two", "depends_on": ["one"]},
201
+ ]
202
+ },
203
+ {"tasks": [{"name": "one", "task": "one", "depends_on": "researcher"}]},
204
+ {"tasks": [{"name": "one", "task": "one", "depends_on": [" "]}]},
205
+ ]
206
+ for kwargs in invalid_calls:
207
+ assert tool.execute(**kwargs).startswith("Error:")
208
+
209
+
124
210
  def test_multi_agent_rejects_ambiguous_or_unbounded_work(monkeypatch):
125
211
  _, tool, _ = parent_agent()
126
212
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes