Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ tinyagent --read-only --cwd ~/src/project -t "Review error handling in pkg/api"
cat review-task.md | tinyagent --read-only -t - -o review.json
```

Subcommands are shortcuts for common flags: `tinyagent sandbox`, `tinyagent memory list`, `tinyagent planner --first`, `tinyagent plugins list`, `tinyagent tinygo status` (see [CLI reference](docs/subcommands.md)).
Run `tinyagent --help` (or plain `tinyagent` at a terminal) for the overview and `tinyagent --help --verbose` for every flag. Subcommands are shortcuts for common flags: `tinyagent sandbox`, `tinyagent memory list`, `tinyagent planner --first`, `tinyagent plugins list`, `tinyagent tinygo status` (see [CLI reference](docs/subcommands.md)); each one alone, such as `tinyagent sandbox`, prints its help.

## Features

Expand All @@ -115,6 +115,9 @@ Subcommands are shortcuts for common flags: `tinyagent sandbox`, `tinyagent memo
- Streaming text, `--json` event output, structured output, SQLite event logs and OpenTelemetry.
- Project memory across runs, with approval before entries become active.
- A task planner that can require a plan before any change.
- Run budgets: turn cap, cost limit and token limit (`--token-limit`), with an 80% warning to the model; delegates, workers and `--attempts` share the budget, and a run that hits it can be resumed.
- Verify before stop (`--verify-on-stop`): a run that changed files isn't accepted until a test or build command passes; otherwise it is marked unverified.
- Help everywhere: `tinyagent` or `tinyagent <subcommand>` alone at a terminal prints help; `--help --verbose` lists every flag by section.
- Undo for file edits, workspace rewind, resume and follow-up runs.
- Sandboxed commands with a network allowlist, permission rules, hooks, untrusted-content lockdown and file-access rules.
- WebAssembly plugin modules run through Extism with permission checks, a version store, rollback and prune. Go source modules are built with TinyGo, an external dependency that tinyagent downloads on first use (pinned version, checksum-verified, cached locally, `tinyagent tinygo update` to update); TinyGo is never embedded in tinyagent.
Expand All @@ -128,7 +131,8 @@ Subcommands are shortcuts for common flags: `tinyagent sandbox`, `tinyagent memo
| Config file, profiles, MCP and command tools | [docs/config.md](docs/config.md) |
| Providers, ChatGPT subscription, reasoning and fallback | [docs/providers.md](docs/providers.md) |
| `--json`, streaming, structured output, event log, OpenTelemetry | [docs/json-output.md](docs/json-output.md) |
| Reviews, worktrees, best-of-N, resume, transcripts, follow-ups, asking for input | [docs/runs.md](docs/runs.md) |
| Reviews, worktrees, best-of-N, resume, transcripts, follow-ups, asking for input, token and cost budgets | [docs/runs.md](docs/runs.md) |
| Verify before stop (`--verify-on-stop`) | [docs/verify.md](docs/verify.md) |
| `undo_file` and `--rewind` | [docs/undo.md](docs/undo.md) |
| Project memory | [docs/memory.md](docs/memory.md) |
| Planner (`update_plan`, `--plan-first`) | [docs/planner.md](docs/planner.md) |
Expand Down
33 changes: 33 additions & 0 deletions cmd/tinyagent/docs_flags_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package main

import (
"flag"
"os"
"strings"
"testing"
)

// TestCLIReferenceListsEveryFlag fails when a registered flag is missing
// from the flag table in docs/subcommands.md.
func TestCLIReferenceListsEveryFlag(t *testing.T) {
b, err := os.ReadFile("../../docs/subcommands.md")
if err != nil {
t.Fatal(err)
}
doc := string(b)
newFlagSet(&options{}).VisitAll(func(f *flag.Flag) {
dash := "--"
if len(f.Name) == 1 {
dash = "-"
}
name := dash + f.Name
for _, prefix := range []string{"`", ", ", " "} {
for _, end := range []string{"`", " ", "=", "[", ","} {
if strings.Contains(doc, prefix+name+end) {
return
}
}
}
t.Errorf("flag %s is not documented in docs/subcommands.md", name)
})
}
6 changes: 1 addition & 5 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,13 +183,9 @@ profiles: # omitted by default; named overlays selected with
review:
model: openai/gpt-5-mini
read_only: true
```

### Verify before stop

With `verify_on_stop.enabled` (or `--verify-on-stop`), a run that changed files does not finish on an unverified final answer. A successful `write_file`, `edit_file` or `apply_patch` call, or a `bash` command that is not a verification command, marks changes pending. A `bash` command counts as verification when it is one of `commands` (alone or followed by arguments) or matches one of `patterns`; when both lists are empty, common test and build commands count (`go test`, `go build`, `go vet`, `cargo test`, `npm test`, `make`, `pytest` and similar, at the start of the command or after `&&`, `;` or `||`). A verification that exits 0 clears the pending changes; one that fails leaves them pending, and the model sees its output as a normal tool result.

When the model gives a final answer (with or without the completion marker) while changes are pending, it gets a fixed user message asking it to verify, naming the configured commands, and the run continues as normal steps, so step, cost and time limits still apply. Tool calls in a submitting message are not run and get an error result saying so. After `max_reprompts` re-prompts, or straight away when `bash` cannot run (denied by the permission policy, a hook or lockdown, or unavailable as in read-only mode), the run finishes with its normal status and is marked unverified: the summary line ends with `, unverified changes`, the `--json` result has `"unverified": true`, and the trajectory has `unverified: true`. `verify_reprompts` counts the re-prompts in both. The guard runs before stop hooks, does not apply to subagent runs or `--output-schema` runs, and keeps its state across `--resume`.
`verify_on_stop` (or `--verify-on-stop`, `--verify-command`, `--max-verify-reprompts`) keeps a run that changed files from finishing until a test or build command has passed, re-prompting the model up to `max_reprompts` times and otherwise marking the run unverified. See [Verify before stop](verify.md#verify-before-stop) for the full rules.

### Profiles

Expand Down
2 changes: 1 addition & 1 deletion docs/eval-framework.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> **Status: experimental; v1 implemented.** `tinyagent --eval suite.yaml --eval-out dir` runs the suite sequentially (code in `internal/eval/` and `cmd/tinyagent/eval.go`). What v1 ships and how it differs from the target design below:
>
> - **Shipped:** strict `version: 1` suite files (`version`, `models`, `profiles`, `trials`, and per task `id`, `repo`, `base`, `task`/`task_file`, `model`, `profile`, `checks`, `checks_on_limit`, `limits`, `tags`); a detached worktree per trial at `base`, removed afterwards (also on Ctrl-C) unless `--eval-keep`; one child tinyagent run per trial with `-o` written to `<eval-out>/trajectories/<trial_id>.json` and its output to `logs/<trial_id>.log`; `results.jsonl`, `report.json`, `report.md` (pass rate, pass@k and pass^k, cost, tokens, steps, wall time, outcome counts, per-task passes); suite-level `--cost-limit` / `--time-limit`; `--dry-run`; `--eval-min-pass-rate` (exit 1). Other flags on the command line (e.g. `-c`, `--no-stream`, `--ask-user`, `--step-limit`) are forwarded to every trial.
> - **Shipped:** strict `version: 1` suite files (`version`, `models`, `profiles`, `trials`, and per task `id`, `repo`, `base`, `task`/`task_file`, `model`, `profile`, `checks`, `checks_on_limit`, `limits`, `tags`); a detached worktree per trial at `base`, removed afterwards (also on Ctrl-C) unless `--eval-keep`; one child tinyagent run per trial with `-o` written to `<eval-out>/trajectories/<trial_id>.json` and its output to `logs/<trial_id>.log`; `results.jsonl`, `report.json`, `report.md` (pass rate, pass@k and pass^k, cost, tokens, steps, wall time, outcome counts, per-task passes); suite-level `--cost-limit` / `--time-limit`; `--dry-run`; `--eval-min-pass-rate` (exit 1); per-task `limits.token_limit`, forwarded to each trial as `--token-limit`. Other flags on the command line (e.g. `-c`, `--no-stream`, `--ask-user`, `--step-limit`) are forwarded to every trial.
> - **Differences:** `trials` is required. `--time-limit` takes seconds (as in single runs), not `30m`. Check output kept in `results.jsonl` is capped to the last 4096 bytes. Checks run on the host in the worktree. A trial's `--profile` is passed only for names other than `default` (unless the config defines a profile called `default`).
> - **Not yet implemented:** `--eval-jobs` (trials are sequential), `--json` eval events, `output_schema` / `expected_json` (rejected as unknown keys), `profile_digest`, the GitHub Action `eval` input.

Expand Down
2 changes: 1 addition & 1 deletion docs/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Use the full path to the binary. Pass `-m`, `-c`, `--profile` and any policy fla
- The first `session/prompt` is the session's task. Each later prompt continues the same conversation, sending the provider exactly what `--resume <trajectory> -t "<follow-up>"` would. This also works after a turn that was cancelled or hit a limit. A second prompt while one is running in the same session gets a JSON-RPC error.
- Prompt content: text blocks are joined; embedded text resources are included as quoted context naming their URI; resource links become a reference line and are not fetched; images are attached like `--image` (PNG, JPEG, GIF or WebP, at most 10, each within `max_image_bytes`). Audio, binary resources and invalid images get an invalid-params error.
- Streaming: assistant text arrives as `agent_message_chunk` (without the submit marker). Each tool call is a `tool_call` with the model's call id, a short title, a `kind` (`read_file` → `read`; `write_file`/`edit_file`/`apply_patch` → `edit`; `grep`/`list_files` → `search`; `bash`/`start_process` → `execute`; `web_fetch` → `fetch`; others → `other`) and absolute `locations` for file tools. It is followed by `tool_call_update`s to `in_progress` and then `completed` or `failed`, carrying the same result text the model sees, after `max_output_chars` and secret redaction. `update_plan` sends `plan` updates, and each model call sends a `usage_update` when the context window is known.
- Stop reasons: `Submitted`/`Completed` → `end_turn`, `StepLimitExceeded` → `max_turn_requests`, `OutputTruncated` → `max_tokens`, `session/cancel` → `cancelled`. Any other status (`CostLimitExceeded`, `TimeLimitExceeded`, `RepeatedFormatError`, `VerificationFailed`, `Error`) ends with a final message naming it and `end_turn`, and the session stays usable. A provider error a headless run would print a setup `hint:` for is returned as a JSON-RPC error instead.
- Stop reasons: `Submitted`/`Completed` → `end_turn`, `StepLimitExceeded` → `max_turn_requests`, `OutputTruncated` → `max_tokens`, `session/cancel` → `cancelled`. Any other status (`CostLimitExceeded`, `TokenLimitExceeded`, `TimeLimitExceeded`, `RepeatedFormatError`, `VerificationFailed`, `Error`) ends with a final message naming it and `end_turn`, and the session stays usable. A provider error a headless run would print a setup `hint:` for is returned as a JSON-RPC error instead.
- `session/cancel` stops the turn the way Ctrl-C does: in-flight requests and commands are cancelled, and background processes and language servers are stopped, as at the end of every turn. Closing stdin cancels running turns, stops MCP servers and exits 0.
- `--step-limit` and `--cost-limit` apply to each prompt (on top of what the session has already used), and `--time-limit` limits each prompt's wall-clock time.
- `--log-db` records each prompt as a run, with an `acp.turn` event naming the session id and turn number. No trajectory files are written.
Expand Down
5 changes: 3 additions & 2 deletions docs/json-output.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ Every event has a `type`. Event names and fields in schema version 1 are:
| `sandbox.network_denied` | `host`, `port`, `reason`; the sandbox allowlist proxy refused a request from a command (see [Network allowlist](sandbox.md#network-allowlist)) |
| `web_fetch` | `url`, `final_url`, `status`, `content_type`, `bytes`, `truncated`, `duration_ms`, and `denied_reason` (for a refused URL) or `error` (for a failed fetch); one per fetch attempt, emitted before its `tool.result` (see [Web fetch](long-runs.md#web-fetch)) |
| `memory.loaded` | `path` (the store file), `loaded` (entry count), `ids` (loaded entry ids, newest first), `omitted` (active entries left out to stay within `memory.max_bytes`); once after `run.started` for a fresh run with [project memory](memory.md#project-memory) on. A resumed or follow-up run of a checkpoint that had memory on emits it too, describing the replayed checkpoint snapshot (the store is not re-read), with `from_checkpoint: true`. Never includes entry text |
| `verify.reprompt` | `reprompt`, `max_reprompts`; the model gave a final answer with unverified changes and was sent back to verify (see [Verify before stop](config.md#verify-before-stop)) |
| `budget.warning` | `step`, `usage` (what was used, as text), `tokens`, `token_limit`, `cost_usd`, `cost_limit`; once per run when a token or cost budget reaches 80% (see [Token and cost budgets](runs.md#token-and-cost-budgets)) |
| `verify.reprompt` | `reprompt`, `max_reprompts`; the model gave a final answer with unverified changes and was sent back to verify (see [Verify before stop](verify.md#verify-before-stop)) |
| `verify.unverified` | `reason`, `reprompts`; the run finishes with unverified changes |
| `memory.saved` | `op` (`add`, `replace` or `forget`), `id`, `status` (`active`, `pending` or `forgotten`), `redacted`, `untrusted_context`; one per successful `remember` write, emitted before its `tool.result`. Never includes entry text |
| `lsp.server` | `name`, `state` (`started`, `unavailable`, `crashed` or `stopped`), `reason` (for `unavailable` and `crashed`); one per language-server state change (see [Language servers (LSP)](long-runs.md#language-servers-lsp)) |
Expand All @@ -36,7 +37,7 @@ Every event has a `type`. Event names and fields in schema version 1 are:
| `model.fallback` | `step`, `from`, `to`, `reason` (`http_<status>` or `network`), `attempts`; the run switched to the next [fallback model](providers.md#model-fallback) after `from` used up its retries |
| `taint` | `source` (`web_fetch`, `mcp`, `review`, `untrusted_task` or `resume`), `step`, and `tool_call_id`, `tool`, `host` (the `web_fetch` host only) and `resumed` when they apply; one per new taint source of a run with [lockdown](permissions.md#untrusted-content-lockdown) on. Never includes content or URLs |
| `lockdown.denied` | `channel` (`command`, `web_fetch` or `mcp`), `tool`, `tool_call_id`, `step`, and `host` for `web_fetch`; one per call lockdown refused after taint |
| `result` | `status`, `submission` (the question for `NeedsInput`), cumulative `steps`, `structured_output` (only with `--output-schema` and status `Submitted`), `question` (`tool_call_id`, `question`, `options` when given; only for status `NeedsInput`, see [Asking for input](runs.md#asking-for-input)), `input_tokens`, `output_tokens`, `cache_read_tokens`, `cache_write_tokens`, `cost_usd`, invocation `duration_ms`, and `tainted` and `lockdown_denials` (a count; only with [lockdown](permissions.md#untrusted-content-lockdown) on), `unverified` (`true` only when the run finished with unverified changes) and `verify_reprompts` (only when > 0; see [Verify before stop](config.md#verify-before-stop)) |
| `result` | `status` (for example `Submitted`, `Completed`, `StepLimitExceeded`, `CostLimitExceeded`, `TokenLimitExceeded`, `TimeLimitExceeded`, `NeedsInput`; see [Exit status](subcommands.md#exit-status)), `submission` (the question for `NeedsInput`), cumulative `steps`, `structured_output` (only with `--output-schema` and status `Submitted`), `question` (`tool_call_id`, `question`, `options` when given; only for status `NeedsInput`, see [Asking for input](runs.md#asking-for-input)), `input_tokens`, `output_tokens`, `cache_read_tokens`, `cache_write_tokens`, `cost_usd`, invocation `duration_ms`, and `tainted` and `lockdown_denials` (a count; only with [lockdown](permissions.md#untrusted-content-lockdown) on), `unverified` (`true` only when the run finished with unverified changes) and `verify_reprompts` (only when > 0; see [Verify before stop](verify.md#verify-before-stop)) |

When the run starts, the final event is `result`, including for model errors, interruptions, and limit stops. Exit codes are unchanged. `--resume` reports cumulative trajectory accounting; `duration_ms` covers only the current invocation. Event streams can contain prompts, code, and tool output, so handle them with the same care as trajectory files. Credentials and environment dumps are not included.

Expand Down
Loading
Loading