barrel_mcp_tasks (barrel_mcp v4.0.0)
View SourceLong-running operation registry (MCP tasks).
Tools registered with task_support => optional | required (long_running => true is the old spelling of optional) can answer with a taskId instead of a result: at once for a legacy client, after the inline window for a modern one. The worker continues in the background; clients poll via tasks/get, enumerate via tasks/list (legacy), and abort via tasks/cancel. State transitions emit notifications/tasks/status on the session's SSE channel. The mode rule lives in the transports (barrel_mcp_http_engine:handle_async_tool_call, barrel_mcp_protocol:task_plan), not here: this module is the table and the lifecycle.
Tasks live in a protected ETS table keyed by TaskId, which is crypto-random and so unique on its own. Who owns a task is a field rather than part of the key: a task id is the durable handle a client holds, and it has to be resolvable without knowing what created it.
The owner is opaque here, and differs by era. A legacy task is owned by its session id; a modern one has no session, so it is owned by the authenticated principal. Either way every lookup matches on it, so one owner cannot reach another's tasks.
A periodic sweep evicts terminal tasks (success / error / cancelled) older than ?TASK_TTL.
Sections, in file order
- Public API: create, get, list, finish, fail, cancel,
set_worker/3,await_input/5forinput_required. - gen_server: every transition, the sweep, generation fencing so a late worker cannot revive an expired task.
- Internal: rendering per era (
ttlversusttlMs), the error object, expiry.
Summary
Functions
Park a task on the input its handler asked for.
Block until a task reaches a terminal state, then return what the underlying request would have returned.
Mark a task as cancelled and notify the client. Sends {cancel, RequestId} to the worker pid (if recorded) so cooperative arity-2 handlers can abort.
Create a new running task. Returns the task id.
Whether this client can be handed a task at all. A modern one must have declared the extension; a legacy one negotiated tasks in the handshake.
Record failure: store the error and emit notification.
Record success: store the result and emit notifications/tasks/status.
Read a task, rendered for the handshake era.
Read a task rendered for a given era. The retention field is named ttl through 2025-11-25 and ttlMs in the extension.
Every task an owner holds, rendered for the legacy era (tasks/list exists only there).
Whether a call to ToolName runs inline, becomes a task, or is refused, given what the tool declared and what the client can be handed. Every transport asks this; only the answer's shape differs.
Which of these task ids the owner actually holds.
The originating request params recorded for a task, if any.
Attach the worker to a task created before it, so cancel and expiry can reach the process; carries the request id for MRTR resumption.
Start the task table owner, registered as barrel_mcp_tasks.
Record answers a client supplied for a task through tasks/update. Merged rather than replaced, so a client answering one key at a time does not drop the others.
Functions
Park a task on the input its handler asked for.
The asking worker has already returned, so everything needed to run the handler again is stored: the originating params, the handler's own state, and which keys are outstanding.
-spec await_result(term(), binary(), timeout()) -> {ok, map()} | {error, not_found} | {error, timeout}.
Block until a task reaches a terminal state, then return what the underlying request would have returned.
The wait happens in the calling process, never in the tasks server: that same process has to accept the transition that ends the wait, so blocking it would deadlock. The server only records who to tell.
Mark a task as cancelled and notify the client. Sends {cancel, RequestId} to the worker pid (if recorded) so cooperative arity-2 handlers can abort.
-spec create(SessionId :: binary() | undefined, Method :: binary(), Opts :: map()) -> {ok, binary()} | {error, too_many_tasks}.
Create a new running task. Returns the task id.
-spec enabled(barrel_mcp_ctx:ctx()) -> boolean().
Whether this client can be handed a task at all. A modern one must have declared the extension; a legacy one negotiated tasks in the handshake.
Record failure: store the error and emit notification.
Record success: store the result and emit notifications/tasks/status.
-spec get(SessionId :: binary() | undefined, TaskId :: binary()) -> {ok, map()} | {error, not_found}.
Read a task, rendered for the handshake era.
Read a task rendered for a given era. The retention field is named ttl through 2025-11-25 and ttlMs in the extension.
Every task an owner holds, rendered for the legacy era (tasks/list exists only there).
-spec mode(binary() | undefined, barrel_mcp_ctx:ctx() | undefined) -> inline | refuse | {task, escalate} | {task, immediate}.
Whether a call to ToolName runs inline, becomes a task, or is refused, given what the tool declared and what the client can be handed. Every transport asks this; only the answer's shape differs.
forbidden, or the client never declared the extension and the tool only saidoptional:inline.requiredwithout the extension:refuse, which the caller renders as the missing-capability error.- otherwise a task:
{task, escalate}in the modern era, where the call is answered in place if the tool beats the inline window, and{task, immediate}in the handshake era, where the handle goes back at once.
Which of these task ids the owner actually holds.
An id naming no task and an id belonging to someone else give the same answer, so a caller cannot use the difference to learn that a task exists.
The originating request params recorded for a task, if any.
-spec set_worker(binary() | undefined, binary(), #{worker := pid(), request_id => integer() | binary()}) -> ok | {error, not_found}.
Attach the worker to a task created before it, so cancel and expiry can reach the process; carries the request id for MRTR resumption.
Start the task table owner, registered as barrel_mcp_tasks.
Record answers a client supplied for a task through tasks/update. Merged rather than replaced, so a client answering one key at a time does not drop the others.