The engine protocol is the engine contract as JSON-RPC messages. The runner drives every engine over it. To run an engine in another process, machine, or language, see Engine servers.
Reference
connectEngine
Turns an engine server into an engine handle, the value a target’s engine
takes.
The runner reads the manifest at config load, before any server runs. A server whose manifest differs fails its first connection.
A handle opens its connections where the harness calls the engine. In the
runner, one connection per target lives from prepare to finish, and
opens only when the server answers lifecycle/prepare or
lifecycle/finish. In each worker, one connection lives from init to
dispose. A call outside init..dispose fails with INVALID_STATE.
A worker that never ran init has no connection, so the server gets no
lifecycle/dispose for it.
Whether two connections share state is the server’s choice: a spawned
server gets a process per connection, and a loopback or socket server may
serve them all from one engine.
With resolveLocators, each locate observes and resolves the expression
with resolveExpression.
fixtures, validateApp, secrets, and session hooks run on the client;
a session hook replaces the server’s method of the same name. See
Keep host work on the client.
lifecycle/prepare, lifecycle/init, and lifecycle/finish carry the
run’s environment, model keys and E2E_SECRET_* values included, and
attempt/resolveSecret answers with plaintext. Connect only to a server you
trust: socketTransport doesn’t encrypt.
serveEngine
Serves an engine built with defineEngine over one connection, until the
client sends exit or the streams close.
Keep the server’s own logging on stderr: stdout carries the protocol. See Serve a TypeScript engine.
describeEngine
Returns the manifest serveEngine announces for an engine handle: its
static members, the methods its declared members answer, and
lifecycle/startAttempt and lifecycle/endAttempt, which serveEngine
always answers. Use it to declare the manifest of a TypeScript engine
served elsewhere.
spawnTransport
Starts the server as a child process per connection and speaks over its stdin and stdout, the way an editor starts a language server.
Closing a connection ends stdin, sends SIGTERM, and sends SIGKILL when
the cleanup budget runs out.
socketTransport
Connects to a server that already listens on a TCP port, on this machine or another. Closing a connection ends the socket; the server keeps running.
loopbackTransport
Serves an engine in this process over in-memory streams. Every call still crosses the protocol, serialized as on any other transport.
Messages
The base protocol is the
Language Server Protocol’s:
JSON-RPC 2.0 messages in UTF-8, each after a Content-Length header and a
blank line. Either side may send requests and notifications.
Lifecycle
A connection opens and closes as an LSP connection does.
Each side refuses any protocolVersion but 1. After shutdown, it leaves
its signals as they are. When a connection closes without shutdown, it
aborts every signal it holds.
Manifest
Its JSON Schema ships in the e2e package as
schema/engine-manifest-v1.schema.json.
Engine methods
Each method is one engine member, a request from client to server. Its
params are the member’s arguments by name, with every OperationContext as
operation. ENGINE_METHODS lists them.
artifacts/screenshot returns { data, mediaType: 'image/png' }, the image
in base64, when the server cannot write the attempt’s artifact directory,
such as a server on a device. The client writes it under screenshots/.
Observation pixels cross the same way: pixels.data is base64.
screens in lifecycle/startAttempt says whether the attempt keeps a
trace, which is when the server may send attempt/screen.
Server to client
What an engine calls back into the harness crosses as server-to-client
messages. Each attempt/* message carries the attemptId it belongs to;
the client drops one for an attempt that already ended.
With derivation, the client asks back with attempt/deriveSecret
{ derivation, plaintext } before it answers. The server returns
{ values }, the forms the engine derives from the value, such as a
basic-auth header, and the client redacts them under the secret’s name.
Signals
An AbortSignal crosses as { id, aborted?, reason? }. The client mints
one id per signal and sends $/abort with { id, reason? } when it aborts.
A signal already aborted when it is sent carries aborted: true. reason
is the abort reason as a JSON-RPC error object, encoded like a failed
method’s error, when the harness aborted with an error of its
own, such as a test timeout. A server aborts its signal with that error, so
an engine that rethrows signal.reason raises what it would in process.
Without reason, the signal aborts with the platform’s AbortError. A
server forgets an id once its signal aborts, and forgets every id first
seen during an attempt once lifecycle/endAttempt settles. An id sent again
after that is a new signal.
Errors
A failed engine method answers with JSON-RPC error code -32000 and the
e2e error in data, so the harness classifies a remote failure exactly as
a local one:
Any other error code carries the message alone. The harness treats it as
an infrastructure failure, as it treats a plain Error from a local
engine. Every error’s data may also carry stack, errno (a Node error
code such as ECONNREFUSED), and cause, an error object of the same
shape, so reports show where the engine failed.
Types
A transport opens one connection’s byte streams. See Write a transport.
Related
Engines compares the ways to build one.
e2e/engine is the contract each message carries.
Engine servers and Writing an engine
walk through building one.