Volter World

Run a tool call and stream its confirmation locally

Use OpenAI's unchanged Node SDK to receive a scripted function call. Your application validates the arguments, creates an issue with the unchanged GitHub SDK, returns that issue as a tool result, and streams a scripted confirmation. Read the stored conversation and issue, then stop and resume the same World without creating another issue.

This example needs Node 22.6 or newer, npm and registry access during installation. It needs no OpenAI or GitHub account, real API key or platform login. The two SDKs use their normal API destinations; the World supplies throwaway credentials and routes the selected vendors to their local twins. The issue belongs to the synthetic GitHub account. The model's tool call and confirmation are authored scenario responses, not a judgment by a live model.

Start in an empty directory. Save files as you reach each section: initialize the World before saving the authored handler, because init creates the initial empty handler file.

Install and select the two twins

Save these first three files, then install and initialize the World.

package.json
{
  "name": "local-openai-tools-example",
  "private": true,
  "type": "module",
  "dependencies": {
    "openai": "4.104.0",
    "@octokit/rest": "22.0.1"
  },
  "devDependencies": {
    "@volter/world": "3.0.158",
    "@volter/world-core": "3.0.148",
    "@volter/world-runtime": "3.0.156",
    "@volter/world-console": "3.0.149",
    "@volter/twin-openai": "3.0.3",
    "@volter/twin-github": "3.0.9"
  }
}
.env.example
OPENAI_API_KEY=
GITHUB_TOKEN=
.gitignore
node_modules/
outcome.json
outcome.json.tmp
npm install
./node_modules/.bin/volter world init --name local-openai-tools-example --twins openai,github --source openai=@volter/twin-openai --source github=@volter/twin-github

Review .volter/world.json before starting: OpenAI is selected for the chat requests and GitHub for the stored issue. The services should name @volter/twin-openai version 3.0.3 and @volter/twin-github version 3.0.9. Keep just these two vendors. Commit the generated config and npm lockfile to preserve the selection. Choose a twin explains how to inspect an exact release's supported operations and exercised coverage.

Author the tool call and its confirmation

After init has finished, save the following handler file, replacing the empty one it created. Saving it before init would let initialization overwrite the authored responses.

The first matching handler wins. Put the tool-result handler first: the later request still contains the original user message and tool definition. The initial handler proposes a call; it does not create the issue. The application performs that stored mutation through Octokit.

.volter/handlers/openai.json
{
  "handlers": [
    {
      "id": "issue-confirmation",
      "on": {
        "toolResultFor": "create_issue",
        "anyTextIncludes": "\"created\":true"
      },
      "respond": {
        "text": "Scripted confirmation: the application returned a stored GitHub issue. Its number and URL are in outcome.json."
      }
    },
    {
      "id": "request-issue",
      "on": {
        "userTextIncludes": "file the duplicate payment receipt",
        "hasTool": "create_issue"
      },
      "respond": {
        "toolCalls": [
          {
            "id": "call_create_issue",
            "name": "create_issue",
            "arguments": {
              "title": "P1: Duplicate payment receipt",
              "body": "A retried webhook creates duplicate receipts. Preserve audit history and deduplicate by provider event ID."
            }
          }
        ]
      }
    }
  ]
}

Select the saved scenario explicitly in the generated config. This helper preserves its other settings and refuses a different vendor selection.

configure-scenario.mjs
import { readFileSync, writeFileSync } from 'node:fs';

const path = '.volter/world.json';
const config = JSON.parse(readFileSync(path, 'utf8'));
const ids = config.services.map((service) => service.id).sort();
if (ids.join(',') !== 'github,openai') {
  throw new Error('Expected only the deliberately selected github and openai services');
}
const service = config.services.find((entry) => entry.id === 'openai');
if (!service?.execution?.colocate) {
  throw new Error('Expected the generated colocated OpenAI service');
}
service.execution.colocate.scenarioPath = '.volter/handlers/openai.json';
writeFileSync(path, JSON.stringify(config, null, 2) + '\n');
console.log('Selected .volter/handlers/openai.json');
node configure-scenario.mjs

Edit handlers while the World is stopped; restart it to load changes. The scenario guide explains matches, misses and authored refusals. This example uses the Chat Completions API supported by the pinned SDK and twin. OpenAI's function-calling guide explains why the application executes the tool, and the Chat Completions reference describes the tools, tool_call_id and stream fields used here.

Create the issue and stream the result

Save the application below. It permits one known tool with two bounded text arguments. The repository is fixed by the application under the authenticated synthetic account; a model response cannot choose another owner or repository. The app creates that repository if absent.

outcome.json is the application's receipt. Immediately after a successful issue response, the app saves its returned ID and number before requesting the streamed confirmation. A later continue uses the saved issue and conversation. A pending issue request without a saved response refuses continuation because its write outcome is unknown.

workflow.mjs
import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
import OpenAI from 'openai';
import { Octokit } from '@octokit/rest';

if (!process.env.VOLTER_WORLD) throw new Error('Run this application through the World');
if (!process.env.OPENAI_API_KEY || !process.env.GITHUB_TOKEN) {
  throw new Error('Expected the World-provided throwaway credentials');
}

const mode = process.argv[2];
if (!['create', 'continue', 'read'].includes(mode)) {
  throw new Error('Use workflow.mjs create, continue, or read');
}
const receiptPath = 'outcome.json';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, maxRetries: 0 });
const github = new Octokit({ auth: process.env.GITHUB_TOKEN });
const tool = {
  type: 'function',
  function: {
    name: 'create_issue',
    description: 'Create one issue in the application-owned triage repository.',
    parameters: {
      type: 'object',
      properties: { title: { type: 'string' }, body: { type: 'string' } },
      required: ['title', 'body'],
      additionalProperties: false
    }
  }
};

function save(receipt) {
  writeFileSync(receiptPath + '.tmp', JSON.stringify(receipt, null, 2) + '\n', { mode: 0o600 });
  renameSync(receiptPath + '.tmp', receiptPath);
}

function argumentsFor(call) {
  if (call?.type !== 'function' || call.function?.name !== 'create_issue' || !call.id) {
    throw new Error('Refused an unknown tool call');
  }
  const args = JSON.parse(call.function.arguments);
  if (!args || typeof args !== 'object' || Array.isArray(args) ||
      Object.keys(args).sort().join(',') !== 'body,title' ||
      typeof args.title !== 'string' || !args.title.trim() || args.title.length > 120 ||
      typeof args.body !== 'string' || !args.body.trim() || args.body.length > 5000) {
    throw new Error('Refused malformed create_issue arguments');
  }
  return { title: args.title, body: args.body };
}

let receipt = existsSync(receiptPath) ? JSON.parse(readFileSync(receiptPath, 'utf8')) : null;
if (mode === 'create') {
  if (receipt) throw new Error('A receipt already exists; use continue or read');
  const owner = (await github.users.getAuthenticated()).data.login;
  if (!owner) throw new Error('The authenticated GitHub account has no login');
  receipt = {
    phase: 'prepared',
    owner,
    repo: 'tool-triage',
    model: 'gpt-4o-mini',
    messages: [{ role: 'user', content: 'File the duplicate payment receipt as a GitHub issue.' }]
  };
  save(receipt);
} else if (!receipt) {
  throw new Error('No outcome.json exists; first run create');
}

if (mode !== 'read') {
  if (receipt.phase === 'issue-write-pending' && !receipt.issue) {
    throw new Error('Issue write outcome is unknown; inspect the World log before resolving the receipt');
  }
  if (!receipt.repositoryReady) {
    try {
      await github.repos.get({ owner: receipt.owner, repo: receipt.repo });
    } catch (error) {
      if (error.status !== 404) throw error;
      await github.repos.createForAuthenticatedUser({
        name: receipt.repo, private: true, description: 'Synthetic tool-call triage example'
      });
    }
    receipt.repositoryReady = true;
    save(receipt);
  }
  if (!receipt.toolCall) {
    const completion = await openai.chat.completions.create({
      model: receipt.model, messages: receipt.messages, tools: [tool], store: true, max_tokens: 128
    });
    receipt.initialCompletionId = completion.id;
    receipt.initialAnswer = completion.choices[0]?.message ?? null;
    save(receipt);
    const calls = receipt.initialAnswer?.tool_calls;
    if (!Array.isArray(calls) || calls.length !== 1) {
      throw new Error('Expected one authored tool call; no issue was created');
    }
    receipt.arguments = argumentsFor(calls[0]);
    receipt.toolCall = calls[0];
    receipt.messages.push({
      role: 'assistant', content: receipt.initialAnswer.content, tool_calls: [receipt.toolCall]
    });
    receipt.phase = 'tool-ready';
    save(receipt);
  }
  if (!receipt.issue) {
    // Revalidate the retained proposal before the stored mutation.
    const args = argumentsFor(receipt.toolCall);
    receipt.phase = 'issue-write-pending';
    save(receipt);
    const { data: issue } = await github.issues.create({
      owner: receipt.owner, repo: receipt.repo, ...args
    });
    receipt.issue = {
      id: issue.id, number: issue.number, url: issue.html_url,
      owner: receipt.owner, repo: receipt.repo, title: issue.title, body: issue.body
    };
    receipt.phase = 'issue-created';
    save(receipt);
    console.log('GitHub issue recorded in outcome.json');
  }
  if (!receipt.toolResult) {
    receipt.toolResult = {
      created: true, owner: receipt.issue.owner, repo: receipt.issue.repo,
      number: receipt.issue.number, url: receipt.issue.url, title: receipt.issue.title
    };
    receipt.messages.push({
      role: 'tool', tool_call_id: receipt.toolCall.id, content: JSON.stringify(receipt.toolResult)
    });
    save(receipt);
  }
  if (receipt.phase !== 'complete') {
    receipt.phase = 'confirmation-pending';
    save(receipt);
    const stream = await openai.chat.completions.create({
      model: receipt.model, messages: receipt.messages, tools: [tool], tool_choice: 'none',
      store: true, stream: true, max_tokens: 128
    });
    let text = '';
    let finishReason = null;
    for await (const chunk of stream) {
      if (chunk.id && chunk.id !== receipt.finalCompletionId) {
        receipt.finalCompletionId = chunk.id;
        save(receipt);
      }
      const choice = chunk.choices[0];
      if (choice?.delta?.tool_calls) throw new Error('Refused another tool call during confirmation');
      const content = choice?.delta?.content ?? '';
      text += content;
      process.stdout.write(content);
      if (choice?.finish_reason) finishReason = choice.finish_reason;
    }
    process.stdout.write('\n');
    if (!receipt.finalCompletionId || finishReason !== 'stop') {
      throw new Error('Confirmation was incomplete; the saved issue remains available');
    }
    receipt.confirmation = text;
    receipt.phase = 'complete';
    save(receipt);
  } else {
    console.log('Saved workflow is complete; no issue or chat request repeated');
  }
  console.log(JSON.stringify({ phase: receipt.phase, issue: receipt.issue }, null, 2));
} else {
  if (!receipt.issue) throw new Error('No successful issue response is saved in the receipt');
  const { data: issue } = await github.issues.get({
    owner: receipt.issue.owner, repo: receipt.issue.repo, issue_number: receipt.issue.number
  });
  console.log('Stored GitHub issue');
  console.log(JSON.stringify({
    id: issue.id, number: issue.number, title: issue.title, body: issue.body,
    state: issue.state, url: issue.html_url
  }, null, 2));
  for (const [label, id] of [
    ['Initial tool call', receipt.initialCompletionId],
    ['Streamed confirmation', receipt.finalCompletionId]
  ]) {
    if (!id) continue;
    const completion = await openai.chat.completions.retrieve(id);
    const messages = await openai.chat.completions.messages.list(id);
    console.log(label + ' — stored request and answer');
    console.log(JSON.stringify({ id: completion.id, messages: messages.data, choices: completion.choices }, null, 2));
  }
  console.log('OpenAI scenario diagnostic');
  try {
    const response = await fetch(`${process.env.OPENAI_TWIN_URL}/twin/scenario`);
    console.log(`HTTP ${response.status}`);
    console.log(await response.text());
    if (!response.ok) {
      console.error('Scenario diagnostic refused; the stored vendor readbacks above completed.');
    }
  } catch (error) {
    console.error(`Scenario diagnostic failed: ${error.message}`);
  }
}
./node_modules/.bin/volter world up
./node_modules/.bin/volter world clock set 2026-01-15T12:00:00Z
./node_modules/.bin/volter world run -- node workflow.mjs create

The expected successful output begins with:

GitHub issue recorded in outcome.json
Scripted confirmation: the application returned a stored GitHub issue. Its number and URL are in outcome.json.

The following JSON prints the returned issue ID, number and URL rather than a guessed value. gpt-4o-mini is the model modeled by these pinned twins at the frozen World time; this example makes no claim about a live model's current availability or quality. Stream chunks come from the authored response through the ordinary SDK's async iterator, as described by OpenAI's streaming guide.

Inspect the prompt, tool result and stored issue

./node_modules/.bin/volter world run -- node workflow.mjs read
./node_modules/.bin/volter world log

read uses issues.get, chat.completions.retrieve and chat.completions.messages.list to print stored vendor data. Both chat requests use store: true; the stream's completion ID is retained from its chunks. The initial stored answer contains the tool call; the final stored request includes the tool-result text; the final answer contains the authored confirmation. The native log shows the repository and issue writes separately from the chat requests.

This OpenAI release's stored-message view retains role, content and name, rather than every tool-correlation field. Inspect outcome.json for the full application conversation, including the assistant's tool call and matching tool_call_id. That receipt complements the SDK reads; it does not stand in for them. The printed /twin/scenario response shows actual handler matches and misses. Call the result an authored confirmation only when request-issue and issue-confirmation matched the two requests. An unmatched labeled stub does not establish that this flow ran, even if the app saved an issue and reached its complete phase. The diagnostic's status and body are printed separately from the vendor reads. If it is refused, keep that refusal visible; the completed SDK reads do not establish scenario counters. Handler counts belong to the running scenario engine and restart when its process restarts.

Stop and resume without repeating the mutation

./node_modules/.bin/volter world down
./node_modules/.bin/volter world up
./node_modules/.bin/volter world run -- node workflow.mjs read
./node_modules/.bin/volter world down
./node_modules/.bin/volter world status

down stops compute and retains the World's state. up resumes that state; read makes no issue mutation or new completion request. Keep outcome.json with this World so the reads use the same returned IDs. The final status should show the stopped World.

If confirmation failed after the issue response was saved, resume the stopped World and run:

./node_modules/.bin/volter world up
./node_modules/.bin/volter world run -- node workflow.mjs continue
./node_modules/.bin/volter world run -- node workflow.mjs read
./node_modules/.bin/volter world down

continue skips the saved issue mutation and sends its actual result to the model again if confirmation remains incomplete. It does not retry an issue request whose outcome is unknown. For that pending boundary, inspect the native log and stored repository before deliberately resolving the receipt; deleting it and rerunning can create a duplicate. Malformed or unknown tool calls refuse before issue creation. API refusals stay visible because OpenAI automatic retries are disabled. A successful GitHub mutation survives a later model refusal or stream failure; the receipt describes that partial outcome.

The World simulates these two vendors for this app. It does not measure production GitHub permissions, a live model's tool choice, or enforce network isolation for unrelated binaries. For a fresh workflow, reset deliberately and start with a new receipt rather than confusing a new World state with this retained issue.

View Markdown source

On this page