turnloop 0.1.1__tar.gz → 0.1.3__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 (150) hide show
  1. {turnloop-0.1.1 → turnloop-0.1.3}/.github/workflows/ci.yml +2 -2
  2. {turnloop-0.1.1 → turnloop-0.1.3}/PKG-INFO +101 -33
  3. {turnloop-0.1.1 → turnloop-0.1.3}/README.md +969 -901
  4. {turnloop-0.1.1 → turnloop-0.1.3}/pyproject.toml +89 -84
  5. {turnloop-0.1.1 → turnloop-0.1.3}/tests/test_config.py +256 -201
  6. {turnloop-0.1.1 → turnloop-0.1.3}/tests/test_loop.py +11 -9
  7. {turnloop-0.1.1 → turnloop-0.1.3}/tests/test_providers.py +149 -1
  8. {turnloop-0.1.1 → turnloop-0.1.3}/tests/test_tools_files.py +27 -0
  9. turnloop-0.1.3/tests/test_websearch.py +160 -0
  10. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/__init__.py +3 -3
  11. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/agent/loop.py +17 -6
  12. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/cli.py +241 -220
  13. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/config.py +16 -4
  14. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/core/messages.py +21 -1
  15. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/diagnostics.py +6 -2
  16. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/runner.py +4 -1
  17. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/permissions/engine.py +3 -3
  18. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/providers/anthropic.py +6 -0
  19. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/providers/base.py +5 -0
  20. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/providers/gemini.py +15 -2
  21. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/providers/openai_compat.py +16 -4
  22. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/sessions/store.py +1 -1
  23. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/tools/base.py +5 -1
  24. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/tools/builtin.py +2 -0
  25. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/tools/read.py +50 -3
  26. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/tools/runner.py +17 -12
  27. turnloop-0.1.3/turnloop/tools/websearch.py +318 -0
  28. {turnloop-0.1.1 → turnloop-0.1.3}/.gitignore +0 -0
  29. {turnloop-0.1.1 → turnloop-0.1.3}/.turnloop/.gitignore +0 -0
  30. {turnloop-0.1.1 → turnloop-0.1.3}/tests/conftest.py +0 -0
  31. {turnloop-0.1.1 → turnloop-0.1.3}/tests/fixtures/anthropic_tool_call.sse +0 -0
  32. {turnloop-0.1.1 → turnloop-0.1.3}/tests/fixtures/glm_tool_call.sse +0 -0
  33. {turnloop-0.1.1 → turnloop-0.1.3}/tests/fixtures/mcp_echo_server.py +0 -0
  34. {turnloop-0.1.1 → turnloop-0.1.3}/tests/test_bash.py +0 -0
  35. {turnloop-0.1.1 → turnloop-0.1.3}/tests/test_compaction.py +0 -0
  36. {turnloop-0.1.1 → turnloop-0.1.3}/tests/test_experiments.py +0 -0
  37. {turnloop-0.1.1 → turnloop-0.1.3}/tests/test_hooks_mcp_commands.py +0 -0
  38. {turnloop-0.1.1 → turnloop-0.1.3}/tests/test_permissions.py +0 -0
  39. {turnloop-0.1.1 → turnloop-0.1.3}/tests/test_tui.py +0 -0
  40. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/__main__.py +0 -0
  41. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/agent/__init__.py +0 -0
  42. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/agent/factory.py +0 -0
  43. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/agent/headless.py +0 -0
  44. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/agent/subagent.py +0 -0
  45. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/agent/system_prompt.py +0 -0
  46. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/commands/__init__.py +0 -0
  47. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/commands/dispatch.py +0 -0
  48. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/commands/loader.py +0 -0
  49. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/context/__init__.py +0 -0
  50. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/context/budget.py +0 -0
  51. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/context/compaction.py +0 -0
  52. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/context/memory.py +0 -0
  53. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/core/__init__.py +0 -0
  54. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/core/events.py +0 -0
  55. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/core/ids.py +0 -0
  56. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/core/tokens.py +0 -0
  57. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/errors.py +0 -0
  58. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/__init__.py +0 -0
  59. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/configs/bakeoff.yaml +0 -0
  60. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/configs/ceiling-check.yaml +0 -0
  61. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/configs/context-glm.yaml +0 -0
  62. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/configs/context.yaml +0 -0
  63. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/configs/glm-selfhosted.yaml +0 -0
  64. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/configs/groq-free.yaml +0 -0
  65. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/configs/hard-calibration.yaml +0 -0
  66. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/configs/hard-glm.yaml +0 -0
  67. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/configs/loop.yaml +0 -0
  68. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/configs/nvidia-smoke.yaml +0 -0
  69. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/configs/reliability.yaml +0 -0
  70. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/configs/rewrite-calibration.yaml +0 -0
  71. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/configs/smoke.yaml +0 -0
  72. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/graders.py +0 -0
  73. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/report.py +0 -0
  74. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/default.yaml +0 -0
  75. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/bulky/module_1.py +0 -0
  76. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/bulky/module_2.py +0 -0
  77. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/bulky/module_3.py +0 -0
  78. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/bulky/module_4.py +0 -0
  79. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/bulky/module_5.py +0 -0
  80. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/bulky/module_6.py +0 -0
  81. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/bulky/module_7.py +0 -0
  82. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/bulky/module_8.py +0 -0
  83. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/circular_import/models.py +0 -0
  84. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/circular_import/services.py +0 -0
  85. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/circular_import/test_cancel.py +0 -0
  86. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/cli_flag/app.py +0 -0
  87. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/cross_file_contract/decode.py +0 -0
  88. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/cross_file_contract/encode.py +0 -0
  89. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/cross_file_contract/test_roundtrip.py +0 -0
  90. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/cross_file_contract/test_wire_format.py +0 -0
  91. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/empty/.keep +0 -0
  92. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/failing_test/calc.py +0 -0
  93. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/failing_test/test_calc.py +0 -0
  94. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/generate_bulky.py +0 -0
  95. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/grep_edit/ingest.py +0 -0
  96. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/grep_edit/load.py +0 -0
  97. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/grep_edit/transform.py +0 -0
  98. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/misleading_traceback/checkout.py +0 -0
  99. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/misleading_traceback/invoice.py +0 -0
  100. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/misleading_traceback/pricing.py +0 -0
  101. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/misleading_traceback/test_checkout.py +0 -0
  102. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/misleading_traceback/test_invoice.py +0 -0
  103. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/needle/inventory.py +0 -0
  104. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/needle/pricing.py +0 -0
  105. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/needle/shipping.py +0 -0
  106. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/regression_trap/test_validators.py +0 -0
  107. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/regression_trap/validators.py +0 -0
  108. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/rename/billing.py +0 -0
  109. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/rename/report.py +0 -0
  110. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/rename/test_billing.py +0 -0
  111. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/spec/parser.py +0 -0
  112. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/spec/test_parser.py +0 -0
  113. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/subtle_spec_edge/leaderboard.py +0 -0
  114. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/subtle_spec_edge/test_leaderboard.py +0 -0
  115. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/experiments/suite/fixtures/util/util.py +0 -0
  116. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/hooks/__init__.py +0 -0
  117. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/hooks/runner.py +0 -0
  118. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/mcp/__init__.py +0 -0
  119. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/mcp/adapter.py +0 -0
  120. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/mcp/client.py +0 -0
  121. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/permissions/__init__.py +0 -0
  122. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/permissions/rules.py +0 -0
  123. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/providers/__init__.py +0 -0
  124. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/providers/mock.py +0 -0
  125. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/providers/pricing.py +0 -0
  126. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/providers/registry.py +0 -0
  127. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/providers/sse.py +0 -0
  128. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/sessions/__init__.py +0 -0
  129. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/sessions/models.py +0 -0
  130. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/tools/__init__.py +0 -0
  131. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/tools/ask.py +0 -0
  132. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/tools/bash.py +0 -0
  133. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/tools/edit.py +0 -0
  134. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/tools/glob.py +0 -0
  135. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/tools/grep.py +0 -0
  136. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/tools/shell.py +0 -0
  137. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/tools/skill.py +0 -0
  138. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/tools/task.py +0 -0
  139. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/tools/textio.py +0 -0
  140. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/tools/todo.py +0 -0
  141. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/tools/webfetch.py +0 -0
  142. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/tools/write.py +0 -0
  143. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/tui/__init__.py +0 -0
  144. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/tui/app.py +0 -0
  145. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/tui/app.tcss +0 -0
  146. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/tui/bridge.py +0 -0
  147. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/tui/widgets/__init__.py +0 -0
  148. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/tui/widgets/permission.py +0 -0
  149. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/tui/widgets/status.py +0 -0
  150. {turnloop-0.1.1 → turnloop-0.1.3}/turnloop/tui/widgets/transcript.py +0 -0
@@ -16,9 +16,9 @@ jobs:
16
16
  python: ["3.11", "3.13"]
17
17
  runs-on: ${{ matrix.os }}
18
18
  steps:
19
- - uses: actions/checkout@v4
19
+ - uses: actions/checkout@v5
20
20
 
21
- - uses: actions/setup-python@v5
21
+ - uses: actions/setup-python@v6
22
22
  with:
23
23
  python-version: ${{ matrix.python }}
24
24
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: turnloop
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: A local-first agentic coding harness: streaming tool loop, permission gating, context compaction, subagents, and any LLM provider.
5
5
  Project-URL: Homepage, https://github.com/Pranesh-2005/turnloop
6
6
  Author: Pranesh
@@ -20,11 +20,11 @@ Requires-Dist: pydantic>=2.7
20
20
  Requires-Dist: pyyaml>=6.0
21
21
  Requires-Dist: textual>=0.80
22
22
  Provides-Extra: dev
23
- Requires-Dist: mypy>=1.11; extra == 'dev'
23
+ Requires-Dist: mypy<3,>=2.1; extra == 'dev'
24
24
  Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
25
25
  Requires-Dist: pytest-textual-snapshot>=1.0; extra == 'dev'
26
26
  Requires-Dist: pytest>=8.0; extra == 'dev'
27
- Requires-Dist: ruff>=0.6; extra == 'dev'
27
+ Requires-Dist: ruff<0.17,>=0.16.1; extra == 'dev'
28
28
  Description-Content-Type: text/markdown
29
29
 
30
30
  # turnloop
@@ -49,7 +49,7 @@ tl experiment run smoke # a measurement run, offline and free
49
49
 
50
50
  Four runtime dependencies: `textual`, `pydantic`, `httpx`, `pyyaml`. No vendor
51
51
  SDKs, no agent framework — the loop is the point, so the loop is written here.
52
- ~18,000 lines of Python, 233 tests, no network in the default test run.
52
+ ~18,000 lines of Python, 255 tests, no network in the default test run.
53
53
 
54
54
  ---
55
55
 
@@ -117,7 +117,7 @@ point.
117
117
  Verify:
118
118
 
119
119
  ```bash
120
- tl --version # turnloop 0.1.0
120
+ tl --version # turnloop 0.1.3
121
121
  tl doctor # every provider, key presence, shell, context budget
122
122
  ```
123
123
 
@@ -126,7 +126,7 @@ tl doctor # every provider, key presence, shell, context budge
126
126
  context arithmetic for the active provider:
127
127
 
128
128
  ```
129
- + context budget 32,000 window − 4,096 output − 835 system − 3,365 tools = 23,704 for history
129
+ + context budget 32,000 window - 4,096 output - 881 system - 3,567 tools = 23,456 for history
130
130
  ```
131
131
 
132
132
  ---
@@ -212,18 +212,36 @@ Custom commands live in `.turnloop/commands/*.md`.
212
212
  - **`TURNLOOP.md`** at the project root for standing instructions (`CLAUDE.md` and
213
213
  `AGENTS.md` are also read)
214
214
  - **Text files of any kind** via the `Read` tool — source, JSON, YAML, CSV, Markdown
215
+ - **Images** — PNG, JPEG, GIF and WEBP via `Read`, detected by magic bytes rather
216
+ than extension, on providers whose `Capabilities.supports_vision` is true
217
+ (Anthropic, OpenAI, Gemini). On a text-only provider `Read` refuses the image and
218
+ names the provider, instead of sending a payload the model cannot parse — which
219
+ matters here, because GLM-5.2 W4A16 as deployed is text-only.
215
220
 
216
- **Not supported:** images (there is no image content block in the message model —
217
- only `TextBlock`, `ThinkingBlock`, `ToolUseBlock`, `ToolResultBlock`) and binary
218
- files (refused by a null-byte sniff, which covers PDFs, `.docx`, archives and
219
- images-on-disk). Anything convertible on the command line comes back in scope,
220
- because `Bash` exists: `pdftotext spec.pdf -` and the agent will reach for it itself.
221
+ **Not supported:** binary files other than those image formats — refused by a
222
+ null-byte sniff, which covers PDFs, `.docx` and archives. Anything convertible on
223
+ the command line comes back in scope, because `Bash` exists: `pdftotext spec.pdf -`
224
+ and the agent will reach for it itself.
221
225
 
222
226
  ### Tools
223
227
 
224
- Ten registered: `Read` `Write` `Edit` `Glob` `Grep` `Bash` `TodoWrite` `Task`
225
- `WebFetch` `AskUserQuestion`. Schemas are generated from pydantic models, so the
226
- declared schema and the validation are the same object and cannot drift.
228
+ Eleven registered: `Read` `Write` `Edit` `Glob` `Grep` `Bash` `TodoWrite` `Task`
229
+ `WebFetch` `WebSearch` `AskUserQuestion`, plus `Skill` when the project has skills.
230
+ Schemas are generated from pydantic models, so the declared schema and the
231
+ validation are the same object and cannot drift.
232
+
233
+ `WebSearch` and `WebFetch` are the pair: search finds URLs, fetch reads one. Both
234
+ are `read_only` and `parallel_safe`, so they work in plan mode and run concurrently
235
+ with `Read`/`Grep`. **Search needs no API key** — the default DuckDuckGo backend
236
+ works on a fresh install with an empty config. Brave and Tavily are opt-in upgrades:
237
+
238
+ ```json
239
+ { "search": { "backend": "brave", "api_key_env": "BRAVE_API_KEY" } }
240
+ ```
241
+
242
+ A keyed backend only errors if you actually select one, so the zero-config path
243
+ never breaks. Results are budgeted for a small window — 10 results maximum,
244
+ 200-character snippets, 4k characters total, roughly 1k tokens against GLM's 65k.
227
245
 
228
246
  ---
229
247
 
@@ -234,8 +252,8 @@ turnloop/
234
252
  core/ messages, events, token estimation, ids
235
253
  providers/ anthropic · openai_compat (GLM/Groq/NVIDIA/vLLM) · gemini · mock
236
254
  + sse parser, pricing/capability presets, registry
237
- tools/ Read Edit Write Bash Glob Grep TodoWrite Task WebFetch Ask Skill
238
- + runner, shell scanner, text io
255
+ tools/ Read Edit Write Bash Glob Grep TodoWrite Task WebFetch WebSearch
256
+ Ask Skill + runner, shell scanner, text io
239
257
  permissions/ rule grammar, matcher, engine
240
258
  agent/ loop, system prompt, subagents, factory, headless
241
259
  context/ budget, three-tier compaction, memory files
@@ -373,7 +391,7 @@ All nine ship preconfigured. `doctor` shows which have keys present.
373
391
  | `groq` | openai_compat | `openai/gpt-oss-120b` | `GROQ_API_KEY` | free tier: 8k TPM |
374
392
  | `anthropic` | anthropic | `claude-sonnet-4-5` | `ANTHROPIC_API_KEY` | |
375
393
  | `openai` | openai_compat | `gpt-4.1` | `OPENAI_API_KEY` | |
376
- | `gemini` | gemini | `gemini-2.5-pro` | `GEMINI_API_KEY` | |
394
+ | `gemini` | gemini | `gemini-flash-latest` | `GEMINI_API_KEY` | alias, not a pin — see below |
377
395
  | `blaxel` | openai_compat | `gpt-4o-mini` | `BL_API_KEY` | sandbox; ignores the model field |
378
396
  | `ollama` | openai_compat | `qwen2.5-coder:14b` | none | local |
379
397
 
@@ -402,6 +420,21 @@ smoke tests, not for experiments.
402
420
  NVIDIA model would silently inherit the self-hosted 65k window and $18.16/hr cost
403
421
  basis. A regression test guards it.
404
422
 
423
+ **Gemini's default model is an alias because pinned ones rot in two different ways.**
424
+ 0.1.1 shipped `gemini-2.5-pro`, whose free-tier quota is literally zero: a brand-new
425
+ key gets `429 ... generate_content_free_tier_input_token_count, limit: 0` on its first
426
+ request, which reads as a broken adapter rather than a model that was never free.
427
+ Pinning `gemini-2.5-flash` instead fails the other way — `404 no longer available to
428
+ new users`. `gemini-flash-latest` follows Google's own pointer. Worth knowing when
429
+ reading a 429 from any provider: the status code says "slow down", the body says
430
+ whether slowing down will ever help.
431
+
432
+ Its free tier is **5 requests per minute**, which an agent loop reaches in one
433
+ ordinary task — a fix-two-bugs-and-write-tests run spent six turns and died on the
434
+ seventh. The retry ladder is correct and still loses, because the window is longer
435
+ than the backoff. Free Gemini is a good way to try the harness and a bad way to do
436
+ sustained work.
437
+
405
438
  ---
406
439
 
407
440
  ## Adding a provider
@@ -798,10 +831,31 @@ suddenly have them exposed.
798
831
  never manifested only because `rg` was not installed on the machine and it had been
799
832
  falling back to a pure-Python walk.
800
833
 
801
- **The lesson, three times over in one project:** an 84-run sweep failing while
802
- printing a tidy report of zeros; a tool returning empty with `ok: true`; a sweep
803
- sitting at 10/18 doing nothing for 25 minutes. Each was caught only by looking past
804
- the summary line.
834
+ **The Gemini adapter passed a live test and was still broken.**
835
+
836
+ Gemini returns a `thoughtSignature` alongside each `functionCall` part. Replay the
837
+ assistant turn without it and the next request is rejected: `400 — Function call is
838
+ missing a thought_signature in functionCall parts`. The adapter decoded the call and
839
+ dropped the signature, so **turn two of every tool conversation failed**.
840
+
841
+ It was verified live before shipping — one text turn, one tool call, both correct —
842
+ and that verification is exactly what hid the bug. A single tool call never replays
843
+ anything. The failure needs a *second* request carrying the first one's output, which
844
+ is the first thing a real agent loop does and the last thing a smoke test does.
845
+
846
+ Found by setting the harness up as a user would and giving it an ordinary task, not by
847
+ testing the adapter. The signature now round-trips through `ToolUseBlock.signature`
848
+ and through session JSONL, so a `--resume`d conversation does not hit the same wall.
849
+
850
+ **0.1.2 shipped with this bug.** It went to PyPI on the strength of the single-turn
851
+ check, and anyone who pointed 0.1.2 at Gemini got one working tool call and then a
852
+ 400. Fixed in 0.1.3.
853
+
854
+ **The lesson, four times over in one project:** an 84-run sweep failing while printing
855
+ a tidy report of zeros; a tool returning empty with `ok: true`; a sweep sitting at
856
+ 10/18 doing nothing for 25 minutes; an adapter passing its live test and failing on
857
+ the turn the test never made. Each was caught only by looking past the summary line —
858
+ and the last one only by using the thing instead of testing it.
805
859
 
806
860
  ---
807
861
 
@@ -835,7 +889,7 @@ one-line edit in a CRLF checkout does not produce a whole-file diff.
835
889
  ## Testing
836
890
 
837
891
  ```bash
838
- pytest # 233 tests, no network
892
+ pytest # 255 tests, no network
839
893
  ruff check turnloop
840
894
  mypy turnloop
841
895
  ```
@@ -844,13 +898,14 @@ mypy turnloop
844
898
  |---|---|---|
845
899
  | `test_permissions.py` | 48 | rule grammar, compound-command splitting, mode enforcement |
846
900
  | `test_experiments.py` | 36 | runner, graders, report, suite invariants, config resolution |
847
- | `test_providers.py` | 27 | adapters against recorded `.sse` fixtures |
848
- | `test_tools_files.py` | 25 | Read/Write/Edit/Glob/Grep, encodings, newline preservation |
901
+ | `test_providers.py` | 35 | adapters vs recorded `.sse`, image blocks, thought signatures |
902
+ | `test_tools_files.py` | 28 | Read/Write/Edit/Glob/Grep, encodings, newlines, image detection |
849
903
  | `test_loop.py` | 20 | streaming, truncation, iteration cap |
850
904
  | `test_hooks_mcp_commands.py` | 20 | lifecycle hooks, MCP client, slash commands |
851
- | `test_config.py` | 19 | layering, env overrides, capability presets |
905
+ | `test_config.py` | 21 | layering, env overrides, presets, narrow-console guard |
852
906
  | `test_compaction.py` | 16 | three tiers, tool_use/tool_result invariant |
853
907
  | `test_bash.py` | 14 | shell selection, process-tree kill |
908
+ | `test_websearch.py` | 9 | HTML parsing, backend selection, truncation, failure modes |
854
909
  | `test_tui.py` | 8 | Textual snapshots |
855
910
 
856
911
  `conftest.py` monkeypatches httpx's transport to raise unless a test is marked
@@ -889,7 +944,8 @@ sessions wrongly.
889
944
 
890
945
  API keys come from the environment or a `.env` at the project root. A real environment
891
946
  variable always wins over the file, and `doctor` reports which key *names* were loaded
892
- — never their values.
947
+ — never their values. This applies to `search.api_key_env` exactly as it does to a
948
+ provider's: settings files name the variable, never the secret.
893
949
 
894
950
  Project instructions go in `TURNLOOP.md` (`CLAUDE.md` and `AGENTS.md` are also read).
895
951
  Slash commands live in `.turnloop/commands/*.md`, skills in
@@ -907,13 +963,25 @@ just because it is on disk.
907
963
 
908
964
  Stated rather than buried:
909
965
 
910
- - **No image support.** No image content block exists in the message model. Adding one
911
- is small (three adapters, a `supports_vision` flag, `Read` returning it instead of
912
- refusing) but useless on the primary target — GLM-5.2 W4A16 as deployed is text-only.
913
- - **No binary file support.** Null-byte sniff refuses them. Convert via `Bash`.
914
- - **The Anthropic, Gemini and OpenAI adapters are fixture-tested but have never sent a
915
- real request.** They are coded against the documented wire format and pass against
916
- recorded SSE, which is not the same as verified.
966
+ - **Image support is verified on two of three encoders.** The `openai_compat` data-URL
967
+ path and Gemini's `inline_data` path both round-trip a real image live; Anthropic's
968
+ is still fixture-only. None of it can be exercised on the primary target, since
969
+ GLM-5.2 W4A16 as deployed is text-only.
970
+ - **No binary file support** beyond those image formats. Null-byte sniff refuses the
971
+ rest. Convert via `Bash`.
972
+ - **`WebSearch`'s default backend parses HTML, not an API.** DuckDuckGo will rate-limit
973
+ and will eventually change its markup; both are handled as distinct, actionable
974
+ errors rather than a crash, but a keyed backend is the durable choice for anything
975
+ that matters. That trade is deliberate: the default has to work with no key at all.
976
+ - **The Anthropic adapter is fixture-tested but has never sent a real request.** It is
977
+ coded against the documented wire format and passes against recorded SSE, which is not
978
+ the same as verified. It is now the only one: Gemini is verified through a real
979
+ multi-turn agent loop on `gemini-flash-latest` — tool calls, edits, file writes and
980
+ images — and OpenAI needs no separate verification because `openai` is
981
+ `kind="openai_compat"`, the same adapter Groq, NVIDIA, Ollama, Blaxel and the
982
+ self-hosted GLM endpoint all run through, making it the most exercised code path in
983
+ the repo. "Verified" here means multi-turn deliberately: a single-turn check on
984
+ Gemini passed while the loop was broken. See [Bugs worth reading about](#bugs-worth-reading-about).
917
985
  - **`subtle_spec_edge` currently passes for nobody** (0/5 on gpt-4o-mini). The rewrite
918
986
  overshot — runs die on collection errors and leave `NotImplementedError`, which is
919
987
  failure-to-produce-working-code, not falling for the decoy. It needs another pass.