Skip to content

docs: document the live tool rail and add capped execution examples - #12

Merged
epanonymous merged 3 commits into
mainfrom
docs/tool-rail
Oct 30, 2026
Merged

epanonymous merged 3 commits into
mainfrom
docs/tool-rail

Conversation

@epanonymous

Copy link
Copy Markdown
Contributor

Replace legacy catalog guidance with the live discover → inspect → estimate → run → poll loop, with curl-captured responses and capped Python/TypeScript examples.

Implementation mapping:

  • Public documentation lane: rewrite Resource discovery, add docs/tools.md, and link it from the README. Discovery, inspect, and estimate responses were captured live on 2026-10-30. Catalog still returns 200 and is labeled legacy.
  • Generic SDK/MCP tool-loop item: document all ten hosted MCP tools, Bearer authentication at https://api.deva.me/v1/mcp, and five upcoming npm stdio deva_tools_* tools, explicitly marked for the next release.
  • Per-task budget item: examples enforce a one-run task limit and explicit credit cap, reject over-budget estimates/minimums, preserve run IDs, and never retry a run POST. The guide explains free steps, spending, settlement, sync/async output, 402, and 503.
  • Validation: mocked HTTP tests for both languages plus opt-in live smoke tests and CI.

Live route correction: POST /run/{slug} is the working endpoint; bare /run returned 404. POST /estimate/{slug} is preferred; the body-slug /estimate alias still works.

Validation results: 20 mocked tests passed. Two live smoke tests skipped because DEVA_API_KEY was not set. Unauthenticated run/poll probes returned 401; successful authenticated execution/poll responses could not be captured and are not represented as live captures. No paid execution was started. Hosted names were checked against the current tool registry; an authenticated hosted tools/list call was not available. npm publishing and server changes are outside this documentation PR.

@claude

claude Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Claude finished @epanonymous's task in 1m 35s —— View job


PR #12 review: live tool rail docs and capped execution examples

  • Read the PR diff (docs, Python/TS examples, tests, CI)
  • Checked the examples against the documented contracts
  • Posted inline comments
  • Ran the test suites. Not done: running python3/node needs approval in this job, which wasn't granted. I didn't verify the reported 20 passing tests myself.

Summary

This is a careful PR. Paid steps and free steps are kept apart, the API key is only sent on run and poll requests, and redirects are rejected so the key can't be forwarded. There is never more than one run POST, and the docs say clearly which responses were captured live and which are templates. Credits and USD are treated as equal, which matches api-reference.md:87 (1 credit = $1). Nothing here should block merging, but the first item is worth fixing before people copy these examples.

Findings

1. Medium: unexpected poll statuses abort a run that is still going. tools_loop.py:73-76, tools_loop.ts:60-61

  • The poll loop treats any status other than running or succeeded as a failure.
  • The poll status values were never captured live, and this API mixes letter case (SYNC, DEVA).
  • If it returns queued, pending or RUNNING, the loop stops with "do not resubmit" while the run keeps spending. That error also leaves out the "resume GET /runs/{id}" hint.
  • In Python, a missing status field causes a bare KeyError, which loses the run ID.
  • Fix: keep polling on unknown statuses until max_polls, and include the resume path in every error raised after the run starts.

2. Low: Python drops the reason for a 402 error. tools_loop.py:30

  • tools.md tells users to read the reason (for example cap_preflight).
  • urllib's HTTPError never reads the response body, so the user only sees HTTP Error 402.
  • The TS example includes the body, so the two examples behave differently.

3. Low: errors from the run POST don't say "do not resubmit". tools_loop.py:56-58, tools_loop.ts:42-44

  • A timeout or 503 on the run POST comes out as a raw exception, unlike every other failure path.
  • The nearby comment says the cap is reserved, but no code reserves anything. The one-run limit only holds per call to run_loop. Reword the comment as something the caller must do, or add a reservation hook.

4. Test coverage: the TS suite has gaps the Python suite doesn't. tests/tools_loop.test.ts

  • No redirect test. redirect: 'error' is the only thing keeping the API key off a redirected request.
  • No maxPolls validation test.
  • The no-retry tests check the total call count rather than counting run POSTs directly.

5. Docs: hosted MCP names were not checked live. docs/mcp-server.md

  • The hosted MCP tool and argument names (max_total_charge_credits, min_cap_credits) come from the registry source, not a live tools/list call. They should carry the same "not captured" note as the run/poll contracts.
  • deva_resources_catalog was removed from the stdio table but is still described as an existing legacy tool.

Smaller points

  • Discovery: discover only checks the first 100 results. That's acceptable, since the error tells the user to refine the query.
  • CI workflow: .github/workflows/tool-examples.yml uses version tags (@v4, @v5). Pinning actions to commit SHAs would be safer. permissions: contents: read is good, and no secrets are exposed.
  • Live smoke test: it expects 404 for an unknown run. A key without read access would get 403, so the failure message could mention key scope.
  • Unused test field: Response in tests/test_tools_loop.py sets .status. Python's request() reads it, but the TS mock path doesn't need it. No action needed.

About the earlier fix-round comment

The "three-model Result" comment mentioned above still isn't on this PR. This review can serve as the findings list instead: items 1–4 are confirmed by reading the code, and item 5 is a documentation-consistency point. I made no code changes or commits.

@epanonymous

Copy link
Copy Markdown
Contributor Author

Fix round blocked: the referenced three-model Result comment is not present. Checked the PR comments, issue-comment API, submitted reviews, and inline review comments; only the Claude error comment is available. Please post the Confirmed and Disputed findings so each fix can be mapped to its commit and regression test.

Baseline validation: Python unittest and Node test suites pass (20 passed, 2 live smoke tests skipped). No lint or typecheck commands are configured in this repository. No code changes or commits were made, and nothing was pushed or merged.

Comment on lines +73 to +76
if result['status'] == 'succeeded':
return result
if result['status'] != 'running':
raise RuntimeError(f'Run {run_id}: {result["status"]}; do not resubmit')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-terminal statuses are handled as failures, and the resume hint is lost. The poll vocabulary was never captured live (the PR says so). If the server returns queued/pending/ready, or uses uppercase like RUNNING (discovery uses SYNC/DEVA), the loop raises Run …: queued; do not resubmit while the run is still going and still spending. That message also leaves out the resume GET /runs/{id} path that every other poll-exit error includes.

Also, result['status'] raises a bare KeyError if the field is missing, so the run ID is lost here too. The TS version doesn't have this problem.

Suggestion: only treat failed (plus any other documented terminal states) as terminal, keep polling on unknown statuses, and include the resume path in every error raised after on_run:

        status = result.get('status')
        if status == 'succeeded':
            return result
        if status == 'failed':
            raise RuntimeError(f'Run {run_id} failed; do not resubmit')
        # Unknown or non-terminal status: keep polling until max_polls.

Same applies to examples/typescript/tools_loop.ts:60-61.

req = Request(BASE + path, data=None if body is None else json.dumps(body).encode(),
headers=headers, method=method)
# Never follow redirects or retry POSTs; credentials stay on the original request.
with _opener.open(req, timeout=60) as response:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 402 reason is dropped in Python. tools.md tells users to "Read the error reason" for 402 (cap_preflight with max_allowed_cap_credits, or cap_floor_above_balance). Here urllib raises HTTPError and never reads the body, so the CLI only prints HTTP Error 402: Payment Required. The TS request includes await response.text(), so the two examples behave differently.

Consider catching HTTPError, reading error.read(), and re-raising with the body. Close the response either way.

Comment on lines +56 to +58
# One-call task budget: reserve the entire cap before this single POST.
# Do not release that reservation or submit again on an ambiguous failure.
code, result = http('POST', '/run/' + path_slug, body, key)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this comment says the cap is reserved, but nothing in run_loop reserves anything. The one-run limit only holds because the function makes a single POST. A caller who loops over run_loop gets no protection. I'd reword it as a caller responsibility (e.g. "Callers must deduct budget from their task budget before calling, and keep it deducted on any exception"), or add an optional ledger/reserve hook that the tests can check.

Related: a transport error on this POST (timeout after the request was sent, or a 503) propagates as a raw TimeoutError/HTTPError with no "do not resubmit" wording, unlike every other failure path. Wrapping it the same way would make the no-retry contract show up in the error itself. The same applies to tools_loop.ts:44.

Comment thread tests/tools_loop.test.ts
const { error, calls } = await execute(t, [[202, { run_id: 'r1' }], [200, { status: 'running' }]], undefined, 0.01, 1);
assert.match(String(error), /resume GET \/runs\/r1/);
assert.equal(calls.length, 5);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TS suite is missing tests the Python suite has:

  • No redirect test. redirect: 'error' in tools_loop.ts:15 is the only thing keeping the Bearer key off a redirected request, and nothing would catch a regression. A small node:http server returning 301/302/307/308 (like redirect_server in the Python tests) would cover it.
  • No maxPolls validation test (Python has test_invalid_poll_limits_do_not_call_http).
  • The "no retry" cases only check calls.length and assert.ok(error). Adding assert.equal(calls.filter(c => c.method === 'POST' && c.url.includes('/run/')).length, 1) would state the main invariant directly.

Comment thread docs/mcp-server.md

Use `list_resources` → `inspect_resource` → `estimate_resource` → `run_resource` → `get_run`. Discovery, inspection, and estimation are free. Poll an asynchronous run until `succeeded` or `failed`; read synchronous output directly. Check MCP `result.isError` as well as HTTP status. Never automatically retry `run_resource`.

Hosted MCP uses `max_total_charge_credits` for the cap and `min_cap_credits` in inspection; REST uses `maxTotalChargeUsd` and `min_cap_usd`. One credit equals one USD. Read the [tool loop guide](tools.md) for budget guards and failure handling.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description says an authenticated hosted tools/list call was not available, so the tool names and the argument names (max_total_charge_credits, min_cap_credits) come from the registry source, not from a live response. api-reference.md already labels the run/poll contracts as "not captured". A matching one-line caveat here ("Argument names from the server registry; confirm with tools/list") would make the doc's evidence standard consistent.

Minor: deva_resources_catalog was removed from the stdio table (line 125), but line 47 still calls it "the existing … legacy discovery" tool. Keeping it in the table with a "(legacy)" label would avoid the mismatch.

@epanonymous

Copy link
Copy Markdown
Contributor Author

Addressed the supplied review in commit 033e01a (pushed to docs/tool-rail).

Finding Disposition / commit Regression test
Confirmed: Python forwards Bearer credentials on redirects 033e01a: reject redirects before any follow-up request test_rejects_redirects_without_forwarding_credentials exercises real loopback HTTP, POST and GET, and 301/302/303/307/308; asserts no destination request
Confirmed: missing synchronous non-success / unexpected 2xx / missing run-ID coverage 033e01a: cover these cases in both suites; synchronous errors now explicitly say not to resubmit Python: test_sync_non_success_never_polls_or_resubmits, test_unexpected_success_response_never_polls_or_resubmits; TypeScript: sync non-success … never polls or resubmits, unexpected success … never polls or resubmits
Confirmed: noninteger poll limits can start paid execution 033e01a: require a positive Python integer before any HTTP call, excluding booleans test_invalid_poll_limits_do_not_call_http
Disputed: credential forwarding Same security fix in 033e01a; duplicate of the confirmed redirect finding test_rejects_redirects_without_forwarding_credentials
Disputed: response contracts labeled verified without captures 033e01a: label them expected and explicitly disclose missing authenticated captures; also qualify unverified idempotency support Documentation inspection; no executable behavior change
Disputed: unknown/non-deterministic estimate bypasses comparison 033e01a: clarify that the sent cap remains the spending limit. Stricter rejection skipped: an unavailable estimate does not remove the server cap Existing test_sync_and_cap_and_auth / sync output, cap, slug path and auth verify the submitted cap
Disputed: cap_hit hidden as plain success Behavioral change skipped: both examples already return and print the complete response, including cap_hit. 033e01a clarifies this and adds preservation assertions test_sync_and_cap_and_auth / sync output, cap, slug path and auth
Disputed: poll errors lose recovery guidance for a paid run 033e01a: wrap poll errors with the resume URL and “do not resubmit,” preserving the original cause test_poll_errors_preserve_resume_guidance_and_cause; poll error preserves resume guidance …
Disputed: contradictory cap_hit wording 033e01a: align both pages on “output may be partial” Documentation inspection

Validation:

  • Full Python and TypeScript suites: 36 passed, 2 skipped. The skipped smoke tests require DEVA_API_KEY; no paid execution was performed.
  • RED → GREEN observed for redirects, invalid poll limits, synchronous failure guidance, and poll-error recovery. Unexpected 2xx / missing-ID guards already worked before this change; the added tests passed immediately, and separate mutation checks in both languages failed when that guard was removed.
  • python3 -m unittest discover -s tests -p 'test_*.py'
  • node --test tests/tools_loop.test.ts
  • uvx ruff check examples tests
  • ESLint 9 over all TypeScript examples/tests, using recommended JavaScript rules plus TypeScript parsing/unused-variable checks.
  • TypeScript 5.9.3: strict tsc --noEmit over all TypeScript examples/tests (ES2022, ES modules, bundler resolution, Node 24 types).
  • git diff --check

There are no repository lint/typecheck scripts, so those tools/configuration were installed outside the repository for local validation. Local HTTP test servers are shut down and joined by the tests. PR remains open; no merge or force-push.

At the pushed head, the examples CI check passed. claude-review is still running; the prior run failed with the known stale-workflow error Claude Code 1.0.88 does not support this model.

@epanonymous
epanonymous merged commit e5808fa into main Oct 30, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant