Voicebox documentation

The Agent Loop

This document traces how a user turn flows through Voicebox from initial speech or text input to action resolution, workspace execution, UI updates, and audit logging.


1. Starting a Turn

A turn can start from two entry points, both converging on the same server-side action executor:

  1. Text Composer & Dictation (POST /api/turn):
    • Submitting text from the stage composer (#text-form), browser SpeechRecognition dictation, or the Picture-in-Picture widget sends { transcript } (or a schema-validated { action: { verb, ... } } from COMMAND_VERBS) to POST /api/turn.
    • POST /api/turn is stateless: it resolves the transcript, executes the resulting action in the active workspace root, records the audit trail, and returns { transcript, action, result }:
      {
        "transcript": "create a file called hello.txt with hi",
        "action": { "verb": "write", "name": "hello.txt", "content": "hi" },
        "result": { "ok": true, "action": "wrote hello.txt (2 bytes)", "file": "hello.txt", "root": { "kind": "machine", "path": "..." } }
      }
      
  2. Live Voice Session (/live):
    • public/live-voice.js streams full-duplex PCM audio over /live (lib/live-session.mjs) to Gemini Live (models/gemini-3.8-live) or OpenAI Realtime (gpt-realtime).
    • When the model invokes a tool (toolCall), the server maps the tool call via commandToAction() (lib/commands.mjs) directly into the shared execute(action) pipeline, returns the structured result to the model, and emits a { type: "tool" } event to update the browser UI in real time.

2. Turn Resolution (lib/resolver.mjs)

For text turns on POST /api/turn, resolveTurn(transcript, provider) in lib/resolver.mjs converts natural language into a structured action:

{ verb, name, content? }   // Resolved action to execute
{ unresolved: "reason" }   // Explicit explanation when no action matches

3. Action Execution (execute(action) in server.mjs)

All resolved actions and live voice tool calls execute through execute(action):


4. Audit Logging (<root>/.audit/<writer>.jsonl)

Every action and refusal inside an active workspace root is recorded to an append-only JSONL log in <root>/.audit/ (core/shared-log.ts, readable via GET /api/audit):


5. Failure Modes & Refusal Handling

Condition Server Behavior
No workspace root declared Returns refused: "root-not-declared", logged: null (declared: false on GET /api/health).
Workspace directory removed mid-session Returns refused: "root-unreachable", distinct from an undeclared root.
Path escapes active workspace (../) Refused pre-flight with refused: "outside-root" and recorded in .audit/.
Transcript matches no known command Returns action: null with the resolver’s unresolved message.
Extension tool exceeds bounds or host list Returns structured refusal (host-not-allowed, budget-exhausted) via lib/channel.mjs.
Extension proposed by model Saved as state: "pending" under proposals/; cannot be called until admitted by the host.

6. Quick Verification

You can inspect and verify the turn loop at any time against a running server:

  1. Check active resolver and root status: GET /api/health
  2. Run sample turns via POST /api/turn:
    • {"transcript": "list files"} → {"verb": "list"}
    • {"transcript": "create a file called hello.txt with hi"} → {"verb": "write"}
    • {"transcript": "book me a flight to Lisbon"} → action: null (unresolved)
  3. Inspect the recorded audit entries: GET /api/audit