Repository navigation
Releases: Particle-Academy/prism-harness-py
Release list
v0.4.2
FileSessionStore writes survive a concurrent reader on Windows
- Fixed: on Windows, saving session state failed with
PermissionError(WinError 5) whenever another process had the
state file open, because Windows will not replace an open file. A write now retries the replacement only on Windows
permission errors, for up to 750 ms, and then raises the original error. On failure the previous state file stays
intact and the temporary file is removed; nothing is written partially or dropped silently. On Windows, state reads
open the file with delete sharing and hold it only for the read itself. - Other platforms are unchanged.
No breaking changes.
v0.4.1
narrow_requested_tools now applies to legacy sessions
- Fixed: in 0.4.0,
AgentRuntime(narrow_requested_tools=True)had no effect on legacy (no-agent) sessions, so a
caller-supplied tool list could still offer tools beyond the mode's list. With the option on, a legacy turn now
offers only tools in the mode's list. A requested tool outside it raisestool_not_available,"*"means the mode's
list, and an empty list offers no tools. - With the option off (the default), legacy behaviour is unchanged from 0.3.0, including an empty list falling back to
the mode's tools. Named agents were already narrow-only.
No breaking changes.
v0.4.0
Agent-scoped harness and an owner-proof file lock
Agents can now share one application without colliding. Existing applications keep their current behaviour, except
for the file lock format described under BREAKING CHANGE.
PrismHarness.for_agent("support")selects an isolated agent namespace for session keys, threads, task lists, tools,
modes and authorization. Choose a stable id; never derive it from a display name.- In a named namespace, duplicate tool and mode names raise
RegistrationCollision(registration_collision), naming
both owners. Replacement is explicit (replace/replace_factory/replace_provider). A caller-supplied tool list
can only narrow the mode, and built-ins are offered only when a mode lists them. AgentHarness.effective_tools(mode, participant, scope)andAgentRuntime.effective_tools(session, mode)return the
exact offered tool set, with each tool's owner and source. They run no handler, send no model request and create no
session or index entry. Expired entries may be purged, as any read does.ParticipantRefandParticipantAdaptersidentify non-user participants, andPendingSession.as_actor()binds a host
actor. A named subagent tool accepts onlytask, and a child cannot switch actor.- Opt-in hardening for the legacy namespace, off by default:
refuse_duplicates=Truerefuses duplicate legacy registrations instead of overwriting them;narrow_requested_tools=TrueonAgentRuntimestops a caller-supplied tool list from adding tools beyond the
mode's list.
Withoutnarrow_requested_tools, a caller-supplied list can still widen a legacy mode, and an empty list still falls
back to the mode's tools, as before; enabling it is recommended.
max_addresses_per_participant(default unlimited) caps indexed addresses withAddressLimitExceeded
(address_limit_exceeded), raised before any write. Indexed namespaces require a store whose lock covers its sharing
scope (index_lock_scope_unsupported). The memory store and the file store both qualify.FileSessionStore.pending_releases()andidentity_status()report a delayed lock release or unavailable host
identity, without doing any I/O.
BREAKING CHANGE: FileSessionStore locks are now owner-proof directories instead of timestamp files. A lock is
reclaimed only when its owner is provably dead on the same host, boot and process space, never just because its TTL
passed. STOP ALL WORKERS running 0.3.x or earlier (Python or TypeScript) before upgrading: no rolling upgrade is
supported. A leftover old lock file is refused with session_locked and the detail legacy_lock_requires_maintenance,
naming the path to inspect and archive. When host identity cannot be established, locking still works but crash
recovery waits for maintenance. On Windows the first lock in a process starts one fixed, read-only PowerShell metadata
query. Local filesystems only: SMB and NFS are unsupported.
v0.3.0
A failed model step is terminal
Before this release, when a model response finished with 'error' or 'unknown', the runtime still invoked the tool calls returned with it and took another model step.
- A response that finishes with 'error' or 'unknown' is recorded (assistant message, one step, its cost) and the run then fails with the new error code
model_step_failed, carrying the boundedfinish_reason('error' | 'unknown'). The message never includes model text. - No tool call from the failed response is invoked, and a later approval resume cannot make those calls dispatchable; they are recorded as refused.
- Valid stop and tool-call continuation is unchanged.
BREAKING CHANGE: a run whose model step failed now fails with ErrorCode model_step_failed instead of continuing or invoking that step's tools. Callers that relied on tools running after an error response will no longer see them run.
v0.2.1
v0.2.1 - Five README examples that could not run
No breaking changes.
No source changed at all; this release is the package page.
ModeRegistry, HarnessError, record_approval and TaskOutcome were used in
examples with no import line. All four are in prism_harness's all, so a
reader copying one of those blocks got a NameError on a name this package
exports -- which reads as a missing feature rather than a missing line of
documentation. A fifth block was a bare dict entry rather than a statement,
which Python will not parse.
Released as a patch rather than left for the next feature because the README IS
the PyPI page: the broken examples were being served to everyone who looked.
TaskOutcome is worth a note for anyone comparing the ports. The TypeScript
README's equivalent line passes the string 'done' and needs no import, because
TaskOutcome there is a union of string literals that exists at compile time
only. Here it is an enum, and converting through it at the call site is what
makes this port and the PHP reference refuse an outcome that is not one of the
four before anything reaches the durable list. The two READMEs read differently
on purpose.
v0.2.0
v0.2.0 - Structured turns
A turn whose answer is a document rather than prose.
response = runtime.send_structured(session, "Plan the release", schema)
response.structured # {"title": "Ship it", "steps": ["write", "test"]}
response.text # the document as the model wrote itIt is the same run as send() — the mode's system prompt, its tools, the step
budget, the events, the approvals. Your client receives the schema as
request.schema and returns what it parsed as LlmResponse.structured.
The assistant message is the raw document, and the parsed object rides along in
the row's additional_content under structured. A later turn replays the
conversation as messages and reads the text, so a thread holding a structured
answer reads like any other — and thr-0011..0013 in prism-parity's
harness-thread-rows corpus pin that shape across PHP, TypeScript and Python.
structured_unreadable— the text held no document at allstructured_schema_violation— a document with the wrong shape, carrying
problems: every way it missed, not the first
Nothing is coerced and nothing is trimmed to the fields that fit. An empty plan
settles a batch as done, which reads exactly like a considered answer of
"nothing to propose". The refused exchange is still recorded and the run marked
failed.
schema_problems() is exported if you want the check directly. It reads the
JSON Schema — declared types, required keys, enum members, array items, and keys
nobody declared where a schema closes itself — and passes $ref, allOf,
oneOf and the numeric and string facets rather than half-implementing them.
The same rules as the PHP reference, message for message.
True is not a number here: bool subclasses int, so an unguarded check
accepts true where a schema asks for a number, and PHP and TypeScript refuse
it without being asked. And a whole float reads as the integer JSON writes —
2.0 is 2 in every JSON document — rather than as repr() spells it. Both
are tested.
An error carrying a document now records its CODE as the run's failure, not its
message. A violation message names the values that missed, so the old behaviour
would have written pieces of the model's document into the run row and into
every listener's RunFailed event — in a package that keeps tool arguments out
of events on purpose.
v0.1.0
v0.1.0 - First release on PyPI
Durable agent sessions for Python: threads, session state and store drivers. The Python port of particle-academy/prism-harness. The distribution is prism-ai-harness; the package imports as prism_harness.
pip install prism-ai-harness
What it has
- Sessions rebuilt from a store on every call. The
ephemeralslot holds the active mode, selected model and run bookkeeping; thedurableslot holds threads and stored capabilities. - A store that reports itself volatile is refused for the durable slot.
MemorySessionStore(volatile) andFileSessionStore(durable: atomic writes and a cross-process lock).Session.key()is byte for byte the key the PHP reference builds, so a PHP app and a Python agent can share one store.- Threads store the rows the PHP reference stores. Positions are assigned inside the store lock, so concurrent turns do not overwrite each other.
- Attachments on a turn. One given by URL or file path, one that is not media, or one with nothing in it is refused with a
HarnessErrorcode. - Approvals. A mode names the tools a person must approve. Record a decision with
record_approval()and resume: approved calls run once, denied calls return their reason, and a call with no decision is refused. - Task lists with leases. Only the worker holding a task can release it, and an agent cannot mark its own task done.
- No runtime dependencies. Python 3.10 or later. The store contract is synchronous.
prism-parity's corpora pin the session key, thread rows, turn attachments and task claims against the PHP reference and the TypeScript port.