Skip to content
72 changes: 71 additions & 1 deletion docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,40 @@ fallback that has not been needed is not what you configured.
default) so it can be read instead of paid for again. Pass `--artifacts ''`
to keep nothing.

### What each role is shown

Worth knowing, because it is what the models are actually judged on, and
because each of these was once absent and cost real attempts to discover.

| Role | Sees |
|---|---|
| planner | the brief, and the repository's file listing |
| implementer | the brief, its own plan, a bounded slice of the repository **target file first**, the check commands that will run on its diff, and why the previous attempt was refused if there was one |
| reviewer | the brief, the diff, **the files that diff touched as they now stand**, and whether the checks passed |

Three of those are recent and are worth stating plainly:

- **The implementer is told the checks.** It is graded by them; keeping them
secret from it costs an attempt and two model calls to discover a formatter.
It does not weaken the gate — the command still runs and still refuses.
- **The reviewer is given the touched files.** Asked whether a change is wired
in where it should be, and holding only the change, it must answer "the diff
does not show" — and "the task cannot be judged from what you were given" is
grounds to reject. A file too large for the budget is **named as absent**,
because a reviewer that thinks a partial view is complete is worse than one
that knows it is partial.
- **A retry is told why the last attempt was refused.** It is *not* a
resumption: the item is re-planned against the current brief exactly as
before, no prior diff is fed back, and nothing is treated as progress. It
simply does not repeat the last mistake blind.

**A brief that does not bound its own scope will be rejected.** The reviewer is
told to assume the work is wrong, and a sufficiently sceptical model can always
name one more path it has not been shown. An item that says what "done" is —
finitely, and including what is *not* in scope — is judged; one that does not
collects rejections that are each individually reasonable. That cost lands as
retries, and it is the plan's to fix, not the reviewer's.

---

## 4. Resume after anything
Expand Down Expand Up @@ -1163,7 +1197,42 @@ curl -sH "Authorization: Bearer $TOKEN" 'localhost:8099/api/work/T4' \
can branch on it: `checks_failed`, `check_escalated`, `check_transient`,
`review_rejected`, `patch_rejected`, `no_target`, `worker_error`,
`provider_exhausted`, `budget_exhausted`, `dependency_invalidated`,
`agent_timeout`, `claim_lost`.
`agent_timeout`, `claim_lost`, `item_wall_clock`, `item_spend`,
`hold_expired`, `context_unavailable`.

### 6a.1 When the target does not fit in the prompt

The implementer is shown a bounded slice of the repository — 60,000 characters
by default, the file the planner named first and a relevance-ordered fallback
after it. A repository whose relevant file is *larger than the whole budget*
therefore has a target that cannot be supplied at all.

That used to proceed anyway: the target was dropped, the fallback filled the
space with whatever else was nearby, and the implementer was asked to change a
file it had never seen. It answers — models do not refuse for want of evidence
— and the diff then fails to apply, which reads in the log as a bad model and
is not one.

Now the item stops **before** the implementer is called:

```json
{"state": "blocked", "disposition": "escalated",
"reason_kind": "context_unavailable", "attempts": 0,
"last_error": "the planner's target(s) crates/gateway/src/main.rs (612334 bytes)
do not fit the context budget of 60000 characters, …"}
```

It costs no attempt, because no attempt could have succeeded and retrying will
not make the file smaller. Two things fix it, and both are yours to choose:

```bash
agent-harness run --context-budget 400000 … # or $HARNESS_CONTEXT_BUDGET
```

or split the file. The ceiling that actually matters is the model's context
window, which the harness does not know and will not guess — a budget large
enough to overflow it turns a working item into a provider error, so raise it
deliberately rather than to the maximum.

### 6b. A check has five answers, not two

Expand Down Expand Up @@ -1350,6 +1419,7 @@ durable stage.
| `HARNESS_ENDPOINT` | `run`, `serve` | Model API base URL. |
| `HARNESS_ROUTE_PRESET` | `run`, `serve` | Default route preset (`--preset`) for roles that name none: the wire protocol, the authentication header, the response reader and a failure classifier, as one name. Default `chat-completions`. |
| `HARNESS_ROUTE_PRESETS` | all | Extra presets to make resolvable, as `name=module:attribute` pairs. For a preset that lives in your own code rather than in an installed distribution's entry points. |
| `HARNESS_CONTEXT_BUDGET` | `run` | How many characters of repository the implementer is shown (`--context-budget`, default 60000). A file bigger than this cannot be supplied at all — see [§6a.1](#6a1-when-the-target-does-not-fit-in-the-prompt). |
| `HARNESS_ROOT_PATH` | `serve` | Prefix when behind a proxy, e.g. `/api/harness`. |
| `AIDEVENV_URL` | `run`, `serve` | Session host, enabling attachable agents. In `serve` it is what makes the deployment supervised rather than monitoring-only. |
| `AIDEVENV_TOKEN` | `run`, `serve` | Session host token. |
Expand Down
19 changes: 18 additions & 1 deletion src/agent_harness/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ def _run(args: argparse.Namespace) -> int:
code, so it says exactly what it will do before doing any of it."""
import json as _json

from .executor import Checks, Executor
from .executor import Checks, ContextPolicy, Executor
from .github import GitHub
from .model_client import Chain, ModelClient, chains_from_map
from .work import RUNNING, WorkQueue, WorkRecord
Expand Down Expand Up @@ -686,6 +686,7 @@ def live_routes() -> dict[str, Chain]:
client,
args.work,
checks=checks,
context_policy=ContextPolicy(budget=args.context_budget),
durability=durability,
github=GitHub(args.repo) if args.repo else None,
base_branch=args.base,
Expand Down Expand Up @@ -830,6 +831,11 @@ def _adopt(args: argparse.Namespace) -> int:


def main(argv: list[str] | None = None) -> int:
# Imported here, as every other executor name in this module is: the CLI
# starts for `--help` on a machine with no queue and no credentials, and
# only the default value is needed to print it.
from .executor import DEFAULT_CONTEXT_BUDGET

parser = argparse.ArgumentParser(prog="agent-harness", description=__doc__)
parser.add_argument(
"--db",
Expand Down Expand Up @@ -1129,6 +1135,17 @@ def main(argv: list[str] | None = None) -> int:
"then the default. The pre-review git checkpoint is unaffected by all "
"three (or $HARNESS_DURABILITY).",
)
p_run.add_argument(
"--context-budget",
type=int,
default=int(os.environ.get("HARNESS_CONTEXT_BUDGET", "") or DEFAULT_CONTEXT_BUDGET),
help="how many characters of repository the implementer is shown "
f"(default {DEFAULT_CONTEXT_BUDGET}, or $HARNESS_CONTEXT_BUDGET). A file "
"larger than this cannot be supplied at all, and an item whose target "
"does not fit is stopped before the implementer is paid rather than "
"asked to change a file it cannot see. Raise it for a repository with "
"large files; the ceiling that matters is the model's context window.",
)
p_run.add_argument(
"--demo",
action="store_true",
Expand Down
1 change: 1 addition & 0 deletions src/agent_harness/adapters/otlp.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
"no_changes": "error",
"agent_timeout": "error",
"agent_failed": "error",
"context_unavailable": "error",
}


Expand Down
Loading
Loading