Studio Backend
Test Suites
The Test section of a project lets its team test agents automatically: a simulated user — an agent
that plays a human — talks to the agent under test, and agent evaluations score the conversation.
The conversations themselves are run by the bridge, the test-runner service of the promethist
repository; the platform defines what to run, starts it and reads the outcome back.
Building Blocks
| Concept | What it is |
|---|---|
Simulated user |
An agent of kind |
Agent evaluation |
An |
Test suite |
A named, project-scoped set of scenarios. Stored as a |
Scenario |
One agent under test, one simulated user — each optionally pinned to a revision — and the settings
they run with: |
Both agents of a scenario must belong to the suite’s project. An agent that is not pinned must have a
Published revision when the suite is saved, because the engine resolves a plain agent:{ref} against
Published only; a pinned revision may be in any active state then, since it can still be promoted.
Revisions count from 0.
maxTurnsPerSession is at least 1, or empty for no limit — the conversation then runs until a side
ends it or the timeout passes. The Studio form starts empty, and only the form can set no limit:
scenarios the AI Assistant or Claude create get 30 turns unless they ask for another limit, at most
500.
conversations is the bridge’s deviceCount and sessions its sessionCount: the bridge runs one
device per conversation and holds sessions sessions on each, one after another. A device keeps its ref
(bridge:{runId}:{deviceIndex}) across them, so those sessions are one engine user coming back — which
is what makes an agent’s cross-session memory testable without anyone in the loop. Each session is
reported as a bridge run of its own, carrying the run id of the whole conversation as multisessionId,
and each is a conversation the account pays for: conversations x sessions is capped at 50 per
scenario. sessionDelaySeconds is the bridge’s sessionDelay, the pause between two sessions, and it
is what gives the engine time to aggregate the one that just ended.
sessionTimeOffset is an ISO-8601 duration between consecutive sessions, which the bridge turns into
the engine’s timeOffset per session: the last session runs at the real time and each earlier one is
backdated by another multiple of it, so the gaps are even. The platform parses it on save with the same
Duration.parse the bridge uses, and refuses a negative one — toProperties() throws on the bridge
only once the run has started, which would otherwise surface as a failed run long after the scenario was
accepted.
Running
POST /api/v1/project/{projectId}/test-suites/{id}/run (Editor) starts a suite (TestSuiteService):
-
The suite’s project must admit sessions with no signed-in user, which is what every bridge session is. One that requires sign-in, or enforces external roles with an identity provider listed, is refused outright: the engine would turn away every conversation, the simulated user’s as well as the agent under test’s, and no scenario change helps. The project is read on a best-effort basis — if it cannot be read, the run goes ahead and the engine decides. The one exception is a suite whose every scenario carries
testerAuthorizationandtestedAgentAuthorizationin its bridge configuration, which the bridge sends as the credentials of the two conversations: the platform never writes them (a save rebuilds the configuration without them), but whoever has access to the bridge can, and such a suite is left to the engine. -
Every scenario is checked against what the engine would run. An agent that is not pinned needs a
Publishedrevision; a pinned revision must exist and bePrevieworPublished— the engine starts noDraftfor a bridge session, which carries no signed-in user. All problems are reported at once and nothing starts. The check runs on every start, because an agent can be archived or reverted after the suite was saved, and the engine’s refusal would otherwise surface only as a run that waits out its timeout. -
The suite is claimed in MongoDB, so a second start — a double click — is refused while a run is starting. A claim older than
promethist.testing.claim-stale-secondsno longer blocks. -
BridgeClienttriggers the test case on the bridge,POST {bridge}/run/test-case/{id}?device-delay={ms}¶llelism={n}, with the token frompromethist.testing.bridge-secret-token. The bridge records the runs and works through them asynchronously, so the call returns as soon as the work is queued. -
Each conversation connects to the engine with the agent under test’s pipeline key —
agent:{ref}, oragent:{ref}.{revision}when pinned — and runs in test mode.
Nothing about a run’s state is copied into the platform. Status and verdict are derived from the bridge’s run documents on every read:
| Value | Meaning |
|---|---|
Run status |
|
Suite status |
|
Suite result |
|
The verdict of a run is filled in by the bridge once the agent evaluations have run; until then the run
reports its verdict as pending. Only a run that ended cleanly carries one: the bridge fetches a verdict
for every run with a session, and stores the engine’s "no evaluations" as passed, including for a
conversation that never got a turn. A single run (GET .../test-suites/{id}/runs/{runId}) adds every
evaluation result with its reasoning and evidence, and the transcript — both sides of the conversation,
read from the simulated user’s session. A Number result carries its definition’s minValue and
maxValue.
The single run also carries score, one figure for the conversation from 0 to 100, which the Studio shows
in the Metrics tab. The platform works it out on every read and stores it nowhere: each Number
result is scaled to its definition’s range — 4 on 0–5 counts as 80 — and the scaled results are averaged
with equal weight. Every number counts as higher-is-better, and a 0 is a score like any other. Bool,
Text and TextList results are left out, and so is a number whose definition no longer exists or whose
range is empty; with nothing left, there is no score. Results are joined to their definitions by id,
the latest result per definition, as in the metrics table. The run list (/runs) carries no score.
The bridge’s own weightedAverageScore is a different figure: raw values weighted by aggregateWeights
on the bridge configuration, which the platform never sets, so it stays empty for suites saved here.
GET .../test-suites/{id}/export?format=xlsx|csv|json returns the results as an Excel workbook, a zip
of CSV files or JSON. Every run carries its score there too: Score (%), the last column of the runs
table, or score in JSON.
Test Mode and Usage
The agent under test’s session is a test-mode session. Test-mode sessions are excluded from the
analytics, and their minutes are reported separately (testMinutes) in the account’s usage report
(Usage and Billing). The simulated user’s own session is an ordinary session of a TEST agent, which
the analytics leave out by agent kind instead
(Who Is Counted).
Configuration
| Property | Role |
|---|---|
|
The bridge’s REST API — |
|
The bridge token, from the |
|
The engine the scenarios connect to. |
|
Run tuning, see Configuration. |
Live Agent Tests
Besides test suites, the MCP tool test_agent lets Claude hold a live text conversation with an agent,
one turn per call (McpTestAgentTools). It is the only MCP tool with no REST endpoint behind it:
-
It drives the engine’s public pipeline socket, the WebSocket
{promethist.mcp.engine-url}/socket/pipeline/agent:{ref}[.{revision}](wss://for anhttps://engine URL), with text input and JSON output: each turn is one text frame, and the tool reads the elements until the turn’sexit— the only reliable end of a turn, because the engine still plans and may speak again after the reply. A turn has a 90-second budget. -
The engine endpoint itself asks for no credentials, so the tool performs its own authorization: the caller needs
Vieweron the agent’s project, and a plain ref runs only thePublishedrevision. -
Sessions are anonymous, because the engine does not accept the MCP token. For projects the engine closes to anonymous sessions — sign-in required, or external roles enforced with an identity provider listed, the same rule the test-suite check uses — the tool signs in with the engine test identity (
promethist.testing.engine-basic-identity). The engine runs aDraftfor no anonymous session, so drafts are tested after promotion toPreview. -
When the identity does not get a turn through, the error says why. An engine from 2026.42.1 on answers credentials it does not know with
401, reported as an unrecognised identity: the configured value is not one this engine has. An identity the engine knows but does not admit to the project gets a refusal, reported as not enabled for the project. A configured value that is not HTTP Basic credentials at all is never sent, and the refusal says so. -
A conversation keeps one connection (
TestAgentConversations). The engine holds the agent’s planning in its live realtime session; a new connection per turn would lose it after every line and make the engine treat every line as a disconnect and a new session start. The socket a first turn opens stays open, and the engine session id travels back to the caller assession_id(sessionId=<uuid>); the next call with it sends its line over the same socket. A conversation belongs to its caller and its agent: another caller’s or another agent’s call with its id is refused, and so is a line sent while a turn is still running. -
The connection closes after four minutes without a turn (below the ingress’s 300-second read timeout on
/socket), after a turn that did not end with its ownexit(cut off, failed, or the call cancelled), when the agent ends the conversation, and on shutdown; a caller keeps at most five conversations open, and the pod 200, closing the least recently used idle one when a new one needs room. Closing is a plain socket close, never#exit: the engine sees the client hang up and about 30 seconds later runs the session’s end-of-conversation work (post-session integrations, evaluations at the end of the conversation) — once per conversation, up to about five minutes after the last turn unless the agent ended it. -
A
session_idwith no live connection — closed, or held by another pod or a restarted one, since the registry lives in the pod’s memory — reconnects to the same engine session, which keeps its messages but not the earlier turns' planning, and the result says so. A conversation that closed on this pod is remembered: its reconnect is still refused to another caller or agent, and keeps the locale, simulated user, time zone and run label of its first turn. -
On the socket a line starting with
#is an engine action (#ready:key=would start a session with any agent past the tool’s guard,#exitwould end the session), so#intro, the agent’s opening, is the only such line the tool sends. -
The engine decodes the socket’s query twice (the decoded query is split on
&and decoded again), so a value carrying an encoded&would arrive as parameters of its own,testMode=falseamong them. The locale must be one language tag (de_DEis sent asde-DE), the simulated user and the run label may use only letters, digits and. _ : @ -, and a time zone with` in it is refused, since `arrives as a space. -
device_refis who the simulated user is, and conversations sharing one are the same engine user coming back;time_offset, an ISO-8601 duration, backdates a whole conversation through the engine’stimeOffset, which the engine reads only because the tool sendstestMode=true. Only a negative offset is accepted: the engine adds it to its own clock, so the positive one a caller writes for "ten days ago" would date the session in the future instead and say nothing about it. Like the locale and the simulated user, the offset is fixed when the conversation connects. -
The result reports when the agent has ended the conversation.
-
Live tests run real engine sessions and count towards usage like any other conversation.
Memory Snapshots
The MCP tools save_memory_snapshot and restore_memory_snapshot (McpMemorySnapshotTools) copy what a simulated
user remembers and put that copy back, so the last session of a multi-session test can be re-run without
talking through the ones that built it up. They drive the engine’s memory endpoints through
EngineMemoryClient: GET /api/user?deviceRef= resolves the device to its user, creating it if the
ref is new, then POST /api/memory/state?userId= stores a state and POST /api/memory/state/{stateId}?userId=
restores one.
-
Restoring is the engine’s own delete-then-write: it clears the user’s memory before loading the state, so whatever the sessions since the snapshot left behind is gone. The tool is confirmation-gated.
-
The engine’s
/api/memoryendpoints arepermitAlland take a bareuserId, so what a caller can reach is bounded by the platform instead.agent_refauthorizes the call — the caller needsVieweron that agent’s project, astest_agentrequires before it drives one — and everydevice_refis forced under themcp:namespace before it is resolved, the same prefixtest_agentstamps on its own. A ref written as another client’s (client:,bridge:) therefore names a device of this connector’s, not the one it looks like. -
A session is aggregated about 30 seconds after its connection closes, so a snapshot taken straight after the last turn can hold nothing. The result reports how many memory units it holds and says so when there are none.
Staff Test Harness
The staff-only MCP tools create_bridge_configuration, list_bridge_configurations and
create_bridge_test_case (BridgeHarnessService) write the bridge’s documents directly into the
bridge’s collections in the shared database, reproducing the bridge’s schema, with the values of
promethist.mcp.bridge.*. They never start a run: the tool hands back the exact command, which the
person runs with their own bridge key, so no shared credential appears in a conversation.