Volter World

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 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.

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"
  }
}
.env.example
ANTHROPIC_API_KEY=
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, with a frozen clock after its release. Keep your app's model choice explicit.

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');
./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
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:

./node_modules/.bin/volter world down
.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:

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));
./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. For an existing chat app, keep its SDK calls and run its usual command inside the World. Inspect the World to examine behavior and handler misses.

View Markdown source

On this page