tether-sdk 0.2.1__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/AGENTS.md +12 -8
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/PKG-INFO +2 -2
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/README.md +1 -1
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/docs/architecture.md +3 -2
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/docs/reference/agent-interface.md +3 -3
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/docs/reference/context-and-store.md +1 -0
- tether_sdk-0.2.3/docs/reference/multiplexer-and-decorator.md +17 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/docs/reference/providers.md +30 -6
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/docs/release-notes.md +32 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/docs/troubleshooting.md +7 -4
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/docs/tutorial/built-in-tools.md +1 -1
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/docs/tutorial/cli-and-headless.md +16 -1
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/docs/tutorial/skills.md +1 -1
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/docs/tutorial/workflows-and-telemetry.md +3 -3
- tether_sdk-0.2.3/tests/fixtures/dialects.jsonl +6 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/providers/test_anthropic.py +12 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/providers/test_declarative.py +119 -1
- tether_sdk-0.2.3/tests/providers/test_dialects.py +79 -0
- tether_sdk-0.2.3/tests/providers/test_retry.py +160 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/test_cli.py +15 -1
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/test_context.py +109 -0
- tether_sdk-0.2.3/tests/test_context_config.py +101 -0
- tether_sdk-0.2.3/tests/test_edit_tool.py +81 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/test_harness.py +189 -1
- tether_sdk-0.2.3/tests/test_misses.py +108 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/test_multiplexer.py +19 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/test_release_hardening.py +1 -1
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/test_skills.py +32 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/test_store.py +12 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/test_tether.py +1 -1
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tether/__init__.py +3 -1
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tether/api.py +1 -1
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tether/cli.py +67 -3
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tether/context.py +97 -7
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tether/harness.py +114 -12
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tether/mcp.py +4 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tether/multiplexer.py +9 -1
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tether/providers/__init__.py +2 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tether/providers/anthropic.py +9 -1
- tether_sdk-0.2.3/tether/providers/base.py +250 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tether/providers/declarative.py +92 -21
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tether/providers/ollama.py +13 -2
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tether/providers/openai.py +9 -1
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tether/skills.py +9 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tether/store.py +63 -6
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tether/tools/edit.py +28 -3
- tether_sdk-0.2.1/docs/reference/multiplexer-and-decorator.md +0 -15
- tether_sdk-0.2.1/tether/providers/base.py +0 -140
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/.github/workflows/ci.yml +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/.github/workflows/delivery.yml +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/.github/workflows/docs.yml +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/.gitignore +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/LICENSE +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/docs/benchmarks.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/docs/extending.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/docs/index.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/docs/javascripts/mathjax.js +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/docs/reference/harness.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/docs/reference/index.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/docs/reference/sandbox-and-skills.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/docs/reference/tools.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/docs/releasing.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/docs/tutorial/agent-interface.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/docs/tutorial/index.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/docs/tutorial/multi-agent.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/docs/tutorial/sdk-and-custom-tools.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/docs/tutorial/security-and-sandbox.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/examples/course/README.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/examples/course/checks.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/examples/course/course.yaml +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/examples/course/ruff.toml +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/examples/course/skills/planner/SKILL.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/examples/course/skills/searcher/SKILL.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/examples/course/skills/searcher/tools.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/examples/course/skills/writer/SKILL.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/examples/course/skills/writer/tools.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/mkdocs.yml +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/pyproject.toml +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/skills/researcher/SKILL.md +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/__init__.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/conftest.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/providers/__init__.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/providers/test_base.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/providers/test_ollama.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/providers/test_openai.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/test_agent_interface.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/test_decorator.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/test_integration.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/test_pipeline.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/test_retro.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/test_sandbox.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/test_security_hardening.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/test_structured_history.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/tools/__init__.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/tools/test_agent.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/tools/test_bash.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/tools/test_done.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/tools/test_edit.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/tools/test_mem.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tests/tools/test_web.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tether/decorator.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tether/pipeline.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tether/retro.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tether/sandbox.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tether/schema.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tether/tools/__init__.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tether/tools/agent.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tether/tools/bash.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tether/tools/done.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tether/tools/mem.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/tether/tools/web.py +0 -0
- {tether_sdk-0.2.1 → tether_sdk-0.2.3}/uv.lock +0 -0
|
@@ -33,13 +33,14 @@ Tether implements the architectural principles from *"LLM-Native Applications as
|
|
|
33
33
|
- 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).
|
|
34
34
|
- 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`.
|
|
35
35
|
- 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`.
|
|
36
|
-
- 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).
|
|
36
|
+
- 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.
|
|
37
37
|
|
|
38
38
|
### Rule 4: Progressive `--help` (JIT Schema Disclosure)
|
|
39
39
|
- The system prompt lists at most **1 line per enabled tool namespace** (`<name> : <short summary>`, target **≤ 12 tokens per entry**).
|
|
40
40
|
- Full parameter signatures, defaults, and docstrings are revealed **Just-In-Time (JIT)** when:
|
|
41
41
|
1. The model explicitly runs `<cmd> --help`, OR
|
|
42
42
|
2. The model invokes `<cmd>` with missing/invalid arguments (self-healing error response, never an unhandled Python exception).
|
|
43
|
+
3. The model invokes a built-in tool that is not enabled: the error says it is disabled, lists the enabled tools and how to enable more (error responses are history, never prefix).
|
|
43
44
|
- Auto-generated `--help` output must remain ultra-compact (**≤ 60 tokens** per command).
|
|
44
45
|
|
|
45
46
|
### Rule 5: Ephemeral Observation Folding
|
|
@@ -48,6 +49,7 @@ Tether implements the architectural principles from *"LLM-Native Applications as
|
|
|
48
49
|
```text
|
|
49
50
|
[obs_3 folded: `web fetch https://...` (3,420 chars) | use `mem recall obs_3` to expand]
|
|
50
51
|
```
|
|
52
|
+
- Exception: the latest read of each of the last 4 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 2,400-char head+tail elision still applies, and the hard context budget remains #81. The `mem recall` hint is shown only when `mem` is enabled (else "re-run the command").
|
|
51
53
|
- 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.
|
|
52
54
|
|
|
53
55
|
### Rule 6: Deterministic Security Envelope (Never Trust the LLM for Safety)
|
|
@@ -70,7 +72,7 @@ Tether implements the architectural principles from *"LLM-Native Applications as
|
|
|
70
72
|
```text
|
|
71
73
|
tether/
|
|
72
74
|
├── __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)
|
|
75
|
+
├── cli.py # Headless-capable CLI: `tether run|pipeline|status|read|query|send|list|wait|stats|retro|events|misses|mcp` (`--json` + documented exit codes)
|
|
74
76
|
├── harness.py # Control Plane: bounded agent loop, pipelines, SQLite inbox, worker spawn
|
|
75
77
|
├── pipeline.py # Workflow Plane: declarative multi-stage YAML pipelines & model routing
|
|
76
78
|
├── api.py # Agent-facing JSON session view, exit codes, events (`--json`)
|
|
@@ -82,7 +84,7 @@ tether/
|
|
|
82
84
|
├── multiplexer.py # Action Plane: `act(cmd, stdin)` parser, command registry, JIT --help
|
|
83
85
|
├── decorator.py # `@tool` introspection: converts Python type hints + docstrings to CLI specs
|
|
84
86
|
├── context.py # State Plane: ContextAssembler, EphemeralFolder, token estimator
|
|
85
|
-
├── skills.py # Skill loader: Markdown + YAML frontmatter + optional sibling `tools.py`
|
|
87
|
+
├── skills.py # Skill loader: Markdown + YAML frontmatter + optional sibling `tools.py` (its tools must be listed in `tools:`)
|
|
86
88
|
├── providers/
|
|
87
89
|
│ ├── base.py # Abstract async ModelProvider protocol (`generate(messages, tools)`)
|
|
88
90
|
│ ├── ollama.py # Native Ollama `/api/chat` httpx adapter (`num_ctx`, `keep_alive`)
|
|
@@ -103,15 +105,15 @@ tether/
|
|
|
103
105
|
- Default DB path: `.tether/tether.db` (configurable via `TETHER_DB` or `Harness(db_path=...)`) with `PRAGMA journal_mode=WAL;`.
|
|
104
106
|
- Tables:
|
|
105
107
|
- `sessions`: `id`, `parent_id`, `status` (`PENDING`, `RUNNING`, `WAITING_INPUT`, `COMPLETED`, `FAILED`), `model`, `skill`, `tools_json`, `sandbox_json`, `goal`, `result`, `step_count`, `max_steps`, `peak_tokens`, `total_prompt_tokens`, `total_completion_tokens`, `pid`, `created_at`, `updated_at`.
|
|
106
|
-
- `turns`: `id`, `session_id`, `step`, `role`, `content`, `tool_cmd`, `tool_stdin`, `obs_id`, `prompt_tokens`, `completion_tokens`, `duration_ms`, `event_type` (`ok` | `nudge` | `loop_stall` | `syntax_error` | `tool_error` | `gate_fail` | `completed`), `created_at`.
|
|
108
|
+
- `turns`: `id`, `session_id`, `step`, `role`, `content`, `tool_cmd`, `tool_stdin`, `obs_id`, `prompt_tokens`, `completion_tokens`, `duration_ms`, `event_type` (`ok` | `nudge` | `loop_stall` | `cycle_stall` | `syntax_error` | `tool_error` | `gate_fail` | `gate_error` | `completed`), `created_at`.
|
|
107
109
|
- `observations`: `id`, `session_id`, `step`, `cmd`, `raw_output`, `summary`, `char_len`.
|
|
108
110
|
- `chunks_fts` *(SQLite FTS5 Virtual Table)*: `session_id`, `source_type` (`result` | `observation` | `pin`), `source_id`, `chunk_index`, `content` — automatically indexes ~150-token paragraph/heading chunks of all observations, pins, and final `done` deliverables for sub-millisecond BM25 RAG (including cross-session project search via `mem search --project <query>`).
|
|
109
111
|
- `pins`: `session_id`, `key`, `value` (supports session-scoped working memory and workspace-wide semantic facts under `GLOBAL_SESSION_ID = "__project__"` via `mem pin --project <key> <val>`).
|
|
110
112
|
- `inbox`: `id`, `session_id`, `message`, `consumed` (`0` or `1`), `created_at`.
|
|
111
113
|
- **Agent-to-Agent Interface (`api.py`, `mcp.py`):** `--json` outputs (schema `v: 1`), exit codes `0` completed / `1` usage / `2` waiting / `3` gate / `4` max steps / `5` worker error, failure text on stderr, `--result-file`, `--output-schema`, `tether events`. `tether mcp` serves the same JSON over stdio and requires the optional `tether-sdk[mcp]` extra (official `mcp` SDK, imported lazily so core stays dependency-light; Rule 7 allows it as an extra only); its sandbox is always rooted at `--workspace` and callers can only narrow it. Details: `docs/reference/agent-interface.md`.
|
|
112
114
|
- **Telemetry & Failure-Mode Inspector (`tether stats`):**
|
|
113
|
-
- `tether stats [--model <m>] [--skill <s>] [--limit <n>]` aggregates per-model pass rates, cumulative token economy (`total_prompt_tokens + total_completion_tokens`), step latency, and self-healing ACI friction (`nudge`, `loop_stall`, `syntax_error`, `tool_error`, `gate_fail`), plus a top-offenders breakdown by tool command.
|
|
114
|
-
- **Post-Mortem Distillation (`tether retro <session_id> [--apply] [--model <m>]`):** offline only (never mutates a live prompt). `store.extract_friction_trace` pulls `inbox` corrections and `loop_stall`/`syntax_error`/`tool_error`/`gate_fail` turns (+ the fixing command); one bounded LLM pass yields ≤2 rules (≤30 tokens each, deduped against existing rules). Prints a unified diff of `SKILL.md`; `--apply` appends under `## Learned Rules` (or pins to project memory when no skill was used).
|
|
115
|
+
- `tether stats [--model <m>] [--skill <s>] [--limit <n>]` aggregates per-model pass rates, cumulative token economy (`total_prompt_tokens + total_completion_tokens`), step latency, and self-healing ACI friction (`nudge`, `loop_stall`, `cycle_stall`, `syntax_error`, `tool_error`, `gate_fail`, `gate_error`), plus a top-offenders breakdown by tool command.
|
|
116
|
+
- **Post-Mortem Distillation (`tether retro <session_id> [--apply] [--model <m>]`):** offline only (never mutates a live prompt). `store.extract_friction_trace` pulls `inbox` corrections and `loop_stall`/`cycle_stall`/`syntax_error`/`tool_error`/`gate_fail`/`gate_error` turns (+ the fixing command); one bounded LLM pass yields ≤2 rules (≤30 tokens each, deduped against existing rules). Prints a unified diff of `SKILL.md`; `--apply` appends under `## Learned Rules` (or pins to project memory when no skill was used).
|
|
115
117
|
- **Headless Worker & Live `send` Contract:**
|
|
116
118
|
- `tether run --headless ...` creates the session row in SQLite and spawns `python -m tether.cli _worker <session_id>` via `subprocess.Popen(..., start_new_session=True)`, printing `<session_id>` immediately to stdout.
|
|
117
119
|
- Before each step, the running worker queries `inbox WHERE session_id = ? AND consumed = 0` and injects any new user/parent messages into the conversation.
|
|
@@ -123,7 +125,7 @@ tether/
|
|
|
123
125
|
- `agent status <id>` $\rightarrow$ returns 1-line status, step count, and last command.
|
|
124
126
|
- `agent wait <id1> [id2 ...]` $\rightarrow$ asynchronously polls SQLite until the specified child session(s) finish and returns a **compact ~50-token digest** (status, total chars/chunks, 2-line preview) so the parent's context window is never flooded.
|
|
125
127
|
- `agent query <id|--all> <query> [--top 3]` $\rightarrow$ queries `chunks_fts` (BM25 ranking) across the sub-agent's final deliverable and raw observations, returning only the top-K relevant chunks (~200–300 tokens).
|
|
126
|
-
- `agent read <id> [--full]` $\rightarrow$ reads the child session's digest or full `result`.
|
|
128
|
+
- `agent read <id> [--full]` $\rightarrow$ reads the child session's digest or full `result`. The CLI `tether read <id>` prints the full result by default (`--full` accepted for parity; `--digest` for the digest).
|
|
127
129
|
- `agent send <id> <message>` $\rightarrow$ queues a message into the child session's inbox (or resumes it if completed).
|
|
128
130
|
- **Deterministic Multi-Stage Pipelines (`tether pipeline <pipeline.yaml> "<goal>"` & `Harness.run_pipeline`):**
|
|
129
131
|
- Each stage runs in its own isolated `SessionRecord` (`parent_id = pipeline_session_id`) with heterogeneous model routing (`stage.model`), stage-level `verify` gates, and strict sandbox monotonicity (`root_sandbox.attenuate(stage.sandbox)`).
|
|
@@ -142,7 +144,7 @@ tether/
|
|
|
142
144
|
- pytest -q
|
|
143
145
|
---
|
|
144
146
|
```
|
|
145
|
-
- If `skills/<name>/tools.py` exists, any `@tool`-decorated functions inside it are loaded with the skill and registered into the session's multiplexer when their name is also listed in the skill's `tools:` (otherwise: `unknown command`).
|
|
147
|
+
- If `skills/<name>/tools.py` exists, any `@tool`-decorated functions inside it are loaded with the skill and registered into the session's multiplexer when their name is also listed in the skill's `tools:` (otherwise: `unknown command`). A skill's own custom tools are opt-in like any other, so they must be listed in `tools:`; loading a skill whose `tools.py` defines an unlisted `@tool` emits a `UserWarning` (0 prompt tokens).
|
|
146
148
|
|
|
147
149
|
#### D. 3 Thin `httpx` Model Providers & SDK Provider Routing (`providers/`)
|
|
148
150
|
- Model identifier format: `<provider>/<model_name>` (e.g., `ollama/qwen3.5:9b`, `openai/gpt-4o-mini`, `anthropic/claude-3-7-sonnet-latest`).
|
|
@@ -155,6 +157,8 @@ tether/
|
|
|
155
157
|
- `ANTHROPIC_API_KEY`, `ANTHROPIC_BASE_URL` (default `https://api.anthropic.com`)
|
|
156
158
|
|
|
157
159
|
---
|
|
160
|
+
- **Timeout/retry (#70):** `providers.yaml` `timeout` applies to every kind; HTTP providers retry timeouts/network errors/429/5xx twice (`post_with_retry` in `providers/base.py`) then raise `ProviderError` naming provider, model and timeout.
|
|
161
|
+
- **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
162
|
|
|
159
163
|
## 4. Development Workflow & Commands
|
|
160
164
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tether-sdk
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
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
|
|
@@ -109,7 +109,7 @@ Every Tether execution is a stateful session persisted in `.tether/tether.db` (S
|
|
|
109
109
|
- If **`WAITING_INPUT` / `COMPLETED` / `IDLE`**: appends the message and automatically spawns a worker to resume the session with its pinned memory and folded context intact.
|
|
110
110
|
|
|
111
111
|
### 3. Progressive Skills (`skills/`)
|
|
112
|
-
Skills are portable capability bundles stored in `./skills/<name>/SKILL.md` (or `./skills/<name>.md`) with YAML frontmatter (including optional `verify` gates) and an optional sibling `tools.py
|
|
112
|
+
Skills are portable capability bundles stored in `./skills/<name>/SKILL.md` (or `./skills/<name>.md`) with YAML frontmatter (including optional `verify` gates) and an optional sibling `tools.py` (every `@tool` in it must also be listed in `tools:`, otherwise it is unavailable and loading the skill emits a `UserWarning`):
|
|
113
113
|
```markdown
|
|
114
114
|
---
|
|
115
115
|
name: researcher
|
|
@@ -88,7 +88,7 @@ Every Tether execution is a stateful session persisted in `.tether/tether.db` (S
|
|
|
88
88
|
- If **`WAITING_INPUT` / `COMPLETED` / `IDLE`**: appends the message and automatically spawns a worker to resume the session with its pinned memory and folded context intact.
|
|
89
89
|
|
|
90
90
|
### 3. Progressive Skills (`skills/`)
|
|
91
|
-
Skills are portable capability bundles stored in `./skills/<name>/SKILL.md` (or `./skills/<name>.md`) with YAML frontmatter (including optional `verify` gates) and an optional sibling `tools.py
|
|
91
|
+
Skills are portable capability bundles stored in `./skills/<name>/SKILL.md` (or `./skills/<name>.md`) with YAML frontmatter (including optional `verify` gates) and an optional sibling `tools.py` (every `@tool` in it must also be listed in `tools:`, otherwise it is unavailable and loading the skill emits a `UserWarning`):
|
|
92
92
|
```markdown
|
|
93
93
|
---
|
|
94
94
|
name: researcher
|
|
@@ -58,6 +58,7 @@ Tether implements a deterministic three-tier memory hierarchy backed by `.tether
|
|
|
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 4 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 2,400-char head+tail elision still applies, and the hard context budget remains #81. The `mem recall` hint is shown only when `mem` is enabled (else "re-run the command").
|
|
61
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`.
|
|
62
63
|
|
|
63
64
|
---
|
|
@@ -74,8 +75,8 @@ Local inference engines (`Ollama`, `llama.cpp`, `vLLM`) and cloud providers (`An
|
|
|
74
75
|
### Deterministic Loop-Stall Breaker & Pre-`done` Verification Gates
|
|
75
76
|
Small 8B/9B models occasionally fall into a probability trap where they emit the exact same command (`web search "..."` or `bash ...`) twice in a row, or prematurely call `done` while tests or linters are failing:
|
|
76
77
|
|
|
77
|
-
- **Loop-Stall Breaker:** `Harness.execute_session` tracks the `(cmd, stdin)` signature of the previous step and intercepts consecutive duplicates before executing the tool, returning a `[LOOP DETECTED]` nudge that forces the model to advance.
|
|
78
|
-
- **Pre-`done` Verification Gates:** When configured via `Harness(verify=[...])`, CLI `--verify`, or Skill YAML `verify: [...]`, Tether intercepts `done` and runs each verification command inside the session's `SandboxPolicy`. It consumes **0 static prompt tokens** unless a gate exits non-zero—at which point termination is rejected and a compact `[VERIFICATION GATE FAILED]` observation is injected so the model can self-heal.
|
|
78
|
+
- **Loop-Stall Breaker:** `Harness.execute_session` tracks the `(cmd, stdin)` signature of the previous step and intercepts consecutive duplicates before executing the tool, returning a `[LOOP DETECTED]` nudge that forces the model to advance. A companion cycle detector (`detect_cycle`) catches a 2-command cycle repeated 3 times or a 3-command cycle repeated twice (6 actions, within the last 8), skips the repeated command, nudges with `[CYCLE DETECTED]` naming the commands, and records `cycle_stall`.
|
|
79
|
+
- **Pre-`done` Verification Gates:** When configured via `Harness(verify=[...])`, CLI `--verify`, or Skill YAML `verify: [...]`, Tether intercepts `done` and runs each verification command inside the session's `SandboxPolicy`. It consumes **0 static prompt tokens** unless a gate exits non-zero—at which point termination is rejected and a compact `[VERIFICATION GATE FAILED]` observation is injected so the model can self-heal. A gate that errors rather than fails (sandbox violation, exit 124/126/127) ends the session `FAILED` immediately as `gate_error` instead of consuming retries.
|
|
79
80
|
|
|
80
81
|
### Connection Pooling & Thread-Local SQLite WAL Reuse
|
|
81
82
|
- Model providers reuse persistent `httpx.AsyncClient` connection pools across all turns in a session, eliminating repeated TCP/TLS handshakes.
|
|
@@ -15,7 +15,7 @@ Machine-readable surface for calling Tether from another agent. Setup per harnes
|
|
|
15
15
|
- `failure` is `{"kind": "gate|max_steps|worker_error", "gate": "<cmd>|output-schema|null", "message": "…"}`.
|
|
16
16
|
- Keys are stable; new keys may be added.
|
|
17
17
|
|
|
18
|
-
`--json` is available on `run`, `pipeline`, `status`, `list`, `read` (also `--digest`, `--steps`), `wait`, `send` and `stats`. With `--headless` it prints `{"v":1,"id":"ses_…"}`.
|
|
18
|
+
`--json` is available on `run`, `pipeline`, `status`, `list`, `read` (also `--digest`, `--steps`, `--full`), `wait`, `send` and `stats`. With `--headless` it prints `{"v":1,"id":"ses_…"}`.
|
|
19
19
|
|
|
20
20
|
## Exit codes
|
|
21
21
|
|
|
@@ -24,7 +24,7 @@ Machine-readable surface for calling Tether from another agent. Setup per harnes
|
|
|
24
24
|
| 0 | completed |
|
|
25
25
|
| 1 | usage error (bad flag, unknown session, invalid pipeline) |
|
|
26
26
|
| 2 | waiting for input (`done --ask`) |
|
|
27
|
-
| 3 | verification gate failed (including `--output-schema`) |
|
|
27
|
+
| 3 | verification gate failed or misconfigured (including `--output-schema`) |
|
|
28
28
|
| 4 | max steps reached |
|
|
29
29
|
| 5 | worker or provider error |
|
|
30
30
|
|
|
@@ -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` |
|
|
@@ -22,6 +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 4 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 2,400-char head+tail elision still applies, 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`).
|
|
25
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.
|
|
26
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.
|
|
27
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`.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Multiplexer and Decorator
|
|
2
|
+
|
|
3
|
+
## `tether.multiplexer`
|
|
4
|
+
|
|
5
|
+
A command that is not enabled returns a self-healing error (`unknown_cmd`, no exception) listing the enabled tools and how to enable more (`--tools`, SDK `tools=[...]`, skill `tools:`). A known built-in that is merely disabled (`bash`, `web`, `edit`, `mem`, `skill`, `agent`) is reported as disabled, not unknown. The text lives in the error response (history), never in the system prompt prefix.
|
|
6
|
+
|
|
7
|
+
::: tether.multiplexer
|
|
8
|
+
options:
|
|
9
|
+
show_root_heading: false
|
|
10
|
+
members_order: source
|
|
11
|
+
|
|
12
|
+
## `tether.decorator`
|
|
13
|
+
|
|
14
|
+
::: tether.decorator
|
|
15
|
+
options:
|
|
16
|
+
show_root_heading: false
|
|
17
|
+
members_order: source
|
|
@@ -34,32 +34,56 @@ 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
|
+
|
|
49
|
+
### Timeout and retry
|
|
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).
|
|
52
|
+
|
|
43
53
|
### Kinds
|
|
44
54
|
|
|
45
55
|
| `kind` | Fields |
|
|
46
56
|
|---|---|
|
|
47
|
-
| `openai` | `base_url`, `api_key_env` |
|
|
48
|
-
| `ollama` | `host` |
|
|
49
|
-
| `anthropic` | `base_url`, `api_key_env` |
|
|
57
|
+
| `openai` | `base_url`, `api_key_env`, optional `timeout` |
|
|
58
|
+
| `ollama` | `host`, optional `timeout` |
|
|
59
|
+
| `anthropic` | `base_url`, `api_key_env`, optional `timeout` |
|
|
50
60
|
| `command` | `argv` (list), `reply` (JSON path), optional `stdin` template, optional `usage`, optional `timeout` (seconds, default 300) |
|
|
51
61
|
|
|
52
|
-
`api_key_env` is the **name** of an environment variable, never the key itself.
|
|
62
|
+
`api_key_env` is the **name** of an environment variable, never the key itself. A declared `api_key_env` must be set and non-empty (otherwise the session fails before any request); omit it to declare a keyless local server. A 403 reads `access denied (HTTP 403)` with the first 120 characters of the response body instead of "missing or rejected".
|
|
53
63
|
|
|
54
64
|
### The `command` kind
|
|
55
65
|
|
|
56
66
|
A `command` entry wraps any program that takes a prompt and prints JSON.
|
|
57
67
|
|
|
58
|
-
- **Placeholders**: `{model}`, `{system}` and `{
|
|
68
|
+
- **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
69
|
- **No shell**: `argv` is executed directly, never through a shell.
|
|
60
70
|
- **Reply**: `reply` is a dotted path into the JSON the command prints (`result` in the example).
|
|
61
71
|
- **Usage**: `usage.prompt` is a path or a list of paths (their values are summed); `usage.completion` is a path. Both are optional.
|
|
62
|
-
- **
|
|
72
|
+
- **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.
|
|
73
|
+
|
|
74
|
+
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.
|
|
75
|
+
- **Protocol**: the model should answer with a single JSON `{"cmd", "stdin"}` object. The `{system}` placeholder already contains that instruction.
|
|
76
|
+
- **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(...)`.
|
|
77
|
+
- **`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.
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
mycli:
|
|
81
|
+
kind: command
|
|
82
|
+
argv: [mycli, "{conversation}"]
|
|
83
|
+
reply: result
|
|
84
|
+
dialects:
|
|
85
|
+
- regex: 'RUN\[(?P<cmd>.*?)\]WITH\[(?P<stdin>.*?)\]'
|
|
86
|
+
```
|
|
63
87
|
|
|
64
88
|
### Rules
|
|
65
89
|
|
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Release Notes
|
|
2
2
|
|
|
3
|
+
## v0.2.3 (2026-10-20)
|
|
4
|
+
|
|
5
|
+
Dogfooding fixes: the issues came from running Tether with a small model (GLM-5.3) on its own backlog.
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- `tether read` now accepts `--full` (the full final result, already its default) so the `agent read <id> --full` form works on the CLI too; `--digest` still gives the compact card (#72).
|
|
10
|
+
|
|
11
|
+
### Fixes
|
|
12
|
+
|
|
13
|
+
- Verification gate errors (sandbox violation, exit 124/126/127) are now told apart from gate failures: the session fails immediately with a `Verification gate ... misconfigured: <reason>` result (exit code 3) instead of burning the 3-failure abort budget, and `tether stats` shows a `GATE_ERR` column (#33).
|
|
14
|
+
- `edit write` over an existing file now returns a deterministic summary (`Replaced N lines with M (X removed, Y added; first removed: ...)`) and, when the file shrank, points to `edit replace --lines a:b` (#40).
|
|
15
|
+
- Skills now warn (`UserWarning`, 0 prompt tokens) at load when `tools.py` defines a `@tool` not listed in the skill `tools:` (#24).
|
|
16
|
+
- Repeated multi-command cycles (e.g. `cat` / `sed` / `pytest`, or two alternating `sed -n` ranges) now trigger a deterministic `[CYCLE DETECTED]` nudge and a new `cycle_stall` event type, shown in `tether stats` as `CYCLES` (#39).
|
|
17
|
+
- HTTP providers now retry timeouts, network errors and 429/5xx twice with backoff, honour `timeout` from `providers.yaml` for all kinds, and fail with a `ProviderError` naming provider, model and timeout instead of a bare `ReadTimeout` (#70).
|
|
18
|
+
- Observation folding no longer makes small models re-read files in a loop: the latest read (`edit view <p>` / `bash cat|head|tail|sed -n <p>`) of each of the last 4 files stays unfolded until that path is edited (`edit write|replace`, or a bash write such as `sed -i`/`tee`) or re-read, and the fold/elision hint only mentions `mem recall` when `mem` is enabled (otherwise "re-run the command"). Static prompt prefix unchanged (#69).
|
|
19
|
+
- Clearer errors (#71): a provider 401/403 now fails with `ProviderError` naming the provider and the `api_key_env` / default key variable as missing or rejected (never its value), a declared `api_key_env` that is empty fails before the first request, and `unknown command` for a built-in tool that is not enabled says it is disabled and how to enable it (`--tools`, `tools=[...]`, skill `tools:`). Static prompt prefix unchanged.
|
|
20
|
+
|
|
21
|
+
## v0.2.2 (2026-10-19)
|
|
22
|
+
|
|
23
|
+
Dogfooding fixes: Tether implemented most of this release itself through the CLI and a `claude -p` provider.
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
- `{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`.
|
|
28
|
+
- `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`.
|
|
29
|
+
|
|
30
|
+
### Fixes
|
|
31
|
+
|
|
32
|
+
- 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`.
|
|
33
|
+
- 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`.
|
|
34
|
+
|
|
3
35
|
## v0.2.1 (2026-10-19)
|
|
4
36
|
|
|
5
37
|
Add model backends without code.
|
|
@@ -6,15 +6,18 @@ Symptom, cause, fix. Most entries come from building the [course-builder example
|
|
|
6
6
|
|---|---|---|
|
|
7
7
|
| Model keeps running `edit view` on the same file, then writes invented data | Old observations fold after 2 turns ([#27](https://github.com/igormcsouza/tether/issues/27)) | Give it one compact tool view right before the write, ending with the exact next call. See [Context and SQLite Store](reference/context-and-store.md#observation-folding-and-small-models) |
|
|
8
8
|
| A long `edit write` is missing a whole section | One big generation drops content | Write one small file per section (`part-N.md`) and join them with a tool. [Same page](reference/context-and-store.md#observation-folding-and-small-models) |
|
|
9
|
-
| Earlier file content vanished after a second `edit write` | `edit write` replaces the whole file ([#34](https://github.com/igormcsouza/tether/issues/34)) | Tell the model to rewrite the complete file, or use `edit replace --lines`. See [Built-in Tools](tutorial/built-in-tools.md#example-course-builder) |
|
|
9
|
+
| Earlier file content vanished after a second `edit write` | `edit write` replaces the whole file ([#34](https://github.com/igormcsouza/tether/issues/34)) | `edit write` now reports what an overwrite removed (#40). Tell the model to rewrite the complete file, or use `edit replace --lines`. See [Built-in Tools](tutorial/built-in-tools.md#example-course-builder) |
|
|
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
|
-
| `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
|
|
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:` (loading the skill emits a `UserWarning` naming unlisted tools). See [Skills](tutorial/skills.md#tools-py-and-the-tools-list) |
|
|
13
|
+
| `Worker error: ProviderError: ... `NVIDIA_API_KEY` is missing or rejected` | The provider answered 401/403, or the `api_key_env` variable is unset/empty ([#71](https://github.com/igormcsouza/tether/issues/71)) | Export a valid key under that variable name (Tether prints the name, never the value). An empty `api_key_env` variable fails the session before any request |
|
|
14
|
+
| `Error: \`bash\` is disabled` / `unknown command` | The tool is not enabled for this session; the message lists the enabled tools | Add it with `--tools bash,edit`, SDK `tools=[...]` or the skill `tools:` list ([#71](https://github.com/igormcsouza/tether/issues/71)) |
|
|
15
|
+
| 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
16
|
| 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
17
|
| `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
18
|
| `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) |
|
|
16
19
|
| Gate exits 0 although work is missing | Missing input file means nothing to check | Make the gate fail closed. See [Gates](tutorial/workflows-and-telemetry.md#gates) |
|
|
17
|
-
| Session `FAILED`
|
|
20
|
+
| Session `FAILED` with `Verification gate ... misconfigured` | The gate itself errored (sandbox violation, command not found, timeout), not your work; it fails fast and shows as `gate_error` in `tether stats` | Fix the gate command and run it by hand first |
|
|
18
21
|
| Model repeats a tool call with a rejected value | Gate or tool message says what failed, not what to do | Return the valid options and the next call in the message. See [Custom Tools](tutorial/sdk-and-custom-tools.md#example-course-builder) |
|
|
19
22
|
| A pipe-delimited file gets extra or shifted columns | A delimiter (` | `) appeared inside a value | Sanitize values in the tool that writes the file |
|
|
20
23
|
| Pipeline stage ends "Max steps reached" | `max_steps` too small for the work ([#35](https://github.com/igormcsouza/tether/issues/35)) | Raise `max_steps` (pipeline or stage), or re-run the stage's skill by hand. See [Pipelines](tutorial/workflows-and-telemetry.md#pipelines) |
|
|
@@ -22,7 +25,7 @@ Symptom, cause, fix. Most entries come from building the [course-builder example
|
|
|
22
25
|
|
|
23
26
|
## Known limitations
|
|
24
27
|
|
|
25
|
-
- [#24](https://github.com/igormcsouza/tether/issues/24) skill `tools.py` tools must also be listed in `tools
|
|
28
|
+
- [#24](https://github.com/igormcsouza/tether/issues/24) skill `tools.py` tools must also be listed in `tools:` (a `UserWarning` is emitted at load otherwise).
|
|
26
29
|
- [#27](https://github.com/igormcsouza/tether/issues/27) observation folding hurts small models.
|
|
27
30
|
- [#32](https://github.com/igormcsouza/tether/issues/32) model reasoning is not in the trace.
|
|
28
31
|
- [#33](https://github.com/igormcsouza/tether/issues/33) gate infrastructure errors count toward the 3-failure abort.
|
|
@@ -82,7 +82,7 @@ act(cmd="mem recall obs_3")
|
|
|
82
82
|
|
|
83
83
|
The [course-builder example](https://github.com/igormcsouza/tether/tree/main/examples/course) uses `edit` and custom tools to turn a plan into a course. Three limits shaped it:
|
|
84
84
|
|
|
85
|
-
- **`edit write` replaces the whole file.** Small models assume it appends and silently lose earlier content. The planner skill says so explicitly: "write the COMPLETE plan in ONE call". (An `edit append` is proposed in [#34](https://github.com/igormcsouza/tether/issues/34).)
|
|
85
|
+
- **`edit write` replaces the whole file.** Overwriting an existing file now reports `Replaced N lines with M (X removed, Y added; first removed: ...)` and, when the file shrank, points to `edit replace --lines a:b` ([#40](https://github.com/igormcsouza/tether/issues/40)). Small models assume it appends and silently lose earlier content. The planner skill says so explicitly: "write the COMPLETE plan in ONE call". (An `edit append` is proposed in [#34](https://github.com/igormcsouza/tether/issues/34).)
|
|
86
86
|
- **`web search` cannot reliably find a specific YouTube video.** Use a [custom tool](sdk-and-custom-tools.md#pattern-numbered-candidates) that returns numbered candidates instead.
|
|
87
87
|
- **Tools are opt-in.** The searcher skill lists `tools: [yt-todo, yt-candidates, yt-save]` and nothing else; `edit` is only loaded by the stages that write files.
|
|
88
88
|
|
|
@@ -63,9 +63,12 @@ tether status "$SESSION_ID"
|
|
|
63
63
|
# Block until the session completes and print a compact ~50-token digest
|
|
64
64
|
tether wait "$SESSION_ID"
|
|
65
65
|
|
|
66
|
-
# Read the full final deliverable
|
|
66
|
+
# Read the full final deliverable (`--full` is optional: it is the default)
|
|
67
67
|
tether read "$SESSION_ID"
|
|
68
68
|
|
|
69
|
+
# Or just the compact digest
|
|
70
|
+
tether read "$SESSION_ID" --digest
|
|
71
|
+
|
|
69
72
|
# Inspect the step-by-step action log
|
|
70
73
|
tether read "$SESSION_ID" --steps
|
|
71
74
|
|
|
@@ -76,10 +79,22 @@ tether query "$SESSION_ID" "write-ahead log checkpointing" --top 3
|
|
|
76
79
|
tether list
|
|
77
80
|
```
|
|
78
81
|
|
|
82
|
+
## Context budget and fold window
|
|
83
|
+
|
|
84
|
+
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.
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
tether run --context-tokens 32000 --fold-after 8 "refactor the parser"
|
|
88
|
+
```
|
|
89
|
+
|
|
79
90
|
## Scripting: `--json` and exit codes
|
|
80
91
|
|
|
81
92
|
`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
93
|
|
|
94
|
+
### When a model's replies are not understood
|
|
95
|
+
|
|
96
|
+
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`.
|
|
97
|
+
|
|
83
98
|
## Live Inbox Injection & Auto-Resume (`tether send`)
|
|
84
99
|
|
|
85
100
|
Every session has a persistent SQLite `inbox` queue:
|
|
@@ -91,7 +91,7 @@ verify:
|
|
|
91
91
|
|
|
92
92
|
### <a id="tools-py-and-the-tools-list"></a>`tools.py` and the `tools:` list
|
|
93
93
|
|
|
94
|
-
Defining a tool in `tools.py` is not enough: name it in the skill `tools:` as well, or the model gets `unknown command` ([#24](https://github.com/igormcsouza/tether/issues/24)). See [Python SDK and Custom Tools](sdk-and-custom-tools.md).
|
|
94
|
+
Defining a tool in `tools.py` is not enough: name it in the skill `tools:` as well, or the model gets `unknown command` ([#24](https://github.com/igormcsouza/tether/issues/24)). A skill's own custom tools are opt-in like built-in ones, so list them in `tools:`. Loading a skill whose `tools.py` defines an unlisted `@tool` emits a `UserWarning` naming it (costs no prompt tokens). See [Python SDK and Custom Tools](sdk-and-custom-tools.md).
|
|
95
95
|
|
|
96
96
|
### Let the model decide, let code enforce hard requirements
|
|
97
97
|
|
|
@@ -10,7 +10,7 @@ A gate is a shell command that must exit `0` before `done` can complete the sess
|
|
|
10
10
|
tether run --tools bash,edit --verify "pytest -q" --verify "ruff check ." "Fix the failing test"
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
If a gate fails, `done` is rejected and a `[VERIFICATION GATE FAILED: ...]` observation is injected so the model can fix the problem and retry. After repeated consecutive failures the session ends `FAILED`. Gates can also come from `Harness(verify=[...])` or a Skill's `verify:` frontmatter list.
|
|
13
|
+
If a gate fails, `done` is rejected and a `[VERIFICATION GATE FAILED: ...]` observation is injected so the model can fix the problem and retry. After repeated consecutive failures the session ends `FAILED`. A gate that is itself broken (sandbox violation, command not found, exit 126/127, timeout) is not retried: the session ends `FAILED` immediately with a `Verification gate ... misconfigured: <reason>` result, the turn is recorded as `gate_error`, and the exit code is still `3`. Gates can also come from `Harness(verify=[...])` or a Skill's `verify:` frontmatter list.
|
|
14
14
|
|
|
15
15
|
## Asking for input (`done --ask`)
|
|
16
16
|
|
|
@@ -70,7 +70,7 @@ Each stage needs a `goal`. Invalid manifests print `Error: invalid pipeline ...`
|
|
|
70
70
|
tether stats [--model <m>] [--skill <s>] [--limit <n>]
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
Prints per-model runs, pass rate, average steps, peak and total tokens, and friction counts (`nudge`, `loop_stall`, `syntax_error`, `tool_error`, `gate_fail`), followed by the commands that cause the most friction. Pipeline root sessions are excluded so tokens are not double-counted.
|
|
73
|
+
Prints per-model runs, pass rate, average steps, peak and total tokens, and friction counts (`nudge`, `loop_stall`, `cycle_stall`, `syntax_error`, `tool_error`, `gate_fail`, `gate_error`), followed by the commands that cause the most friction. Pipeline root sessions are excluded so tokens are not double-counted.
|
|
74
74
|
|
|
75
75
|
## Post-mortem rules (`tether retro`)
|
|
76
76
|
|
|
@@ -126,4 +126,4 @@ sqlite3 .tether/tether.db \
|
|
|
126
126
|
"select step, event_type, tool_cmd from turns where session_id='$SID' and tool_cmd is not null order by step"
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
-
`turns.event_type` is one of `ok`, `nudge`, `loop_stall`, `syntax_error`, `tool_error`, `gate_fail`, `completed`; the non-`ok` ones are what `tether stats` counts as friction. Model **reasoning is not recorded** (only the action and its output), so a "thinking" model that loops shows no visible cause ([#32](https://github.com/igormcsouza/tether/issues/32)).
|
|
129
|
+
`turns.event_type` is one of `ok`, `nudge`, `loop_stall`, `cycle_stall`, `syntax_error`, `tool_error`, `gate_fail`, `gate_error`, `completed`; the non-`ok` ones are what `tether stats` counts as friction. Model **reasoning is not recorded** (only the action and its output), so a "thinking" model that loops shows no visible cause ([#32](https://github.com/igormcsouza/tether/issues/32)).
|
|
@@ -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}
|
|
@@ -5,8 +5,10 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
|
|
7
7
|
import httpx
|
|
8
|
+
import pytest
|
|
8
9
|
|
|
9
10
|
from tether.providers.anthropic import AnthropicProvider
|
|
11
|
+
from tether.providers.base import ProviderError
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
async def test_anthropic_provider_and_ephemeral_cache() -> None:
|
|
@@ -60,3 +62,13 @@ async def test_anthropic_provider_and_ephemeral_cache() -> None:
|
|
|
60
62
|
c_anth_new = anth._get_client()
|
|
61
63
|
assert not c_anth_new.is_closed
|
|
62
64
|
await anth.aclose()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def test_anthropic_401_names_env_var(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
68
|
+
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-SECRET")
|
|
69
|
+
prov = AnthropicProvider("m")
|
|
70
|
+
prov._client = httpx.AsyncClient(transport=httpx.MockTransport(lambda r: httpx.Response(401)))
|
|
71
|
+
with pytest.raises(ProviderError) as ei:
|
|
72
|
+
await prov.generate([{"role": "user", "content": "x"}], [])
|
|
73
|
+
assert "`ANTHROPIC_API_KEY` is missing or rejected" in str(ei.value)
|
|
74
|
+
assert "sk-SECRET" not in str(ei.value)
|
|
@@ -5,6 +5,7 @@ import json
|
|
|
5
5
|
import sys
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
+
import httpx
|
|
8
9
|
import pytest
|
|
9
10
|
|
|
10
11
|
from tether.providers import (
|
|
@@ -13,8 +14,9 @@ from tether.providers import (
|
|
|
13
14
|
OpenAIProvider,
|
|
14
15
|
resolve_provider,
|
|
15
16
|
)
|
|
16
|
-
from tether.providers.base import ModelResponse
|
|
17
|
+
from tether.providers.base import ModelResponse, ProviderError
|
|
17
18
|
from tether.providers.declarative import (
|
|
19
|
+
ACT_SCHEMA,
|
|
18
20
|
CommandProvider,
|
|
19
21
|
build_declared,
|
|
20
22
|
load_declared,
|
|
@@ -95,6 +97,25 @@ def test_build_openai_with_api_key_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
95
97
|
assert provider.base_url.startswith("http://x/v1")
|
|
96
98
|
|
|
97
99
|
|
|
100
|
+
def test_build_declared_empty_key_env_fails_early(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
101
|
+
monkeypatch.setenv("MY_KEY", "")
|
|
102
|
+
with pytest.raises(ProviderError, match="`MY_KEY` is missing or rejected"):
|
|
103
|
+
build_declared({"kind": "openai", "api_key_env": "MY_KEY"}, "m", num_ctx=1024)
|
|
104
|
+
monkeypatch.delenv("MY_KEY")
|
|
105
|
+
with pytest.raises(ProviderError, match="`MY_KEY`"):
|
|
106
|
+
build_declared({"kind": "anthropic", "api_key_env": "MY_KEY"}, "m", num_ctx=1024)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
async def test_declared_401_names_env_var(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
110
|
+
monkeypatch.setenv("MY_KEY", "sk-SECRET")
|
|
111
|
+
prov = build_declared({"kind": "openai", "api_key_env": "MY_KEY"}, "m", num_ctx=1024)
|
|
112
|
+
assert isinstance(prov, OpenAIProvider)
|
|
113
|
+
prov._client = httpx.AsyncClient(transport=httpx.MockTransport(lambda r: httpx.Response(401)))
|
|
114
|
+
with pytest.raises(ProviderError) as ei:
|
|
115
|
+
await prov.generate([{"role": "user", "content": "x"}], [])
|
|
116
|
+
assert "`MY_KEY` is missing or rejected" in str(ei.value) and "sk-SECRET" not in str(ei.value)
|
|
117
|
+
|
|
118
|
+
|
|
98
119
|
def test_build_ollama(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
99
120
|
cfg = {"kind": "ollama", "host": "http://h:11434"}
|
|
100
121
|
provider = build_declared(cfg, "m", num_ctx=1024)
|
|
@@ -300,3 +321,100 @@ def test_command_validation_errors(
|
|
|
300
321
|
_write(monkeypatch, tmp_path, f"cli:\n kind: command\n {body}")
|
|
301
322
|
with pytest.raises(ValueError, match=f"providers.yaml: entry `cli`.*{message}"):
|
|
302
323
|
load_declared()
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def test_command_custom_dialect_end_to_end() -> None:
|
|
327
|
+
from tether.providers.declarative import _build_dialects
|
|
328
|
+
|
|
329
|
+
text = "RUN[ls -la]WITH[body]"
|
|
330
|
+
code = f"import json; print(json.dumps({{'r': {text!r}}}))"
|
|
331
|
+
dialects = _build_dialects([{"regex": r"RUN\[(?P<cmd>.*?)\]WITH\[(?P<stdin>.*?)\]"}])
|
|
332
|
+
resp = _run(CommandProvider("m", argv=_script(code), reply="r", dialects=dialects))
|
|
333
|
+
assert resp.action is not None
|
|
334
|
+
assert resp.action.cmd == "ls -la"
|
|
335
|
+
assert resp.action.stdin == "body"
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def test_command_custom_dialect_no_match_and_empty_cmd() -> None:
|
|
339
|
+
from tether.providers.declarative import _build_dialects
|
|
340
|
+
|
|
341
|
+
dialects = _build_dialects([{"regex": r"RUN\[(?P<cmd>.*?)\]"}])
|
|
342
|
+
assert dialects[0]("nothing here") is None
|
|
343
|
+
assert dialects[0]("RUN[ ]") is None
|
|
344
|
+
call = dialects[0]("RUN[ pwd ]")
|
|
345
|
+
assert call is not None
|
|
346
|
+
assert call.cmd == "pwd"
|
|
347
|
+
assert call.stdin is None
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@pytest.mark.parametrize(
|
|
351
|
+
("dialects", "message"),
|
|
352
|
+
[
|
|
353
|
+
("dialects: nope", "invalid `dialects`"),
|
|
354
|
+
("dialects: [1]", "`regex` required"),
|
|
355
|
+
("dialects: [{foo: bar}]", "`regex` required"),
|
|
356
|
+
("dialects: [{regex: '('}]", "invalid `dialects` regex"),
|
|
357
|
+
("dialects: [{regex: 'abc'}]", "named group `cmd`"),
|
|
358
|
+
],
|
|
359
|
+
)
|
|
360
|
+
def test_command_dialects_validation_errors(
|
|
361
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, dialects: str, message: str
|
|
362
|
+
) -> None:
|
|
363
|
+
_write(
|
|
364
|
+
monkeypatch,
|
|
365
|
+
tmp_path,
|
|
366
|
+
f"cli:\n kind: command\n argv: [echo]\n reply: r\n {dialects}\n",
|
|
367
|
+
)
|
|
368
|
+
with pytest.raises(ValueError, match=f"providers.yaml: entry `cli`.*{message}"):
|
|
369
|
+
load_declared()
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
_SCHEMA = json.dumps(ACT_SCHEMA, separators=(",", ":"))
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def test_schema_placeholder_in_argv_and_stdin() -> None:
|
|
376
|
+
code = (
|
|
377
|
+
"import sys,json;"
|
|
378
|
+
"s = sys.argv[1] + '|' + sys.stdin.read();"
|
|
379
|
+
"print(json.dumps({'r': json.dumps({'cmd': 'a', 'stdin': s})}))"
|
|
380
|
+
)
|
|
381
|
+
provider = CommandProvider("m", argv=_script(code, "{schema}"), reply="r", stdin="in:{schema}")
|
|
382
|
+
resp = _run(provider)
|
|
383
|
+
assert resp.action is not None
|
|
384
|
+
assert resp.action.stdin == f"{_SCHEMA}|in:{_SCHEMA}"
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def test_reply_dict_with_cmd_and_stdin() -> None:
|
|
388
|
+
code = "import json;print(json.dumps({'r': {'cmd': 'ls', 'stdin': 'x'}}))"
|
|
389
|
+
resp = _run(CommandProvider("m", argv=_script(code), reply="r"))
|
|
390
|
+
assert resp.action is not None
|
|
391
|
+
assert (resp.action.cmd, resp.action.stdin) == ("ls", "x")
|
|
392
|
+
assert json.loads(resp.content) == {"cmd": "ls", "stdin": "x"}
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def test_reply_dict_with_cmd_no_stdin() -> None:
|
|
396
|
+
code = "import json;print(json.dumps({'r': {'cmd': 'ls', 'stdin': None}}))"
|
|
397
|
+
resp = _run(CommandProvider("m", argv=_script(code), reply="r"))
|
|
398
|
+
assert resp.action is not None
|
|
399
|
+
assert resp.action.stdin is None
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def test_structured_output_fallback() -> None:
|
|
403
|
+
code = "import json;print(json.dumps({'result': '', 'structured_output': {'cmd': 'pwd'}}))"
|
|
404
|
+
resp = _run(CommandProvider("m", argv=_script(code), reply="missing"))
|
|
405
|
+
assert resp.action is not None
|
|
406
|
+
assert resp.action.cmd == "pwd"
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def test_reply_dict_without_cmd_falls_back() -> None:
|
|
410
|
+
code = "import json;print(json.dumps({'r': {'foo': 1}}))"
|
|
411
|
+
resp = _run(CommandProvider("m", argv=_script(code), reply="r"))
|
|
412
|
+
assert resp.content == ""
|
|
413
|
+
assert resp.action is None
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def test_reply_string_still_works() -> None:
|
|
417
|
+
code = "import json;print(json.dumps({'r': json.dumps({'cmd': 'ls'})}))"
|
|
418
|
+
resp = _run(CommandProvider("m", argv=_script(code), reply="r"))
|
|
419
|
+
assert resp.action is not None
|
|
420
|
+
assert resp.action.cmd == "ls"
|