wai-quantum-cli: a job on a provider is an MCP task #609

Merged
dcharlot merged 1 commit from wai/quantum-cli-provider-tasks into main 2026-11-04 00:36:36 -05:00
Owner

A client on MCP 2025-11-25 can now run a job on one of the person's own providers as a task, the protocol's own way to wait. Until now an agent ran run_on_provider and then had to remember to come back with get_provider_job. The job service gets the same feature for its own runs in a separate change. This PR covers the local server's provider jobs, which are where the waits are long: a device's queue can take hours or days.

What changes

  • The task is the job. initialize declares tasks (list, cancel, requests.tools.call) from 2025-11-25 on. run_on_provider declares execution.taskSupport: "optional".
    • tools/call with task records the job, sends it, and answers as soon as the provider has taken it. The taskId is the job's pj-… handle.
    • A run that is not free and not confirmed makes no task and sends nothing. The error's data carries the plan to show the person.
  • Every recorded job is a task, whatever sent it: run --no-wait, an earlier session, another agent.
    • tasks/get asks the provider once. If the question fails, the task is returned as recorded and its message says why.
    • tasks/list reads the records and asks no provider. It is listed in full here because the records are one person's own; the service leaves list out for that reason.
    • tasks/cancel cancels at the provider, and refuses a task that has ended.
    • ttl is null, because records are kept.
  • tasks/result waits without holding the server. The local server is stdio and handles one message at a time, so a tasks/result that blocked for a device's queue would freeze it, including the tasks/cancel meant for that job.
    • A result asked before the job ends is answered by its own thread when the job ends. That thread asks the provider at an interval growing from 2 s to 60 s.
    • Replies go through one shared writer, a whole line at a time.
    • A tasks/cancel here wakes the waits at once. A request the client cancels (notifications/cancelled) is never answered.
    • At most 64 results wait at once. Nothing is lost if the server stops: the job is in its record.
  • One question at a time. A process-wide lock means a tool call and a task's wait cannot both seal the same job.

Tests

an_agent_runs_on_a_provider_as_a_task_and_the_server_answers_while_it_waits runs over stdio against this binary's reference provider served over HTTP. It covers each of these:

  • a short queue's result is the sealed job, names its task, and its receipt verifies;
  • a long queue's result waits while ping and tasks/get are answered;
  • by 7 s the wait asks every 8 s, and a tasks/cancel still ends it within 3 s;
  • a request the client cancelled gets no reply;
  • tasks/list, and the refusals: a tool that isn't a run, a priced run without confirmation (nothing recorded), a bad handle, a cursor;
  • a task outlives its session.

The unit tests cover the job-to-task mapping and the wait's wake and cancel. All three are added to the CLI workflow's by-name list. Each test cleans up the processes it started, even when it fails.

Ran on the x86_64 Linux build host:

  • cargo test --locked --release;
  • clippy -D warnings with default features, with none, with service only and with providers only;
  • the wasm32-wasip2 build without default features;
  • deliberate mutations, each of which the test must catch: listed below.

Each mutation was applied on its own, and the end-to-end test failed for all 9:

  • tasks/result answered in the read loop rather than in its own thread;
  • tasks/cancel not waking the waits;
  • notifications/cancelled ignored;
  • tasks declared to every protocol version;
  • tasks/list empty;
  • a priced run without confirm_cost making a task;
  • a numeric ttl;
  • an ended task cancelled again;
  • the result's _meta not naming its task.

The in-process lock that keeps two questions from both sealing one job has no test; it is a race the test cannot force.

🤖 Generated with Claude Code

A client on MCP 2025-11-25 can now run a job on one of the person's own providers as a task, the protocol's own way to wait. Until now an agent ran `run_on_provider` and then had to remember to come back with `get_provider_job`. The job service gets the same feature for its own runs in a separate change. This PR covers the local server's provider jobs, which are where the waits are long: a device's queue can take hours or days. ## What changes - **The task is the job.** `initialize` declares `tasks` (`list`, `cancel`, `requests.tools.call`) from 2025-11-25 on. `run_on_provider` declares `execution.taskSupport: "optional"`. - `tools/call` with `task` records the job, sends it, and answers as soon as the provider has taken it. The `taskId` is the job's `pj-…` handle. - A run that is not free and not confirmed makes no task and sends nothing. The error's data carries the plan to show the person. - **Every recorded job is a task**, whatever sent it: `run --no-wait`, an earlier session, another agent. - `tasks/get` asks the provider once. If the question fails, the task is returned as recorded and its message says why. - `tasks/list` reads the records and asks no provider. It is listed in full here because the records are one person's own; the service leaves `list` out for that reason. - `tasks/cancel` cancels at the provider, and refuses a task that has ended. - `ttl` is `null`, because records are kept. - **`tasks/result` waits without holding the server.** The local server is stdio and handles one message at a time, so a `tasks/result` that blocked for a device's queue would freeze it, including the `tasks/cancel` meant for that job. - A result asked before the job ends is answered by its own thread when the job ends. That thread asks the provider at an interval growing from 2 s to 60 s. - Replies go through one shared writer, a whole line at a time. - A `tasks/cancel` here wakes the waits at once. A request the client cancels (`notifications/cancelled`) is never answered. - At most 64 results wait at once. Nothing is lost if the server stops: the job is in its record. - **One question at a time.** A process-wide lock means a tool call and a task's wait cannot both seal the same job. ## Tests `an_agent_runs_on_a_provider_as_a_task_and_the_server_answers_while_it_waits` runs over stdio against this binary's reference provider served over HTTP. It covers each of these: - a short queue's result is the sealed job, names its task, and its receipt verifies; - a long queue's result waits while `ping` and `tasks/get` are answered; - by 7 s the wait asks every 8 s, and a `tasks/cancel` still ends it within 3 s; - a request the client cancelled gets no reply; - `tasks/list`, and the refusals: a tool that isn't a run, a priced run without confirmation (nothing recorded), a bad handle, a cursor; - a task outlives its session. The unit tests cover the job-to-task mapping and the wait's wake and cancel. All three are added to the CLI workflow's by-name list. Each test cleans up the processes it started, even when it fails. Ran on the x86_64 Linux build host: - `cargo test --locked --release`; - clippy `-D warnings` with default features, with none, with `service` only and with `providers` only; - the `wasm32-wasip2` build without default features; - deliberate mutations, each of which the test must catch: listed below. Each mutation was applied on its own, and the end-to-end test failed for all 9: - `tasks/result` answered in the read loop rather than in its own thread; - `tasks/cancel` not waking the waits; - `notifications/cancelled` ignored; - `tasks` declared to every protocol version; - `tasks/list` empty; - a priced run without `confirm_cost` making a task; - a numeric `ttl`; - an ended task cancelled again; - the result's `_meta` not naming its task. The in-process lock that keeps two questions from both sealing one job has no test; it is a race the test cannot force. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
wai-quantum-cli: a job on a provider is an MCP task
All checks were successful
wai-quantum views in agent hosts / does this change touch it (pull_request) Successful in 7s
wai-quantum studio and desktop / desktop app · windows (pull_request) Successful in 10m19s
wai-quantum service / service energy · linux packages (pull_request) Successful in 1m39s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m37s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (pull_request) Successful in 39s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (pull_request) Successful in 2m39s
wai-quantum contract / the policy is linked, and the deprecations page reads (pull_request) Successful in 6s
wai-quantum command line / command line · linux (pull_request) Successful in 3m22s
wai-quantum docs, course and locales / the Studio's words in every locale · linux (pull_request) Successful in 13s
wai-quantum contract / API compatibility · linux (pull_request) Successful in 4m36s
wai-quantum docs, course and locales / the course and the command line's languages · linux (pull_request) Successful in 2m16s
wai-quantum for Python / python · linux (pull_request) Successful in 3m13s
wai-quantum contract / schemas and the committed tool lists · linux (pull_request) Successful in 6m46s
wai-quantum editors / editors · linux (pull_request) Successful in 5m25s
wai-quantum service / service · linux (pull_request) Successful in 4m49s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 4m11s
wai-quantum views in agent hosts / views · the page and its engine (pull_request) Successful in 3m11s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 6m41s
wai-quantum command line / command line · macos (pull_request) Successful in 1m28s
wai-quantum views in agent hosts / views · a scripted host (pull_request) Successful in 3m32s
wai-quantum studio and desktop / desktop app · linux (pull_request) Successful in 5m57s
wai-quantum for Python / python · macos (pull_request) Successful in 2m4s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 14m46s
wai-quantum service / service · macos (pull_request) Successful in 3m46s
wai-quantum studio and desktop / desktop app · macos (pull_request) Successful in 3m45s
wai-quantum editors / editors · macos (pull_request) Successful in 6m36s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 15m28s
wai-quantum docs, course and locales / every command and snippet in the docs · linux (pull_request) Successful in 20m4s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 13m3s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 29m23s
0f78a99ea4
A client that speaks MCP 2025-11-25 can run run_on_provider as a task.
initialize declares tasks (list, cancel, tools/call) from that version
on, and run_on_provider declares execution.taskSupport "optional".
tools/call with task records and sends the job and answers once the
provider has taken it, the job's handle its taskId; a run that is not
free and not confirmed makes no task and sends nothing, its plan in the
error's data.

Every job recorded here is a task, whatever sent it. tasks/get asks the
provider once; tasks/list reads the records and asks no one; tasks/cancel
cancels at the provider and refuses a task that has ended. The ttl is
null, as records are kept.

tasks/result asked before the job has ended is answered when it ends, by
a thread of its own asking at 2 s doubling to a minute, so the stdio
server answers everything else meanwhile. Replies go through one shared
writer. A tasks/cancel here wakes the wait at once; a request the client
cancels (notifications/cancelled) is never answered; at most 64 wait at
once. A process-wide lock keeps a tool call and a wait from both sealing
one job.

Tested against the reference provider over HTTP: the result of a short
queue verifies; ping answers while a long one's result waits; a cancel
ends that wait within 3 s once it asks every 8 s; a cancelled request
gets no reply; tasks outlive the session. The CLI workflow names the new
tests, so a run that skips one fails.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Transaction-Science/open-standards!609
No description provided.