Skip to main content
The agent surface has two layers under createAgent:
  1. createToolLoopExecutor: the same loop as the built-in agent, with your prompt and your tool vocabulary. You keep the verdict tool, budget hard stops, loop guards, wind-down near the clock, model accounting, and --debug transcripts.
  2. StepExecutor: any object with runStep(ctx). No AI SDK, no model, any decision process you like.
Whichever you pick, the runner still bounds the step, redacts what you observe, polices what you do, and records the verdict.

Keep the loop, change the vocabulary

This example gives a browser agent two tools: read the screen and tap a button by its accessible name. The same loop can wrap a device SDK.
e2e.config.ts
buildPrompt returns a string, or a message history to carry a conversation across steps. prepareMessages runs between turns for compaction and may return { messages, stop } when your own history shows the step is not progressing. guard() stops a tool from running once the step is concluding and turns budget and timeout stops into a clean end of the loop. runTool reserves the action budget before the body runs, serializes mutations, and records their outcomes. A mutating body must call its engine directly; queuing another grammar operation from inside it would wait on itself. Keep tool bodies short and interruptible: the step deadline settles the verdict either way, but a device operation that ignores cancellation finishes on its own time.

Replace the executor

This model-free executor supports one assertion, agent.assert('the screen says Ready'). Other instructions return a blocked verdict so the executor cannot claim to have checked something it does not understand.
e2e.config.ts
The context tells the executor where it is and what it may do: Secrets in params arrive as name-only placeholders. actions.typeSecret(target, name) performs the authorized fill without the plaintext passing through your code. A custom executor can run both agent.act and agent.assert without a model. Its agent.assert dispatches to runStep under the act budgets and config.timeout; vision and screenshot are UNSUPPORTED_CAPABILITY there. Without a configured executor, the runner checks for a model when agent is first acquired, after the engine attempt starts. A missing model raises MODEL_UNAVAILABLE then. A custom executor with no model passes that check. Its agent.waitFor and agent.extract calls still use the built-in judgment tier and raise MODEL_UNAVAILABLE when they need a model and none is configured.

Driving the page yourself

An executor that brings its own browser tooling needs the page e2e opened, not a second one. @e2edev/playwright exports surfaceOf(handle): the live Page and BrowserContext of the current attempt behind the handle you passed to the target. Both accessors throw INVALID_STATE before an attempt is running.
What you do there is out of band: the harness still bounds the step and records the verdict, but it witnesses no actions, records no trace for replay, and attaches no artifacts. Report tool calls and model calls through ctx.budgets so the step keeps its metrics.

The verdict

Whatever the executor, runStep resolves with passed, failed, or blocked and a summary. A blocked verdict carries the error code that names the cause; How agent steps work lists them and the exit each one produces.

agent reference

Every method, option, and budget.

Writing an engine

Plug in a whole new surface below the executor.