wai-quantum-service: the runs are MCP tasks, each task its job #615

Merged
dcharlot merged 1 commit from wai/quantum-service-tasks into main 2026-11-04 13:06:25 -05:00
Owner

The job service's three runs are MCP tasks (protocol 2025-11-25). A client that speaks tasks gets the job's id at once and follows the job by it, instead of holding a call open for 50 s and then switching to get_quantum_job.

It is one commit on main, after the idempotency key (#607) and the rerun (#613). It was written on top of their earlier commits and replayed onto main; the patch is unchanged.

What changes

wai-quantum-service (src/mcp.rs, src/http.rs, src/jobs.rs)

  • Declared: initialize declares tasks (cancel, and requests.tools.call) when the version it settles on is 2025-11-25, and not before. run_quantum_circuit, run_quantum_set and run_on_target declare execution.taskSupport: "optional".
  • The task is the job.
    • tools/call with task admits the job exactly as the call does (the idempotency key, the target and set checks, the verdict asked), then answers at once with { "task": … }. Its taskId is the job's id.
    • A program that does not read makes no task. The answer is a JSON-RPC error (-32602) whose data is the refusal, with its codes and verdict.
    • A tool that does not run as a task gets -32601. A dry run gets -32602: it answers at once.
  • tasks/get gives the job as a task.
    • working while queued or running, with statusMessage saying which; completed; cancelled; or failed for a job that failed or that the service interrupted, with the error.
    • createdAt and lastUpdatedAt are in ISO 8601, pollInterval is 2 s, and ttl is the service's retention. A job outlasts that from its creation, because it is kept that long after it ends.
  • tasks/result gives get_quantum_job's answer for the ended job. isError is set where the job failed or was interrupted, and _meta names the task (io.modelcontextprotocol/related-task).
    • The spec says it blocks until the task ends. A plain reply cannot be held that long here: the tunnel in front ends a reply held silent for long.
    • So to a client that takes text/event-stream, the answer is an event stream. It is kept open with keep-alives, and its one event, when the job ends, is the JSON-RPC answer.
    • A client that takes only JSON is held up to 50 s, then told to poll tasks/get.
  • tasks/cancel cancels the job and answers with the cancelled task. A task that has ended is refused with -32602.
  • Binding: there is no tasks/list. The public service binds a task to no one, so a list would give every client's tasks. A task's id, 128 random bits, is what reads it, as a job's id is, and the README says so as the spec asks. On a private deployment that takes API keys, a task is its key's alone: tasks/get, tasks/result and tasks/cancel from another key find no task (Service::owned_by).
  • Refactor: the tool call's answer (verdicts, views) moves into tool_answer, shared by tools/call and tasks/result. The run admission is shared by the call and the task (admit_run, run_refusal).
  • The instructions, README, CHANGELOG, the tool snapshot and the agent docs are updated.

Tests

  • mcp::tests::a_run_as_a_task_is_its_job:
    • the capability under 2025-11-25 and not under 2025-06-18;
    • execution on exactly the three runs;
    • a task made working (queued), with its ttl, pollInterval and timestamps (iso checked at 1970 and at 2000-02-29);
    • tasks/get; tasks/cancel once, then refused; tasks/result of the cancelled task, with its _meta;
    • an unknown task; a tool that is not a task; a dry run; a program that does not read (no task, the refusal in data); run_on_target with no target; and tasks/list not offered;
    • with API keys on, another key's tasks/get, tasks/result and tasks/cancel all find no task, and its cancel cancels nothing.
  • tests/tasks.rs, against the service's binary:
    • a Bell pair made as a task and followed by tasks/get to completed; tasks/result gives the job with its task in _meta, and its receipt verifies;
    • a 16-qubit job asked for its result while it works answers text/event-stream, whose event is the finished answer.

Checked

On the build host, on this commit as replayed onto #613's:

  • the service's tests, both task tests and the restart test among them, and its clippy with -D warnings;
  • the command line's tests, which hold the committed tool lists and agent docs to what the binary generates;
  • check_schemas.py over every schema file, both tool lists and a release build's answers.

The public-name check is clean, and so are this description and the commit message.

🤖 Generated with Claude Code

The job service's three runs are MCP tasks (protocol 2025-11-25). A client that speaks tasks gets the job's id at once and follows the job by it, instead of holding a call open for 50 s and then switching to `get_quantum_job`. It is one commit on main, after the idempotency key (#607) and the rerun (#613). It was written on top of their earlier commits and replayed onto main; the patch is unchanged. ## What changes **wai-quantum-service** (`src/mcp.rs`, `src/http.rs`, `src/jobs.rs`) - **Declared:** `initialize` declares `tasks` (`cancel`, and `requests.tools.call`) when the version it settles on is 2025-11-25, and not before. `run_quantum_circuit`, `run_quantum_set` and `run_on_target` declare `execution.taskSupport: "optional"`. - **The task is the job.** - `tools/call` with `task` admits the job exactly as the call does (the idempotency key, the target and set checks, the verdict asked), then answers at once with `{ "task": … }`. Its `taskId` is the job's id. - A program that does not read makes no task. The answer is a JSON-RPC error (`-32602`) whose `data` is the refusal, with its codes and verdict. - A tool that does not run as a task gets `-32601`. A dry run gets `-32602`: it answers at once. - **`tasks/get`** gives the job as a task. - `working` while queued or running, with `statusMessage` saying which; `completed`; `cancelled`; or `failed` for a job that failed or that the service interrupted, with the error. - `createdAt` and `lastUpdatedAt` are in ISO 8601, `pollInterval` is 2 s, and `ttl` is the service's retention. A job outlasts that from its creation, because it is kept that long after it ends. - **`tasks/result`** gives `get_quantum_job`'s answer for the ended job. `isError` is set where the job failed or was interrupted, and `_meta` names the task (`io.modelcontextprotocol/related-task`). - The spec says it blocks until the task ends. A plain reply cannot be held that long here: the tunnel in front ends a reply held silent for long. - So to a client that takes `text/event-stream`, the answer is an event stream. It is kept open with keep-alives, and its one event, when the job ends, is the JSON-RPC answer. - A client that takes only JSON is held up to 50 s, then told to poll `tasks/get`. - **`tasks/cancel`** cancels the job and answers with the cancelled task. A task that has ended is refused with `-32602`. - **Binding:** there is no `tasks/list`. The public service binds a task to no one, so a list would give every client's tasks. A task's id, 128 random bits, is what reads it, as a job's id is, and the README says so as the spec asks. On a private deployment that takes API keys, a task is its key's alone: `tasks/get`, `tasks/result` and `tasks/cancel` from another key find no task (`Service::owned_by`). - **Refactor:** the tool call's answer (verdicts, views) moves into `tool_answer`, shared by `tools/call` and `tasks/result`. The run admission is shared by the call and the task (`admit_run`, `run_refusal`). - The instructions, README, CHANGELOG, the tool snapshot and the agent docs are updated. ## Tests - **`mcp::tests::a_run_as_a_task_is_its_job`:** - the capability under 2025-11-25 and not under 2025-06-18; - `execution` on exactly the three runs; - a task made `working` (queued), with its `ttl`, `pollInterval` and timestamps (`iso` checked at 1970 and at 2000-02-29); - `tasks/get`; `tasks/cancel` once, then refused; `tasks/result` of the cancelled task, with its `_meta`; - an unknown task; a tool that is not a task; a dry run; a program that does not read (no task, the refusal in `data`); `run_on_target` with no target; and `tasks/list` not offered; - with API keys on, another key's `tasks/get`, `tasks/result` and `tasks/cancel` all find no task, and its cancel cancels nothing. - **`tests/tasks.rs`, against the service's binary:** - a Bell pair made as a task and followed by `tasks/get` to `completed`; `tasks/result` gives the job with its task in `_meta`, and its receipt verifies; - a 16-qubit job asked for its result while it works answers `text/event-stream`, whose event is the finished answer. ## Checked On the build host, on this commit as replayed onto #613's: - the service's tests, both task tests and the restart test among them, and its clippy with `-D warnings`; - the command line's tests, which hold the committed tool lists and agent docs to what the binary generates; - `check_schemas.py` over every schema file, both tool lists and a release build's answers. The public-name check is clean, and so are this description and the commit message. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
wai-quantum-service: the runs are MCP tasks, each task its job
All checks were successful
wai-quantum views in agent hosts / does this change touch it (pull_request) Successful in 8s
wai-quantum studio and desktop / does this change touch it (pull_request) Successful in 14s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (pull_request) Successful in 39s
wai-quantum service / service energy · linux packages (pull_request) Successful in 1m45s
wai-quantum docs, course and locales / the Studio's words in every locale · linux (pull_request) Successful in 9s
wai-quantum command line / command line · macos (pull_request) Successful in 2m25s
wai-quantum command line / command line · linux (pull_request) Successful in 2m58s
wai-quantum contract / the policy is linked, and the deprecations page reads (pull_request) Successful in 4s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 5m32s
wai-quantum editors / editors · macos (pull_request) Successful in 2m22s
wai-quantum docs, course and locales / the course and the command line's languages · linux (pull_request) Successful in 2m39s
wai-quantum editors / editors · linux (pull_request) Successful in 2m13s
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 6m32s
wai-quantum contract / API compatibility · linux (pull_request) Successful in 5m19s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 8m56s
wai-quantum service / service · linux (pull_request) Successful in 4m51s
wai-quantum views in agent hosts / views · a scripted host (pull_request) Successful in 3m6s
wai-quantum views in agent hosts / views · the page and its engine (pull_request) Successful in 4m3s
wai-quantum service / service · macos (pull_request) Successful in 4m34s
wai-quantum for Python / python · linux (pull_request) Successful in 5m11s
wai-quantum for Python / python · macos (pull_request) Successful in 4m41s
wai-quantum studio and desktop / desktop app · windows (pull_request) Successful in 9m57s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 11m50s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 13m17s
wai-quantum studio and desktop / desktop app · linux (pull_request) Successful in 6m20s
wai-quantum studio and desktop / desktop app · macos (pull_request) Successful in 6m41s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 15m8s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 8m34s
wai-quantum contract / schemas and the committed tool lists · linux (pull_request) Successful in 13m25s
wai-quantum docs, course and locales / every command and snippet in the docs · linux (pull_request) Successful in 43m58s
037fa0a878
A client that speaks MCP 2025-11-25 may call run_quantum_circuit,
run_quantum_set and run_on_target as tasks. initialize declares `tasks`
(cancel, requests.tools.call) from that version on, and the three declare
execution.taskSupport "optional". The task is the job: tools/call with
`task` admits it as the call does and answers at once, its taskId the
job's id; a program that does not read makes no task, the refusal in the
error's data. tasks/get gives the job as a task (working, completed,
cancelled, failed for a job that failed or was interrupted), its ttl the
service's retention. tasks/result gives get_quantum_job's answer for the
ended job, naming the task in _meta; asked early by a client that takes
text/event-stream it is a stream kept open until the job ends, since the
proxies end a reply held silent, and any other client is held 50 s and
told to poll. tasks/cancel cancels the job and refuses one that has ended.

There is no tasks/list: the public service binds a task to no one, its
id (128 random bits) being what reads it; where the service takes API
keys a task is its key's alone. tools/call's answer moves into
tool_answer, shared with tasks/result, and the run admission into
admit_run and run_refusal, shared with the task.

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!615
No description provided.