tether-sdk 0.2.5__tar.gz → 0.2.7__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.
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/AGENTS.md +3 -3
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/PKG-INFO +1 -1
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/README.md +10 -2
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/grade.py +88 -2
- tether_sdk-0.2.7/bench/harness-bench/README.md +25 -0
- tether_sdk-0.2.7/bench/harness-bench/tether-adapter.patch +134 -0
- tether_sdk-0.2.7/bench/report.py +48 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/run.py +30 -4
- tether_sdk-0.2.7/bench/tasks/codereview/goal.txt +1 -0
- tether_sdk-0.2.7/bench/tasks/codereview/ws/inventory.py +40 -0
- tether_sdk-0.2.7/bench/tasks/plan/goal.txt +1 -0
- tether_sdk-0.2.7/bench/tasks/plan/ws/README.md +3 -0
- tether_sdk-0.2.7/bench/tasks/plan/ws/svc/client.py +18 -0
- tether_sdk-0.2.7/bench/tasks/plan/ws/svc/config.py +3 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/architecture.md +2 -2
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/reference/context-and-store.md +1 -1
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/reference/providers.md +1 -1
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/release-notes.md +26 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/testing.md +1 -1
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/tutorial/security-and-sandbox.md +1 -1
- tether_sdk-0.2.7/e2e/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/e2e/test_faults.py +12 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/e2e/test_limits.py +17 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/e2e/test_sandbox.py +15 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_cli.py +16 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_context.py +24 -1
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_harness.py +70 -1
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_params.py +30 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_security_hardening.py +52 -1
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_store.py +23 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_tether.py +1 -1
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/__init__.py +1 -1
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/cli.py +15 -1
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/context.py +6 -2
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/harness.py +25 -4
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/providers/anthropic.py +3 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/providers/base.py +17 -1
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/providers/ollama.py +3 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/providers/openai.py +3 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/sandbox.py +30 -9
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/store.py +18 -1
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/.claude/feature-context/active/.gitkeep +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/.claude/feature-context/active/v0-2-bugs.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/.claude/feature-context/archive/.gitkeep +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/.github/workflows/ci.yml +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/.github/workflows/delivery.yml +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/.github/workflows/docs.yml +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/.gitignore +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/LICENSE +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/README.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/naive_agent.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/goal.txt +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/hidden/test_hidden.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/ref/calc/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/ref/calc/errors.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/ref/calc/evaluator.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/ref/calc/parser.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/ref/calc/tokenizer.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/ws/calc/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/ws/calc/errors.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/ws/calc/evaluator.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/ws/calc/parser.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/ws/calc/tokenizer.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/ws/tests/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-hard/ws/tests/test_calc.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-medium/goal.txt +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-medium/hidden/test_hidden.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-medium/ws/cli.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-medium/ws/ledger/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-medium/ws/ledger/parser.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-medium/ws/ledger/report.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-medium/ws/sample.csv +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-medium/ws/tests/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-medium/ws/tests/test_report.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-simple/goal.txt +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-simple/hidden/test_hidden.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-simple/ws/tests/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-simple/ws/tests/test_textutils.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/code-simple/ws/textutils.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/content/goal.txt +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/content/ws/.keep +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/focus/goal.txt +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/focus/hidden/test_hidden.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/focus/ws/AGENTS.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/focus/ws/README.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/focus/ws/docs/architecture.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/focus/ws/docs/style.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/focus/ws/mini/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/focus/ws/mini/cli.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/focus/ws/mini/fmt.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/focus/ws/mini/stats.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/focus/ws/mini/textio.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/focus/ws/tests/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/focus/ws/tests/test_stats.py +0 -0
- {tether_sdk-0.2.5/e2e → tether_sdk-0.2.7/bench/tasks/plan/ws/svc}/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/qa/goal.txt +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/qa/ws/.keep +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/review/goal.txt +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/bench/tasks/review/ws/paper.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/benchmarks.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/extending.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/index.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/javascripts/mathjax.js +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/reference/agent-interface.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/reference/harness.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/reference/index.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/reference/multiplexer-and-decorator.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/reference/sandbox-and-skills.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/reference/tools.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/releasing.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/troubleshooting.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/tutorial/agent-interface.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/tutorial/built-in-tools.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/tutorial/cli-and-headless.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/tutorial/index.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/tutorial/multi-agent.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/tutorial/sdk-and-custom-tools.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/tutorial/skills.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/docs/tutorial/workflows-and-telemetry.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/e2e/conftest.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/e2e/proxy.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/e2e/test_agents.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/e2e/test_defaults.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/e2e/test_lifecycle.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/e2e/test_quirks.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/e2e/test_surface.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/examples/course/README.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/examples/course/checks.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/examples/course/course.yaml +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/examples/course/ruff.toml +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/examples/course/skills/planner/SKILL.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/examples/course/skills/searcher/SKILL.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/examples/course/skills/searcher/tools.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/examples/course/skills/writer/SKILL.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/examples/course/skills/writer/tools.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/mkdocs.yml +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/pyproject.toml +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/scripts/record_golden.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/skills/researcher/SKILL.md +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/conftest.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/fixtures/dialects.jsonl +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/golden/coding_edit_failure_modes.json +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/golden/coding_edit_failure_modes.snap.json +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/golden/done_nudge.json +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/golden/done_nudge.snap.json +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/golden/read_range_pingpong.json +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/golden/read_range_pingpong.snap.json +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/golden/research_web.json +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/golden/research_web.snap.json +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/golden/shell_wrappers.json +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/golden/shell_wrappers.snap.json +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/golden/waiting_input_resume.json +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/golden/waiting_input_resume.snap.json +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/providers/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/providers/test_anthropic.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/providers/test_base.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/providers/test_declarative.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/providers/test_dialects.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/providers/test_ollama.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/providers/test_openai.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/providers/test_retry.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_agent_interface.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_context_config.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_decorator.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_edit_tool.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_golden.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_integration.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_misses.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_multiplexer.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_pipeline.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_release_hardening.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_retro.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_sandbox.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_skills.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/test_structured_history.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/tools/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/tools/test_agent.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/tools/test_bash.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/tools/test_done.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/tools/test_edit.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/tools/test_mem.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tests/tools/test_web.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/api.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/decorator.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/mcp.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/multiplexer.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/pipeline.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/providers/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/providers/declarative.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/retro.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/schema.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/skills.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/tools/__init__.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/tools/agent.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/tools/bash.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/tools/done.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/tools/edit.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/tools/mem.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/tether/tools/web.py +0 -0
- {tether_sdk-0.2.5 → tether_sdk-0.2.7}/uv.lock +0 -0
|
@@ -45,7 +45,7 @@ Reviewers and the Team Leader cite the failing line when they reject a change.
|
|
|
45
45
|
- Sessions do **not** terminate simply because the model emitted a text response without a tool call (small 9B models frequently "think out loud" before acting).
|
|
46
46
|
- A task only completes when the agent explicitly calls `act("done", stdin="<final deliverable>")` (or `act("done '<short answer>'")`), pauses for human/parent input via `act("done --ask", stdin="<question>")` (transitioning the session to `WAITING_INPUT` and releasing the worker process until resumed via `send`), or hits `max_steps`.
|
|
47
47
|
- If the model emits plain text without calling `act`, the harness appends a brief deterministic nudge reminding it to either execute a command or call `done`.
|
|
48
|
-
- After 6 consecutive read-only/verification steps following a change, the harness appends a one-shot `[NUDGE]` to that step's observation reminding the model to call `done` if the task is complete (0 static prompt tokens, #74). Seven reads of one file in a row with no change are a `cycle_stall`, and 4 stall warnings without a change end the session `FAILED` as `Stalled` (#92); `tether stop <id>` terminates a detached worker (#89).
|
|
48
|
+
- After 6 consecutive read-only/verification steps following a change, the harness appends a one-shot `[NUDGE]` to that step's observation reminding the model to call `done` if the task is complete (0 static prompt tokens, #74). Before any change, 12 read-only steps in a row (then every 8 more) append a one-line `[NUDGE]` to make the edit or call `done` (#127). Seven reads of one file in a row with no change are a `cycle_stall`, and 4 stall warnings without a change end the session `FAILED` as `Stalled` (#92); `tether stop <id>` terminates a detached worker (#89).
|
|
49
49
|
- When `verify` gate commands are configured (via `Harness(verify=[...])`, CLI `--verify`, or Skill YAML `verify: [...]`), `Harness.execute_session` deterministically executes each command inside the session's `SandboxPolicy` before allowing `done` to transition to `COMPLETED` (consuming 0 static prompt tokens until a gate fails). A gate that errors instead of failing (sandbox violation, exit 124/126/127) ends the session `FAILED` at once as `gate_error` ("gate misconfigured"), without counting toward the consecutive-failure abort.
|
|
50
50
|
|
|
51
51
|
### Rule 4: Progressive `--help` (JIT Schema Disclosure)
|
|
@@ -62,7 +62,7 @@ Reviewers and the Team Leader cite the failing line when they reject a change.
|
|
|
62
62
|
```text
|
|
63
63
|
[obs_3 folded: `web fetch https://...` (3,420 chars) | use `mem recall obs_3` to expand]
|
|
64
64
|
```
|
|
65
|
-
- Exception: the latest read of each of the last 8 files (`edit view <p>`, `bash cat/head/tail/sed -n <p>`; paths normalized against the workspace) is not folded until `<p>` is edited (`edit write|replace`, or any other bash command naming it: `sed -i`, `tee`, redirects, `python -c`) or re-read. The
|
|
65
|
+
- Exception: the latest read of each of the last 8 files (`edit view <p>`, `bash cat/head/tail/sed -n <p>`; paths normalized against the workspace) is not folded until `<p>` is edited (`edit write|replace`, or any other bash command naming it: `sed -i`, `tee`, redirects, `python -c`) or re-read. The head+tail elision still applies (an observation may use up to a third of `max_context_tokens`, at least 2,400 chars), and under `max_context_tokens` pressure it is folded like any other (oldest first). The `mem recall` hint is shown only when `mem` is enabled (else "re-run the command").
|
|
66
66
|
- History is replayed as structured `tool_calls` + `tool` results (providers with `structured_history = True`; textual `act('...')` form only as fallback), with `stdin` replayed as a real 400-char prefix, never a `<N chars>` placeholder. A call and its result are trimmed together.
|
|
67
67
|
|
|
68
68
|
### Rule 6: Deterministic Security Envelope (Never Trust the LLM for Safety)
|
|
@@ -71,7 +71,7 @@ Reviewers and the Team Leader cite the failing line when they reject a change.
|
|
|
71
71
|
- All `edit` and `bash` paths are canonicalized (`Path.resolve()`) and confined to `workspace_root`.
|
|
72
72
|
- Sensitive files/directories (`.env*`, `.git/config`, `.git/hooks`, `~/.ssh`, `.tether/`) are deterministically protected from mutation or read exfiltration.
|
|
73
73
|
- `bash` subprocesses get an allowlisted environment only (secrets with any name are dropped; opt-in via `TETHER_ENV_PASSTHROUGH`). `web fetch` blocks loopback/private/metadata addresses (SSRF guard). `SandboxPolicy(network=False)` / `--no-network` adds `--unshare-net` (fails closed without `bwrap`; children can only narrow it).
|
|
74
|
-
- On Linux, `bash` automatically uses **`bwrap` (Bubblewrap) / `unshare`** when available (read-only `/`, hidden `/home` and `/var/tmp` tmpfs (existing `PATH` dirs under them, e.g. `~/.local/bin`, are re-bound read-only so `uv`/`node` still run, #76), `/run` unix sockets such as `docker.sock` masked, read-write bind on `workspace_root` with secret-looking files at any depth (`.env*`, `*.pem`, `*.key`, ...), `.tether/`, `.git/config` and `.git/hooks` masked at the mount level, `PR_SET_NO_NEW_PRIVS`), falling back to a strict deterministic command allowlist/denylist (`rm -rf /`, `sudo`, `dd`, `mkfs`, `chmod 777`, etc. blocked).
|
|
74
|
+
- On Linux, `bash` automatically uses **`bwrap` (Bubblewrap) / `unshare`** when available (read-only `/`, hidden `/home` and `/var/tmp` tmpfs (existing `PATH` dirs under them, e.g. `~/.local/bin`, are re-bound read-only so `uv`/`node` still run, #76), `/run` unix sockets such as `docker.sock` masked, read-write bind on `workspace_root` with secret-looking files at any depth (`.env*`, `*.pem`, `*.key`, ...), `.tether/`, `.git/config` and `.git/hooks` masked at the mount level, `PR_SET_NO_NEW_PRIVS`), falling back to a strict deterministic command allowlist/denylist (path, `..` and `.env` text rules run only in this fallback, the mounts enforce them under `bwrap`, #125; `.env.example`-style templates are not secrets) (`rm -rf /`, `sudo`, `dd`, `mkfs`, `chmod 777`, etc. blocked).
|
|
75
75
|
- Sub-agents spawned via `agent spawn` strictly inherit or narrow their parent session's `SandboxPolicy` (no privilege escalation).
|
|
76
76
|
|
|
77
77
|
### Rule 7: Zero Framework Bloat
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tether-sdk
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: General-purpose, headless-first LLM-Native micro-harness for small language models
|
|
5
5
|
Project-URL: Homepage, https://github.com/igormcsouza/tether
|
|
6
6
|
Project-URL: Repository, https://github.com/igormcsouza/tether
|
|
@@ -4,11 +4,19 @@ Not part of the `tether` package, not run in CI. Use it to onboard a new model o
|
|
|
4
4
|
release for regressions that deterministic tests cannot see (unit, golden and e2e tests stay the
|
|
5
5
|
gate; see `docs/testing.md`).
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
`focus` (one exact edit in a distractor-heavy repo, scope-checked), `code-simple`, `code-medium`, `code-hard` (graded by hidden unit tests; editing the visible tests
|
|
7
|
+
Nine tasks in `tasks/<name>/` (workspace in `ws/`, goal in `goal.txt`, hidden tests in `hidden/`):
|
|
8
|
+
`focus` (one exact edit in a distractor-heavy repo, scope-checked), `plan` (an implementation plan, checked for named files, failure handling and tests), `codereview` (eight seeded bugs to find, no edits), `code-simple`, `code-medium`, `code-hard` (graded by hidden unit tests; editing the visible tests
|
|
9
9
|
scores 0), `qa`, `content`, `review` (graded by constraint counts and keyword checks, so they are
|
|
10
10
|
a smoke test for quality, not a judge).
|
|
11
11
|
|
|
12
|
+
**Polyglot-Python** is the score we answer to. `--tasks polyglot` runs the 34 Python exercises of Aider's
|
|
13
|
+
[polyglot-benchmark](https://github.com/Aider-AI/polyglot-benchmark) (cloned once into
|
|
14
|
+
`~/.cache/tether-bench/polyglot-benchmark`, or set `POLYGLOT_DIR`); `--tasks polyglot-fast` runs a 12-exercise
|
|
15
|
+
subset that mixes passes, partials and failures. Each task is graded by the exercise's own tests (the test file is
|
|
16
|
+
restored before grading, so editing it cannot pass), `--poly-verify` adds a `--verify` gate on those tests. Then
|
|
17
|
+
`python3 bench/report.py <out>` prints pass rate and mean score with a failure breakdown that keeps provider outages
|
|
18
|
+
(`outage`, `timeout`) apart from harness outcomes (`max_steps`, `stalled`, `wrong_answer`, `worker_error`).
|
|
19
|
+
|
|
12
20
|
Three runners, so a gap can be attributed:
|
|
13
21
|
|
|
14
22
|
| runner | what it is |
|
|
@@ -135,10 +135,96 @@ def _focus(task, ws, result):
|
|
|
135
135
|
return r
|
|
136
136
|
|
|
137
137
|
|
|
138
|
+
def _steps(t):
|
|
139
|
+
return len(re.findall(r"^\s*\d+[.)]", t, re.M))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _plan(_task, _ws, result):
|
|
143
|
+
t = result or ""
|
|
144
|
+
c = {
|
|
145
|
+
"6-10 numbered steps": 6 <= _steps(t) <= 10,
|
|
146
|
+
"names client.py": "client.py" in t,
|
|
147
|
+
"names config.py": "config.py" in t,
|
|
148
|
+
"names get/post": bool(re.search(r"\bget\b", t)) and bool(re.search(r"\bpost\b", t)),
|
|
149
|
+
"backoff+jitter": bool(re.search(r"exponential", t, re.I)) and bool(re.search(r"jitter", t, re.I)),
|
|
150
|
+
"retry settings": bool(re.search(r"max[_ -]?(retries|attempts)|retry[_ -]?(limit|count)", t, re.I)) and bool(re.search(r"base[_ -]?(delay|backoff)|backoff[_ -]?(base|factor)", t, re.I)),
|
|
151
|
+
"429 and 5xx": "429" in t and bool(re.search(r"5xx|50\d", t)),
|
|
152
|
+
"not retried (4xx)": bool(re.search(r"4xx|not retr|never retr|do not retry|don't retry|non-retry", t, re.I)),
|
|
153
|
+
"Retry-After": bool(re.search(r"retry-after", t, re.I)),
|
|
154
|
+
"tests without network": bool(re.search(r"mock|fake|stub|patch|monkeypatch|inject", t, re.I)) and bool(re.search(r"test", t, re.I)),
|
|
155
|
+
"idempotency of post": bool(re.search(r"idempot|post.{0,60}(retry|retri)|(retry|retri).{0,60}post", t, re.I)),
|
|
156
|
+
}
|
|
157
|
+
return {"score": round(sum(c.values()) / len(c), 3), "pass": sum(c.values()) >= 9, "detail": {"missed": [k for k, v in c.items() if not v]}}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
BUGS = {
|
|
161
|
+
"page off-by-one": r"page.{0,200}(off.by.one|number\s*-\s*1|\(number - 1\)|1-based|zero|skips? the first)|(off.by.one|1-based).{0,200}page",
|
|
162
|
+
"mutable default": r"mutable default|tags=\[\]|default (argument|arg|list)|shared (between|across)",
|
|
163
|
+
"float money": r"float.{0,120}(money|price|currenc|rounding|precision|decimal)|decimal",
|
|
164
|
+
"float equality": r"(==|equal).{0,100}(float|0\.0)|is_free|float.{0,60}(compar|equal)|isclose",
|
|
165
|
+
"sql injection": r"sql.?inject|parameteri[sz]|placeholder|string concat|bind (var|param)",
|
|
166
|
+
"file not closed": r"(not|never) clos|resource leak|context manager|with open|leak",
|
|
167
|
+
"keyerror/missing": r"keyerror|missing (key|item|name)|\.get\(|unknown name|not in (the )?(stock|dict)",
|
|
168
|
+
"split malformed": r"split.{0,100}(more than|exactly|two|malformed|blank|empty|valueerror)|blank line|malformed|empty line|valueerror",
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _codereview(_task, _ws, result):
|
|
173
|
+
t = result or ""
|
|
174
|
+
hit = {k: bool(re.search(v, t, re.I | re.S)) for k, v in BUGS.items()}
|
|
175
|
+
must = ["page off-by-one", "mutable default", "sql injection", "file not closed", "keyerror/missing"]
|
|
176
|
+
return {"score": round(sum(hit.values()) / len(hit), 3), "pass": all(hit[k] for k in must), "detail": {"missed": [k for k, v in hit.items() if not v], "items": _steps(t)}}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# Fast set (12): easy passes, partials and hard failures from the 0.2.6 baseline; tether's weak spots.
|
|
180
|
+
POLY_FAST = ("affine-cipher bottle-song list-ops paasio dot-dsl food-chain forth go-counting poker pov react "
|
|
181
|
+
"rest-api").split()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def poly_dir(ex=""):
|
|
185
|
+
"""Aider polyglot-benchmark Python exercises (cloned once, never vendored)."""
|
|
186
|
+
root = os.environ.get("POLYGLOT_DIR") or os.path.expanduser("~/.cache/tether-bench/polyglot-benchmark")
|
|
187
|
+
if not os.path.isdir(root):
|
|
188
|
+
subprocess.run(["git", "clone", "--depth", "1", "https://github.com/Aider-AI/polyglot-benchmark", root], check=True)
|
|
189
|
+
return os.path.join(root, "python", "exercises", "practice", ex)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def poly_goal(ex):
|
|
193
|
+
d = poly_dir(ex)
|
|
194
|
+
docs = "\n\n".join(open(os.path.join(d, ".docs", f)).read() for f in ("instructions.md", "instructions.append.md")
|
|
195
|
+
if os.path.exists(os.path.join(d, ".docs", f)))
|
|
196
|
+
sol = ex.replace("-", "_") + ".py"
|
|
197
|
+
return (f"Implement `{sol}` in the current directory so that `python3 -m unittest discover -p '*_test.py'` passes. "
|
|
198
|
+
f"Do not edit the test file.\n\n{docs}")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _poly(task, ws, _result):
|
|
202
|
+
ex = task.removeprefix("poly-")
|
|
203
|
+
tmp = tempfile.mkdtemp(prefix="g_")
|
|
204
|
+
try:
|
|
205
|
+
shutil.copytree(ws, tmp, dirs_exist_ok=True)
|
|
206
|
+
test = ex.replace("-", "_") + "_test.py"
|
|
207
|
+
shutil.copy(os.path.join(poly_dir(ex), test), os.path.join(tmp, test)) # restore: editing tests cannot pass
|
|
208
|
+
try:
|
|
209
|
+
p = subprocess.run([sys.executable, "-m", "unittest", "-v", test[:-3]], cwd=tmp, capture_output=True, text=True, timeout=90)
|
|
210
|
+
except subprocess.TimeoutExpired:
|
|
211
|
+
return {"score": 0.0, "pass": False, "detail": {"error": "tests timed out"}}
|
|
212
|
+
out = p.stderr + p.stdout
|
|
213
|
+
ok = len(re.findall(r"\.\.\. ok$", out, re.M))
|
|
214
|
+
bad = len(re.findall(r"\.\.\. (FAIL|ERROR)$", out, re.M))
|
|
215
|
+
total = ok + bad
|
|
216
|
+
return {"score": round(ok / total, 3) if total else 0.0, "pass": p.returncode == 0 and total > 0, "detail": {"ok": ok, "total": total}}
|
|
217
|
+
finally:
|
|
218
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
219
|
+
|
|
220
|
+
|
|
138
221
|
def grade(task, ws, result):
|
|
139
|
-
|
|
222
|
+
orig = task
|
|
223
|
+
if task.startswith("poly-"):
|
|
224
|
+
task = "poly"
|
|
225
|
+
fn = {"poly": _poly, "qa": _qa, "content": _content, "review": _review, "focus": _focus, "plan": _plan, "codereview": _codereview}.get(task, _code)
|
|
140
226
|
try:
|
|
141
|
-
return fn(
|
|
227
|
+
return fn(orig, ws, result)
|
|
142
228
|
except Exception as e: # grader bug must never look like a model result
|
|
143
229
|
return {"score": 0.0, "pass": False, "detail": {"grader_error": repr(e)}}
|
|
144
230
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Harness-Bench adapter (pilot, not yet run end to end)
|
|
2
|
+
|
|
3
|
+
[Harness-Bench](https://github.com/Qihoo360/harness-bench) ([paper](https://arxiv.org/abs/2605.27922)) scores
|
|
4
|
+
whole harnesses on 274 offline workflow tasks (plus 26 multimodal) with hidden oracles. This directory holds
|
|
5
|
+
the patch that registers Tether as an eighth harness. It is a way to benchmark, not a Tether feature.
|
|
6
|
+
|
|
7
|
+
`tether-adapter.patch` applies to harness-bench commit `b8d89634c6f445fcc68ceb6744a59bef6cdf4863`:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
git clone https://github.com/Qihoo360/harness-bench && cd harness-bench
|
|
11
|
+
git apply /path/to/tether/bench/harness-bench/tether-adapter.patch
|
|
12
|
+
uv sync --extra dev
|
|
13
|
+
export PYTHONPATH=$PWD/src:$PWD/tools:$PWD/tools/task_maintenance
|
|
14
|
+
python tools/run_benchmark.py --base-url "$URL" --model "$MODEL" --harness tether \
|
|
15
|
+
--tasks 001-meeting-summary --workers 1 --run-root /tmp/hb-run
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
What the patch adds: `docker/harnesses/tether/` (their toolchain image plus `tether-sdk`, an entrypoint that runs
|
|
19
|
+
`tether run --tools bash,edit` with state kept outside the graded workspace), a `tether` `HarnessSpec`, and a
|
|
20
|
+
`providers.yaml` renderer.
|
|
21
|
+
|
|
22
|
+
Status (2026-10-23): image builds, `tether run` starts inside the trial container and reaches `ProviderError ...
|
|
23
|
+
ConnectError`, because on the machine used containers cannot reach host ports (`host.docker.internal`, where the
|
|
24
|
+
per-trial model proxy listens). The host firewall must allow the docker bridge to the proxy's ports. No task has
|
|
25
|
+
been scored yet.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
diff --git a/docker/harnesses/tether/Dockerfile b/docker/harnesses/tether/Dockerfile
|
|
2
|
+
new file mode 100644
|
|
3
|
+
index 0000000..d9f3e28
|
|
4
|
+
--- /dev/null
|
|
5
|
+
+++ b/docker/harnesses/tether/Dockerfile
|
|
6
|
+
@@ -0,0 +1,45 @@
|
|
7
|
+
+FROM python:3.12-slim-bookworm@sha256:d50fb7611f86d04a3b0471b46d7557818d88983fc3136726336b2a4c657aa30b AS task-toolchain
|
|
8
|
+
+
|
|
9
|
+
+ARG PIP_INDEX_URL
|
|
10
|
+
+ARG PIP_EXTRA_INDEX_URL
|
|
11
|
+
+ARG PIP_TRUSTED_HOST
|
|
12
|
+
+COPY docker/openclaw/requirements.lock /tmp/harnessbench-requirements.lock
|
|
13
|
+
+RUN python -m pip install --disable-pip-version-check --no-cache-dir \
|
|
14
|
+
+ --requirement /tmp/harnessbench-requirements.lock \
|
|
15
|
+
+ && rm /tmp/harnessbench-requirements.lock
|
|
16
|
+
+
|
|
17
|
+
+FROM python:3.12-slim@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de
|
|
18
|
+
+
|
|
19
|
+
+ARG TETHER_VERSION=0.2.5
|
|
20
|
+
+ARG PYPI_INDEX_URL=https://pypi.org/simple
|
|
21
|
+
+RUN apt-get update \
|
|
22
|
+
+ && apt-get install -y --no-install-recommends bash git ripgrep sqlite3 \
|
|
23
|
+
+ && rm -rf /var/lib/apt/lists/*
|
|
24
|
+
+
|
|
25
|
+
+
|
|
26
|
+
+
|
|
27
|
+
+RUN useradd --create-home --uid 1000 harness
|
|
28
|
+
+COPY --from=task-toolchain /usr/local/ /usr/local/
|
|
29
|
+
+COPY docker/openclaw/verify_toolchain.py /opt/harnessbench/verify_toolchain.py
|
|
30
|
+
+COPY docker/harnesses/tether/entrypoint.sh /opt/harness/bin/run
|
|
31
|
+
+RUN ln -sf /usr/local/bin/python3 /usr/bin/python3 \
|
|
32
|
+
+ && python /opt/harnessbench/verify_toolchain.py --python-only \
|
|
33
|
+
+ && python -m venv /opt/tether \
|
|
34
|
+
+ && /opt/tether/bin/python -m pip install --no-cache-dir \
|
|
35
|
+
+ --index-url "${PYPI_INDEX_URL}" "tether-sdk==${TETHER_VERSION}" \
|
|
36
|
+
+ && chmod 0755 /opt/harness/bin/run
|
|
37
|
+
+
|
|
38
|
+
+ENV PATH="/opt/harnessbench/frontend-bin:/usr/local/bin:/opt/tether/bin:${PATH}" \
|
|
39
|
+
+ NANOBOT_STREAM_IDLE_TIMEOUT_S=7200 \
|
|
40
|
+
+ NANOBOT_OPENAI_COMPAT_TIMEOUT_S=7200 \
|
|
41
|
+
+ NANOBOT_LLM_TIMEOUT_S=0
|
|
42
|
+
+
|
|
43
|
+
+# BEGIN NANOBOT LOGIN-SHELL PATH
|
|
44
|
+
+RUN printf '%s\n' \
|
|
45
|
+
+ 'export PATH="/usr/local/bin:/opt/tether/bin:$PATH"' \
|
|
46
|
+
+ > /etc/profile.d/harnessbench-path.sh \
|
|
47
|
+
+ && chmod 0644 /etc/profile.d/harnessbench-path.sh
|
|
48
|
+
+# END NANOBOT LOGIN-SHELL PATH
|
|
49
|
+
+
|
|
50
|
+
+USER 1000:1000
|
|
51
|
+
+ENTRYPOINT ["/opt/harness/bin/run"]
|
|
52
|
+
diff --git a/docker/harnesses/tether/entrypoint.sh b/docker/harnesses/tether/entrypoint.sh
|
|
53
|
+
new file mode 100644
|
|
54
|
+
index 0000000..105cee6
|
|
55
|
+
--- /dev/null
|
|
56
|
+
+++ b/docker/harnesses/tether/entrypoint.sh
|
|
57
|
+
@@ -0,0 +1,38 @@
|
|
58
|
+
+#!/usr/bin/env bash
|
|
59
|
+
+set -euo pipefail
|
|
60
|
+
+
|
|
61
|
+
+: "${HARNESSBENCH_PROMPT:?}"
|
|
62
|
+
+: "${HARNESSBENCH_CONFIG:?}"
|
|
63
|
+
+: "${HARNESSBENCH_WORKSPACE:?}"
|
|
64
|
+
+: "${HARNESSBENCH_STATE:?}"
|
|
65
|
+
+: "${HARNESSBENCH_TRACE:?}"
|
|
66
|
+
+
|
|
67
|
+
+export HOME="${HARNESSBENCH_STATE}/home"
|
|
68
|
+
+mkdir -p "${HOME}" "${HARNESSBENCH_TRACE}"
|
|
69
|
+
+runtime_config="${HARNESSBENCH_STATE}/providers.yaml"
|
|
70
|
+
+cp "${HARNESSBENCH_CONFIG}" "${runtime_config}"
|
|
71
|
+
+# The render step writes `# model: <id>` and `# turns: <n>` on the first lines.
|
|
72
|
+
+model="$(sed -n 's/^# model: //p' "${runtime_config}" | head -1)"
|
|
73
|
+
+turns="$(sed -n 's/^# turns: //p' "${runtime_config}" | head -1)"
|
|
74
|
+
+prompt="$(<"${HARNESSBENCH_PROMPT}")"
|
|
75
|
+
+
|
|
76
|
+
+export TETHER_PROVIDERS="${runtime_config}"
|
|
77
|
+
+export VLLM_API_KEY="${VLLM_API_KEY:-local-vllm-no-auth}" # placeholder; the trial proxy injects the real key
|
|
78
|
+
+export TETHER_DB="${HARNESSBENCH_STATE}/tether.db" # state stays outside the graded workspace
|
|
79
|
+
+export TETHER_ENV_PASSTHROUGH="PATH,NODE_PATH,GIT_CONFIG_GLOBAL"
|
|
80
|
+
+
|
|
81
|
+
+set +e
|
|
82
|
+
+cd "${HARNESSBENCH_WORKSPACE}"
|
|
83
|
+
+tether run \
|
|
84
|
+
+ --model "harnessbench/${model}" \
|
|
85
|
+
+ --tools bash,edit \
|
|
86
|
+
+ --max-steps "${turns:-150}" \
|
|
87
|
+
+ "${prompt}" \
|
|
88
|
+
+ > >(tee "${HARNESSBENCH_TRACE}/events.log") \
|
|
89
|
+
+ 2> >(tee "${HARNESSBENCH_TRACE}/stderr.log" >&2)
|
|
90
|
+
+status=$?
|
|
91
|
+
+set -e
|
|
92
|
+
+
|
|
93
|
+
+[[ -f "${TETHER_DB}" ]] && cp "${TETHER_DB}" "${HARNESSBENCH_TRACE}/tether.db"
|
|
94
|
+
+chmod -R a+rX "${HARNESSBENCH_WORKSPACE}" "${HARNESSBENCH_TRACE}" 2>/dev/null || true
|
|
95
|
+
+exit "${status}"
|
|
96
|
+
diff --git a/tools/harness_runtime.py b/tools/harness_runtime.py
|
|
97
|
+
index c57ba22..e8763dc 100644
|
|
98
|
+
--- a/tools/harness_runtime.py
|
|
99
|
+
+++ b/tools/harness_runtime.py
|
|
100
|
+
@@ -67,6 +67,14 @@ SPECS = {
|
|
101
|
+
config_name="config.json",
|
|
102
|
+
docker_context=HARNESS_DOCKER_DIR / "nanobot",
|
|
103
|
+
),
|
|
104
|
+
+ "tether": HarnessSpec(
|
|
105
|
+
+ harness_id="tether",
|
|
106
|
+
+ version="0.2.5",
|
|
107
|
+
+ image="harnessbench/tether:0.2.5",
|
|
108
|
+
+ protocol="chat_completions",
|
|
109
|
+
+ config_name="providers.yaml",
|
|
110
|
+
+ docker_context=HARNESS_DOCKER_DIR / "tether",
|
|
111
|
+
+ ),
|
|
112
|
+
"goose": HarnessSpec(
|
|
113
|
+
harness_id="goose",
|
|
114
|
+
version="1.44.0",
|
|
115
|
+
@@ -291,6 +299,19 @@ def render_harness_config(
|
|
116
|
+
}
|
|
117
|
+
return json.dumps(config, ensure_ascii=False, indent=2) + "\n"
|
|
118
|
+
|
|
119
|
+
+ if spec.harness_id == "tether":
|
|
120
|
+
+ entry = {
|
|
121
|
+
+ "kind": "openai",
|
|
122
|
+
+ "base_url": base_url,
|
|
123
|
+
+ "api_key_env": "VLLM_API_KEY",
|
|
124
|
+
+ "context_tokens": context_window,
|
|
125
|
+
+ "fold_after": 8,
|
|
126
|
+
+ "timeout": 1800,
|
|
127
|
+
+ "params": {"temperature": temperature, "max_tokens": max_tokens},
|
|
128
|
+
+ }
|
|
129
|
+
+ header = f"# model: {model}\n# turns: {max_agent_turns}\n"
|
|
130
|
+
+ return header + yaml.safe_dump({"harnessbench": entry}, sort_keys=False)
|
|
131
|
+
+
|
|
132
|
+
if spec.harness_id == "goose":
|
|
133
|
+
config = {
|
|
134
|
+
"provider": "openai",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Failure breakdown for a results dir: `python3 bench/report.py <out> [<out> ...]`.
|
|
2
|
+
|
|
3
|
+
Splits every run into pass, model-side outcomes (wrong answer, ran out of steps, stalled) and
|
|
4
|
+
failures that say nothing about the harness's logic (provider outage, killed by timeout), so a
|
|
5
|
+
score never hides infrastructure noise and infrastructure never hides a harness bug.
|
|
6
|
+
"""
|
|
7
|
+
import collections
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
OUTAGE = re.compile(r"unavailable after|429|503|Not Found|ConnectError|TimeoutError")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def outcome(r):
|
|
17
|
+
if r["pass"]:
|
|
18
|
+
return "pass"
|
|
19
|
+
try:
|
|
20
|
+
j = json.loads(open(r["dir"] + "/stdout.txt").read())
|
|
21
|
+
except (OSError, ValueError):
|
|
22
|
+
return "timeout" if r.get("run_status") == "timeout" else "no_output"
|
|
23
|
+
f = j.get("failure") or {}
|
|
24
|
+
kind, msg = f.get("kind"), f.get("message", "")
|
|
25
|
+
if kind == "worker_error":
|
|
26
|
+
return "outage" if OUTAGE.search(msg) else "worker_error"
|
|
27
|
+
return kind or ("wrong_answer" if j.get("status") == "COMPLETED" else "timeout")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main(dirs):
|
|
31
|
+
rows = []
|
|
32
|
+
for d in dirs:
|
|
33
|
+
rows += [json.loads(line) for line in open(os.path.join(d, "results.jsonl"))]
|
|
34
|
+
groups = collections.defaultdict(list)
|
|
35
|
+
for r in rows:
|
|
36
|
+
groups[(r["runner"], r["model"])].append(r)
|
|
37
|
+
for (runner, model), rs in sorted(groups.items()):
|
|
38
|
+
c = collections.Counter(outcome(r) for r in rs)
|
|
39
|
+
valid = [r for r in rs if outcome(r) not in ("outage", "timeout", "no_output")]
|
|
40
|
+
mean = lambda l: round(sum(x["score"] for x in l) / len(l), 3) if l else 0.0 # noqa: E731
|
|
41
|
+
wall = round(sum(r["wall_s"] for r in rs) / len(rs))
|
|
42
|
+
print(f"{runner} {model}: {len(rs)} runs, pass {c['pass']} ({c['pass'] / len(rs):.0%}), mean {mean(rs)}, "
|
|
43
|
+
f"mean over valid runs {mean(valid)}, {wall}s/run")
|
|
44
|
+
print(" " + ", ".join(f"{k} {v}" for k, v in c.most_common()))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
main(sys.argv[1:])
|
|
@@ -20,10 +20,22 @@ HERE = os.path.dirname(os.path.abspath(__file__))
|
|
|
20
20
|
TASK_CFG = { # tether tools / step cap / wall timeout
|
|
21
21
|
"code-simple": ("edit,bash", 30, 600), "code-medium": ("edit,bash", 40, 900), "code-hard": ("edit,bash", 50, 1200),
|
|
22
22
|
"focus": ("edit,bash,mem", 30, 600),
|
|
23
|
+
"plan": ("edit,bash", 20, 420), "codereview": ("edit,bash", 20, 420),
|
|
23
24
|
"qa": ("", 10, 300), "content": ("", 10, 300), "review": ("edit", 15, 420),
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
|
|
28
|
+
POLY_VERIFY = False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def cfg(task):
|
|
32
|
+
return ("edit,bash", 30, 600) if task.startswith("poly-") else TASK_CFG[task]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def poly_tasks():
|
|
36
|
+
return ",".join("poly-" + e for e in sorted(os.listdir(grade.poly_dir())))
|
|
37
|
+
|
|
38
|
+
|
|
27
39
|
def group(model):
|
|
28
40
|
return "nvidia" if model.startswith("openai/") else "ollama" if model.startswith("ollama/") else "claude" if model.startswith(("claude", "cc/")) else "nvidia"
|
|
29
41
|
|
|
@@ -39,7 +51,7 @@ def flags(tether_bin):
|
|
|
39
51
|
|
|
40
52
|
|
|
41
53
|
def build_cmd(runner, model, task, goal, ws, rd, tether_bin, params=()):
|
|
42
|
-
tools, steps, _ =
|
|
54
|
+
tools, steps, _ = cfg(task)
|
|
43
55
|
if runner == "tether":
|
|
44
56
|
h = flags(tether_bin)
|
|
45
57
|
cmd = [tether_bin, "run", "--model", model, "--max-steps", str(steps), "--db", f"{rd}/t.db"]
|
|
@@ -49,6 +61,8 @@ def build_cmd(runner, model, task, goal, ws, rd, tether_bin, params=()):
|
|
|
49
61
|
cmd += ["--tools", tools]
|
|
50
62
|
for item in params:
|
|
51
63
|
cmd += ["--param", item]
|
|
64
|
+
if task.startswith("poly-") and POLY_VERIFY:
|
|
65
|
+
cmd += ["--verify", "python3 -m unittest discover -p '*_test.py'"]
|
|
52
66
|
return cmd + [goal]
|
|
53
67
|
if runner == "naive":
|
|
54
68
|
base, key = ("http://localhost:11435/v1", "") if model.startswith("ollama/") else ("https://integrate.api.nvidia.com/v1", "NVIDIA_API_KEY")
|
|
@@ -98,8 +112,13 @@ async def one(sem, rec, args, results):
|
|
|
98
112
|
async with sem[group(model)]:
|
|
99
113
|
shutil.rmtree(rd, ignore_errors=True)
|
|
100
114
|
os.makedirs(rd)
|
|
101
|
-
|
|
102
|
-
|
|
115
|
+
if task.startswith("poly-"):
|
|
116
|
+
ex = task[5:]
|
|
117
|
+
shutil.copytree(grade.poly_dir(ex), ws, ignore=shutil.ignore_patterns(".meta", ".docs"))
|
|
118
|
+
goal = grade.poly_goal(ex)
|
|
119
|
+
else:
|
|
120
|
+
shutil.copytree(f"{HERE}/tasks/{task}/ws", ws)
|
|
121
|
+
goal = open(f"{HERE}/tasks/{task}/goal.txt").read().strip()
|
|
103
122
|
cmd = build_cmd(runner, model, task, goal, ws, rd, args.tether_bin, args.param)
|
|
104
123
|
env = dict(os.environ, OLLAMA_HOST="http://localhost:11435", PYTHONDONTWRITEBYTECODE="1")
|
|
105
124
|
if model.startswith("openai/"): # old tether has no providers.yaml: route openai/ to NVIDIA's OpenAI-compatible API
|
|
@@ -108,7 +127,7 @@ async def one(sem, rec, args, results):
|
|
|
108
127
|
p = await asyncio.create_subprocess_exec(*cmd, cwd=ws, env=env, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, start_new_session=True)
|
|
109
128
|
status = "ok"
|
|
110
129
|
try:
|
|
111
|
-
out, err = await asyncio.wait_for(p.communicate(),
|
|
130
|
+
out, err = await asyncio.wait_for(p.communicate(), cfg(task)[2])
|
|
112
131
|
except asyncio.TimeoutError:
|
|
113
132
|
os.killpg(p.pid, signal.SIGKILL)
|
|
114
133
|
out, err = await p.communicate()
|
|
@@ -160,7 +179,14 @@ async def main():
|
|
|
160
179
|
ap.add_argument("--par", default="nvidia=4,ollama=1,claude=3")
|
|
161
180
|
ap.add_argument("--force", action="store_true")
|
|
162
181
|
ap.add_argument("--redo-infra", action="store_true", help="rerun cells that failed on provider errors")
|
|
182
|
+
ap.add_argument("--poly-verify", action="store_true", help="verify gate on the exercise tests: gate runs at `done`")
|
|
163
183
|
args = ap.parse_args()
|
|
184
|
+
global POLY_VERIFY
|
|
185
|
+
POLY_VERIFY = args.poly_verify
|
|
186
|
+
if args.tasks == "polyglot":
|
|
187
|
+
args.tasks = poly_tasks()
|
|
188
|
+
elif args.tasks == "polyglot-fast":
|
|
189
|
+
args.tasks = ",".join("poly-" + e for e in grade.POLY_FAST)
|
|
164
190
|
os.makedirs(args.out, exist_ok=True)
|
|
165
191
|
sem = {k: asyncio.Semaphore(int(v)) for k, v in (x.split("=") for x in args.par.split(","))}
|
|
166
192
|
recs = [(r, m, t, k) for r in args.runners.split(",") for m in args.models.split(",") for t in args.tasks.split(",") for k in range(args.trials)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Do not edit any file. Review inventory.py. Call done with a numbered list of the real defects you find: for each, name the function, say what is wrong and give the one-line fix. Report only real bugs, no style remarks.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Inventory helpers for a small shop."""
|
|
2
|
+
import sqlite3
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def page(items, number, size=10):
|
|
6
|
+
"""Return page `number` (1-based) of `items`."""
|
|
7
|
+
start = number * size
|
|
8
|
+
return items[start:start + size]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def add_tag(item, tag, tags=[]):
|
|
12
|
+
tags.append(tag)
|
|
13
|
+
item["tags"] = tags
|
|
14
|
+
return item
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def total_price(lines):
|
|
18
|
+
total = 0.0
|
|
19
|
+
for line in lines:
|
|
20
|
+
total += line["price"] * line["qty"]
|
|
21
|
+
return total
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def is_free(price):
|
|
25
|
+
return price == 0.0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def find_by_name(conn, name):
|
|
29
|
+
cur = conn.execute("SELECT id, name FROM items WHERE name = '" + name + "'")
|
|
30
|
+
return cur.fetchall()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_stock(path):
|
|
34
|
+
f = open(path)
|
|
35
|
+
data = f.read()
|
|
36
|
+
return dict(line.split(",") for line in data.splitlines())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def stock_of(stock, name):
|
|
40
|
+
return int(stock[name])
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Do not edit any file. Read svc/client.py and svc/config.py, then call done with an implementation plan (a numbered list of 6 to 10 steps) for adding client-side retries with exponential backoff and jitter to the HTTP client. The plan must name the exact files and functions to change, say how the retry limit and base delay become settings, say which failures are retried (for example HTTP 429 and 5xx, timeouts) and which are not, say how Retry-After is honoured, and say how it will be tested without a real network. Plan only, no code.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""HTTP client used by every service call."""
|
|
2
|
+
import json
|
|
3
|
+
import urllib.request
|
|
4
|
+
|
|
5
|
+
from svc.config import BASE_URL, TIMEOUT
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get(path):
|
|
9
|
+
with urllib.request.urlopen(BASE_URL + path, timeout=TIMEOUT) as resp:
|
|
10
|
+
return json.loads(resp.read())
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def post(path, payload):
|
|
14
|
+
req = urllib.request.Request(
|
|
15
|
+
BASE_URL + path, data=json.dumps(payload).encode(), headers={"Content-Type": "application/json"}
|
|
16
|
+
)
|
|
17
|
+
with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
|
|
18
|
+
return json.loads(resp.read())
|
|
@@ -53,12 +53,12 @@ In *"MemGPT: Towards LLMs as Operating Systems"*, Packer et al. (2023) introduce
|
|
|
53
53
|
|
|
54
54
|
Tether implements a deterministic three-tier memory hierarchy backed by `.tether/tether.db` (SQLite WAL + FTS5):
|
|
55
55
|
|
|
56
|
-
1. **Head + Tail L1 Active Elision**: Even within the active $K=2$ turn window, if a command emits an oversized output (
|
|
56
|
+
1. **Head + Tail L1 Active Elision**: Even within the active $K=2$ turn window, if a command emits an oversized output (more than a third of `max_context_tokens` in chars, at least 2,400), `ContextAssembler` keeps the first 60% of the cap (headers/command summary) + the last 40% (stack trace/exit status) and elides the middle with a pointer to `mem recall <obs_id>`.
|
|
57
57
|
2. **Ephemeral $K$-Turn Folding**: Once an observation ages past $K=2$ steps, it is folded into a single-line tombstone:
|
|
58
58
|
```text
|
|
59
59
|
[obs_1 folded: `web fetch https://...` (3,420 chars) | use `mem recall obs_1` or `mem search <query>`]
|
|
60
60
|
```
|
|
61
|
-
Exception (#69): the latest read of each of the last 8 files (`edit view <p>`, `bash cat/head/tail/sed -n <p>`; paths normalized against the workspace) is not folded until `<p>` is edited (`edit write|replace`, or any other bash command naming it: `sed -i`, `tee`, redirects, `python -c`) or re-read. The
|
|
61
|
+
Exception (#69): the latest read of each of the last 8 files (`edit view <p>`, `bash cat/head/tail/sed -n <p>`; paths normalized against the workspace) is not folded until `<p>` is edited (`edit write|replace`, or any other bash command naming it: `sed -i`, `tee`, redirects, `python -c`) or re-read. The head+tail elision still applies (an observation may use up to a third of `max_context_tokens`, at least 2,400 chars), and the hard context budget remains #81. The `mem recall` hint is shown only when `mem` is enabled (else "re-run the command").
|
|
62
62
|
3. **Sub-Millisecond SQLite FTS5 BM25 RAG**: Every raw observation and final deliverable is automatically chunked (~150 tokens per paragraph/section) and indexed into an FTS5 virtual table (`chunks_fts`) so the agent (or a parent agent) can retrieve exact passages via `mem search` or `agent query`.
|
|
63
63
|
|
|
64
64
|
---
|
|
@@ -22,7 +22,7 @@ When the provider sets `structured_history = True` (Ollama, OpenAI-compatible, A
|
|
|
22
22
|
|
|
23
23
|
Raw tool outputs stay in the prompt for $K=2$ turns, then fold into a one-line tombstone (see [Built-in Tools](../tutorial/built-in-tools.md#mem-pinned-scratchpad-bm25-observation-recall)). For small models this has consequences ([#27](https://github.com/igormcsouza/tether/issues/27)), seen in the [course-builder example](https://github.com/igormcsouza/tether/tree/main/examples/course):
|
|
24
24
|
|
|
25
|
-
- **Fix (#69).** Folding exception: the latest read of each of the last 8 files (`edit view <p>`, `bash cat/head/tail/sed -n <p>`; paths normalized against the workspace) is not folded until `<p>` is edited (`edit write|replace`, or any other bash command naming it: `sed -i`, `tee`, redirects, `python -c`) or re-read. The
|
|
25
|
+
- **Fix (#69).** Folding exception: the latest read of each of the last 8 files (`edit view <p>`, `bash cat/head/tail/sed -n <p>`; paths normalized against the workspace) is not folded until `<p>` is edited (`edit write|replace`, or any other bash command naming it: `sed -i`, `tee`, redirects, `python -c`) or re-read. The head+tail elision still applies (an observation may use up to a third of `max_context_tokens`, at least 2,400 chars), and the hard context budget remains #81. The `mem recall` hint is shown only when `mem` is enabled (else "re-run the command"). (`ContextAssembler(mem_enabled=..., workspace_root=...)`, `MAX_LIVE_READS`).
|
|
26
26
|
- **Re-viewing files loops, then invents data.** The model runs `edit view`, the output folds two turns later, it views again, and eventually writes from memory.
|
|
27
27
|
- **What worked: one compact joined view, used right before the write, that ends with the exact next call.** The `course-source <module>` tool returns a module's plan and chosen videos in one output and finishes with `NOW WRITE THIS MODULE: act("edit write part-N.md", ...)`, so the data is still in the prompt when the write happens.
|
|
28
28
|
- **Write long outputs in small pieces.** One big write dropped a whole section. The writer skill writes one `part-N.md` per module with `edit write`, then a `course-build` tool concatenates them into `course.md`.
|
|
@@ -48,7 +48,7 @@ Every entry, of any kind, may set optional positive integers: `context_tokens` (
|
|
|
48
48
|
|
|
49
49
|
### Timeout and retry
|
|
50
50
|
|
|
51
|
-
Every entry may set `timeout` (positive seconds). It applies to the HTTP kinds (`openai`, `ollama`, `anthropic`; defaults 120 / 120 / 180 s) and to `command` (default 300 s). The HTTP providers retry automatically, at most 2 times with 1 s then 2 s backoff, on timeouts, network errors and HTTP 429/500/502/503/504. A 401/403 is never retried: it raises `ProviderError` naming the provider and the credential variable (`openai: HTTP 401: `NVIDIA_API_KEY` is missing or rejected; set it to a valid key`; the value is never printed), and a declared `api_key_env` that is unset or empty fails the session before any request with the same message. Other 4xx responses are never retried. When retries run out the call raises `ProviderError`, so the session fails with a readable `Worker error: ProviderError: openai model `gpt-4o` unavailable after 3 attempts (timeout 120s, last error: ReadTimeout)`. The retry is invisible to the prompt (0 tokens, no prefix change).
|
|
51
|
+
Every entry may set `timeout` (positive seconds). It applies to the HTTP kinds (`openai`, `ollama`, `anthropic`; defaults 120 / 120 / 180 s) and to `command` (default 300 s). The HTTP providers retry automatically, at most 2 times with 1 s then 2 s backoff, on timeouts, network errors and HTTP 429/500/502/503/504. A 401/403 is never retried: it raises `ProviderError` naming the provider and the credential variable (`openai: HTTP 401: `NVIDIA_API_KEY` is missing or rejected; set it to a valid key`; the value is never printed), and a declared `api_key_env` that is unset or empty fails the session before any request with the same message. Other 4xx responses are never retried. An HTTP 500 whose body names a rejected tool call (Ollama: `invalid tool call arguments`, a cut-off tool-call JSON) is not retried either, since the same request fails the same way; `ollama`, `openai` and `anthropic` turn it into a one-line hint to the model (a no-action reply), so the session continues instead of failing. The match is the text `tool call` in a 500 body, for any server. When retries run out the call raises `ProviderError`, so the session fails with a readable `Worker error: ProviderError: openai model `gpt-4o` unavailable after 3 attempts (timeout 120s, last error: ReadTimeout)`. The retry is invisible to the prompt (0 tokens, no prefix change).
|
|
52
52
|
|
|
53
53
|
### Kinds
|
|
54
54
|
|
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Release Notes
|
|
2
2
|
|
|
3
|
+
## v0.2.7 (2026-10-24)
|
|
4
|
+
|
|
5
|
+
Found by running Tether on the Polyglot-Python benchmark (34 Aider polyglot Python exercises) and reading why runs failed instead of guessing: sessions that ended mid-task on a server error, and models that re-read one file until the step cap. Closes #138, #141.
|
|
6
|
+
|
|
7
|
+
### Benchmark
|
|
8
|
+
|
|
9
|
+
- `bench/run.py --tasks polyglot|polyglot-fast` runs the Polyglot-Python exercises, graded by each exercise's own tests; `bench/report.py` prints pass rate, mean score and a failure breakdown that keeps provider outages apart from harness outcomes. On the 12-exercise fast set (2 trials), the observation-cap fix below moved `qwen3.5:9b` from mean 0.64 to 0.80 and cut re-reads of one file from 32% to 19% of steps; `nemotron-3.5-lightning` went from 10 to 12 passes of 24.
|
|
10
|
+
|
|
11
|
+
### Fixes
|
|
12
|
+
|
|
13
|
+
- Context: one active observation may now use up to a third of the context budget (`max_context_tokens * 4 / 3` chars, at least 2,400) before head+tail elision, instead of a fixed 2,400 chars. Before, a 6-12 KB test file or a 100-line slice of one was always cut to head+tail, so the model asked for another slice, got that cut too, and re-read the same file 10+ times: on the Polyglot-Python fast set 24 of 48 runs ended at the step cap, 45% of their steps were reads and only 19% were edits. The hard budget still folds older observations first, so the prompt cannot overflow; `ContextAssembler(active_char_cap=...)` still sets a fixed cap.
|
|
14
|
+
- Providers: when a server (Ollama, vLLM, llama.cpp behind `openai`, `anthropic`) answers HTTP 500 because it rejected the model's own tool call (`invalid tool call arguments for "act": unexpected end of JSON input`), the call is no longer retried and no longer ends the session as `worker_error` (#138). The model gets one line (`Your last call was rejected: the tool-call JSON was invalid or cut off. Send a shorter call, e.g. write long files in smaller pieces.`) through the existing no-action reminder and carries on, bounded by `max_steps`. Before, 13 of 34 Polyglot-Python runs on `qwen3.5:9b` died this way, mid-task and often with most tests passing. `ModelResponse` gains an optional `hint`. 0 static prompt tokens.
|
|
15
|
+
|
|
16
|
+
## v0.2.6 (2026-10-23)
|
|
17
|
+
|
|
18
|
+
Found by using tether as its own SWE agent and by the v0.2.5 post-release check: a sandbox that refused harmless commands, sessions that only read, and a `query` that could not say why it found nothing. Closes #77, #93, #125, #127.
|
|
19
|
+
|
|
20
|
+
### Fixes
|
|
21
|
+
|
|
22
|
+
- CLI: `tether query` now says why nothing matched (#93): `Error: unknown session` with exit code 1 for a wrong id, `No chunks are indexed for <id> yet` for an empty session, and the number of indexed chunks otherwise. `search_chunks` also tries a prefix match (`loop*`) after the AND and OR matches, so `loop` finds `looping`.
|
|
23
|
+
- Store: `SQLiteStore(":memory:")` no longer creates a file literally named `:memory:` in the working directory (#77). The path now maps to a temporary file outside the workspace, so every store method still shares one database.
|
|
24
|
+
- Sandbox: with `bwrap` active, `bash` no longer refuses commands by text for `..`, absolute paths outside the workspace, or `.env`/`.tether`/`.git/config` mentions (#125). The kernel mounts already enforce them (read-only root, hidden `$HOME`, masked secrets), so the text rules only produced false refusals such as `ls ~/..` or `cat /etc/hosts`. They remain the fallback when `bwrap` is missing, and `sudo`, `su`, disk tools, `rm -rf /`, `chmod 777`, fork bombs and `~/.ssh` stay refused in both modes.
|
|
25
|
+
- Sandbox: `.env.example`, `.env.sample`, `.env.template` and `.env.dist` are readable and writable; real `.env`, `.env.local`, `.env.production` stay masked and protected. Templates hold no secrets.
|
|
26
|
+
|
|
3
27
|
## v0.2.5 (2026-10-23)
|
|
4
28
|
|
|
5
29
|
Found by benchmarking the same models inside Tether and inside a plain tool loop (7 tasks, 10+ models): model idioms made the single-`act` grammar fail invisibly, and loops or oversized prompts burned steps and tokens. Closes #76, #81, #86, #89, #92 and the shell-idiom bugs #109, #110.
|
|
@@ -46,6 +70,8 @@ Dogfooding fixes: the four milestone bugs (#74 #75 #85 #88) plus the friction fo
|
|
|
46
70
|
|
|
47
71
|
### Fixes
|
|
48
72
|
|
|
73
|
+
- Harness: a session that only reads gets a deterministic hint (#127). After 12 consecutive read-only steps with no change at all, then every 8 more, the observation ends with `[NUDGE] No file changes in N steps. Make your edit now, or call `done` with what you found.` (0 static prompt tokens). Before, the quiet nudge started only after a first change, so a model that never edited (nemotron-3-ultra and glm-5.3 on scoped briefs: 40 steps, 150k to 215k prompt tokens, no change) was never told.
|
|
74
|
+
- Harness: `awk`/`gawk`/`mawk` count as read-only (unless they print to a file, pipe to a shell, call `system()` or edit in place) and as range-shift reads of one file, so the #92 stall guard sees `awk 'NR>=a && NR<=b' f` loops. `NR>=5` used to look like a redirect, also inside compound commands (`grep ...; awk 'NR>=5' f`), which silently turned the nudges off.
|
|
49
75
|
- Store: a `RUNNING` session whose worker pid no longer exists is flipped to `FAILED` (`Worker died (pid N) before finishing`) when read via `get_session`/`list_sessions`, so `status`, `list`, `wait` and `read` no longer report a killed worker as running forever (#85). A pid owned by another user counts as alive.
|
|
50
76
|
- Providers: `429` retries wait `Retry-After` (else 10s doubling, capped 30s) and get up to 5 attempts instead of 3 quick ones, so rate-limited hosted models (NVIDIA free tier) no longer fail the session after 3 quick attempts.
|
|
51
77
|
- Providers: each `post_with_retry` attempt is now bounded by a total `asyncio.wait_for(timeout)` deadline, so a trickling/stalled response cannot hang past the configured timeout; the deadline is retried like other transient errors and shows as `TimeoutError` (vs `ReadTimeout`) in the final `ProviderError` (#88).
|
|
@@ -85,7 +85,7 @@ trigger it, script the reply; if infrastructure must fail, add a fault to the pr
|
|
|
85
85
|
|
|
86
86
|
## Model conformance bench (developers only)
|
|
87
87
|
|
|
88
|
-
`bench/` holds
|
|
88
|
+
`bench/` holds nine tasks (the `focus` task checks that an exact single-file goal is not lost in a distractor-heavy repo) with hidden-test graders and three runners (Tether, a plain tool loop,
|
|
89
89
|
Claude Code) so the same model can be compared across harnesses and releases. It needs real
|
|
90
90
|
models, so it is not in CI and not in the package: use it when onboarding a model or before a
|
|
91
91
|
release to see friction that scripted tests cannot (the shell-wrapper, bare-command and
|