> ## Documentation Index
> Fetch the complete documentation index at: https://docs.surfacearea.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# World sessions

> Open a live world session from Node, every WorldSession method with its signature, the session token as the credential your client already sends, inline tasks, and stored tasks.

A session is one live container of a world, held warm while your agent works against it. The agent stays in your process. Every `call()` executes inside the platform's container and is recorded in the session's call log.

Sessions show up under Sessions on the platform without your agent instrumenting anything.

## Open a session

`openSession(slugOrRef, { task, versionId?, reuseWarm?, surfaces?, browserTier?, options? })` returns as soon as the platform accepts the session. Call `ready()` to wait for the container.

```typescript theme={null}
import { openSession } from "@withgateway/sdk/worlds";

const session = await openSession("acme-billing@main", {
  task: "refund-double-charge",
  surfaces: ["api"],
});

await session.ready();
console.log(session.sessionId, session.instruction);
```

`world.open(task, opts?)` on a [`WorldSet`](/sdk-ts/worlds) does the same thing and pins the session to the version the world already resolved.

| Option | Type | Meaning |
| - | - | - |
| `task` | `WorldTaskRef` | A bundled scenario name, a stored task `{ id, version? }`, or an inline spec |
| `versionId` | `string` | Pin an exact version instead of the world's head |
| `reuseWarm` | `boolean` | Accept a pooled container for the same task |
| `surfaces` | `("tools" \| "api" \| "ui" \| "browser")[]` | What answers besides tool calls |
| `browserTier` | `"standard" \| "premium"` | Capacity for a World Host browser: `standard` (spot, may be reclaimed) or `premium` (on-demand). Default: the organization's setting |
| `options` | `WorldsClientOptions` | Host and keys, when not the environment's |

Asking for a surface the world does not declare is refused with HTTP 400, rather than served with fewer surfaces.

## Attach to a session someone else opened

`attachSession(sessionId, options?)` returns a `WorldSession` for an existing session. Use it to resume after a crash, or to pick up a session that CI opened and handed to a developer.

```typescript theme={null}
import { attachSession } from "@withgateway/sdk/worlds";

const session = await attachSession(process.env.SESSION_ID!);
console.log(await session.status());
```

## Read what the session is

These are properties, not requests. They reflect the last state the session read.

| Property | Type | What it holds |
| - | - | - |
| `sessionId` | `string` | The session's id |
| `task` | `string \| null` | The task's display name |
| `taskName` | `string \| null` | The same value, spelled the long way |
| `taskInfo` | `WorldSessionTask` | Where the task came from: `source`, `name`, `hash`, `id`, `version`, `metadata` |
| `instruction` | `string \| null` | What this task asks the agent to do |
| `versionId` | `string` | The world version serving the session |
| `isReady` | `boolean` | Whether the container answered ready at the last read |

```typescript theme={null}
console.log(session.taskInfo.source);   // "bundled" | "inline" | "stored"
console.log(session.instruction);
```

## Wait for the container

`ready(opts?)` polls until the session answers calls: the World Host holds it, or the process inside a container has asked the platform for work. It waits ten minutes by default and throws `WorldSessionTimeout` past the deadline, or `WorldSessionOpenFailed` (a `WorldSessionCallError` carrying the platform's reason and `code`) if the session fails or stops first.

```typescript theme={null}
await session.ready({ timeoutMs: 15 * 60_000 });
```

`status()` reads the session's latest state in one request and makes no attempt to wait.

```typescript theme={null}
const state = await session.status();
console.log(state.state, state.ready, state.contentHash);
```

## Call the world's tools

`call(tool, args?, opts?)` runs one of the world's tools inside the container. The arguments and the result are the world's own contract. Nothing in between validates or reshapes them, so a tool error reads exactly as it would in a hosted run.

```typescript theme={null}
const invoices = await session.call("list_invoices", { status: "open" });
await session.call("mark_paid", { id: "INV-7" }, { timeoutMs: 60_000 });
```

A call fails with `WorldSessionCallError` when the tool errors, and `WorldSessionTimeout` when the container does not answer inside three minutes.

## Hand the world's tools to a model

`toolkit()` returns the world's tools in the shape a model expects. Schemas come from the world's own signatures, and each implementation calls into this session.

```typescript theme={null}
const { schemas, impls, specs } = await session.toolkit();

// schemas: OpenAI-shaped function definitions, ready to pass to a model
// impls:   name -> (args) => Promise<unknown>, each one a call into this session
// specs:   the world's raw name, signature and docstring per tool

const reply = await model.chat({ tools: schemas });
const result = await impls[reply.toolName](reply.toolArgs);
```

A tool added to the world appears here on the next run. A tool renamed in the world renames here.

## Point an existing client at the world

`api()` returns the base URL of the world's own HTTP API for this session. `apiHeaders()` returns the headers every request to it needs.

```typescript theme={null}
const base = await session.api();
const headers = await session.apiHeaders();

const charges = await fetch(`${base}/v1/charges?status=settled`, { headers });
```

A World Host session gates its API surface on a per-session token, and `apiHeaders()` returns it as `{ Authorization: "Bearer <token>" }`. A pod session or a customer-hosted twin answers with the world's own declared auth, and `apiHeaders()` returns `{}`.

### Send the token the way your client already sends credentials

Worlds whose vendor uses a header or basic scheme accept the same token the vendor's own way. Read it from the surfaces block and build whichever header your client already sends.

```typescript theme={null}
const { api } = await session.surfaces();
const token = api?.token;

// A client written against the real vendor works unchanged.
await fetch(`${api!.url}/v1/charges`, {
  headers: { "x-api-key": token! },
});
```

| The vendor's scheme | Also accepted |
| - | - |
| `header` | The vendor's own header, for example `x-api-key: <token>` |
| `basic` | Basic auth with `<token>` as the user |
| `oauth2` | The token as the password in a password grant |

<Info>
  Pointing a client at a world is a base URL and a key. No code in your client has to know a world exists.
</Info>

<Info>
  Send the token as a header, never in a URL. A query token outlives the request in access logs, and the platform refuses it.
</Info>

`surfaces()` returns the whole block, and `ui()` returns the URL of the world's UI entry page with the session token already attached. Both throw `WorldSurfaceUnavailable` when the session was not opened asking for that surface.

## Put rows in, read rows back, start over

`seed(rows, opts?)` writes rows through the world's contract in one batch. `append` is the default and upserts by primary key. `replace` makes the named entities hold exactly these rows. The batch is atomic, so a refused row leaves the session unchanged and the error names the entity, row and field.

```typescript theme={null}
const result = await session.seed(
  { invoices: [{ id: "INV-7", status: "open", amount: 4200 }] },
  { mode: "append" },
);
console.log(result.seeded, result.mode, result.entity_counts);
```

Seeded rows are stored as given. Pass `{ redact: "apply" }` to run the world's `[redact]` policy over them first, or `{ redact: "refuse" }` to reject a row carrying plaintext in a redacted field; both need the world to have a `connector.toml`. See [Keep real data out](/worlds/redaction).

`state(opts?)` returns every entity's rows as the world holds them now, which is the same dump a grader reads.

```typescript theme={null}
const state = await session.state();
```

`reset(opts?)` returns the world to its opening state: the version's data plus the task's seed. Rows written since are gone and the call log stays.

```typescript theme={null}
await session.reset();
```

## Grade the end state

`grade(opts?)` runs the task's grader against the container's end state and returns `{ reward, rewards, raw }`. `reward` is the scalar, `rewards` holds every named component, and `raw` is the grader's untouched answer.

```typescript theme={null}
const { reward, rewards, raw } = await session.grade();
console.log(reward);              // 0.75, or null when the task has no grader
console.log(rewards);             // { refund_issued: 1, note_added: 0.5 }
```

A rubric grade is finalized by a model on the platform rather than by the container. The call then waits on the judge's budget of fifteen minutes instead of a call's three. Pass `timeoutMs` to override either.

## Trace your agent inside the session

`withTraceContext(fn)` runs `fn` as work inside this session. Every span it records takes the session's id as its session id and carries `world_session_id` in its trace metadata, so your agent's traces sit beside the platform's trace of the session's tool calls. `runSessions` wraps every agent call in it.

```typescript theme={null}
import { tracing } from "@withgateway/sdk";

tracing.init();
await session.withTraceContext(() => myAgent(toolkit, session.instruction));
```

A `tracing.withSession(...)` block inside still sets the session id. Nothing is recorded until `tracing.init()` has run.

## Read the episode back

`export()` returns the session's call log oldest first, paging through the whole thing. Every tool call, seed, reset and grade is in it, with its arguments, result and error.

```typescript theme={null}
for (const call of await session.export()) {
  console.log(call.seq, call.kind, call.tool, call.error, call.completedAt);
}
```

`export()` holds what the platform brokered. The world's own request log is the `calls` a grader reads: every call the world answered, its route requests (including the ones your agent made through the browser or straight to `api.url`) and its tools called by name, each with its `via` and with credential fields read as `[redacted]`. `WorldRequestLog.read(session)` reads it from the live session, so read it before you close.

```typescript theme={null}
import { WorldRequestLog } from "@withgateway/sdk/worlds";

const log = await WorldRequestLog.read(session); // null when the world keeps no request log
for (const request of log?.requests ?? []) {
  console.log(request.seq, request.via, request.method, request.path, request.status);
}
const lines = log?.asCalls() ?? []; // the same requests as call-log entries with kind "request"
```

## Keep an inline task

`saveTask(opts?)` promotes this session's inline task into a stored task of the project, at version 1, pinned to the session's world.

```typescript theme={null}
const task = await session.saveTask({
  name: "refund-late-invoice",
  message: "found while triaging ticket 4821",
});
console.log(task.id, task.version);
```

A session on a bundled task has nothing to save. A session on a stored task is already saved.

## Close it

`close(opts?)` ends the session. `keepWarm: true` hands the container back to the pool so the next session on the same task starts warm.

```typescript theme={null}
await session.close({ keepWarm: true });
```

<Info>
  Always close in a `finally` block. A leaked session holds its container until it closes for being idle.
</Info>

## Write the task yourself

Instead of a bundled scenario name, pass an inline `WorldTaskSpec`. It is one end user's task: what the agent is told, the rows the session starts from, and how it is graded.

```typescript theme={null}
const session = await openSession("acme-billing@main", {
  task: {
    name: "pay-inv-7",
    instruction: "Mark invoice INV-7 as paid.",
    seed: { rows: { invoices: [{ id: "INV-7", status: "open" }] } },
    grader: {
      kind: "assertions",
      checks: [{ entity: "invoices", where: { id: "INV-7", status: "paid" } }],
    },
    tools: ["list_invoices", "mark_paid"],
    metadata: { externalUserId: "u_42" },
  },
  surfaces: ["api"],
});
```

| Field | Type | Meaning |
| - | - | - |
| `instruction` | `string` | What the agent is told, required |
| `name` | `string` | A label for results, optional |
| `seed` | `{ mode?, redact?, rows }` | Rows applied after the version's snapshot is cloned. `redact` is `off` (default), `apply` or `refuse` — see [Keep real data out](/worlds/redaction) |
| `grader` | `WorldTaskGrader` | How the end state is scored, see below |
| `tools` | `string[]` | Allow-list of the version's tools, never its routes; a call outside it is refused and logged, and the session's routes answer 403 |
| `metadata` | `Record<string, unknown>` | Yours, echoed on the session and never read |

Without a `grader`, `grade()` answers `reward: null`.

### The three grader kinds

```typescript theme={null}
// Declarative checks the host evaluates against the final rows.
{ kind: "assertions", checks: [
  { id: "paid", entity: "invoices", where: { id: "INV-7" },
    assert: "all", field: "status", op: "eq", value: "paid" },
] }

// A verify.py body, run through the host's confined verifier.
{ kind: "python", source: "...", timeoutSeconds: 30 }

// Prose, judged by a model on the platform.
{ kind: "rubric", rubric: "The invoice is paid and the customer was told.",
  criteria: [{ id: "paid", description: "INV-7 is settled", weight: 2 }] }
```

A check's `assert` is `exists` by default, and may be `absent`, `count`, `all` or `any`. Each check's `id` names its entry in `rewards`, and the task's reward is the weighted share of checks that passed.

A `python` grader reads the end state as JSON on stdin: each entity as `{ "<primary key>": row }`, the call log as `calls`, and the agent's report as `report` (a cross-world verifier sees `<alias>.<entity>`). It prints its grade as the last line of stdout, one flat JSON object: `reward` plus one key per check, each a number from 0 to 1. `true` and `false` read as 1 and 0, and `null` or a string marks that check ungraded. Without `reward` the task is ungraded. The script must exit 0 within `timeoutSeconds` (default and maximum 30). A nested object or array, a number outside 0 to 1, a non-zero exit, a last line that is not JSON, or a timeout fails the grade with the reason.

<Info>
  Inline and stored tasks run on a host engine world.
</Info>

## Store a task so many sessions can open it

An inline task belongs to one session. A stored task is the same spec as a named, versioned project resource. Updating the spec appends a version rather than rewriting history, and a session opened on `{ id, version: 2 }` keeps running version 2 after version 3 exists.

```typescript theme={null}
import { createTask, openSession } from "@withgateway/sdk/worlds";

const task = await createTask({
  name: "refund-late-invoice",
  world: "acme-billing",
  spec: { instruction: "Refund INV-7.", grader: { kind: "assertions", checks: [] } },
});

const session = await openSession("acme-billing", { task: { id: task.id } });
```

| Function | Signature | What it does |
| - | - | - |
| `createTask` | `(input, options?)` | Stores a task. `input` is `{ spec, name?, world?, message? }`; `name` defaults to `spec.name` |
| `updateTask` | `(taskId, input, options?)` | A new `spec` becomes the next version; `name` and `world` change in place |
| `listTasks` | `(filter?, options?)` | Newest change first. `filter` is `{ world?, metadata?, page?, limit? }` |
| `getTask` | `(taskId, opts?, options?)` | One task with its version history; `opts.version` reads that version's spec |
| `deleteTask` | `(taskId, options?)` | Removes the task and every version |
| `validateTask` | `(taskId, target?, options?)` | Checks the spec against a world version without opening a session |

`world` pins a task to one world's slug, and the spec is validated against that world's contract. Passing `null` leaves the task usable with any world.

```typescript theme={null}
import { listTasks, validateTask } from "@withgateway/sdk/worlds";

const page = await listTasks({ world: "acme-billing", metadata: { externalUserId: "u_42" } });
console.log(page.meta.totalItems);

const check = await validateTask(page.tasks[0].id, { slug: "acme-billing" });
console.log(check.ok, check.checked, check.problems);
```

`validateTask` reports `checked: "entities"` when the world's contract was read and every seed and check entity was matched against it. It reports `checked: "none"` when the version has no contract to check, in which case `ok` says only that the version exists and is ready.

Tasks are project-scoped. Another project's task id comes back as not found, never as forbidden.

## Tasks that need several worlds

A task whose spec has a `worlds` table is one instruction over several hosted worlds. `openTask` brings every one of them up in one platform call and hands back the sessions keyed by the aliases the spec chose.

```typescript theme={null}
import { createTask, openTask } from "@withgateway/sdk/worlds";

const task = await createTask({
  spec: {
    name: "orion-infostealer",
    instruction: "Find the stolen credentials and lock the account.",
    worlds: {
      spycloud: {
        slug: "spycloud-world",
        seed: { rows: { breaches: [{ id: "b1", email: "a@x.io" }] } },
        grader: { kind: "assertions", checks: [{ entity: "breaches", where: { id: "b1" } }] },
      },
      torchlight: { slug: "torchlight-world", ref: "v2" },
    },
  },
});

const live = await openTask(task.id, { surfaces: ["api"] });
await live.ready();
await myAgent(live.instruction, live.sessions);   // { spycloud: WorldSession, torchlight: WorldSession }
const grade = await live.grade();                 // { reward, rewards, worlds, ungraded }
await live.close();
```

Each entry of `worlds` is `{ slug, ref?, seed?, grader?, tools? }`: the world, one of its refs (default `main`), and that world's share of the task. With `worlds` present the top-level `seed`, `grader` and `tools` are refused and the task is not pinned to a world. The `instruction` and `metadata` are shared.

| Function | Signature | What it does |
| - | - | - |
| `openTask` | `(ref, opts?)` | Brings every world up. `ref` is a task id, `{ id, version? }` or a spec with `worlds`; `opts` is `{ version?, surfaces?, reuseWarm?, browserTier?, options? }` (`browserTier` applies to every world). All or nothing: a world that fails to open closes the rest and the error names it |
| `attachTask` | `(taskId, options?)` | Rebuilds a `TaskSessions` from a stored task's live sessions; refuses when an alias is up twice |
| `attachManifest` | `(manifest, options?)` | Rebuilds one from the manifest `gateway worlds task up --out` wrote |
| `listTaskSessions` | `(taskId, options?)` | A stored task's live sessions, newest first |
| `aggregateTaskGrade` | `(worlds)` | The pure aggregation `grade()` uses, for grades you collected yourself |

`TaskSessions` carries `taskId`, `taskVersion`, `name`, `instruction`, `sessions` (alias to `WorldSession`) and `aliases`, plus the task-wide verbs, each fanned out over every world:

| Method | Returns | What it does |
| - | - | - |
| `session(alias)` | `WorldSession` | One world's session, or an error naming the aliases there are |
| `ready(opts?)` | `Promise<void>` | Waits until every world answers ready |
| `status()` | `Record<alias, WorldSessionState>` | Every world's current state |
| `manifest()` | `Promise<TaskManifest>` | `{ taskId, taskVersion, name, instruction, sessions: { alias: { slug, ref, sessionId, ready, state, surfaces, api?, ui?, browser? } } }` — what the CLI prints |
| `grade(opts?)` | `Promise<TaskGrade>` | Every world's `GradeResult` under `worlds` and the task's cross-world verifiers under `verifiers`; `rewards` flattened as `<alias>` / `<alias>.<key>` and `x.<name>` / `x.<name>.<key>`; `reward` the mean of the worlds and verifiers that answered a number, `null` when none did; anything without a reward under `ungraded` (a verifier as `x.<name>`), never counted as 0. Names the task's other sessions on each grade so the platform can gather a verifier's evidence; `session.grade({ worlds })` does the same by hand. See [Verifiers that span worlds](/worlds/sessions#verifiers-that-span-worlds) |
| `export()` | `Record<alias, WorldSessionCall[]>` | Every world's call log |
| `close(opts?)` | `Record<alias, { state }>` | Closes every world; tries all of them and names the ones that did not close |

Open a multi-world task with `openTask`, not `openSession`. Under the hood `openTask` is `POST /api/public/world-task-sessions` and `listTaskSessions` is `GET /api/public/world-task-sessions?taskId=`.

## Run a task end to end

`runTask` is `gateway worlds task run` as a function: open every world of a task, run your agent once against all of
them, grade with its own report, file one rollout and close — the TypeScript twin of the Python SDK's
`gatewaysdk.run_task`.

```typescript theme={null}
import { runTask } from "@withgateway/sdk/worlds";

const summary = await runTask(
  "wt_1",
  async (run) => {
    const { url, token } = await run.api("spycloud");
    const answer = await myAgent(run.instruction, url, token);
    run.record({ visited: url });
    return answer; // the report every world's grader sees
  },
  { model: "claude-sonnet-4-5", outDir: "runs" },
);
```

The agent is called as `agent(run)` with a `TaskRun`:

| Member | Type | What it is |
| - | - | - |
| `runId` | `string` | `wtr-<12 hex>`, also the session id spans are grouped under (see below) |
| `instruction` | `string` | The task's objective |
| `model` | `string` | The `model` option, echoed back |
| `sessions` | `Record<alias, WorldSession>` | Every world's live session, same as `TaskSessions.sessions` |
| `manifest` | `TaskManifest` | The same object `task up` prints |
| `api(alias)` | `Promise<{ url, token? }>` | That world's HTTP API; throws `WorldSurfaceUnavailable` unless the run asked for `surfaces: ["api"]` |
| `toolkit()` | `Promise<WorldToolkit>` | Every world's tools merged into one, each name prefixed `<alias>.<tool>` — a world with none contributes nothing |
| `record(notes)` | `void` | Merges `notes` into the run's `run.json` (a later call overrides a key an earlier one set) |

The agent's return value is the report every world's grader sees: a string, or an object with a `report` key (any
other keys are kept in `run.json` as-is); a `Promise` is awaited. `runTask`'s options:

| Option | Default | What it does |
| - | - | - |
| `model` | required | Recorded on the filed run |
| `surfaces` | `undefined` | Asked of every world besides `tools` |
| `outDir` | `"runs"` | The run's own `<runId>/` folder is created under here |
| `trace` | `true` | Trace the agent's own spans with the host and keys the run's own requests use: `options`, then `GATEWAY_HOST`/`GATEWAY_PUBLIC_KEY`/`GATEWAY_SECRET_KEY`. `gateway worlds task run` passes the credentials the command resolved, so a saved `gateway auth login` traces too. A `GATEWAY_OTLP_ENDPOINT` on another host is followed only when the keys are the environment's own. When tracing cannot start (the optional `@opentelemetry/*` packages are missing, or that redirect would carry other keys), the run continues untraced (`traced: false`) and the process prints one warning saying why, never a key; for missing packages it prints the `npm install` line, and the `npm install -g` line for the CLI installed globally. A process that already started tracing (an agent that called `tracing.init()`) keeps that setup. `trace: false` runs untraced without the warning |
| `file` | `true` | File one rollout when the run ends |
| `keepUp` | `false` | Leave every session up instead of closing it when the run ends |
| `reuseWarm` | `undefined` | Passed through to `openTask` |
| `browserTier` | `undefined` | Passed through to `openTask`: `standard` or `premium` |
| `options` | `undefined` | `WorldsClientOptions` (host/publicKey/secretKey), same as everywhere else in `@withgateway/sdk/worlds` |

Every step happens even when the agent throws: it still grades (with `report: null`), still files (the evaluation's
status `FAILED`, `run.json.agentError` set to the error's message), and still closes every session unless
`keepUp: true`. A world whose tools the platform could not read (a `tool-contract-unread` notice) throws
`WorldToolsUnread` (its `code` is `tool-contract-unread`) before the agent runs: every session closes, even with
`keepUp: true`, and nothing is filed. `runSessions` records such a task as failed without calling its agent. The run writes `<outDir>/<runId>/{manifest,grades,run}.json` (and `report.md` when the agent gave a
report) and returns the same object as `run.json`: `{ runId, taskId, taskVersion, name, instruction, model, aliases,
reward, rewards, ungraded, agentError, report, traced, runDir, durationSeconds, notes, filed, evaluationId? }`.

### Group a task run's own spans into one session

`tracing.withSession(sessionId, fn)` groups every trace recorded inside `fn` under one session id, the TypeScript
twin of the Python SDK's `tracing.session(session_id)`. `runTask` uses it to put the agent's own LLM calls and tool
spans under a session named by the run id:

```typescript theme={null}
import { tracing } from "@withgateway/sdk";

tracing.init(); // registers SessionSpanProcessor alongside the identity processor

await tracing.withSession("checkout-regression-001", async () => {
  await tracing.observe("turn-1", async () => {
    // every span started in here (including auto-instrumented LLM calls) carries
    // gateway.session.id = "checkout-regression-001"
  });
});
```

`withSession(sessionId, fn)` sets the session id in an `AsyncLocalStorage` for the duration of `fn` (so it survives
`await`s and propagates to auto-instrumented spans), and the `SessionSpanProcessor` — registered automatically by
`tracing.init()`/`initIsolated()` — stamps `gateway.session.id` on every span started while it is set. Nesting is
fine: the innermost `withSession()` wins for the code that runs inside it. A `gateway.session.id` the span is
started with, or set on it afterwards with `span.setAttribute`, overrides this ambient default for that span.

## Where to go next

* [Run a task suite](/sdk-ts/run-sessions) to open many of these at once with a score gate.
* [Drive a world UI](/sdk-ts/browser) for the browser and computer-use half of a session.
* [Spin worlds up and down](/worlds/sessions) for the same model from the terminal.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.