turnloop 0.1.1__tar.gz → 0.1.2__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.
- {turnloop-0.1.1 → turnloop-0.1.2}/.github/workflows/ci.yml +2 -2
- {turnloop-0.1.1 → turnloop-0.1.2}/PKG-INFO +65 -26
- {turnloop-0.1.1 → turnloop-0.1.2}/README.md +62 -23
- {turnloop-0.1.1 → turnloop-0.1.2}/pyproject.toml +8 -3
- {turnloop-0.1.1 → turnloop-0.1.2}/tests/test_config.py +256 -201
- {turnloop-0.1.1 → turnloop-0.1.2}/tests/test_loop.py +11 -9
- {turnloop-0.1.1 → turnloop-0.1.2}/tests/test_providers.py +64 -1
- {turnloop-0.1.1 → turnloop-0.1.2}/tests/test_tools_files.py +27 -0
- turnloop-0.1.2/tests/test_websearch.py +160 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/__init__.py +1 -1
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/agent/loop.py +17 -6
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/cli.py +241 -220
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/config.py +16 -4
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/core/messages.py +14 -1
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/diagnostics.py +6 -2
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/runner.py +4 -1
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/permissions/engine.py +3 -3
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/providers/anthropic.py +6 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/providers/base.py +5 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/providers/gemini.py +5 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/providers/openai_compat.py +16 -4
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/sessions/store.py +1 -1
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/tools/base.py +5 -1
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/tools/builtin.py +2 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/tools/read.py +50 -3
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/tools/runner.py +17 -12
- turnloop-0.1.2/turnloop/tools/websearch.py +318 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/.gitignore +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/.turnloop/.gitignore +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/tests/conftest.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/tests/fixtures/anthropic_tool_call.sse +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/tests/fixtures/glm_tool_call.sse +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/tests/fixtures/mcp_echo_server.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/tests/test_bash.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/tests/test_compaction.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/tests/test_experiments.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/tests/test_hooks_mcp_commands.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/tests/test_permissions.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/tests/test_tui.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/__main__.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/agent/__init__.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/agent/factory.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/agent/headless.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/agent/subagent.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/agent/system_prompt.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/commands/__init__.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/commands/dispatch.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/commands/loader.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/context/__init__.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/context/budget.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/context/compaction.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/context/memory.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/core/__init__.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/core/events.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/core/ids.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/core/tokens.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/errors.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/__init__.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/configs/bakeoff.yaml +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/configs/ceiling-check.yaml +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/configs/context-glm.yaml +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/configs/context.yaml +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/configs/glm-selfhosted.yaml +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/configs/groq-free.yaml +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/configs/hard-calibration.yaml +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/configs/hard-glm.yaml +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/configs/loop.yaml +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/configs/nvidia-smoke.yaml +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/configs/reliability.yaml +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/configs/rewrite-calibration.yaml +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/configs/smoke.yaml +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/graders.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/report.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/default.yaml +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/bulky/module_1.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/bulky/module_2.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/bulky/module_3.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/bulky/module_4.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/bulky/module_5.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/bulky/module_6.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/bulky/module_7.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/bulky/module_8.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/circular_import/models.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/circular_import/services.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/circular_import/test_cancel.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/cli_flag/app.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/cross_file_contract/decode.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/cross_file_contract/encode.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/cross_file_contract/test_roundtrip.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/cross_file_contract/test_wire_format.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/empty/.keep +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/failing_test/calc.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/failing_test/test_calc.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/generate_bulky.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/grep_edit/ingest.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/grep_edit/load.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/grep_edit/transform.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/misleading_traceback/checkout.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/misleading_traceback/invoice.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/misleading_traceback/pricing.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/misleading_traceback/test_checkout.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/misleading_traceback/test_invoice.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/needle/inventory.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/needle/pricing.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/needle/shipping.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/regression_trap/test_validators.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/regression_trap/validators.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/rename/billing.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/rename/report.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/rename/test_billing.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/spec/parser.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/spec/test_parser.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/subtle_spec_edge/leaderboard.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/subtle_spec_edge/test_leaderboard.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/util/util.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/hooks/__init__.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/hooks/runner.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/mcp/__init__.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/mcp/adapter.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/mcp/client.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/permissions/__init__.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/permissions/rules.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/providers/__init__.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/providers/mock.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/providers/pricing.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/providers/registry.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/providers/sse.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/sessions/__init__.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/sessions/models.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/tools/__init__.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/tools/ask.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/tools/bash.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/tools/edit.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/tools/glob.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/tools/grep.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/tools/shell.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/tools/skill.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/tools/task.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/tools/textio.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/tools/todo.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/tools/webfetch.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/tools/write.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/tui/__init__.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/tui/app.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/tui/app.tcss +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/tui/bridge.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/tui/widgets/__init__.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/tui/widgets/permission.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/turnloop/tui/widgets/status.py +0 -0
- {turnloop-0.1.1 → turnloop-0.1.2}/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@
|
|
19
|
+
- uses: actions/checkout@v5
|
|
20
20
|
|
|
21
|
-
- uses: actions/setup-python@
|
|
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.
|
|
3
|
+
Version: 0.1.2
|
|
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
|
|
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
|
|
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,
|
|
52
|
+
~18,000 lines of Python, 249 tests, no network in the default test run.
|
|
53
53
|
|
|
54
54
|
---
|
|
55
55
|
|
|
@@ -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:**
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
225
|
-
`WebFetch` `AskUserQuestion
|
|
226
|
-
|
|
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
|
|
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-
|
|
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,15 @@ 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
|
+
|
|
405
432
|
---
|
|
406
433
|
|
|
407
434
|
## Adding a provider
|
|
@@ -835,7 +862,7 @@ one-line edit in a CRLF checkout does not produce a whole-file diff.
|
|
|
835
862
|
## Testing
|
|
836
863
|
|
|
837
864
|
```bash
|
|
838
|
-
pytest #
|
|
865
|
+
pytest # 249 tests, no network
|
|
839
866
|
ruff check turnloop
|
|
840
867
|
mypy turnloop
|
|
841
868
|
```
|
|
@@ -844,13 +871,14 @@ mypy turnloop
|
|
|
844
871
|
|---|---|---|
|
|
845
872
|
| `test_permissions.py` | 48 | rule grammar, compound-command splitting, mode enforcement |
|
|
846
873
|
| `test_experiments.py` | 36 | runner, graders, report, suite invariants, config resolution |
|
|
847
|
-
| `test_providers.py` |
|
|
848
|
-
| `test_tools_files.py` |
|
|
874
|
+
| `test_providers.py` | 31 | adapters against recorded `.sse` fixtures, image block encoding |
|
|
875
|
+
| `test_tools_files.py` | 28 | Read/Write/Edit/Glob/Grep, encodings, newlines, image detection |
|
|
849
876
|
| `test_loop.py` | 20 | streaming, truncation, iteration cap |
|
|
850
877
|
| `test_hooks_mcp_commands.py` | 20 | lifecycle hooks, MCP client, slash commands |
|
|
851
878
|
| `test_config.py` | 19 | layering, env overrides, capability presets |
|
|
852
879
|
| `test_compaction.py` | 16 | three tiers, tool_use/tool_result invariant |
|
|
853
880
|
| `test_bash.py` | 14 | shell selection, process-tree kill |
|
|
881
|
+
| `test_websearch.py` | 9 | HTML parsing, backend selection, truncation, failure modes |
|
|
854
882
|
| `test_tui.py` | 8 | Textual snapshots |
|
|
855
883
|
|
|
856
884
|
`conftest.py` monkeypatches httpx's transport to raise unless a test is marked
|
|
@@ -889,7 +917,8 @@ sessions wrongly.
|
|
|
889
917
|
|
|
890
918
|
API keys come from the environment or a `.env` at the project root. A real environment
|
|
891
919
|
variable always wins over the file, and `doctor` reports which key *names* were loaded
|
|
892
|
-
— never their values.
|
|
920
|
+
— never their values. This applies to `search.api_key_env` exactly as it does to a
|
|
921
|
+
provider's: settings files name the variable, never the secret.
|
|
893
922
|
|
|
894
923
|
Project instructions go in `TURNLOOP.md` (`CLAUDE.md` and `AGENTS.md` are also read).
|
|
895
924
|
Slash commands live in `.turnloop/commands/*.md`, skills in
|
|
@@ -907,13 +936,23 @@ just because it is on disk.
|
|
|
907
936
|
|
|
908
937
|
Stated rather than buried:
|
|
909
938
|
|
|
910
|
-
- **
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
-
|
|
914
|
-
- **
|
|
915
|
-
|
|
916
|
-
|
|
939
|
+
- **Image support is verified on two of three encoders.** The `openai_compat` data-URL
|
|
940
|
+
path and Gemini's `inline_data` path both round-trip a real image live; Anthropic's
|
|
941
|
+
is still fixture-only. None of it can be exercised on the primary target, since
|
|
942
|
+
GLM-5.2 W4A16 as deployed is text-only.
|
|
943
|
+
- **No binary file support** beyond those image formats. Null-byte sniff refuses the
|
|
944
|
+
rest. Convert via `Bash`.
|
|
945
|
+
- **`WebSearch`'s default backend parses HTML, not an API.** DuckDuckGo will rate-limit
|
|
946
|
+
and will eventually change its markup; both are handled as distinct, actionable
|
|
947
|
+
errors rather than a crash, but a keyed backend is the durable choice for anything
|
|
948
|
+
that matters. That trade is deliberate: the default has to work with no key at all.
|
|
949
|
+
- **The Anthropic adapter is fixture-tested but has never sent a real request.** It is
|
|
950
|
+
coded against the documented wire format and passes against recorded SSE, which is not
|
|
951
|
+
the same as verified. It is now the only one: the Gemini adapter is verified live
|
|
952
|
+
(text, tool calls and images on `gemini-flash-latest`), and OpenAI needs no separate
|
|
953
|
+
verification because `openai` is `kind="openai_compat"` — the same adapter Groq,
|
|
954
|
+
NVIDIA, Ollama, Blaxel and the self-hosted GLM endpoint all run through, making it
|
|
955
|
+
the most exercised code path in the repo.
|
|
917
956
|
- **`subtle_spec_edge` currently passes for nobody** (0/5 on gpt-4o-mini). The rewrite
|
|
918
957
|
overshot — runs die on collection errors and leave `NotImplementedError`, which is
|
|
919
958
|
failure-to-produce-working-code, not falling for the decoy. It needs another pass.
|
|
@@ -20,7 +20,7 @@ tl experiment run smoke # a measurement run, offline and free
|
|
|
20
20
|
|
|
21
21
|
Four runtime dependencies: `textual`, `pydantic`, `httpx`, `pyyaml`. No vendor
|
|
22
22
|
SDKs, no agent framework — the loop is the point, so the loop is written here.
|
|
23
|
-
~18,000 lines of Python,
|
|
23
|
+
~18,000 lines of Python, 249 tests, no network in the default test run.
|
|
24
24
|
|
|
25
25
|
---
|
|
26
26
|
|
|
@@ -183,18 +183,36 @@ Custom commands live in `.turnloop/commands/*.md`.
|
|
|
183
183
|
- **`TURNLOOP.md`** at the project root for standing instructions (`CLAUDE.md` and
|
|
184
184
|
`AGENTS.md` are also read)
|
|
185
185
|
- **Text files of any kind** via the `Read` tool — source, JSON, YAML, CSV, Markdown
|
|
186
|
+
- **Images** — PNG, JPEG, GIF and WEBP via `Read`, detected by magic bytes rather
|
|
187
|
+
than extension, on providers whose `Capabilities.supports_vision` is true
|
|
188
|
+
(Anthropic, OpenAI, Gemini). On a text-only provider `Read` refuses the image and
|
|
189
|
+
names the provider, instead of sending a payload the model cannot parse — which
|
|
190
|
+
matters here, because GLM-5.2 W4A16 as deployed is text-only.
|
|
186
191
|
|
|
187
|
-
**Not supported:**
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
because `Bash` exists: `pdftotext spec.pdf -` and the agent will reach for it itself.
|
|
192
|
+
**Not supported:** binary files other than those image formats — refused by a
|
|
193
|
+
null-byte sniff, which covers PDFs, `.docx` and archives. Anything convertible on
|
|
194
|
+
the command line comes back in scope, because `Bash` exists: `pdftotext spec.pdf -`
|
|
195
|
+
and the agent will reach for it itself.
|
|
192
196
|
|
|
193
197
|
### Tools
|
|
194
198
|
|
|
195
|
-
|
|
196
|
-
`WebFetch` `AskUserQuestion
|
|
197
|
-
|
|
199
|
+
Eleven registered: `Read` `Write` `Edit` `Glob` `Grep` `Bash` `TodoWrite` `Task`
|
|
200
|
+
`WebFetch` `WebSearch` `AskUserQuestion`, plus `Skill` when the project has skills.
|
|
201
|
+
Schemas are generated from pydantic models, so the declared schema and the
|
|
202
|
+
validation are the same object and cannot drift.
|
|
203
|
+
|
|
204
|
+
`WebSearch` and `WebFetch` are the pair: search finds URLs, fetch reads one. Both
|
|
205
|
+
are `read_only` and `parallel_safe`, so they work in plan mode and run concurrently
|
|
206
|
+
with `Read`/`Grep`. **Search needs no API key** — the default DuckDuckGo backend
|
|
207
|
+
works on a fresh install with an empty config. Brave and Tavily are opt-in upgrades:
|
|
208
|
+
|
|
209
|
+
```json
|
|
210
|
+
{ "search": { "backend": "brave", "api_key_env": "BRAVE_API_KEY" } }
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
A keyed backend only errors if you actually select one, so the zero-config path
|
|
214
|
+
never breaks. Results are budgeted for a small window — 10 results maximum,
|
|
215
|
+
200-character snippets, 4k characters total, roughly 1k tokens against GLM's 65k.
|
|
198
216
|
|
|
199
217
|
---
|
|
200
218
|
|
|
@@ -205,8 +223,8 @@ turnloop/
|
|
|
205
223
|
core/ messages, events, token estimation, ids
|
|
206
224
|
providers/ anthropic · openai_compat (GLM/Groq/NVIDIA/vLLM) · gemini · mock
|
|
207
225
|
+ sse parser, pricing/capability presets, registry
|
|
208
|
-
tools/ Read Edit Write Bash Glob Grep TodoWrite Task WebFetch
|
|
209
|
-
+ runner, shell scanner, text io
|
|
226
|
+
tools/ Read Edit Write Bash Glob Grep TodoWrite Task WebFetch WebSearch
|
|
227
|
+
Ask Skill + runner, shell scanner, text io
|
|
210
228
|
permissions/ rule grammar, matcher, engine
|
|
211
229
|
agent/ loop, system prompt, subagents, factory, headless
|
|
212
230
|
context/ budget, three-tier compaction, memory files
|
|
@@ -344,7 +362,7 @@ All nine ship preconfigured. `doctor` shows which have keys present.
|
|
|
344
362
|
| `groq` | openai_compat | `openai/gpt-oss-120b` | `GROQ_API_KEY` | free tier: 8k TPM |
|
|
345
363
|
| `anthropic` | anthropic | `claude-sonnet-4-5` | `ANTHROPIC_API_KEY` | |
|
|
346
364
|
| `openai` | openai_compat | `gpt-4.1` | `OPENAI_API_KEY` | |
|
|
347
|
-
| `gemini` | gemini | `gemini-
|
|
365
|
+
| `gemini` | gemini | `gemini-flash-latest` | `GEMINI_API_KEY` | alias, not a pin — see below |
|
|
348
366
|
| `blaxel` | openai_compat | `gpt-4o-mini` | `BL_API_KEY` | sandbox; ignores the model field |
|
|
349
367
|
| `ollama` | openai_compat | `qwen2.5-coder:14b` | none | local |
|
|
350
368
|
|
|
@@ -373,6 +391,15 @@ smoke tests, not for experiments.
|
|
|
373
391
|
NVIDIA model would silently inherit the self-hosted 65k window and $18.16/hr cost
|
|
374
392
|
basis. A regression test guards it.
|
|
375
393
|
|
|
394
|
+
**Gemini's default model is an alias because pinned ones rot in two different ways.**
|
|
395
|
+
0.1.1 shipped `gemini-2.5-pro`, whose free-tier quota is literally zero: a brand-new
|
|
396
|
+
key gets `429 ... generate_content_free_tier_input_token_count, limit: 0` on its first
|
|
397
|
+
request, which reads as a broken adapter rather than a model that was never free.
|
|
398
|
+
Pinning `gemini-2.5-flash` instead fails the other way — `404 no longer available to
|
|
399
|
+
new users`. `gemini-flash-latest` follows Google's own pointer. Worth knowing when
|
|
400
|
+
reading a 429 from any provider: the status code says "slow down", the body says
|
|
401
|
+
whether slowing down will ever help.
|
|
402
|
+
|
|
376
403
|
---
|
|
377
404
|
|
|
378
405
|
## Adding a provider
|
|
@@ -806,7 +833,7 @@ one-line edit in a CRLF checkout does not produce a whole-file diff.
|
|
|
806
833
|
## Testing
|
|
807
834
|
|
|
808
835
|
```bash
|
|
809
|
-
pytest #
|
|
836
|
+
pytest # 249 tests, no network
|
|
810
837
|
ruff check turnloop
|
|
811
838
|
mypy turnloop
|
|
812
839
|
```
|
|
@@ -815,13 +842,14 @@ mypy turnloop
|
|
|
815
842
|
|---|---|---|
|
|
816
843
|
| `test_permissions.py` | 48 | rule grammar, compound-command splitting, mode enforcement |
|
|
817
844
|
| `test_experiments.py` | 36 | runner, graders, report, suite invariants, config resolution |
|
|
818
|
-
| `test_providers.py` |
|
|
819
|
-
| `test_tools_files.py` |
|
|
845
|
+
| `test_providers.py` | 31 | adapters against recorded `.sse` fixtures, image block encoding |
|
|
846
|
+
| `test_tools_files.py` | 28 | Read/Write/Edit/Glob/Grep, encodings, newlines, image detection |
|
|
820
847
|
| `test_loop.py` | 20 | streaming, truncation, iteration cap |
|
|
821
848
|
| `test_hooks_mcp_commands.py` | 20 | lifecycle hooks, MCP client, slash commands |
|
|
822
849
|
| `test_config.py` | 19 | layering, env overrides, capability presets |
|
|
823
850
|
| `test_compaction.py` | 16 | three tiers, tool_use/tool_result invariant |
|
|
824
851
|
| `test_bash.py` | 14 | shell selection, process-tree kill |
|
|
852
|
+
| `test_websearch.py` | 9 | HTML parsing, backend selection, truncation, failure modes |
|
|
825
853
|
| `test_tui.py` | 8 | Textual snapshots |
|
|
826
854
|
|
|
827
855
|
`conftest.py` monkeypatches httpx's transport to raise unless a test is marked
|
|
@@ -860,7 +888,8 @@ sessions wrongly.
|
|
|
860
888
|
|
|
861
889
|
API keys come from the environment or a `.env` at the project root. A real environment
|
|
862
890
|
variable always wins over the file, and `doctor` reports which key *names* were loaded
|
|
863
|
-
— never their values.
|
|
891
|
+
— never their values. This applies to `search.api_key_env` exactly as it does to a
|
|
892
|
+
provider's: settings files name the variable, never the secret.
|
|
864
893
|
|
|
865
894
|
Project instructions go in `TURNLOOP.md` (`CLAUDE.md` and `AGENTS.md` are also read).
|
|
866
895
|
Slash commands live in `.turnloop/commands/*.md`, skills in
|
|
@@ -878,13 +907,23 @@ just because it is on disk.
|
|
|
878
907
|
|
|
879
908
|
Stated rather than buried:
|
|
880
909
|
|
|
881
|
-
- **
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
-
|
|
885
|
-
- **
|
|
886
|
-
|
|
887
|
-
|
|
910
|
+
- **Image support is verified on two of three encoders.** The `openai_compat` data-URL
|
|
911
|
+
path and Gemini's `inline_data` path both round-trip a real image live; Anthropic's
|
|
912
|
+
is still fixture-only. None of it can be exercised on the primary target, since
|
|
913
|
+
GLM-5.2 W4A16 as deployed is text-only.
|
|
914
|
+
- **No binary file support** beyond those image formats. Null-byte sniff refuses the
|
|
915
|
+
rest. Convert via `Bash`.
|
|
916
|
+
- **`WebSearch`'s default backend parses HTML, not an API.** DuckDuckGo will rate-limit
|
|
917
|
+
and will eventually change its markup; both are handled as distinct, actionable
|
|
918
|
+
errors rather than a crash, but a keyed backend is the durable choice for anything
|
|
919
|
+
that matters. That trade is deliberate: the default has to work with no key at all.
|
|
920
|
+
- **The Anthropic adapter is fixture-tested but has never sent a real request.** It is
|
|
921
|
+
coded against the documented wire format and passes against recorded SSE, which is not
|
|
922
|
+
the same as verified. It is now the only one: the Gemini adapter is verified live
|
|
923
|
+
(text, tool calls and images on `gemini-flash-latest`), and OpenAI needs no separate
|
|
924
|
+
verification because `openai` is `kind="openai_compat"` — the same adapter Groq,
|
|
925
|
+
NVIDIA, Ollama, Blaxel and the self-hosted GLM endpoint all run through, making it
|
|
926
|
+
the most exercised code path in the repo.
|
|
888
927
|
- **`subtle_spec_edge` currently passes for nobody** (0/5 on gpt-4o-mini). The rewrite
|
|
889
928
|
overshot — runs die on collection errors and leave `NotImplementedError`, which is
|
|
890
929
|
failure-to-produce-working-code, not falling for the decoy. It needs another pass.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "turnloop"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "A local-first agentic coding harness: streaming tool loop, permission gating, context compaction, subagents, and any LLM provider."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -35,8 +35,13 @@ dev = [
|
|
|
35
35
|
"pytest>=8.0",
|
|
36
36
|
"pytest-asyncio>=0.23",
|
|
37
37
|
"pytest-textual-snapshot>=1.0",
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
# Pinned to a minor range, not floated. Ruff adds lint rules in minor releases,
|
|
39
|
+
# so `>=0.6` means a build that is green today goes red tomorrow with no commit
|
|
40
|
+
# in between — which is how this CI first broke. Bump deliberately.
|
|
41
|
+
"ruff>=0.16.1,<0.17",
|
|
42
|
+
# Must match what is actually run locally, or CI type-checks with a different
|
|
43
|
+
# tool than the author does — which is exactly how this broke.
|
|
44
|
+
"mypy>=2.1,<3",
|
|
40
45
|
]
|
|
41
46
|
|
|
42
47
|
[project.scripts]
|