Skip to content

Releases: Particle-Academy/prism-harness-ts

v0.9.2

Choose a tag to compare

@github-actions github-actions released this 29 Oct 23:14

FileSessionStore writes survive a concurrent reader on Windows

  • Fixed: on Windows, saving session state failed with EPERM 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. State reads hold the file only for the read itself.
  • Other platforms are unchanged.

No breaking changes.

v0.9.1

Choose a tag to compare

@github-actions github-actions released this 29 Oct 17:40

narrowRequestedTools now applies to legacy sessions

  • Fixed: in 0.9.0, AgentRuntimeOptions.narrowRequestedTools: 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 raises tool_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.8.0. Named agents were already narrow-only.

No breaking changes.

v0.9.0

Choose a tag to compare

@github-actions github-actions released this 29 Oct 14:56
55236a1

Agent-scoped harness, an owner-proof file lock, and a Session.lock that runs your callback once

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.forAgent('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. Per-agent options are set through
    HarnessOptions.agents, AgentRuntimeOptions.agents and ToolAuthorizer agents.
  • In a named namespace, duplicate tool and mode names throw RegistrationCollision (registration_collision),
    naming both owners. Replacement is explicit (replace / replaceFactory / replaceProvider). A caller-supplied tool
    list can only narrow the mode, and built-ins are offered only when a mode lists them.
  • AgentHarness.effectiveTools(mode, participant, scope) and AgentRuntime.effectiveTools(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.
  • ParticipantRef and ParticipantAdapters identify non-user participants, and PendingSession.asActor() binds a host
    actor. A named subagent tool accepts only task, and a child cannot switch actor.
  • Opt-in hardening for the legacy namespace, off by default:
    • RegistryOptions.refuseDuplicates refuses duplicate legacy registrations instead of overwriting them;
    • AgentRuntimeOptions.narrowRequestedTools stops a caller-supplied tool list from adding tools beyond the mode's
      list.
      Without narrowRequestedTools, a caller-supplied list can still widen a legacy mode, as it did before; enabling it is
      recommended.
  • maxAddressesPerParticipant (default unlimited) caps indexed addresses with AddressLimitExceeded
    (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.
  • Fixed: Session.lock / FileSessionStore.withLock re-ran a callback that threw EEXIST and then reported
    session_locked, which said the callback never ran. The callback now runs at most once, and its errors reach you
    unchanged. A standalone process waiting for a lock no longer exits before the wait ends.
  • FileSessionStore.pendingReleases() and identityStatus() 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.8.x or earlier (TypeScript or Python) before upgrading: no rolling upgrade is
supported, and on Windows a running old worker's lock can be replaced. 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.8.0

Choose a tag to compare

@github-actions github-actions released this 27 Oct 22:01

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 bounded finishReason ('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 HarnessErrorCode 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.7.0

Choose a tag to compare

@github-actions github-actions released this 15 Oct 21:29

@particle-academy/prism-harness 0.7.0

No breaking changes.

Threads are append-only logs, so recording a message costs its own size
instead of a rewrite of the whole conversation.

  • FileSessionStore keeps each thread as one JSON line per message
    (.jsonl beside the payload), appended in one write and fsynced.
    Before this, every Thread.record() re-read, re-parsed and rewrote the
    whole thread: quadratic over a session, measured at 9 MB per row in
    Genie 2.
  • A thread stored by 0.6.0 is read as before and moved into the log the
    first time it grows. Nothing to migrate by hand.
  • A line cut off by a crash is dropped and the next append starts after
    the last complete line; a damaged line anywhere else is refused with
    unmappable_content.
  • New optional capability for custom stores: AppendLogStore, with
    supportsAppendLog(). A store without it keeps working through
    get/put as before.
  • messages() and count() are unchanged for callers.

The thread log is not yet read by the PHP and Python ports, so a worker
in another language pointed at the same store directory does not see it.

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 07 Oct 19:15

v0.6.0 - Node 22 is the floor

Breaking changes: the declared Node floor moves from 20 to 22. Node 20
reached end of life on 2026-04-30, and engines >=20 told consumers that running
there was sanctioned. Be exact about what this does: npm checks engines and
emits EBADENGINE, but it is a WARNING and the install still proceeds. It is a
refusal only where you set engine-strict=true; pnpm refuses by default. No
source changed, so the code would most likely still run on Node 20 -- what
changed is that it is no longer claimed to.

Tested on Node 22, 24 and 26, on Linux and Windows.

This release also corrects the README, which said the package was
private while coordinated parity work was in progress. It is published, and
has been. The README ships inside the tarball and is the npm page, so that
sentence was being served to everyone who looked.

It also fixes four README examples that could not run. ModeRegistry,
HarnessError and recordApproval were used with no import line -- all three are
real exports, so a reader copying one of those blocks got a ReferenceError on a
name this package has. HarnessError was the worst: without it the instanceof
does not narrow, so three further lines of that example were errors too. The
fourth was a bare guarded: { ... }, object property, which is not compilable
TypeScript at all; a parse failure there had been masking the other three.

Every TypeScript example in this README is now typechecked against the
package's own source.

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 21 Sep 01:58

v0.5.0 - Structured turns

A turn whose answer is a document rather than prose.

const response = await runtime.sendStructured(session, 'Plan the release', schema);

response.structured; // { title: 'Ship it', steps: ['write', 'test'] }
response.text;       // the document as the model wrote it

It 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 response.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 all
  • structured_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.

schemaProblems() 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.

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 run.failed event — in a package that keeps tool arguments out
of events on purpose.

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 14 Sep 17:40

v0.4.0 - Runs stored in the reference's shape; approvals resume where they stopped

Approvals

A pending approval now works with a real provider.

  • A step that calls a gated tool gives each such call an approval id (apr_...) and records it on the assistant row. The calls that need nobody run straight away, instead of the whole step waiting.
  • recordApproval() records the decision. On the next send(), recorded decisions are acted on before the model is asked anything:
    • approved calls run once;
    • denied calls return their reason;
    • a pending call with no decision is refused with "No approval response provided".
  • Before, the model was asked again. A real provider issues a repeated call under a new id, so a decision never matched it.
  • A call that has a result never runs again.
  • Resolution holds the session lock, so two workers resuming the same session run an approved call once.
  • An approved call whose tool is no longer offered to the run, or that the authorizer now refuses, is recorded as not run. Before, it failed every later turn.

Record every decision, then resume:

for (const pending of response.pendingApprovals) {
  await recordApproval(session, pending.id, true);
}
await runtime.send(session, '');

Rows

The thread now holds what prism-harness (PHP) stores:

  • an assistant row with each tool call's arguments, result_id, reasoning_id and reasoning_summary, its additional_content and its tool_approval_requests;
  • one tool_result row per step, with tool_results and tool_approval_responses.

Your client receives the thread through threadView(). Consecutive tool result rows reach it as one, so each output goes to the provider once. LlmToolCall takes optional resultId, reasoningId and reasoningSummary. prism-parity's harness-thread-rows corpus pins these rows against the reference and the Python port.

assistantRow, toolResultRow, toolResultEntry and threadView are exported.

Breaking

  • PendingApproval.id is the approval id, not the tool call id. toolCallId is new.
  • A client reading request.messages gets tool_results on a tool result row, not one row per call. A failed tool's result has no failed flag.
  • A caller that holds the session lock around send() gets SessionLocked when the resume runs approved calls.

Rows recorded by 0.3.0, including a pending approval, still read and resume.

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 14 Sep 16:18

v0.3.0 - Each step records what the next one has to send back

Tool arguments and provider state are recorded

Each step of a run recorded the assistant's tool calls as an id and a name only. The next step's request is built from the thread, so a client had no input to send back for a tool_use and no arguments for a function_call.

The assistant row now carries each call's arguments, and an additional_content map from a new optional field on LlmResponse:

const client: LlmClient = async (request) => {
  const response = await callYourProvider(request);

  return {
    text: response.text,
    toolCalls: response.toolCalls,
    finishReason: response.finishReason,
    // Provider state the next request must send back, e.g. Anthropic's
    // thinking and thinking_signature.
    additionalContent: response.additionalContent,
  };
};

additional_content uses prism's AssistantMessage key, so a client built on @particle-academy/prism 0.2.0 can pass response.additionalContent straight through and replay it from request.messages.

Rows recorded before this release read the same as before. Run events and the run record still carry tool names only.

Also

  • The README's provider options example uses { thinking: { type: 'adaptive' } }. { thinking: { enabled: true } } is not a shape Anthropic accepts.
  • The publish check waits up to fifteen minutes for npm to serve a release.

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 14 Sep 07:49

v0.2.0 - Turns carry attachments; modes set provider options

Attachments on a turn

runtime.send() takes an optional trailing list of media to send with the prompt: prism-ts media objects, or media already serialized.

await runtime.send(session, 'What is wrong with this layout?', undefined, undefined, [
  Image.fromBase64(screenshot, 'image/png'),
]);

Attachments are stored with the turn in the shape UserMessage.fromObject() rebuilds. A turn without attachments is stored exactly as before.

Refused before a run starts, with a HarnessError code:

Code When
attachment_by_reference built from a URL (including "", and a URL whose content was already fetched), or read from a file
attachment_not_media anything other than an image, document, audio or video
attachment_empty no bytes (base64 that decodes to nothing counts as none), no file id, no chunks
attachment_without_prompt attachments with an empty prompt

A media object is asked isUrl() and isFile() before it is serialized. Media read from a local path serializes as bytes with no path, so the serialized form alone would admit it.

The same rules and codes apply in the PHP reference (prism-harness v0.10.0) and the Python port, pinned by prism-parity's harness-turn-attachments corpus.

Provider options per mode

A mode can declare provider_options. They reach the model client unchanged as request.providerOptions, on every step. A value that is not a map is refused with mode_malformed.

LlmRequest now always carries providerOptions (an empty map when the mode declares none). A client that ignores the field is unaffected.