Ochat – text-first toolkit for custom AI agents, LLM workflows, and vector search
Build custom AI agents and scripted LLM workflows as plain text files.
Ochat is an OCaml toolkit for building reproducible, composable, tool-using LLM workflows without locking the workflow into a single UI or heavyweight framework.
Instead of hiding prompts, tool permissions, shell authority, transcript state, and orchestration inside an application, Ochat keeps them in static, diffable files that you can version-control, review, branch, and run in different hosts.
If you like tools like Claude Code or Codex, Ochat operates at a more fundamental level: it gives you the building blocks to create your own prompt packs, agents, and workflow systems.
Contents
- Why Ochat exists
- Design Principles
- Ochat in one minute
- What is Ochat?
- What makes Ochat different?
- How Ochat compares
- Who Ochat is for
- Quick start
- What can I do with Ochat?
- Common use cases
- First 10 minutes with Ochat
- Example ChatMD prompts
- Build from source
- Core concepts
- Architecture overview
- Documentation
- OCaml integration
- Future directions
- Project status
Why Ochat exists
Most LLM tools today either:
- hide workflows inside polished UIs,
- require you to rebuild everything in a code-first framework, or
- make prompts and agent state hard to inspect, reproduce, and evolve.
Ochat exists to make LLM workflows feel more like engineering artifacts.
With Ochat, prompts, tools, transcript state, and orchestration live in plain text files that you can:
- version-control
- diff and review
- branch and resume
- compose into larger workflows
- run in the terminal, scripts, CI, or over MCP
The goal is simple: make agent workflows explicit, inspectable, portable, and reproducible.
Design principles
Ochat is built around a few core principles:
- Everything important should be inspectable
- Workflows should be versionable
- Agent runs should be reproducible
- Tools should be explicit
- Custom workflows should not depend on a single UI
- Advanced orchestration should remain auditable
- Shell authority should be explicit, inspectable, and fail closed
Ochat in one minute
- A workflow is usually a
.mdfile written in ChatMarkdown (ChatMD). - That file can contain the prompt, model config, tool permissions, shell runtime authority, transcript, and execution artifacts.
- The same workflow can run in the TUI, the CLI, or over MCP.
- Workflows can call tools, other workflows, and an optional host-managed ChatML script.
- Because everything is stored as text, runs are diffable, reproducible, resumable, and easy to version-control.
What is Ochat?
Ochat is a toolkit for building agent workflows and orchestrations as static files.
Its core format is ChatMarkdown (ChatMD), a Markdown + XML dialect for defining and running agents. A single ChatMD file can contain:
- model and generation parameters
- tool declarations and permissions
- developer and user instructions
- the full conversation history
- tool calls and tool results
- imported artifacts such as documents or images
- optional host-managed scripting for orchestration and moderation
- named shell runtimes with capabilities, policy, approvals, sandboxing, interceptors, secrets, limits, and audit
In Ochat, an agent is often just a .md file.
That file is not only a prompt — it can also be:
- the execution log of a run,
- a reusable workflow component,
- a tool callable by another agent,
- a portable artifact you can inspect and refine over time.
Because everything is captured in text files, workflows become:
- reproducible – the exact config and transcript are version-controlled
- diffable – reviews show exactly what changed and what the model did
- composable – workflows can call other workflows (prompt-as-tool)
- portable – prompts are plain text, not locked into one interface
- editable – use any text editor, IDE, or terminal workflow
- LLM-friendly – the XML structure is easy for models to parse and generate
The same .md workflow definition can be executed in multiple hosts:
- the terminal UI (
chat_tui) for interactive work, with a canonical Chat page and a transient Agent page for live tool progress - scripts and CI via
ochat chat-completion - a remote MCP server via
mcp_server, so IDEs and other applications can call agents over stdio or HTTP/SSE
The ChatMD language provides a rich set of features for prompt engineering in a modular way, supporting workflows from simple prompts to more advanced orchestrations. See the language reference.
ChatMD can also be the source of truth for a shell-capable agent harness. A
<shell_access> declaration requests exact process authority; ochat resolves
imports and built-in profiles into a canonical manifest, applies host ceilings,
authorizes that manifest, and only then publishes fixed, structured, chain,
raw, or script-file tools. See the
shell runtime reference and
security guide.
ChatMD prompts can also embed one active host-managed ChatML moderation script. Shell runtimes may independently declare multiple typed ChatML extension scripts for matching, review, interception, effect analysis, and audit filtering:
- declare it with
<script language="chatml" kind="moderator" ...> - keep it invisible to the model request itself
- prepend, append, replace, or delete effective transcript items
- inspect and construct structured items through the
Itembuiltin module - moderate tool calls by approving, rejecting, rewriting, or redirecting them
- persist only serializable runtime state between resumed sessions
- request another ordinary model turn after
turn_endviaRuntime.request_turn() - schedule idle follow-up turns after background internal events or startup work
- orchestrate additional model-backed work via
Model.callandModel.spawn - accept canonical user steering during streaming and append it after pending tool outputs at the next safe model-input boundary
Prompts without a <script> keep the baseline behavior: the shared drivers, chat_tui, export flow, and nested agent execution continue to work without the moderation layer.
Ochat is implemented in OCaml, but the workflows themselves are language-agnostic. Tools exchange JSON, prompts are plain files, and the system makes no assumptions about the kinds of applications your workflows target.
Current provider support: nativley uses OpenAI Responses API format.
The architecture is designed to support additional providers in the future. To use other providers right now you can use a proxy-server and set the enviorment url API_URL to the proxy url
Runtime conversation state is identity-bearing: each canonical occurrence has
an application-owned History_entry.Id. OpenAI item IDs and tool call_id
values remain transport/correlation metadata. Moderator-effective entries add
provenance without rewriting canonical identity, while Chat-TUI rows retain
stable IDs and use indexes only for current layout geometry. Provider request
APIs receive raw OpenAI items only through an explicit payload projection.
What makes Ochat different?
Ochat is not just:
- a coding assistant,
- a prompt playground,
- an orchestration framework,
- or an MCP wrapper.
It is a text-first workflow toolkit built around a few core ideas:
-
Prompt-as-program
A workflow can live in a single file that contains config, tools, transcript state, and execution artifacts. -
Transcript-as-artifact
Runs are inspectable, diffable, branchable, and resumable. -
Prompt packs instead of fixed apps
Build your own Claude Code/Codex-style workflows instead of being limited to a single built-in agent UX. -
Composable agents
Mount one prompt as a tool inside another workflow. -
Host-managed control logic
Use ChatML scripts to moderate tool calls, manage workflow state, and orchestrate multi-step behavior. -
Manifest-bound shell authority Configure structured commands, conservative chains, reviewed raw shells, sandboxing, policy, approvals, custom hooks, redaction, and audit directly in ChatMD. Security-relevant changes invalidate prior authorization.
-
Run anywhere
Use the same workflow in the TUI, the CLI, or through MCP.
How Ochat compares
Ochat occupies a different niche in the LLM tooling landscape than most agent tools.
Compared to coding-agent products
Tools like Claude Code, Codex-style CLIs, or Aider are polished agent applications for working in a repo.
Ochat is more fundamental: it is a toolkit for building your own agent workflows as plain files. Instead of shipping a single hard-coded agent experience, Ochat lets you define prompts, tools, transcript state, and orchestration explicitly.
Compared to orchestration frameworks
Frameworks like LangGraph and similar Python agent stacks are typically code-first: you build workflows in application code.
Ochat is artifact-first: workflows live as text files that can be version-controlled, diffed, composed, resumed, and run in different hosts.
Compared to observability / prompt-management platforms
Platforms like LangSmith, PromptLayer, or Humanloop focus on tracing, evaluation, and hosted prompt management.
Ochat focuses on authoring and running workflows locally as inspectable artifacts. It is not primarily a dashboard or hosted prompt registry.
Compared to MCP tools
MCP provides a protocol for exposing tools and resources to models and clients.
Ochat supports MCP, but it is broader than that. It is a workflow system with:
- ChatMD prompt files
- transcript persistence
- prompt-as-tool composition
- host-managed scripting
- TUI and CLI execution
- local indexing and retrieval tools
The simplest way to think about it
If other tools are:
- agent apps
- Python orchestration frameworks
- hosted observability platforms
- or protocol/tool adapters
then Ochat is best thought of as:
a text-first toolkit for reproducible, composable LLM workflows
Who Ochat is for
Ochat is for developers and power users who want:
- custom AI agents instead of fixed app behavior
- workflows they can version-control and diff
- explicit tool and transcript state
- reproducible runs across local, development, and CI environments
- a local-first, text-first workflow model
Ochat is probably not the best fit if you just want the simplest possible chat UI with minimal setup and no interest in workflow artifacts.
Quick start
New to OCaml?
If you do not already have an OCaml environment set up, start here:
Ochat uses the standard OCaml tooling stack:
- opam for package management and compiler switches
- dune for builds and tests
Once your OCaml environment is installed, you can build and run Ochat as follows.
Build and run a minimal ChatMD prompt.
1. Install dependencies and build
opam switch create .
opam install . --deps-only
dune build
2. Create a prompt file
Create prompts/hello.md:
<config model="gpt-5.2" reasoning_effort="medium"/>
<developer>
You are a helpful assistant.
</developer>
<user>
Say hello and explain what Ochat is in one sentence.
</user>
3. Run it in the terminal UI
dune exec chat_tui -- -file prompts/hello.md
4. Or run it non-interactively
ochat chat-completion \
-prompt-file prompts/hello.md \
-output-file .chatmd/hello-run.md
The output file captures the run as a plain text artifact that you can inspect, diff, resume, or share.
5. Add safe shell access
For a read-oriented repository agent, add a named runtime and fixed tool:
<shell_access id="readonly" extends="builtin:workspace-readonly@1"/>
<tool name="search" type="shell" mode="fixed" runtime="readonly">
<command program="rg"><arg value="--json"/></command>
<arguments mode="required" min_count="1"/>
</tool>
Inspect the expanded authority before running it:
ochat shell inspect prompts/hello.md -canonical
Interactive shell manifests fail closed unless authorized. For a one-process interactive grant:
dune exec chat_tui -- -file prompts/hello.md --authorize-shell-manifest
For intentionally unrestricted local execution, ochat also ships
builtin:yolo@1. YOLO grants the model the user’s local process privileges,
uses direct execution, and disables command approvals. Read the
YOLO security notes
before using it.
First 10 minutes with Ochat
A simple way to get a feel for Ochat:
-
Set up OCaml tooling
If needed, install OCaml, opam, and the toolchain: -
Build the project
opam switch create . opam install . --deps-only dune build -
Run a minimal prompt Create
prompts/hello.mdand run it in the TUI:dune exec chat_tui -- -file prompts/hello.md -
Try a tool-using prompt Run the refactor example:
dune exec chat_tui -- -file prompts/refactor.mdWhile a tool is active, press
Ctrl-Gto inspect live progress. Usej/kto switch calls, arrows (Ctrl-arrows in terminals that encode trackpad gestures that way) or the trackpad to scroll output, andCtrl-GorEscto return to Chat. -
Inspect the workflow artifact Export or save the run and open the resulting
.md/.chatmdfile to see:- the prompt
- the transcript
- tool calls and results
- the exact workflow state captured as text
-
Try a non-interactive run
ochat chat-completion \ -prompt-file prompts/hello.md \ -output-file .chatmd/hello-run.md -
Explore deeper features From there, try:
- agent-as-tool composition
- MCP export via
mcp_server - retrieval/indexing tools
- ChatML moderator scripts
- manifest-authorized ChatMD shell runtimes
What can I do with Ochat?
Author workflows as plain files
Write agents as .md files using ChatMD. A file can act as both:
- a reusable prompt definition
- the execution log of a run
That means prompts, tool calls, results, and transcripts can all be version-controlled and diffed like code.
Build tool-using agents
Combine:
- ChatMD prompt instructions
- built-in tools
- fixed, structured, chain, raw, and script-file shell tools backed by named runtimes
- remote MCP tools
- other agents mounted as tools
Built-in tools include capabilities such as:
- repo-safe editing via
apply_patch - filesystem access via
read_dirand root-scopedread_file - web ingestion via
webpage_to_markdown - retrieval over docs via
index_markdown_docsandmarkdown_search - retrieval over OCaml code via
index_ocaml_codeandquery_vector_db - image import via
import_image
See Tools – built-ins, custom helpers & MCP. For shell-specific schemas and safety behavior, see ChatMD shell tools.
read_file defaults to the directory from which ochat was launched. A ChatMD
file can instead declare any number of named roots, and those roots are
included dynamically in the tool description and JSON schema sent to the
model:
<tool name="read_file" description="Use docs for manuals and source for code.">
<read id="source" path="lib" description="Project OCaml source"/>
<read id="docs" path="${workspace}/docs-src" description="Project documentation"/>
<read id="opam-docs" path="${home}/.opam/default/doc"
description="Installed package documentation"/>
</tool>
The self-closing form creates a root named cwd at ${tool_dir}. In
chat-tui and ochat chat-completion, ${workspace} and ${tool_dir} are
the process launch directory, not the prompt file's directory; launch ochat
from the project that should be the workspace. ${prompt_dir} names the root
prompt directory and ${source_dir} names the file containing a declaration.
The model calls the tool with file, optional root, and optional
non-negative offset and line_count values. Ochat resolves each root to an
absolute path and includes its ID, path, description, and exact root enum in
the tool metadata sent to the model. Requested paths are canonicalized,
remain confined beneath a configured root, and must identify regular text
files. An explicit root with path="/" grants read access across the host
filesystem subject to operating-system permissions. See
configuring read_file roots.
Compose agents into prompt packs
Build Claude Code/Codex-style applications out of multiple prompts:
- planning agents
- coding agents
- test agents
- documentation agents
- orchestration agents
Because prompts can be mounted as tools, you can create modular multi-agent systems without hard-coding everything into one app.
Run the same workflow in different hosts
Use:
chat_tuifor interactive workochat chat-completionfor scripts, CI, and cronmcp_serverto expose prompts as tools to IDEs and other clients
Ground agents in your own corpus
Create indexes over docs or source trees and let prompts query them using natural language. See Search, indexing & code intelligence.
Refine prompts iteratively
Use the mp-refine-run binary to generate, evaluate, and improve prompts and tool descriptions through iterative meta-prompting.
Version, branch, and resume runs
Because conversation state is stored in text files, you can export full runs, branch them, resume them later, and review exactly what changed.
Common use cases
Ochat is useful anywhere you want LLM workflows to be explicit, reproducible, and easy to evolve.
Typical use cases include:
-
Repo-aware coding assistants
Build agents that inspect a codebase, read files, propose patches, and run in a controlled local workflow. -
Documentation agents
Create prompts that summarize docs, update documentation, or answer questions over local documentation sets. -
Planning / review / test workflows
Compose multiple prompts into planning, implementation, review, and test stages. -
Prompt packs for internal tools
Define reusable sets of prompts and tools for domain-specific workflows without burying them inside a UI. -
Retrieval-grounded assistants
Index local docs or source trees and let prompts query them with natural language. -
CI and scripted runs
Run prompts non-interactively in scripts, CI jobs, or recurring automation tasks. -
MCP-exposed prompt tools
Publish prompts as MCP tools so IDEs and other hosts can call them over stdio or HTTP/SSE.
Example ChatMD prompts
Example: minimal prompt
Create prompts/hello.md:
<config model="gpt-5.2" reasoning_effort="medium"/>
<developer>
You are a helpful assistant.
</developer>
<user>
Say hello and explain what Ochat is in one sentence.
</user>
Run it:
dune exec chat_tui -- -file prompts/hello.md
Or:
ochat chat-completion \
-prompt-file prompts/hello.md \
-output-file .chatmd/hello-run.md
Example: interactive refactor agent
Turn a .md file into a refactoring bot that reads files and applies patches under your control.
Create prompts/refactor.md:
<config model="gpt-5.2" reasoning_effort="medium"/>
<tool name="read_dir"/>
<tool name="read_file"/>
<tool name="apply_patch"/>
<developer>
You are a careful refactoring assistant. Work in small, reversible steps.
Before calling apply_patch, explain the change you want to make and wait for
confirmation from the user.
</developer>
<user>
We are in a codebase. Look under ./lib, find a small improvement and
propose a patch.
</user>
Open it in the TUI:
dune exec chat_tui -- -file prompts/refactor.md
From there you can ask the assistant to rename a function, extract a helper, or
update documentation. It will use read_dir and read_file to inspect the
code, then generate apply_patch diffs and apply them.
Press Ctrl-G while tools are active to open the Agent live view without
pausing the Chat stream or tool execution.
Example: publish a prompt as an MCP tool
Export a .md file as a remote tool that other MCP-compatible clients can call.
Create prompts/hello.md:
<config model="gpt-5.2" reasoning_effort="medium"/>
<tool name="read_dir"/>
<tool name="read_file"/>
<developer>You are a documentation assistant.</developer>
<user>
List the files under docs-src/ and summarize what each top-level folder is for.
</user>
Start the MCP server so it exports hello.md as a tool:
dune exec mcp_server -- --http 8080
Any MCP client can now discover the hello tool via tools/list and call it
with tools/call over JSON-RPC. For example:
curl -s http://localhost:8080/mcp \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
The response includes an entry for hello whose JSON schema is inferred from
the ChatMD file.
Example: moderated ChatMD prompt
You can attach one ChatML moderator script to a prompt and keep it host-managed.
Create prompts/review.chatmd:
<config model="gpt-5.2" reasoning_effort="medium"/>
<tool name="read_file"/>
<tool name="apply_patch"/>
<script language="chatml" kind="moderator" id="main">
type state =
{ reminded : bool }
type event =
[ `Session_start
| `Session_resume
| `Turn_start
| `Item_appended(item)
| `Pre_tool_call(tool_call)
| `Post_tool_response(tool_result)
| `Turn_end
]
let initial_state : state =
{ reminded = false }
let on_event : context -> state -> event -> state task =
fun ctx st ev ->
match ev with
| `Session_start ->
let* () =
Turn.prepend_system(
"Before calling apply_patch, explain the change briefly."
)
in
Task.pure(st)
| _ ->
Task.pure(st)
</script>
<developer>
You are a careful code assistant.
</developer>
<user>
Review lib/example.ml and suggest a small safe improvement.
</user>
This example prepends a system instruction at session start, requiring the assistant to explain changes briefly before using apply_patch.
Run it in the TUI or CLI:
dune exec chat_tui -- -file prompts/review.chatmd
ochat chat-completion \
-prompt-file prompts/review.chatmd \
-output-file .chatmd/review-run.chatmd
For a richer end-to-end example, see:
Writing moderator scripts with Item.*
By default, moderator scripts get the Item, Tool_call, and Context
helper modules on the installed moderator surface, so common transcript,
tool-call, and context queries do not need raw record or JSON plumbing.
Moderator scripts receive ctx.items, where each item has the shape:
type item =
{ id : string
; value : json
}
The Item module provides helpers so scripts do not need to hand-author raw
JSON for common cases:
Item.id(item)returns the stable item id used by overlay operationsItem.value(item)returns the underlying structured JSON payloadItem.kind(item)reads the serialized item"type"field when presentItem.role(item)extracts a message role when the item has oneItem.text_parts(item)collects text fragments from common message-like itemsItem.text(item)returns the first text fragment when one is presentItem.input_text_message(id, role, text)builds a structured input messageItem.output_text_message(id, text)builds a structured assistant messageItem.user_text(id, text),Item.assistant_text(id, text),Item.system_text(id, text), andItem.notice(id, text)are convenience constructors over those message shapesItem.is_user(item),Item.is_assistant(item),Item.is_system(item),Item.is_tool_call(item), andItem.is_tool_result(item)are predicate helpers over the serialized role/kind fieldsItem.create(id, value)wraps arbitrary structured JSON as an item
The default moderator surface also exposes pure inspector helpers for tool calls and the current moderation context:
Tool_call.arg(call, name)returns the raw JSON argument when it is presentTool_call.arg_string(call, name),Tool_call.arg_bool(call, name), andTool_call.arg_array(call, name)returnOption.none()when the argument is missing or has the wrong JSON shapeTool_call.is_named(call, name)andTool_call.is_one_of(call, names)match against the serialized tool name using exact string equalityContext.last_item(ctx),Context.last_user_item(ctx),Context.last_assistant_item(ctx),Context.last_system_item(ctx),Context.last_tool_call(ctx), andContext.last_tool_result(ctx)return the last matching item inctx.itemsContext.find_item(ctx, id)uses exact item-id equalityContext.items_since_last_user_turn(ctx)andContext.items_since_last_assistant_turn(ctx)return the suffix beginning at the matching boundary item, or the full item list when no such boundary existsContext.items_by_role(ctx, role)uses exact role-string equalityContext.find_tool(ctx, name)andContext.has_tool(ctx, name)inspectctx.available_toolsusing exact tool-name equality
Example:
let first_text : string array -> string =
fun parts ->
if Array.length(parts) == 0 then "" else Array.get(parts, 0)
let on_event : context -> state -> event -> state task =
fun ctx st ev ->
match ev with
| `Item_appended(item) ->
let summary =
Item.id(item)
++ ":"
++ Option.get_or(Item.role(item), "unknown")
++ ":"
++ first_text(Item.text_parts(item))
in
Task.bind(Turn.append_item(Item.output_text_message("summary", summary)), fun ignored ->
Task.pure(st))
| _ ->
Task.pure(st)
Prefer Turn.append_item, Turn.replace_item, and Turn.delete_item.
The older append_message, replace_message, and delete_message names are
still accepted as aliases.
Additional Phase 1 script helpers:
Turn.replace_or_append(target_id_opt, item)replaces whentarget_id_optisOption.some(id)and appends when it isOption.none()Turn.append_notice(text)appends a synthetic system notice item using a stablesystem:-prefixed id derived from the notice textModel.call_text(recipe, text)is shorthand forModel.call(recipe,String(text))`Model.call_json(recipe, payload)is a named alias forModel.call(recipe, payload)when the payload is already structured JSONModel.spawn_text(recipe, text)is shorthand forModel.spawn(recipe,String(text))`
Runtime semantics in chat_tui
When a prompt runs in chat_tui, moderation is split across three layers:
Moderator_managerowns durable moderator state, overlay state, and queued internal events.In_memory_streamowns one active model/tool turn.chat_tuiowns the session controller that drains wakeups while idle, refreshes visible transcript state, and schedules follow-up turns without creating fake user messages.
The visible transcript in the UI is a projection of canonical history through
the moderator overlay. During streaming, chat_tui still applies token and
tool patches directly for responsiveness, then reprojects the visible
transcript at safe points such as:
- idle/background moderator drains,
- the end of a streamed turn,
- the end of compaction,
- startup or resume moderation.
Long-running foreground work is shown in the status bar with an animated snake shimmer that eases as it grows across the whole label, then falls away from the beginning toward the end. It remains active for the complete assistant turn, changes from “Thinking” to “Writing” as assistant text arrives, uses “Working” while tool calls are emitted or run, and stops only when the final turn completion or error is reduced. Context compaction uses the same animation loop with a separate “Compacting” label.
Chat displays an animated startup barrier while an aggregate background operation partitions the initial transcript across two domains using domain-local TextMate registries and caches. The UI validates one ordered immutable result batch and atomically publishes exact geometry before enabling interaction.
The detached render service remains available for resizing. A cached recent
width restores immediately. For an uncached width, Chat keeps the active exact
width isolated while workers prepare a bounded target-width corridor in
16-row batches. The Resizing snake barrier is replaced atomically by an exact
Corridor; nearby scrolling is cache-only and clamps at prepared boundaries
without replaying rejected movement. Home, End, and off-corridor search results
prepare a bounded destination asynchronously. Background batches then complete
the transcript and promote it to globally exact Warm state.
Width-independent semantic preparation and highlighted spans survive width changes and exact-width eviction; only wrapping, row images, heights, chunks, and geometry are width-specific. The UI domain exclusively owns model, geometry, anchors, publication, redraws, and terminal presentation. Workers own immutable jobs and private bounded caches. Newer resize generations and incompatible history replacements cancel stale work. An isolated failure is retried once; exhausted work shows the resize barrier and uses synchronous full relayout as the last resort. At most three complete recent widths and one preparing width are retained; production worker admission is bounded to 64 queued jobs plus two workers, with 128 entries in each worker-local cache.
Fully warm history uses exact row geometry and a chunked complete image, so arbitrary scrolling avoids virtual-list measurement and convergence. Chunk metadata is keyed by stable row identity, revision, and displayed selection variant, so ordinary updates rebuild only the affected 64-row chunks through a row-to-chunk index rather than rescanning the transcript. Structurally identical terminal frames and unchanged cursors are not presented again. While reviewing older history, exact viewport-damage classification suppresses terminal redraws for streaming changes below the viewport while preserving redraws for visible or preceding rows. Moderator/canonical reprojection uses the same damage classification. Loader animation also pauses during manual review. This isolation—not an assumption that TextMate internals are thread-safe— provides domain safety. See Chat-TUI guide.
Startup diagnostics are disabled by default. Set
OCHAT_TUI_STARTUP_TIMING=1 for phase timings on stderr, and set
OCHAT_TUI_RENDER_METRICS=1 for one shutdown JSON record containing
startup_loader_duration_ms and publication_latency_ms. For
unusually large histories, use these diagnostics to distinguish worker render
cost from final exact-history publication cost. Startup uses two fixed
background domains and has no per-row UI event queue.
Set OCHAT_TUI_SCROLL_TRACE=1 for chat-tui-scroll-trace.jsonl. Resize
records include observation/settlement, exact-width lookup, first exact
corridor readiness, full-width completion, frame submission/presentation,
worker retry, and synchronous fallback. The first-corridor and full-completion
timestamps are separate so visible readiness is distinguishable from
background completion.
For the concrete host/session-controller contract behind those behaviors, see ChatML host session-controller contract.
For the canonical safe-point and effective-history semantics behind request preparation, overlay projection, deferred steering, restore, and visible history refresh, see ChatML safe-point and effective-history semantics.
For a consolidated overview of the moderator runtime layers, builtin surfaces, event types, helper modules, runtime requests, internal events, and UI-only capabilities, see ChatML moderator runtime guide.
For the concrete budget contract that bounds self-triggered turns, idle
internal-event drains, automatic follow-up scheduling, and the ownership of
max_spawned_jobs, see
ChatML budget policy.
For the end-to-end async completion and wakeup path behind Model.spawn,
queued moderator internal events, idle drains, and the current Job deferral,
see
ChatML async completion lifecycle.
For the optional UI-only notification and ask-user capability layer used by
interactive hosts such as chat_tui, see
ChatML UI host capabilities.
That UI layer adds Ui.notify, Approval.ask_text, and
Approval.ask_choice only on the dedicated UI surface.
Ui.notify is a host-local notice path: it does not mutate canonical history
and does not append transcript items automatically. The Approval.ask_* flow
is live-session-only: it pauses the current script, exposes one pending UI
request to the host, and resumes the same script execution from that call site
when the UI submits a validated response. It is not the same as deferred
safe-point steering input, and pending approvals are not persisted across
restart or snapshot restore.
Two host behaviors are especially important:
-
Idle async wakeups
Background producers such as
Model.spawncompletions may enqueue moderator internal events while no turn is active. The host wakes the session controller, drains those internal events, refreshes visible transcript state, and may schedule an idle follow-up turn if the moderator requests one. -
Deferred steering notes
If the user submits steering text while a turn is already streaming, the host does not splice a new canonical user message into the in-flight model request. Instead it stores a deferred steering note and injects it only at the next safe model-input boundary. This preserves the current reasoning and tool workflow while still letting the user steer the next request.
An end-to-end idle async completion looks like this:
- a moderator script previously calls
Model.spawn(...) - the spawned job finishes and is reinjected as an internal event
- the idle
chat_tuisession receives a moderator wakeup - the reducer drains queued internal events through
Moderator_manager - any overlay changes are reprojected into the visible transcript
- if the moderator emitted
Runtime.request_turn(), the host starts one more ordinary turn from the current session state
An end-to-end deferred-steering flow during a tool run looks like this:
- the assistant is in the middle of a streamed turn or tool workflow
- the user submits steering text
- the host records a deferred steering note instead of appending a canonical user item mid-turn
- the current turn reaches a safe point and eventually completes
- the next request is prepared from moderator-effective history
- the deferred steering note is appended as transient system input for that request only
Build from source (OCaml)
Install dependencies, build, and run tests:
opam switch create .
opam install . --deps-only
dune build
dune runtest
On Apple Silicon (macOS arm64), Owl's OpenBLAS dependency can sometimes fail to build during
opam install. If you see BLAS/OpenBLAS errors while installing dependencies or runningdune build, see Build & installation troubleshooting for a proven workaround.
Run an interactive session with the terminal UI:
dune exec chat_tui -- -file prompts/interactive.md
Or run a non-interactive chat completion over a ChatMD prompt as a smoke test:
ochat chat-completion \
-prompt-file prompts/hello.md \
-output-file .chatmd/smoke.md
For more on ochat chat-completion (flags, exit codes, ephemeral runs), see
docs-src/cli/chat-completion.md.
Core concepts
-
ChatMarkdown (ChatMD)
A Markdown + XML dialect that stores model config, tool declarations, requested shell authority, and the full conversation (including tool calls, reasoning traces, and imported artifacts) in a single.mdfile. See the language reference. -
Tools
Functions the model can call, described by explicit JSON schemas. They can be built-ins, manifest-authorized shell tools, other ChatMD agents, or remote MCP tools. See Tools – built-ins, custom helpers & MCP. -
Shell runtime manifest The deterministic expansion of runtime declarations, imports, profiles, tools, scripts, paths, limits, policy, and security settings. Authorization binds to its SHA-256 digest rather than only a prompt path.
-
chat_tui
A Notty-based terminal UI for editing and running.mdfiles. Its Chat page keeps the canonical streaming transcript and editor, while its transient Agent page shows live progress from ac
No comments yet
Be the first to share your take.