Skip to main content

The Chat Loop

A single message from you can become many model passes. The chat loop (server/ai.js) orchestrates that: build the prompt, stream the model, parse and execute tools, feed results back, repeat until the model is done.

The system prompt

Before streaming, buildSystemPrompt() assembles a large prompt from:
  • The AI’s name and personality (from config).
  • How to customize the OS — themes, surfaces, files.
  • The app-vs-real-code rubric — when to build a BOR surface vs. write a real project (and to ask when the platform is ambiguous).
  • The surface-choice rubric — app vs. shortcut vs. just answering.
  • The design gate — call get_ux_ui_strict_guidelines before the first surface.
  • Tool docsbuildToolDocs() renders every registered tool.
  • The filesystem snapshot, time/locale, and system configuration.
  • The artifact policy — where AI-created files go (.bor/…).
  • The active theme and the existing surfaces list.
The prompt is the product. Its rubrics are why BOR knows that “a coffee-shop landing page” is a website to build-and-preview, not a BOR app to wrap.

The pass loop

your message
  └─ pass 1: stream model → parse tool blocks → execute → collect results
       └─ pass 2: feed <tool_results> back → stream → execute → …
            └─ … until the model emits no more feedback tools
  • Tools marked feedsBack: true (most of them) cause another pass: the model reads the result and continues.
  • <say> (stream-only) and <attempt_completion> do not feed back — they end the turn.
  • A safety ceiling, BOR_CHAT_MAX_PASSES, prevents runaway loops.

One tool per stream

After the first feedback tool in a stream, the runtime aborts the provider stream. This kills a failure mode where a weaker model hallucinates the <tool_results> block inline and “runs” many tools in one stream (replaying entire web pages, looping). One real tool runs; the loop feeds its result back cleanly on the next pass.

Open-tag continuation

If a stream ends mid-tag (e.g. the model was cut off inside a <write_file>), the runtime asks the model to continue from the exact next character — up to a few continuation attempts — so a long file completes instead of being dropped.

Completion & human-in-the-loop

  • <attempt_completion><result>…</result></attempt_completion> — the model signals it’s done. The result is shown to you; the turn ends. The model must not call tools after it.
  • <wait_for_human_input> — the model needs something only you can give (a profile choice, an OTP, a CAPTCHA, credentials, a manual click). It states what it needs and ends the turn, waiting for your reply. It must not call more tools while waiting.

Tool-result discipline

The model is told to inspect tool results before acting, not restate them. After execute_command, web_search, file tools, browser actions, or surface tools, it waits for the result pass, reads it, and only then decides to continue, complete, or wait. This is why BOR doesn’t claim “I’m on the page” before a screenshot proves it.

Tuning

VariableEffect
BOR_CHAT_MAX_PASSESMax tool-use passes per turn (safety ceiling).
BOR_CHAT_STREAM_ATTEMPTSStream retry attempts on failure.
BOR_TOOL_MAX_ATTEMPTSPer-tool retry attempts on transient failure.
BOR_STREAM_IDLE_TIMEOUT_MSIdle timeout before a stalled stream is retried.
If BOR ever feels like it’s stalling, the first thing to audit is these constants and the hard caps in the request path — not the model.

Streaming to the bubble

As tools execute, the loop emits SSE events: tag_start (a tool began), tag_delta (streaming content), say_delta (spoken text), and the final result event per tool. The presence turns these into the live cards.