Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion .claude/skills/write-docs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,17 @@ Spelling to enforce on every page:

- **"realtime", one word.** Never "real time" or "real-time" when describing Ably delivery ("sees the same conversation in realtime", "realtime messaging"). This is Ably house spelling. Sweep with `grep -rn "real[- ]time" "$P"` and expect zero hits.

### Stay inside the product vocabulary

Use the terms the product actually defines. Do not introduce vocabulary that is not part of the AI Transport domain — not internal-codebase jargon, and not generic AI/ML terms the docs have chosen not to use. A reader who has read the concept pages should never hit a word the docs never taught them.

- **Prefer the named primitives.** The concept pages define the vocabulary: [Session](/docs/ai-transport/concepts/sessions), [Connection](/docs/ai-transport/concepts/connections), [Run](/docs/ai-transport/concepts/runs), [Step](/docs/ai-transport/concepts/steps), [Invocation](/docs/ai-transport/concepts/invocations), [Codec](/docs/ai-transport/concepts/codecs), [conversation tree](/docs/ai-transport/concepts/conversation-tree), and view. Reach for these before inventing a synonym.
- **Do not leak internal jargon.** Words that name SDK internals but never appear in the concept or API docs (for example `projection`, `RunNode`, or other type names not documented for readers) read as jargon. Use the documented term instead: "the conversation" or "the Run's view", not "the projection"; "the Run" or "the conversation tree", not "`RunNode`".
- **Do not smuggle in generic AI/ML vocab the docs avoid.** Describe a loop cycle as an "iteration", not a "pass". Describe a model invocation as a "model call" or "response", not an "inference pass". The verb "pass" (to pass an argument, hook, or signal) is fine; the noun "pass" (meaning a loop cycle) is not.
- **Respect the Run/turn split.** "Run" is the only primitive the docs explain. Use "turn" only in jobs-to-be-done framing (titles, intros, the "concurrent turns" feature name), never as a defined primitive alongside Run.

When a word is genuinely new and needed, define it on first use or link to the concept page that does. If you cannot point to where it is defined, it does not belong on the page.

## Per-page workflow

1. **Identify the page type.** Pick the matching template above.
Expand Down Expand Up @@ -401,6 +412,11 @@ grep -rn "—" "$P"
# "realtime" is one word (no "real time" / "real-time")
grep -rn "real[- ]time" "$P"

# No off-vocabulary terms. Noun "pass" (loop cycle), "inference pass", and internal
# jargon that never appears in the concept/API docs. The verb "pass" (an argument,
# hook, signal) is fine, so this targets the noun forms and known jargon only.
grep -rniE "inference pass|\b(this|that|next|current|each|one|its|a|the|per|another) (pass|passes)\b|\bprojection\b|\bRunNode\b" "$P"

# No bold-prefix bullets
grep -rnE "^\s*[\*\-]\s+\*\*[^*]+:\*\*" "$P"

Expand Down Expand Up @@ -481,7 +497,7 @@ When reviewing someone else's docs PR (or your own at PR time), walk this list.
**Writing standards**

9. Page complies with the rules in [`writing-style-guide.md`](../../../writing-style-guide.md). Use the verification greps above to spot the common violations (em dashes, bold-prefix bullets, Latin abbreviations, subjective phrases, vague modals, "we" as the subject, AI-fingerprint patterns).
10. Author ran `/deslop` (or equivalent AI-pattern check).

@VeskeR VeskeR Jul 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should keep this as item 11

@zknill zknill Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's not a shared skill, so I think this is a meaningless line

10. Page stays inside the product vocabulary — no internal-codebase jargon (`projection`, `RunNode`) and no generic AI/ML terms the docs avoid (noun "pass", "inference pass"). Every non-obvious term is defined on first use or links to the concept page that defines it. Use the off-vocabulary grep above.

**Code accuracy**

Expand Down
2 changes: 1 addition & 1 deletion src/data/languages/languageData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default {
android: '1.2',
},
aiTransport: {
javascript: '0.5',
javascript: '0.6',
},
spaces: {
javascript: '0.5',
Expand Down
68 changes: 34 additions & 34 deletions src/data/nav/aitransport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,76 +132,76 @@ export default {
name: 'Features',
pages: [
{
name: 'Token streaming',
link: '/docs/ai-transport/features/token-streaming',
name: 'Agent presence',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bringing in @m-hulbert comment from #3468 regarding alphabetical ordering here:

I agree that the feature list is rapidly expanding, and it would help to make that easier to navigate; however I'm not convinced that alphabetising the list is the right way to do this. In terms of releative 'importance' - is 'Agent presence' the first thing we believe should be read to understand the value proposition and how AI Transport works?

IMO, the core value proposition should be immediately stated and clear in the "About AI Transport". "How AI Transport works" is covered by "Concepts".
And "Features" then shows you what you can build with it. If you're just skimming through the docs to see the overall feature set, I don't think there's a material difference whether "Features" is sorted alphabetically or based on our assumptions of what we deem important - people will still be able to find what they want. But when you're returning to the documentation, having the list sorted makes it way easier to navigate.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, well @m-hulbert if you have a concrete proposal for an order then please share that here. In the mean time, this re-ordering makes the features findable when you want to return to a page you already know exists.

(I'm highly allergic to comments along the lines of "not this thing", without "this instead")

link: '/docs/ai-transport/features/agent-presence',
},
{
name: 'Cancellation',
link: '/docs/ai-transport/features/cancellation',
name: 'Branching, edit, and regenerate',
link: '/docs/ai-transport/features/branching',
},
{
name: 'Reconnection and recovery',
link: '/docs/ai-transport/features/reconnection-and-recovery',
name: 'Cancellation',
link: '/docs/ai-transport/features/cancellation',
},
{
name: 'Multi-device sessions',
link: '/docs/ai-transport/features/multi-device',
name: 'Chain of thought',
link: '/docs/ai-transport/features/chain-of-thought',
},
{
name: 'History and replay',
link: '/docs/ai-transport/features/history',
name: 'Concurrent turns',
link: '/docs/ai-transport/features/concurrent-turns',
},
{
name: 'Database hydration',
link: '/docs/ai-transport/features/database-hydration',
},
{
name: 'Branching, edit, and regenerate',
link: '/docs/ai-transport/features/branching',
},
{
name: 'Interruption',
link: '/docs/ai-transport/features/interruption',
},
{
name: 'Concurrent turns',
link: '/docs/ai-transport/features/concurrent-turns',
},
{
name: 'Tool calling',
link: '/docs/ai-transport/features/tool-calling',
name: 'Double texting',
link: '/docs/ai-transport/features/double-texting',
},
{
name: 'Durable execution',
link: '/docs/ai-transport/features/durable-execution',
},
{
name: 'Human-in-the-loop',
link: '/docs/ai-transport/features/human-in-the-loop',
name: 'History and replay',
link: '/docs/ai-transport/features/history',
},
{
name: 'Optimistic updates',
link: '/docs/ai-transport/features/optimistic-updates',
name: 'Human-in-the-loop',
link: '/docs/ai-transport/features/human-in-the-loop',
},
{
name: 'Agent presence',
link: '/docs/ai-transport/features/agent-presence',
name: 'Interruption and steering',
link: '/docs/ai-transport/features/interruption-and-steering',
},
{
name: 'LiveObjects State',
link: '/docs/ai-transport/features/liveobjects',
},
{
name: 'Multi-device sessions',
link: '/docs/ai-transport/features/multi-device',
},
{
name: 'Optimistic updates',
link: '/docs/ai-transport/features/optimistic-updates',
},
{
name: 'Push notifications',
link: '/docs/ai-transport/features/push-notifications',
},
{
name: 'Chain of thought',
link: '/docs/ai-transport/features/chain-of-thought',
name: 'Reconnection and recovery',
link: '/docs/ai-transport/features/reconnection-and-recovery',
},
{
name: 'Double texting',
link: '/docs/ai-transport/features/double-texting',
name: 'Token streaming',
link: '/docs/ai-transport/features/token-streaming',
},
{
name: 'Tool calling',
link: '/docs/ai-transport/features/tool-calling',
},
],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ const run = session.createRun(invocation, { signal: req.signal });
| onCancelled | Called when the run is cancelled. Receives a `write` function to publish final outputs before cancellation finalises. | `(write: (output: TOutput) => Promise<void>) => void \| Promise<void>` |
| onCancel | Called when a cancel arrives. Return `true` to accept, `false` to reject. Defaults to accepting all. | `(req: CancelRequest) => Promise<boolean>` |
| onError | Called with non-fatal run-scoped errors. | `(error: Ably.ErrorInfo) => void` |
| onSteer | Called once per steering message as it folds into this Run, so the agent can race the arrival against an in-flight model call and decide whether to interrupt it. A hint only; the SDK never interrupts the model call itself, and [`hasInput`](#has-input) remains the authoritative check. Keep it synchronous and cheap. | `() => void` |

</Table>

Expand Down Expand Up @@ -246,6 +247,24 @@ Wait until the Run's triggering input has been observed on the channel (see [`lo

There is no built-in deadline: `start()` does not time out waiting for the trigger. It rejects only if the run is cancelled or the session is closed before the trigger is observed. Race it against your own timeout if you need one.

### Check for pending input <a id="has-input"/>

<MethodSignature>{`hasInput(): boolean`}</MethodSignature>

Drives the agent's loop: returns `true` before the Run has produced any output (the triggering input always needs a first response), and again whenever a steering message has folded into the Run since the previous check. Returns `false` once the Run has produced output and no steering message is pending, or once [`abortSignal`](#run) has fired.

Calling `hasInput()` drains any pending steering messages: the next output the agent pipes stamps their codec-message-ids, resolving each steering client's [`outcome`](/docs/ai-transport/features/interruption-and-steering#steer) as consumed. There is no observe-only variant; treat every call as a commitment to respond to whatever it reports.

<Code>
```javascript
while (run.hasInput()) {
const result = streamText({ messages: run.view.getMessages().map(({ message }) => message), model });
await run.pipe(result.toUIMessageStream());
}
await run.end({ reason: 'complete' });
```
</Code>

### Pipe the response stream <a id="pipe"/>

<MethodSignature>{`pipe(stream: ReadableStream<TOutput>, options?: PipeOptions<TOutput>): Promise<StreamResult>`}</MethodSignature>
Expand Down Expand Up @@ -320,7 +339,7 @@ The Run must be open first, via [`start`](#run-start) or an adopting [`load`](#a
| Property | Description | Type |
| --- | --- | --- |
| stepId | A stable identifier used to coalesce retries. A fresh attempt under an existing `stepId` supersedes the prior attempt's output. Omit for in-process work; supply the workflow engine's own stable per-activity id for cross-process retries. | String |
| stepClientId | The `clientId` to attribute this Step to. Omit for the common case; the SDK inherits the prior Step's value (sticky), defaulting to the triggering input's publisher for the Run's first Step. Supply an explicit value when a steer incorporates a fresh input mid-run. | String |
| stepClientId | The `clientId` to attribute this Step to. Omit for the common case; the SDK inherits the prior Step's value (sticky), defaulting to the triggering input's publisher for the Run's first Step. Supply an explicit value when a steering message incorporates a fresh input mid-run. | String |

</Table>

Expand Down
87 changes: 85 additions & 2 deletions src/pages/docs/ai-transport/api/javascript/core/client-session.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "ClientSession"
meta_description: "API reference for the AI Transport ClientSession: factory, properties, lifecycle methods, and the View interface returned by createView()."
meta_keywords: "AI Transport, ClientSession, createClientSession, View, Tree, connect, createView, cancel, Ably"
meta_description: "API reference for the AI Transport ClientSession: factory, properties, lifecycle methods, the ClientRun handle with steer, and the View interface returned by createView()."
meta_keywords: "AI Transport, ClientSession, createClientSession, ClientRun, steer, View, Tree, connect, createView, cancel, Ably"
redirect_from:
- /docs/ai-transport/api-reference/client-transport
- /docs/ai-transport/api/javascript/client-transport
Expand Down Expand Up @@ -196,6 +196,89 @@ await clientRun.cancel();

`Promise<void>`. Resolves once the cancel message has been published. The cancel is best-effort: if the agent has already ended the Run, the cancel is a no-op.

## ClientRun <a id="client-run"/>

The handle returned by [`view.send`](#properties), `regenerate`, and `edit`. It extends the shared `BaseRun` read-model (`runId`, `status`, `error`, `messages`) with the client's control surface, including the run-scoped [`steer`](#steer) verb.

### Properties <a id="client-run-properties"/>

<Table id='ClientRunProperties'>

| Property | Description | Type |
| --- | --- | --- |
| runId | The Run's unique identifier, minted by the agent. Empty until the agent's `ai-run-start` is observed; await [`started`](#client-run-properties) before reading it. | String |
| status | The Run's lifecycle status, read live off the tree. | `RunStatus` |
| error | The terminal error, present exactly when `status` is `'error'`. | `Ably.ErrorInfo` or Undefined |
| messages | This Run's whole turn: its originating input followed by the Run's output, deduplicated by `codecMessageId`. | `TMessage[]` |
| started | Resolves when the agent's `ai-run-start` (or `ai-run-resume`) is observed, the point at which `runId` is populated. No built-in deadline; race it against your own timeout. | `Promise<void>` |
| inputCodecMessageId | The triggering input's codec-message-id, owned by the client the moment it publishes. Stream routing and cancel key on this, so it is known synchronously, unlike `runId`. | String |
| inputEventId | The input event's unique identifier, stamped on the published input event and forwarded in the POST body so the agent can locate the trigger. | String |

</Table>

### Steer the run <a id="steer"/>

<MethodSignature>{`steer(input: TInput): SteerResult`}</MethodSignature>

Publish a codec input event that targets this Run. The steering message carries this Run's `run-id` so the agent folds it into the active Run rather than starting a new Run. Pass the same shape [`view.send`](#properties) accepts, typically `codec.createUserMessage(...)`. The SDK awaits `runId` internally, so this is safe to call as soon as the handle is returned. Once an `ai-run-end` has folded for this Run the handle is dead and further `steer()` calls return immediately-rejected promises. See [Interruption and steering](/docs/ai-transport/features/interruption-and-steering).

<Code>
```javascript
const { published, outcome } = activeRun.steer(UIMessageCodec.createUserMessage({
id: crypto.randomUUID(),
role: 'user',
parts: [{ type: 'text', text: 'Also include vegan options.' }],
}));

const { serial } = await published;
const { consumed, runTerminalReason } = await outcome;
```
</Code>

#### Parameters <a id="steer-params"/>

<Table id='SteerParameters'>

| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| input | required | The codec input event to publish, in the codec's input shape. Wrap a domain message via `codec.createUserMessage` for a follow-up user message. | `TInput` |

</Table>

#### Returns <a id="steer-returns"/>

`SteerResult`. Two promises: `published` for the channel-publish acknowledgement, and `outcome` for the consumed determination resolved at the Run's next terminal event. <Table id='SteerResult'/>

<Table id='SteerResult' hidden>

| Property | Description | Type |
| --- | --- | --- |
| published | Resolves when the steering message is published to the channel, carrying the Ably-assigned `serial`. Rejects if the publish (or the internal `runId` await) fails. | `Promise<{ serial: string \| undefined }>` |
| outcome | A `Promise` that resolves once a terminal lifecycle event (`ai-run-end`, or `ai-run-suspend` for a consumed steering message) folds for the Run. Rejects if the handle dies first (for example the session closes). | <Table id='SteerOutcome'/> |

</Table>

<Table id='SteerOutcome' hidden>

| Property | Description | Type |
| --- | --- | --- |
| consumed | `true` when the steering message's codec-message-id appears in the union of `steer-codec-message-ids` stamps on the Run's responses (the agent's loop had it visible when it produced that response), `false` when it never appeared before `ai-run-end`. For an `ai-run-suspend` a not-consumed steering message stays pending. | Boolean |
| runTerminalReason | How the Run ended, present when the outcome was determined by an `ai-run-end`. Absent when determined by an `ai-run-suspend`, since the Run has not ended. | `RunEndReason` |

</Table>

### Cancel the run <a id="client-run-cancel"/>

<MethodSignature>{`cancel(): Promise<void>`}</MethodSignature>

Cancel this specific Run. Keyed by [`inputCodecMessageId`](#client-run-properties), which the client owns synchronously, so a cancel issued before the agent mints the `runId` is still honoured (the agent buffers it and fires it once its input-event watcher matches the trigger). Resolves once the cancel is published; it does not wait for [`started`](#client-run-properties).

### Build the invocation <a id="to-invocation"/>

<MethodSignature>{`toInvocation(): Invocation`}</MethodSignature>

Build the [`Invocation`](/docs/ai-transport/api/javascript/core/agent-session#invocation) pointer for this Run, carrying only `inputEventId` and the session's channel name. POST `run.toInvocation().toJSON()` to your agent endpoint to wake the agent; run identity lives on the channel, not in the invocation body.

## Subscribe to session errors <a id="on"/>

<MethodSignature>{`on(event: 'error', handler: (error: Ably.ErrorInfo) => void): () => void`}</MethodSignature>
Expand Down
31 changes: 31 additions & 0 deletions src/pages/docs/ai-transport/concepts/runs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,37 @@ await clientRun.cancel();

On the agent side, the symmetric primitive is [`AgentSession.createRun(invocation, runtime?)`](/docs/ai-transport/api/javascript/core/agent-session#create-run). The agent mints `runId` (for a fresh run) or reads the existing `runId` off the triggering input event (for a continuation), and stamps it on every event it publishes; the client reads it from `clientRun.runId` once `clientRun.started` resolves.

## Steer a Run <a id="steer"/>

While a Run is active, a client can send a follow-up message into it. This is called *steering* the Run. The agent picks up the new message on the next loop iteration; no new Run starts.

The follow-up has the same shape as the input that started the Run. It joins the conversation tree under the same `RunNode` and every connected client sees it. Unlike a fresh `view.send`, it carries the active Run's `runId`, which tells the agent to add it to the existing Run.

Steering messages don't change how a Run ends. The terminal `RunEndReason` reports how the Run finished. Each steering message also gets its own outcome, so the client can tell which ones the agent saw before the Run ended and which it didn't.

<Code>
```javascript
const activeRun = await session.view.send(UIMessageCodec.createUserMessage({
id: crypto.randomUUID(),
role: 'user',
parts: [{ type: 'text', text: 'Plan a 3-day trip to Lisbon.' }],
}));

const { published, outcome } = activeRun.steer(UIMessageCodec.createUserMessage({
id: crypto.randomUUID(),
role: 'user',
parts: [{ type: 'text', text: 'Make it 5 days, and keep it under £800.' }],
}));

await published; // The steering message has been published to the channel.

const { consumed } = await outcome; // Resolves when the Run ends.
console.log('Reached the agent:', consumed);
```
</Code>

See [interruption and steering](/docs/ai-transport/features/interruption-and-steering#steer) for how to call `steer()`, what `outcome` contains, and how to write the agent's loop.

## Run concurrent Runs <a id="concurrent"/>

A session can hold multiple Runs in flight at the same time. They share the channel; they don't share state. See [concurrent turns](/docs/ai-transport/features/concurrent-turns) for the patterns that arise when more than one Run is `'active'`.
Expand Down
Loading
Loading