tether-sdk 0.2.1__tar.gz → 0.2.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.
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/AGENTS.md +2 -1
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/PKG-INFO +1 -1
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/docs/reference/agent-interface.md +1 -1
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/docs/reference/providers.md +22 -2
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/docs/release-notes.md +14 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/docs/troubleshooting.md +1 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/docs/tutorial/cli-and-headless.md +12 -0
- tether_sdk-0.2.2/tests/fixtures/dialects.jsonl +6 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/providers/test_declarative.py +98 -0
- tether_sdk-0.2.2/tests/providers/test_dialects.py +79 -0
- tether_sdk-0.2.2/tests/test_context_config.py +101 -0
- tether_sdk-0.2.2/tests/test_misses.py +108 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/test_tether.py +1 -1
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tether/__init__.py +3 -1
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tether/cli.py +55 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tether/harness.py +14 -4
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tether/mcp.py +4 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tether/providers/__init__.py +2 -0
- tether_sdk-0.2.2/tether/providers/base.py +183 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tether/providers/declarative.py +74 -13
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tether/store.py +26 -0
- tether_sdk-0.2.1/tether/providers/base.py +0 -140
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/.github/workflows/ci.yml +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/.github/workflows/delivery.yml +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/.github/workflows/docs.yml +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/.gitignore +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/LICENSE +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/README.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/docs/architecture.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/docs/benchmarks.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/docs/extending.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/docs/index.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/docs/javascripts/mathjax.js +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/docs/reference/context-and-store.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/docs/reference/harness.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/docs/reference/index.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/docs/reference/multiplexer-and-decorator.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/docs/reference/sandbox-and-skills.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/docs/reference/tools.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/docs/releasing.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/docs/tutorial/agent-interface.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/docs/tutorial/built-in-tools.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/docs/tutorial/index.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/docs/tutorial/multi-agent.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/docs/tutorial/sdk-and-custom-tools.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/docs/tutorial/security-and-sandbox.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/docs/tutorial/skills.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/docs/tutorial/workflows-and-telemetry.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/examples/course/README.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/examples/course/checks.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/examples/course/course.yaml +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/examples/course/ruff.toml +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/examples/course/skills/planner/SKILL.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/examples/course/skills/searcher/SKILL.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/examples/course/skills/searcher/tools.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/examples/course/skills/writer/SKILL.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/examples/course/skills/writer/tools.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/mkdocs.yml +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/pyproject.toml +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/skills/researcher/SKILL.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/__init__.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/conftest.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/providers/__init__.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/providers/test_anthropic.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/providers/test_base.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/providers/test_ollama.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/providers/test_openai.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/test_agent_interface.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/test_cli.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/test_context.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/test_decorator.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/test_harness.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/test_integration.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/test_multiplexer.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/test_pipeline.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/test_release_hardening.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/test_retro.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/test_sandbox.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/test_security_hardening.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/test_skills.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/test_store.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/test_structured_history.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/tools/__init__.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/tools/test_agent.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/tools/test_bash.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/tools/test_done.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/tools/test_edit.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/tools/test_mem.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tests/tools/test_web.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tether/api.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tether/context.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tether/decorator.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tether/multiplexer.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tether/pipeline.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tether/providers/anthropic.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tether/providers/ollama.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tether/providers/openai.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tether/retro.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tether/sandbox.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tether/schema.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tether/skills.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tether/tools/__init__.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tether/tools/agent.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tether/tools/bash.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tether/tools/done.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tether/tools/edit.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tether/tools/mem.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/tether/tools/web.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.2}/uv.lock +0 -0
|
@@ -70,7 +70,7 @@ Tether implements the architectural principles from *"LLM-Native Applications as
|
|
|
70
70
|
```text
|
|
71
71
|
tether/
|
|
72
72
|
├── __init__.py # Public SDK exports: Harness, Pipeline, Session, Skill, SandboxPolicy, tool
|
|
73
|
-
├── cli.py # Headless-capable CLI: `tether run|pipeline|status|read|query|send|list|wait|stats|retro|events|mcp` (`--json` + documented exit codes)
|
|
73
|
+
├── cli.py # Headless-capable CLI: `tether run|pipeline|status|read|query|send|list|wait|stats|retro|events|misses|mcp` (`--json` + documented exit codes)
|
|
74
74
|
├── harness.py # Control Plane: bounded agent loop, pipelines, SQLite inbox, worker spawn
|
|
75
75
|
├── pipeline.py # Workflow Plane: declarative multi-stage YAML pipelines & model routing
|
|
76
76
|
├── api.py # Agent-facing JSON session view, exit codes, events (`--json`)
|
|
@@ -155,6 +155,7 @@ tether/
|
|
|
155
155
|
- `ANTHROPIC_API_KEY`, `ANTHROPIC_BASE_URL` (default `https://api.anthropic.com`)
|
|
156
156
|
|
|
157
157
|
---
|
|
158
|
+
- **Context config (#54):** `max_context_tokens` / `fold_observations_after` resolve as explicit argument (`--context-tokens` / `--fold-after`, MCP `context_tokens` / `fold_after`) > `providers.yaml` entry keys `context_tokens` / `fold_after` > defaults 8192 / 2.
|
|
158
159
|
|
|
159
160
|
## 4. Development Workflow & Commands
|
|
160
161
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tether-sdk
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
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
|
|
@@ -42,7 +42,7 @@ Optional: `pip install "tether-sdk[mcp]"` (installs the official `mcp` SDK; core
|
|
|
42
42
|
|
|
43
43
|
| Tool | Arguments |
|
|
44
44
|
|------|-----------|
|
|
45
|
-
| `tether_run` | `goal`, `model`, `skill`, `tools`, `verify[]`, `max_steps`, `sandbox`, `no_network`, `output_schema`, `wait` (default true) |
|
|
45
|
+
| `tether_run` | `goal`, `model`, `skill`, `tools`, `verify[]`, `max_steps`, `context_tokens`, `fold_after`, `sandbox`, `no_network`, `output_schema`, `wait` (default true) |
|
|
46
46
|
| `tether_status` | `id` |
|
|
47
47
|
| `tether_wait` | `ids[]`, `timeout` |
|
|
48
48
|
| `tether_read` | `id`, `mode` = `full` \| `digest` \| `steps` |
|
|
@@ -34,12 +34,18 @@ claude-cli:
|
|
|
34
34
|
reply: result
|
|
35
35
|
usage: {prompt: [usage.input_tokens, usage.cache_creation_input_tokens, usage.cache_read_input_tokens], completion: usage.output_tokens}
|
|
36
36
|
timeout: 180
|
|
37
|
+
context_tokens: 32000
|
|
38
|
+
fold_after: 8
|
|
37
39
|
```
|
|
38
40
|
|
|
39
41
|
```bash
|
|
40
42
|
tether run --model claude-cli/sonnet "fix the failing test"
|
|
41
43
|
```
|
|
42
44
|
|
|
45
|
+
### Context budget
|
|
46
|
+
|
|
47
|
+
Every entry, of any kind, may set two optional positive integers: `context_tokens` (the context budget, default 8192) and `fold_after` (how many recent turns keep full observations, default 2). The `--context-tokens` / `--fold-after` flags and the Harness arguments override them.
|
|
48
|
+
|
|
43
49
|
### Kinds
|
|
44
50
|
|
|
45
51
|
| `kind` | Fields |
|
|
@@ -55,11 +61,25 @@ tether run --model claude-cli/sonnet "fix the failing test"
|
|
|
55
61
|
|
|
56
62
|
A `command` entry wraps any program that takes a prompt and prints JSON.
|
|
57
63
|
|
|
58
|
-
- **Placeholders**: `{model}`, `{system}` and `{
|
|
64
|
+
- **Placeholders**: `{model}`, `{system}`, `{conversation}` and `{schema}` (the act JSON schema, compact) are substituted in `argv` items and in the `stdin` template. If `stdin` is omitted it defaults to `{conversation}`.
|
|
59
65
|
- **No shell**: `argv` is executed directly, never through a shell.
|
|
60
66
|
- **Reply**: `reply` is a dotted path into the JSON the command prints (`result` in the example).
|
|
61
67
|
- **Usage**: `usage.prompt` is a path or a list of paths (their values are summed); `usage.completion` is a path. Both are optional.
|
|
62
|
-
- **
|
|
68
|
+
- **Constrained output**: use `{schema}` to make the CLI constrain its reply to the act schema, for example `argv: [claude, -p, --json-schema, "{schema}", --output-format, json]`. The reply may then be an object with `cmd` and optional `stdin` instead of a string; if the `reply` path gives no string or object, a top-level `structured_output` object is used.
|
|
69
|
+
|
|
70
|
+
Measured cost (v0.2.2, one small task): with `claude -p`, `--json-schema` adds about 2k prompt tokens to every call (the CLI wraps the schema in a hidden tool), so the same 5-step task used ~26k prompt tokens instead of ~7k. Use it when a model's plain replies are not being understood, not as a default.
|
|
71
|
+
- **Protocol**: the model should answer with a single JSON `{"cmd", "stdin"}` object. The `{system}` placeholder already contains that instruction.
|
|
72
|
+
- **Dialects**: when the reply is not that JSON, Tether tries table-driven extractors in order, first match wins: `<tool_call>` JSON, raw JSON containing `"cmd"`, `<parameter name="cmd">X</parameter>` with optional `<parameter name="stdin">Y</parameter>` (inside any wrapper such as `<invoke>`), and text `act(...)`.
|
|
73
|
+
- **`dialects:`** (optional): a list of mappings, each with a `regex` (compiled with `re.DOTALL`) that defines the named group `cmd` and optionally `stdin`. Custom dialects are tried after the built-in ones. Python users can also call `tether.register_dialect(fn)` with a `(str) -> ActionCall | None` function.
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
mycli:
|
|
77
|
+
kind: command
|
|
78
|
+
argv: [mycli, "{conversation}"]
|
|
79
|
+
reply: result
|
|
80
|
+
dialects:
|
|
81
|
+
- regex: 'RUN\[(?P<cmd>.*?)\]WITH\[(?P<stdin>.*?)\]'
|
|
82
|
+
```
|
|
63
83
|
|
|
64
84
|
### Rules
|
|
65
85
|
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Release Notes
|
|
2
2
|
|
|
3
|
+
## v0.2.2 (2026-10-19)
|
|
4
|
+
|
|
5
|
+
Dogfooding fixes: Tether implemented most of this release itself through the CLI and a `claude -p` provider.
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- `{schema}` placeholder for `command` providers (#51): constrain output to the act schema (e.g. `claude -p --json-schema '{schema}'`); replies may be structured objects with `cmd`/`stdin`.
|
|
10
|
+
- `tether misses [session_id] [--model M] [--limit N] [--json]` (#53): list unparsed model replies (nudges) as fixture-ready JSONL; paste lines into `tests/fixtures/dialects.jsonl` after filling `expected`.
|
|
11
|
+
|
|
12
|
+
### Fixes
|
|
13
|
+
|
|
14
|
+
- The context budget and observation fold window are now configurable (#54): `--context-tokens` / `--fold-after` on `tether run` and `tether pipeline`, `context_tokens` / `fold_after` on `tether_run`, and per-provider keys in `providers.yaml`.
|
|
15
|
+
- Tool-call dialect extraction is now table-driven (#52): built-in extractors for `<tool_call>` JSON, raw JSON, `<parameter name="cmd">` blocks and text `act(...)`, extendable with `tether.register_dialect` or a `dialects:` key on `command` entries in `providers.yaml`.
|
|
16
|
+
|
|
3
17
|
## v0.2.1 (2026-10-19)
|
|
4
18
|
|
|
5
19
|
Add model backends without code.
|
|
@@ -10,6 +10,7 @@ Symptom, cause, fix. Most entries come from building the [course-builder example
|
|
|
10
10
|
| Files contain a literal `\n` | Model sent escaped newlines in stdin | Fixed in v0.1.2 for `edit write`, `edit replace` and `done` |
|
|
11
11
|
| Tool with multi-word argument fails (`unexpected keyword argument`, wrong value) | Unquoted words were not joined | Fixed in v0.1.2. See [argument parsing](tutorial/sdk-and-custom-tools.md#argument-parsing-v012) |
|
|
12
12
|
| `Error: unknown command` for a tool defined in `tools.py` | The tool is not in the skill `tools:` list ([#24](https://github.com/igormcsouza/tether/issues/24)) | Add its name to `tools:`. See [Skills](tutorial/skills.md#tools-py-and-the-tools-list) |
|
|
13
|
+
| Model answers are not understood (many nudges in `tether stats`) | The reply format is not parsed into an action | Run `tether misses`, then add a dialect: paste the lines into `tests/fixtures/dialects.jsonl` or add a `dialects:` regex to `providers.yaml` |
|
|
13
14
|
| Gate with inline `awk`/`sed` is blocked as "absolute path outside workspace" | Fixed in v0.1.4 ([#31](https://github.com/igormcsouza/tether/issues/31)); older versions saw a program starting with `/` as a path | Upgrade, or move the logic into a script (`python3 checks.py ...`). See [Security and Sandbox](tutorial/security-and-sandbox.md#example-awksed-programs-that-start-with) |
|
|
14
15
|
| `Ollama model … not found; installed: …` | The model name is not pulled; Tether no longer substitutes another one ([#21](https://github.com/igormcsouza/tether/issues/21)) | `ollama pull <model>`, fix the name, or set `TETHER_OLLAMA_FALLBACK=1` to opt in to a substitute |
|
|
15
16
|
| `tether mcp` prints "MCP support is not installed" | The optional extra is missing | `pip install "tether-sdk[mcp]"`. See [Use Tether Inside Your Harness](tutorial/agent-interface.md) |
|
|
@@ -76,10 +76,22 @@ tether query "$SESSION_ID" "write-ahead log checkpointing" --top 3
|
|
|
76
76
|
tether list
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
+
## Context budget and fold window
|
|
80
|
+
|
|
81
|
+
By default Tether keeps an 8192-token context budget and folds observations older than 2 turns. Tune both per run with `--context-tokens N` and `--fold-after N` (on `tether run` and `tether pipeline`), or per model in `providers.yaml` with `context_tokens` / `fold_after`. Flags override the provider entry, which overrides the defaults.
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
tether run --context-tokens 32000 --fold-after 8 "refactor the parser"
|
|
85
|
+
```
|
|
86
|
+
|
|
79
87
|
## Scripting: `--json` and exit codes
|
|
80
88
|
|
|
81
89
|
`run`, `pipeline`, `status`, `list`, `read`, `wait`, `send` and `stats` accept `--json`. Exit codes: `0` completed, `1` usage error, `2` waiting for input, `3` gate failed, `4` max steps, `5` worker error. A failure reason goes to stderr, never stdout. `--result-file <path>` writes the deliverable to a file, `--output-schema <schema.json>` rejects a `done` whose JSON does not match, and `tether events <id> --follow` streams turns as JSONL. Details: [Agent Interface reference](../reference/agent-interface.md).
|
|
82
90
|
|
|
91
|
+
### When a model's replies are not understood
|
|
92
|
+
|
|
93
|
+
If a model keeps answering in a format Tether cannot parse into an action (many nudges in `tether stats`), run `tether misses [session_id] [--model M] [--limit N] [--json]`. It lists the unparsed replies; with `--json` each line is fixture-ready. Paste lines into `tests/fixtures/dialects.jsonl` after filling `expected`, or add a `dialects:` regex to `providers.yaml`.
|
|
94
|
+
|
|
83
95
|
## Live Inbox Injection & Auto-Resume (`tether send`)
|
|
84
96
|
|
|
85
97
|
Every session has a persistent SQLite `inbox` queue:
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{"reply": "<tool_call>{\"name\": \"act\", \"arguments\": {\"cmd\": \"ls -la\"}}</tool_call>", "cmd": "ls -la", "stdin": null}
|
|
2
|
+
{"reply": "Sure: {\"cmd\": \"echo hi\"}", "cmd": "echo hi", "stdin": null}
|
|
3
|
+
{"reply": "act(\"done\", stdin=\"x\")", "cmd": "done", "stdin": "x"}
|
|
4
|
+
{"reply": "<invoke name=\"act\">\n<parameter name=\"cmd\"> pytest -q </parameter>\n</invoke>", "cmd": "pytest -q", "stdin": null}
|
|
5
|
+
{"reply": "<invoke name=\"act\">\n<parameter name=\"cmd\">edit write a.txt</parameter>\n<parameter name=\"stdin\">line1\nline2\n</parameter>\n</invoke>", "cmd": "edit write a.txt", "stdin": "line1\nline2\n"}
|
|
6
|
+
{"reply": "I will think about this and answer in plain prose.", "cmd": null, "stdin": null}
|
|
@@ -15,6 +15,7 @@ from tether.providers import (
|
|
|
15
15
|
)
|
|
16
16
|
from tether.providers.base import ModelResponse
|
|
17
17
|
from tether.providers.declarative import (
|
|
18
|
+
ACT_SCHEMA,
|
|
18
19
|
CommandProvider,
|
|
19
20
|
build_declared,
|
|
20
21
|
load_declared,
|
|
@@ -300,3 +301,100 @@ def test_command_validation_errors(
|
|
|
300
301
|
_write(monkeypatch, tmp_path, f"cli:\n kind: command\n {body}")
|
|
301
302
|
with pytest.raises(ValueError, match=f"providers.yaml: entry `cli`.*{message}"):
|
|
302
303
|
load_declared()
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def test_command_custom_dialect_end_to_end() -> None:
|
|
307
|
+
from tether.providers.declarative import _build_dialects
|
|
308
|
+
|
|
309
|
+
text = "RUN[ls -la]WITH[body]"
|
|
310
|
+
code = f"import json; print(json.dumps({{'r': {text!r}}}))"
|
|
311
|
+
dialects = _build_dialects([{"regex": r"RUN\[(?P<cmd>.*?)\]WITH\[(?P<stdin>.*?)\]"}])
|
|
312
|
+
resp = _run(CommandProvider("m", argv=_script(code), reply="r", dialects=dialects))
|
|
313
|
+
assert resp.action is not None
|
|
314
|
+
assert resp.action.cmd == "ls -la"
|
|
315
|
+
assert resp.action.stdin == "body"
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def test_command_custom_dialect_no_match_and_empty_cmd() -> None:
|
|
319
|
+
from tether.providers.declarative import _build_dialects
|
|
320
|
+
|
|
321
|
+
dialects = _build_dialects([{"regex": r"RUN\[(?P<cmd>.*?)\]"}])
|
|
322
|
+
assert dialects[0]("nothing here") is None
|
|
323
|
+
assert dialects[0]("RUN[ ]") is None
|
|
324
|
+
call = dialects[0]("RUN[ pwd ]")
|
|
325
|
+
assert call is not None
|
|
326
|
+
assert call.cmd == "pwd"
|
|
327
|
+
assert call.stdin is None
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@pytest.mark.parametrize(
|
|
331
|
+
("dialects", "message"),
|
|
332
|
+
[
|
|
333
|
+
("dialects: nope", "invalid `dialects`"),
|
|
334
|
+
("dialects: [1]", "`regex` required"),
|
|
335
|
+
("dialects: [{foo: bar}]", "`regex` required"),
|
|
336
|
+
("dialects: [{regex: '('}]", "invalid `dialects` regex"),
|
|
337
|
+
("dialects: [{regex: 'abc'}]", "named group `cmd`"),
|
|
338
|
+
],
|
|
339
|
+
)
|
|
340
|
+
def test_command_dialects_validation_errors(
|
|
341
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, dialects: str, message: str
|
|
342
|
+
) -> None:
|
|
343
|
+
_write(
|
|
344
|
+
monkeypatch,
|
|
345
|
+
tmp_path,
|
|
346
|
+
f"cli:\n kind: command\n argv: [echo]\n reply: r\n {dialects}\n",
|
|
347
|
+
)
|
|
348
|
+
with pytest.raises(ValueError, match=f"providers.yaml: entry `cli`.*{message}"):
|
|
349
|
+
load_declared()
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
_SCHEMA = json.dumps(ACT_SCHEMA, separators=(",", ":"))
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def test_schema_placeholder_in_argv_and_stdin() -> None:
|
|
356
|
+
code = (
|
|
357
|
+
"import sys,json;"
|
|
358
|
+
"s = sys.argv[1] + '|' + sys.stdin.read();"
|
|
359
|
+
"print(json.dumps({'r': json.dumps({'cmd': 'a', 'stdin': s})}))"
|
|
360
|
+
)
|
|
361
|
+
provider = CommandProvider("m", argv=_script(code, "{schema}"), reply="r", stdin="in:{schema}")
|
|
362
|
+
resp = _run(provider)
|
|
363
|
+
assert resp.action is not None
|
|
364
|
+
assert resp.action.stdin == f"{_SCHEMA}|in:{_SCHEMA}"
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def test_reply_dict_with_cmd_and_stdin() -> None:
|
|
368
|
+
code = "import json;print(json.dumps({'r': {'cmd': 'ls', 'stdin': 'x'}}))"
|
|
369
|
+
resp = _run(CommandProvider("m", argv=_script(code), reply="r"))
|
|
370
|
+
assert resp.action is not None
|
|
371
|
+
assert (resp.action.cmd, resp.action.stdin) == ("ls", "x")
|
|
372
|
+
assert json.loads(resp.content) == {"cmd": "ls", "stdin": "x"}
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def test_reply_dict_with_cmd_no_stdin() -> None:
|
|
376
|
+
code = "import json;print(json.dumps({'r': {'cmd': 'ls', 'stdin': None}}))"
|
|
377
|
+
resp = _run(CommandProvider("m", argv=_script(code), reply="r"))
|
|
378
|
+
assert resp.action is not None
|
|
379
|
+
assert resp.action.stdin is None
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def test_structured_output_fallback() -> None:
|
|
383
|
+
code = "import json;print(json.dumps({'result': '', 'structured_output': {'cmd': 'pwd'}}))"
|
|
384
|
+
resp = _run(CommandProvider("m", argv=_script(code), reply="missing"))
|
|
385
|
+
assert resp.action is not None
|
|
386
|
+
assert resp.action.cmd == "pwd"
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def test_reply_dict_without_cmd_falls_back() -> None:
|
|
390
|
+
code = "import json;print(json.dumps({'r': {'foo': 1}}))"
|
|
391
|
+
resp = _run(CommandProvider("m", argv=_script(code), reply="r"))
|
|
392
|
+
assert resp.content == ""
|
|
393
|
+
assert resp.action is None
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def test_reply_string_still_works() -> None:
|
|
397
|
+
code = "import json;print(json.dumps({'r': json.dumps({'cmd': 'ls'})}))"
|
|
398
|
+
resp = _run(CommandProvider("m", argv=_script(code), reply="r"))
|
|
399
|
+
assert resp.action is not None
|
|
400
|
+
assert resp.action.cmd == "ls"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Table-driven tests for tool-call dialect extractors."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from tether.providers import base
|
|
9
|
+
from tether.providers.base import ActionCall, extract_fallback_action, register_dialect
|
|
10
|
+
|
|
11
|
+
FIXTURE = Path(__file__).parent.parent / "fixtures" / "dialects.jsonl"
|
|
12
|
+
ROWS = [json.loads(line) for line in FIXTURE.read_text().splitlines() if line.strip()]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.parametrize("row", ROWS, ids=[str(i) for i in range(len(ROWS))])
|
|
16
|
+
def test_dialect_fixture_rows(row: dict) -> None:
|
|
17
|
+
call = extract_fallback_action(row["reply"])
|
|
18
|
+
if row["cmd"] is None:
|
|
19
|
+
assert call is None
|
|
20
|
+
else:
|
|
21
|
+
assert call is not None
|
|
22
|
+
assert call.cmd == row["cmd"]
|
|
23
|
+
assert call.stdin == row["stdin"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_fixture_covers_required_forms() -> None:
|
|
27
|
+
assert len(ROWS) >= 6
|
|
28
|
+
assert any(r["cmd"] is None for r in ROWS)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_empty_text_returns_none() -> None:
|
|
32
|
+
assert extract_fallback_action("") is None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_parameter_tags_empty_cmd_is_none() -> None:
|
|
36
|
+
text = '<parameter name="cmd"> </parameter>'
|
|
37
|
+
assert extract_fallback_action(text) is None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_register_dialect_appends_and_is_used() -> None:
|
|
41
|
+
def custom(text: str) -> ActionCall | None:
|
|
42
|
+
if text.startswith("RUN:"):
|
|
43
|
+
return ActionCall(cmd=text[4:].strip(), stdin=None)
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
before = list(base.DIALECTS)
|
|
47
|
+
try:
|
|
48
|
+
assert register_dialect(custom) is custom
|
|
49
|
+
assert base.DIALECTS[-1] is custom
|
|
50
|
+
call = extract_fallback_action("RUN: ls")
|
|
51
|
+
assert call is not None
|
|
52
|
+
assert call.cmd == "ls"
|
|
53
|
+
finally:
|
|
54
|
+
base.DIALECTS.remove(custom)
|
|
55
|
+
assert base.DIALECTS == before
|
|
56
|
+
assert extract_fallback_action("RUN: ls") is None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_extra_extractors_tried_after_builtins() -> None:
|
|
60
|
+
def custom(text: str) -> ActionCall | None:
|
|
61
|
+
return ActionCall(cmd="extra", stdin=None) if "zzz" in text else None
|
|
62
|
+
|
|
63
|
+
call = extract_fallback_action("zzz", extra=[custom])
|
|
64
|
+
assert call is not None
|
|
65
|
+
assert call.cmd == "extra"
|
|
66
|
+
assert extract_fallback_action("zzz") is None
|
|
67
|
+
builtin = extract_fallback_action('{"cmd": "ls"} zzz', extra=[custom])
|
|
68
|
+
assert builtin is not None
|
|
69
|
+
assert builtin.cmd == "ls"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_tool_call_block_with_non_object_payload_is_ignored() -> None:
|
|
73
|
+
assert extract_fallback_action("<tool_call>[1, 2]</tool_call>") is None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_act_ast_rejects_non_call_expression() -> None:
|
|
77
|
+
from tether.providers.base import _act_from_ast
|
|
78
|
+
|
|
79
|
+
assert _act_from_ast(") or (1") is None
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Configurable context budget and fold window (#54)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from tether import ActionCall, Harness, ModelResponse, register_provider
|
|
12
|
+
from tether.cli import _async_main
|
|
13
|
+
from tether.providers.declarative import load_declared
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _yaml(monkeypatch: pytest.MonkeyPatch, tmp_path: Path, text: str) -> None:
|
|
17
|
+
p = tmp_path / "p.yaml"
|
|
18
|
+
p.write_text(text, encoding="utf-8")
|
|
19
|
+
monkeypatch.setenv("TETHER_PROVIDERS", str(p))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
ENTRY = "decl:\n kind: openai\n base_url: http://x\n api_key_env: K\n"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_precedence(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
26
|
+
db = tmp_path / "t.db"
|
|
27
|
+
monkeypatch.setenv("TETHER_PROVIDERS", str(tmp_path / "none.yaml"))
|
|
28
|
+
h = Harness(model="decl/m", db_path=db)
|
|
29
|
+
assert (h.max_context_tokens, h.fold_observations_after) == (8192, 2)
|
|
30
|
+
_yaml(monkeypatch, tmp_path, ENTRY + " context_tokens: 32000\n fold_after: 8\n")
|
|
31
|
+
h = Harness(model="decl/m", db_path=db)
|
|
32
|
+
assert (h.max_context_tokens, h.fold_observations_after) == (32000, 8)
|
|
33
|
+
h = Harness(model="decl/m", db_path=db, max_context_tokens=100, fold_observations_after=5)
|
|
34
|
+
assert (h.max_context_tokens, h.fold_observations_after) == (100, 5)
|
|
35
|
+
_yaml(monkeypatch, tmp_path, "decl:\n kind: nope\n")
|
|
36
|
+
h = Harness(model="decl/m", db_path=db)
|
|
37
|
+
assert h.max_context_tokens == 8192
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.parametrize("key", ["context_tokens", "fold_after"])
|
|
41
|
+
@pytest.mark.parametrize("bad", ["0", "-1", "true", "'8'", "1.5"])
|
|
42
|
+
def test_validation(monkeypatch: pytest.MonkeyPatch, tmp_path: Path, key: str, bad: str) -> None:
|
|
43
|
+
_yaml(monkeypatch, tmp_path, ENTRY + f" {key}: {bad}\n")
|
|
44
|
+
with pytest.raises(ValueError, match=f"entry `decl` has invalid `{key}` "):
|
|
45
|
+
load_declared()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class _P:
|
|
49
|
+
model_name = "x"
|
|
50
|
+
|
|
51
|
+
async def generate(self, messages: Any, tools: Any) -> ModelResponse:
|
|
52
|
+
return ModelResponse(
|
|
53
|
+
content="",
|
|
54
|
+
action=ActionCall(cmd="done", stdin="ok"),
|
|
55
|
+
prompt_tokens=1,
|
|
56
|
+
completion_tokens=1,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def test_cli_flags(
|
|
61
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
62
|
+
) -> None:
|
|
63
|
+
seen: list[Harness] = []
|
|
64
|
+
|
|
65
|
+
class Rec(Harness):
|
|
66
|
+
def __init__(self, *a: Any, **kw: Any) -> None:
|
|
67
|
+
super().__init__(*a, **kw)
|
|
68
|
+
seen.append(self)
|
|
69
|
+
|
|
70
|
+
monkeypatch.setattr("tether.cli.Harness", Rec)
|
|
71
|
+
register_provider("ctxcfg", lambda name: _P())
|
|
72
|
+
db = str(tmp_path / "t.db")
|
|
73
|
+
flags = ["--context-tokens", "123", "--fold-after", "7", "--db", db, "--model", "ctxcfg/m"]
|
|
74
|
+
assert await _async_main(["run", "--json", *flags, "g"]) == 0
|
|
75
|
+
assert json.loads(capsys.readouterr().out)["status"] == "COMPLETED"
|
|
76
|
+
await _async_main(["pipeline", *flags, str(tmp_path / "missing.yaml"), "g"])
|
|
77
|
+
assert len(seen) == 2
|
|
78
|
+
assert all((h.max_context_tokens, h.fold_observations_after) == (123, 7) for h in seen)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def test_mcp_args(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
82
|
+
import mcp
|
|
83
|
+
|
|
84
|
+
from tether.mcp import build_server
|
|
85
|
+
|
|
86
|
+
seen: list[Harness] = []
|
|
87
|
+
|
|
88
|
+
class Rec(Harness):
|
|
89
|
+
def __init__(self, *a: Any, **kw: Any) -> None:
|
|
90
|
+
super().__init__(*a, **kw)
|
|
91
|
+
seen.append(self)
|
|
92
|
+
|
|
93
|
+
monkeypatch.setattr("tether.mcp.Harness", Rec)
|
|
94
|
+
register_provider("ctxmcp", lambda name: _P())
|
|
95
|
+
async with mcp.Client(build_server(tmp_path / "t.db", tmp_path)) as c:
|
|
96
|
+
res = await c.call_tool(
|
|
97
|
+
"tether_run",
|
|
98
|
+
{"goal": "g", "model": "ctxmcp/m", "context_tokens": 50, "fold_after": 4},
|
|
99
|
+
)
|
|
100
|
+
assert not res.is_error
|
|
101
|
+
assert (seen[0].max_context_tokens, seen[0].fold_observations_after) == (50, 4)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""`tether misses`: unparsed model replies as fixture-ready JSONL (#53)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from tether import ActionCall, ModelResponse, register_provider
|
|
12
|
+
from tether.cli import _async_main
|
|
13
|
+
from tether.store import SQLiteStore
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Prose:
|
|
17
|
+
model_name = "x"
|
|
18
|
+
|
|
19
|
+
def __init__(self, text: str) -> None:
|
|
20
|
+
self.text = text
|
|
21
|
+
self.i = 0
|
|
22
|
+
|
|
23
|
+
async def generate(self, messages: Any, tools: Any) -> ModelResponse:
|
|
24
|
+
self.i += 1
|
|
25
|
+
return ModelResponse(
|
|
26
|
+
content=f"{self.text} #{self.i}", action=None, prompt_tokens=1, completion_tokens=1
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Done:
|
|
31
|
+
model_name = "x"
|
|
32
|
+
|
|
33
|
+
async def generate(self, messages: Any, tools: Any) -> ModelResponse:
|
|
34
|
+
return ModelResponse(
|
|
35
|
+
content="",
|
|
36
|
+
action=ActionCall(cmd="done", stdin="ok"),
|
|
37
|
+
prompt_tokens=1,
|
|
38
|
+
completion_tokens=1,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def _run(capsys: pytest.CaptureFixture[str], argv: list[str]) -> tuple[int, str, str]:
|
|
43
|
+
code = await _async_main(argv)
|
|
44
|
+
cap = capsys.readouterr()
|
|
45
|
+
return code, cap.out, cap.err
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def _session(capsys: pytest.CaptureFixture[str], db: str, prefix: str, provider: Any) -> str:
|
|
49
|
+
register_provider(prefix, lambda name: provider)
|
|
50
|
+
argv = ["run", "--json", "--model", f"{prefix}/m", "--db", db, "--max-steps", "3", "g"]
|
|
51
|
+
_, out, _ = await _run(capsys, argv)
|
|
52
|
+
return str(json.loads(out)["id"])
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def test_misses_outputs_and_filters(
|
|
56
|
+
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
57
|
+
) -> None:
|
|
58
|
+
db = str(tmp_path / "t.db")
|
|
59
|
+
s1 = await _session(capsys, db, "miss-a", Prose("a" * 500))
|
|
60
|
+
s2 = await _session(capsys, db, "miss-b", Prose("b"))
|
|
61
|
+
|
|
62
|
+
code, out, _ = await _run(capsys, ["misses", "--db", db])
|
|
63
|
+
assert code == 0
|
|
64
|
+
assert f"[{s1} step" in out and "miss-a/m]" in out
|
|
65
|
+
assert "a" * 300 in out and "a" * 301 not in out
|
|
66
|
+
|
|
67
|
+
code, out, _ = await _run(capsys, ["misses", "--json", "--db", db])
|
|
68
|
+
rows = [json.loads(line) for line in out.splitlines()]
|
|
69
|
+
assert code == 0 and len(rows) >= 2
|
|
70
|
+
assert {r["session"] for r in rows} == {s1, s2}
|
|
71
|
+
assert all(r["expected"] is None and r["reply"] and "step" in r for r in rows)
|
|
72
|
+
assert any(len(r["reply"]) > 300 for r in rows)
|
|
73
|
+
|
|
74
|
+
_, out, _ = await _run(capsys, ["misses", "--json", "--limit", "1", "--db", db])
|
|
75
|
+
assert len(out.splitlines()) == 1
|
|
76
|
+
|
|
77
|
+
_, out, _ = await _run(capsys, ["misses", "--json", "--model", "miss-b/m", "--db", db])
|
|
78
|
+
assert {json.loads(x)["session"] for x in out.splitlines()} == {s2}
|
|
79
|
+
|
|
80
|
+
_, out, _ = await _run(capsys, ["misses", s1, "--json", "--db", db])
|
|
81
|
+
assert {json.loads(x)["session"] for x in out.splitlines()} == {s1}
|
|
82
|
+
|
|
83
|
+
code, _, err = await _run(capsys, ["misses", "ses_nope", "--db", db])
|
|
84
|
+
assert code == 1 and "session `ses_nope` not found." in err
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def test_misses_empty(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
|
88
|
+
db = str(tmp_path / "t.db")
|
|
89
|
+
await _session(capsys, db, "miss-ok", Done())
|
|
90
|
+
code, out, _ = await _run(capsys, ["misses", "--db", db])
|
|
91
|
+
assert code == 0 and out.strip() == "No unparsed replies."
|
|
92
|
+
code, out, _ = await _run(capsys, ["misses", "--json", "--db", db])
|
|
93
|
+
assert code == 0 and out == ""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def test_store_list_nudges(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
|
97
|
+
db = str(tmp_path / "t.db")
|
|
98
|
+
s1 = await _session(capsys, db, "miss-c", Prose("c"))
|
|
99
|
+
store = SQLiteStore(db_path=db)
|
|
100
|
+
rows = store.list_nudges()
|
|
101
|
+
assert len(rows) >= 2
|
|
102
|
+
assert all(t.event_type == "nudge" and s.id == s1 for s, t in rows)
|
|
103
|
+
ids = [t.id for _, t in rows]
|
|
104
|
+
assert ids == sorted(ids, reverse=True)
|
|
105
|
+
assert len(store.list_nudges(limit=1)) == 1
|
|
106
|
+
assert store.list_nudges(session_id="nope") == []
|
|
107
|
+
assert store.list_nudges(model="other/m") == []
|
|
108
|
+
assert len(store.list_nudges(session_id=s1, model="miss-c/m")) == len(rows)
|
|
@@ -7,6 +7,6 @@ import tether
|
|
|
7
7
|
|
|
8
8
|
def test_version_and_public_exports() -> None:
|
|
9
9
|
"""Verify package __version__ and public SDK symbols."""
|
|
10
|
-
assert tether.__version__ == "0.2.
|
|
10
|
+
assert tether.__version__ == "0.2.2"
|
|
11
11
|
for symbol in tether.__all__:
|
|
12
12
|
assert hasattr(tether, symbol)
|
|
@@ -17,6 +17,7 @@ from tether.providers import (
|
|
|
17
17
|
ModelResponse,
|
|
18
18
|
ProviderResponse,
|
|
19
19
|
extract_fallback_action,
|
|
20
|
+
register_dialect,
|
|
20
21
|
register_provider,
|
|
21
22
|
)
|
|
22
23
|
from tether.sandbox import SandboxPolicy, SandboxViolationError
|
|
@@ -30,7 +31,7 @@ from tether.store import (
|
|
|
30
31
|
ToolOffender,
|
|
31
32
|
)
|
|
32
33
|
|
|
33
|
-
__version__ = "0.2.
|
|
34
|
+
__version__ = "0.2.2"
|
|
34
35
|
|
|
35
36
|
__all__ = [
|
|
36
37
|
"ActionCall",
|
|
@@ -55,6 +56,7 @@ __all__ = [
|
|
|
55
56
|
"extract_fallback_action",
|
|
56
57
|
"load_pipeline",
|
|
57
58
|
"parse_pipeline_yaml",
|
|
59
|
+
"register_dialect",
|
|
58
60
|
"register_provider",
|
|
59
61
|
"tool",
|
|
60
62
|
]
|