agent.send(input, ctx).
defineAgent
Use this for direct agent integrations:
defineDirectAgent remains available as a deprecated compatibility alias. It references the same function; use defineAgent in new code.
Agent.kind produced by defineAgent is always "direct" (an internal discriminant field you never declare). There are no capability bits on t that need declaring to unlock — filesystem assertions like t.sandbox exist only on agents constructed with defineSandboxAgent (kind: "sandbox"); everything else is judged by the events send actually returned and whether ctx.session was used. See the capabilities reference. The full field set is in “Agent and Adapter Context fields” below.
Direct agent example
input (TurnInput)
string
Text passed by the current
t.send(...).readonly InputFile[] | undefined
Files attached to this turn (images, etc.). Adapters that do not support multimodal input can ignore it.
readonly InputResponse[] | undefined
Present only on answer turns (
t.respond / t.respondAll): per-request structured answers, matched by requestId.defineSandboxAgent
Use this for coding agent CLIs. The produced Agent.kind is always "sandbox" — filesystem assertions like t.sandbox and t.sandbox.fileChanged() unlock only on this kind of agent. The full field set is likewise in “Agent and Adapter Context fields” below, under SandboxAgentDef; the complete methods of ctx.sandbox (the Sandbox interface) are in the “Sandbox interface” section further below.
Registration
Agent and Adapter Context fields
Construction parameters fordefineAgent (DirectAgentDef) and defineSandboxAgent (SandboxAgentDef), plus the ctx (AgentContext) that both receive in send(input, ctx):
DirectAgentDef
name
Agent.name — not a registry lookup key, only used for display, result attribution, and dedup fingerprinting.
evidenceCoverage
completeEvidenceCoverage only when all six evidence channels are fully collected.
setup
tracing
spanMapper
send
classifySendFailure
send calls. It classifies structured execution failures; a trusted failed Turn remains a domain result.
teardown
setup point in time
(a setup throw does not exempt it); runs once in finally.
SandboxAgentDef
name
Agent.name — not a registry lookup key, only used for display, result attribution, and dedup fingerprinting.
evidenceCoverage
completeEvidenceCoverage only when all six evidence channels are fully collected.
ensure
installers
ensure identities. When omitted, a failed probe reports the missing identity instead of guessing an installer.
setup
agent.ensure. The runner
calls setup once after the Sandbox is ready (after layer prepare/ensure/baseline) and before the first send; it returns no value.
tracing
spanMapper
send
classifySendFailure
send calls. It does not reinterpret a returned failed Turn.
teardown
setup point in time
(a setup throw does not exempt it); runs once in finally.
AgentContext
signal
fetch) to exit early and
gracefully, but this is not the only hard boundary — even if the adapter ignores it entirely, the runner still
force-finishes with Effect.timeoutTo as a fallback (stopping the sandbox container).
model
model field. Sandbox-type agents typically use it in setup to write config; Direct Agents typically use it in send to pick the model.
reasoningEffort
flags
flags field, passed through as-is; its content and shape are entirely defined by the experiment author
(e.g. { webResearch: true }, { systemPrompt: "..." }). Adapters read fields from it by their own convention;
the framework itself does not interpret or validate its content. The name is deliberately distinct from the CLI-parsed
flag (run-level things like —timeout/—budget) — the two are unrelated concepts.
experimentId
runWho / AgentRun.experimentId);
undefined when the run does not go through an experiment (e.g. outside the CLI, constructing an AgentRun directly).
Typical uses: a SandboxLayer.setup hook isolating cross-attempt state per experiment (cache directory names,
snapshot tags, etc. partitioned by ctx.experimentId), or an adapter switching auth / routing per experiment. It is a
different dimension from flags (the concrete values of the experiment’s conditions) — this is only the stable identifier
of “which experiment is running”, and carries no condition content.
evalId
AgentContext directly may omit it.
attempt
session
telemetry
tracing block / the config’s telemetry exists):
this run’s OTLP trace receiving information (endpoint + env-based export env).
How to hand it to the CLI is declared by the agent’s tracing block: env-based agents spread ctx.telemetry.env
into send; file-based agents write config in tracing.configure. For remote HTTP integrations, send
only needs to spread the headers into the request headers (a fresh traceparent per turn); the endpoint is fixed at
startup-time configuration (defineConfig({ telemetry: { port } })) and is not passed here.
progress
human profile updates the active row; agent/ci do not print it line by line, and it never lands in the
final result. Do not call it for every token/delta. The runner attributes it to whichever lifecycle phase the
callback is currently in (agent.setup / agent.run / agent.teardown); the caller cannot pretend to be in a
different phase (see docs/feature/experiments/library.md).
diagnostic
dedupeKey. Even a level of “error” does not change Turn.status /
verdict — throw an exception when you cannot continue.
fact
niceeval.. One owner/name can be written once; a second write is a typed error and never replaces or appends. The value can be any JsonValue. The JSON-stringified { observedAt, value } document is limited to 65,536 UTF-8 bytes; exceeding it throws record-custom-fact-too-large synchronously and leaves no partial file. A fact does not change Turn status, Verdict, score, or fingerprint.
log
progress({ message: msg }), not a second channel (see “Attempt phases” in
docs/feature/experiments/cli.md). On a timeout failure, the most recent lines are merged into the result’s
error information, to help locate where it got stuck.
ctx.session (AgentSession) is the state slot for one conversation line: every send on the same conversation line gets the same ctx.session, and a new conversation line (the eval’s first turn / after t.newSession()) gets a fresh one. Accessors:
id?: string/capture(id): void— session continuation when the server keeps history:idis the session id recorded on this line (undefinedon a new line),capturerecords the returned id (it only lands when none has been recorded yet).get<T>(slot: SessionSlot<T>): T | undefined/set<T>(slot, value): void/take<T>(slot): T | undefined— adapter-private typed slots. Create each slot once withcreateSessionSlot<T>();takeclears on read (consume once), which suits a paused HITL scene.
Sandbox interface
ctx.sandbox (Sandbox) on sandbox-type agents is the handle to the current isolated environment; CommandOptions are the options for runCommand / runShell:
Sandbox
workdir
cwd/targetDir is omitted.
runCommand
args is passed as a separate argv array and is not interpreted by a shell (no &&, pipes, or glob expansion).
Prefer this when you just want to run one executable, arguments come from external input, and you’re worried about injection.
runShell
&&, pipes, $(), redirection, etc.
Use this when you need to chain multiple commands or do conditional logic.
runCommandOrThrow
runCommand, but throw when it exits non-zero. A successful result has exitCode: 0 in its type.
runShellOrThrow
runShell, but throw when it exits non-zero. A successful result has exitCode: 0 in its type.
readText
writeText
readBytes
Buffer.
writeBytes
pathExists
uploadDirectory
targetDir defaults to workdir; options.ignore excludes matching basenames.
stop
sandboxId
otlpHost
string: inside the sandbox, this hostname can reach the host’s receiver.null: the provider does not promise that the sandbox can reach back to the host; the runner tries to start an attempt-scope receiver inside the sandbox instead. This does not guarantee tracing succeeds; when the image lacks the runtime the receiver needs, only a supplemental diagnostic is recorded.defineConfig({ telemetry: { host } })can override this explicitly when you have already provided a controlled tunnel.
appendLog
docker logs /
the Docker UI’s Logs tab shows the agent’s turn-by-turn activity live. Implemented by the docker provider; others can omit it.
downloadFile
readBytes when the caller needs bytes in memory.
uploadFile
writeBytes when the content is already in memory.
downloadDirectory
options.ignore excludes matching basenames.
CommandOptions
env
PATH, which are not guaranteed to be overridable here).
cwd
Sandbox.workdir when omitted. Relative paths resolve against workdir, absolute paths are used as-is.
stream
docker logs / the Docker UI’s Logs
tab shows it live). Turn this on for agent commands (codex exec / bub run / claude) and you’ll see the agent’s raw output
in the container logs. Each provider implements this its own way (docker: tee to the file PID1 tails;
providers that don’t support it ignore it) — how the log surfaces is the provider’s concern, the adapter only declares intent.
onStdout
CommandResult. When a
provider does not support true streaming, it must at least call this once with the complete stdout after the
command finishes — it cannot silently drop it.
onStderr
onStdout; the complete stderr is still retained in CommandResult.
user
USER,
the Compose service user:, the E2B template’s default user, or the host’s current user).
Semantics stay consistent across providers, and each provider maps this to its own native mechanism
(docker: exec --user; E2B: { user }; Vercel: only accepts "root", mapped to { sudo: true }, other values throw; local: any value throws).
Providers that always run as root treat it as a no-op; providers with no way to switch identity at all may not support it (throw) — but the semantics for omission and
explicit values stay consistent regardless of provider.
timeoutMs
signal
ctx and t
ctx is the run context seen by the adapter side; t is the test context seen by the eval author. They use the same run data but have different responsibilities.