Core concepts
Agent steps
The agent's four calls, act, assert, extract and waitFor, with examples, parameters and limits.
The agent fixture gives a test four calls. Each takes plain English. Each is checked, recorded
the first time it passes, and replayed with no model call after that.
import { test } from "@danwo/sdk"
test("upgrades to Pro", async ({ app, agent }) => {
await app.open("/settings/billing")
await agent.act("upgrade the workspace to the Pro plan")
await agent.assert("the plan shows as Pro")
})Pick a model
Agent steps need a model in your config. A model id goes through the Vercel AI Gateway, which
reads AI_GATEWAY_API_KEY from the environment:
model: "anthropic/claude-sonnet-5",danwo init can set up Anthropic, OpenAI, Google, OpenRouter or any
OpenAI-compatible server instead.
How much of a page the model reads
The model reads the page as text. A long page is cut to fit the model, so a small model still has room for the instruction and its answer:
| Model's input window | Page text it reads, at most |
|---|---|
| 128k tokens or more, or a model unknown | 120 000 characters (about 30k tokens) |
| Smaller, such as 32k tokens | a quarter of it: 32 000 characters (8k tokens) |
| The smallest windows | 8 000 characters |
A step whose page was cut says so in its record, with how long the page was. If a step misses something far down a long page, scroll or filter the page first.
act: do something
act carries out an instruction on the page: clicks, typing, choosing, scrolling, across several
pages if needed.
import { test, unique } from "@danwo/sdk"
test("signs up", async ({ app, agent, secret }) => {
await app.open("/")
await agent.act("sign up as {email} with the password {password}", {
params: { email: unique("ada@shop.test"), password: secret("password") },
})
})Values in braces come from params:
unique(value)gives a fresh value on every run, such as a new email address. A replay accepts the new value.secret(name)passes a configured secret. The model only ever sees the name.
assert: check what the page means
assert asks a judge whether a claim is true of the page right now. It throws when it isn't:
await agent.assert("the order is confirmed and shows one item")
await agent.assert("the status says paid", { poll: true })With { poll: true }, the judge looks again until the claim holds or time runs out. Use it when
the page gets there on its own, like after a webhook.
On replay, assert waits for the text that proved the claim last time, with no model call. A
claim can stop holding while that text is still shown, such as "the total matches the items"
after a pricing change. For a claim like that, ask the judge on every run:
await agent.assert("the total matches the items in the cart", { replay: false })It costs a judge call on every run, and fails under --replay-only.
extract: read typed values
extract reads values off the page and checks them against a schema. Use it for numbers you then
compare with the backend:
import { test, z } from "@danwo/sdk"
test("shows the balance", async ({ app, agent, expect }) => {
await app.open("/account")
const { balance } = await agent.extract("the account balance", z.object({ balance: z.number() }))
expect(balance).toBe(500)
})The values come from the elements the model points at, read by the runner, not typed by the model.
waitFor: wait for a state
waitFor waits until a condition holds, then returns:
await agent.waitFor("the order shows as paid", { timeout: 20_000 })On replay, it waits for the text that proved the condition last time, with no model call.
Options
| Option | On | What it does | Default |
|---|---|---|---|
params | act, extract, waitFor | Values the instruction names in braces. | none |
files | act | Files the step may upload, by name. | none |
budget | act | Limits for the step: turns, model calls, cost, time. | the session's |
vision | act, assert, extract | Whether the step may see a screenshot: true, "auto" or false. | "auto" |
time | act | Lets the step move time forward, e.g. "wait until the trial ends". | false |
poll | assert | Ask again until the claim holds. | off |
replay | assert | false asks the judge on every run and keeps no recording. | true |
timeout | waitFor | How long to wait, in milliseconds. | 30 000 |
With vision: "auto", the agent is told when part of a page isn't in its text view, such as a
chart or an icon button with no name. It can then look at a screenshot. Those steps replay with no
model too.
Icon buttons with no name
A button that holds only an icon has no name for the agent to read. When the icon's markup names
it, such as a lucide-trash class or a trash.svg image, the agent's view shows the word:
- listitem [ref=e7]:
- text: Invoice 43
- generic [ref=e9] [cursor=pointer] [icon="trash"]The agent clicks it, and the recording finds it again by its icon. An icon with no such markup is
left for a screenshot. Giving the button an aria-label is better still: people using a screen
reader need the name too.