---
title: Read and stream a local AI response
---

# Read and stream a local AI response

Use Anthropic's unchanged Node SDK to read the same deterministic response normally and as a
stream. This checks your application's response handling. The default response is a labeled
stub; it does not measure model quality or make a real model call.

Use Node 22.6 or newer, npm and registry access during installation. Start in an empty directory.
No Anthropic account, real key or platform login is needed.

## Select the release

Save these files. This example pins
[@volter/twin-anthropic 1.0.3](https://world.volter.ai/twins/anthropic/~40volter~2Ftwin-anthropic/1~2E0~2E3)
in the catalog. The catalog may select a newer default; keep the pins below for this example.
Other versions do not inherit its execution scope.

```json file=package.json
{
  "name": "local-ai-messages",
  "private": true,
  "type": "module",
  "dependencies": { "@anthropic-ai/sdk": "0.131.0" },
  "devDependencies": {
    "@volter/world": "3.0.158",
    "@volter/world-core": "3.0.148",
    "@volter/twin-anthropic": "1.0.3"
  }
}
```

```text file=.env.example
ANTHROPIC_API_KEY=
```

```bash
npm install
./node_modules/.bin/volter world init --name local-ai-messages --twins anthropic --source anthropic=@volter/twin-anthropic
```

Review the selected vendor and the package/version in `.volter/world.json`. Init creates a
project-owned scenario file for this model twin. Keep the config and lockfile with your app.

## Consume the same response two ways

The selected twin has its own model catalog, filtered by the World clock. A model listed on
Anthropic's live service is not automatically available in an older twin release. This example
uses `claude-sonnet-4-6`, listed as active in [Anthropic's model status](https://platform.claude.com/docs/en/about-claude/model-deprecations),
with a frozen clock after its release. Keep your app's model choice explicit.

```js file=messages.mjs
import assert from 'node:assert/strict';
import Anthropic from '@anthropic-ai/sdk';
assert.ok(process.env.VOLTER_WORLD, 'run through the World');
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY, maxRetries: 0 });
const input = {
  model: 'claude-sonnet-4-6', max_tokens: 128,
  messages: [{ role: 'user', content: 'A deterministic local example' }]
};
const response = await client.messages.create(input);
const text = response.content.filter(block => block.type === 'text').map(block => block.text).join('');
assert.match(text, /\[twin-stub:/);
assert.equal(response.role, 'assistant');
assert.equal(response.stop_reason, 'end_turn');
let streamed = '';
for await (const event of await client.messages.create({ ...input, stream: true })) {
  if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') streamed += event.delta.text;
}
assert.equal(streamed, text);
console.log('normal and streamed responses match the labeled local stub');
```

```bash
./node_modules/.bin/volter world up
./node_modules/.bin/volter world clock set 2026-03-01T12:00:00Z
./node_modules/.bin/volter world run -- node messages.mjs
./node_modules/.bin/volter world down
./node_modules/.bin/volter world up
./node_modules/.bin/volter world run -- node messages.mjs
./node_modules/.bin/volter world down
```

```text
normal and streamed responses match the labeled local stub
Stopped local-ai-messages
```

These assertions cover the normal Messages response and text-delta stream with this SDK and
input. They establish neither every API operation nor model reasoning. Stop/resume here repeats
the calls; it does not claim a stored conversation. Your app stores its own conversation history.

## Author the answer your app needs

The labeled stub is a starting point. To exercise an application's handling of a particular
answer, stop your World and replace `.volter/handlers/anthropic.json` with this ordered rule:

```bash
./node_modules/.bin/volter world down
```

```json file=.volter/handlers/anthropic.json
{
  "handlers": [
    {
      "id": "example-answer",
      "on": { "userTextIncludes": "A deterministic local example" },
      "respond": { "text": "billing" }
    }
  ]
}
```

Init already names that file in the Anthropic service's `execution.colocate.scenarioPath`.
Inspect the generated config before booting; saving a different file does not select it.
Restarting loads the edited file and retains this branch's state. Save this separate client;
the earlier stub assertions intentionally do not accept an authored answer:

```js file=authored-message.mjs
import Anthropic from '@anthropic-ai/sdk';
if (!process.env.VOLTER_WORLD) throw new Error('Run through the World.');
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY, maxRetries: 0 });
const input = {
  model: 'claude-sonnet-4-6', max_tokens: 128,
  messages: [{ role: 'user', content: 'A deterministic local example' }]
};
const answer = await client.messages.create(input);
const text = answer.content.filter(block => block.type === 'text').map(block => block.text).join('');
let streamed = '';
for await (const event of await client.messages.create({ ...input, stream: true })) {
  if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') streamed += event.delta.text;
}
const scenario = await fetch(`${process.env.ANTHROPIC_TWIN_URL}/twin/scenario`).then(r => r.json());
console.log(JSON.stringify({ text, streamed, scenario }, null, 2));
```

```bash
./node_modules/.bin/volter world up
./node_modules/.bin/volter world clock set 2026-03-01T12:00:00Z
./node_modules/.bin/volter world run -- node authored-message.mjs
./node_modules/.bin/volter world down
```

The two answers are `billing`; the scenario output names `example-answer` and its two matches.
This exercises response handling, not whether a model would choose that label. For your own
prompt, edit the matcher and response together. Unmatched calls fall back to a labeled stub;
inspect `/twin/scenario` for misses rather than treating the stub as your authored decision.
Handlers never fake a successful stored mutation. Use the vendor's own API for records an
application's tool creates, and keep tool execution in the application.

For one-time refusals and other matcher grammars, see [scenario handlers](../../docs/guides/shape-the-world-for-a-test.md).
For an existing chat app, keep its SDK calls and [run its usual command inside the World](../../docs/guides/use-with-an-existing-app.md).
[Inspect the World](../../docs/guides/inspect-a-world.md) to examine behavior and handler misses.

<!-- Fenced tutorial executor: packages/cli/src/journeys/tutorial.ts. Execution scope is recorded separately. -->
