ChatML runtime Experimental
Moderator runtime
Understand ChatML surfaces, transactional state, effective history, approval suspension, and host ownership.
View Markdown source ↗For current native/daemon hosting, see host modes and agent-host orchestration. Daemon work belongs to the session actor, not a connected UI. The existing language/tool APIs remain shared; file-backed session/controller descriptions should be read in that host context. Instruction helper compatibility names emit developer-role messages.
This guide describes the current ChatML moderator runtime as exposed by the repository today.
It is the consolidated entry point for:
- builtin surfaces,
- helper modules,
- event types,
- task/effect execution,
- history and overlay semantics,
- runtime requests and internal events,
- UI notifications and approval suspension,
- persistence boundaries,
- and the host/runtime split used by
chat_tuiand other embedders.
When this guide and the implementation disagree, the implementation is authoritative.
Runtime layers
Section titled “Runtime layers”The moderator stack is split into four public layers.
Chatml_runtime
Section titled “Chatml_runtime”The generic host-side runtime for compiled ChatML scripts.
It is responsible for:
- compiling a script against a selected builtin surface,
- instantiating a per-session runtime,
- invoking
on_event, - interpreting returned task values,
- buffering transactional effects,
- committing or rolling back state and effects,
- exposing queued internal events,
- and, when the UI surface is installed, exposing suspended approval state.
Chat_response.Chatml_moderation
Section titled “Chat_response.Chatml_moderation”The stable vocabulary shared across embedders.
It defines:
- event types,
- context, item, tool-call, and tool-result shapes,
- overlay operations,
- tool moderation results,
- runtime requests,
- UI notifications,
- and the host capability bundle used by the shared moderation manager.
Chat_response.Chatml_moderator
Section titled “Chat_response.Chatml_moderator”The durable moderator boundary.
It is responsible for:
- caching compiled moderator artifacts,
- creating or restoring a moderator session,
- applying the durable overlay,
- exposing effective items and effective history,
- replaying queued internal events,
- extracting a persisted snapshot,
- and exposing the live
pending_ui_request/resume_ui_requestboundary.
Chat_response.Chatml_turn_driver
Section titled “Chat_response.Chatml_turn_driver”The turn-boundary helper layer.
It is responsible for:
- preparing model inputs at turn start,
- applying pre-tool and post-tool moderation,
- handling turn-end runtime requests,
- and exposing the same pending-approval boundary to turn-oriented hosts.
It does not own host scheduling policy. Idle drains, wakeups, submit routing, and automatic follow-up policy remain host-owned behavior.
Builtin surfaces
Section titled “Builtin surfaces”ChatML uses composable builtin surfaces rather than one hard-coded builtin universe.
Chatml_builtin_surface.core_surface
Section titled “Chatml_builtin_surface.core_surface”The language core surface. It includes:
- global helpers such as
print,to_string, andlength, - core modules such as
Task,String,Array,Json,Option, andHashtbl, - and builtin type aliases such as
json.
Chatml_builtin_surface.moderator_surface
Section titled “Chatml_builtin_surface.moderator_surface”The default surface for moderator scripts.
It extends core_surface with moderator-oriented modules and aliases,
including:
- helper modules:
ItemTool_callContext
- effectful capability modules:
LogTurnToolModelProcessScheduleRuntime
- structural type aliases:
itemtool_desctool_calltool_resultcontext
Chatml_builtin_surface.ui_moderator_surface
Section titled “Chatml_builtin_surface.ui_moderator_surface”The optional UI-capable surface for interactive hosts.
It extends moderator_surface with:
UiApproval
This surface split is deliberate:
- non-UI embedders can keep the default moderator surface unchanged;
- scripts compiled against
ui_moderator_surfacedo not silently run on hosts that never opted into UI-only capabilities.
Script entrypoints
Section titled “Script entrypoints”The moderator runtime expects two convention-based entrypoints:
let initial_state = ...
let on_event : context -> state -> event -> state task = fun ctx st ev -> ...initial_state provides the durable committed script state for a fresh
session.
on_event is invoked for each moderator event. It must return a task value.
Event model
Section titled “Event model”The script-visible moderation event constructors are:
type event = [ `Session_start | `Session_resume | `Turn_start | `Item_appended(item) | `Pre_tool_call(tool_call) | `Post_tool_response(tool_result) | `Turn_end ]Hosts may also deliver arbitrary internal events by reinjecting raw ChatML values such as:
`Queued("later")`Tick`Model_job_succeeded(job_id, recipe, result)Those arrive through the Internal_event host path and are matched directly by
the script as ordinary variants.
Context
Section titled “Context”Scripts receive a context record with:
type context = { session_id : string ; now_ms : int ; phase : string ; items : item array ; available_tools : tool_desc array ; session_meta : json }context.phase is a string view of the current host phase, using names such
as:
session_startsession_resumeturn_startmessage_appendedpre_tool_callpost_tool_responseturn_endinternal_event
The event constructor and the phase string are related but not identical. For
example, the event constructor is `Item_appended(item), while
context.phase for that handler is "message_appended".
Helper modules on the moderator surface
Section titled “Helper modules on the moderator surface”Item provides constructors and accessors for common transcript items.
Instruction helpers emit the developer role. The compatibility names
Item.system_text, Turn.prepend_system, and notice helpers remain available;
Item.input_text_message(id, "system", text) also creates a developer message.
Item.role reports the actual developer role. Item.is_system and
Context.last_system_item recognize both developer instructions and legacy system
items. This changes newly constructed messages only: existing snapshots, canonical
history, and raw values supplied to Item.create are not rewritten.
Useful helpers include:
Item.idItem.valueItem.kindItem.roleItem.text_partsItem.textItem.input_text_messageItem.output_text_messageItem.user_textItem.assistant_textItem.system_textItem.noticeItem.is_userItem.is_assistantItem.is_systemItem.is_tool_callItem.is_tool_result
Tool_call
Section titled “Tool_call”Tool_call provides payload inspection helpers:
Tool_call.argTool_call.arg_stringTool_call.arg_boolTool_call.arg_arrayTool_call.is_namedTool_call.is_one_of
Context
Section titled “Context”Context provides selectors over projected history and tool availability:
Context.last_itemContext.last_user_itemContext.last_assistant_itemContext.last_system_itemContext.last_tool_callContext.last_tool_resultContext.find_itemContext.items_since_last_user_turnContext.items_since_last_assistant_turnContext.items_by_roleContext.find_toolContext.has_tool
Task values and task syntax
Section titled “Task values and task syntax”Moderator scripts are effectful through task values.
The core task combinators are:
Task.pure : 'a -> 'a taskTask.bind : 'a task -> ('a -> 'b task) -> 'b taskTask.map : 'a task -> ('a -> 'b) -> 'b taskTask.fail : string -> 'a taskTask.catch : 'a task -> (string -> 'a task) -> 'a taskChatML also supports task let-syntax:
let* x = task_value in ...let+ x = task_value in ...These desugar to Task.bind and Task.map.
Effectful capability modules
Section titled “Effectful capability modules”Diagnostic logging:
Log.debug : string -> unit taskLog.info : string -> unit taskLog.warn : string -> unit taskLog.error : string -> unit taskTransactional overlay operations:
Turn.prepend_system : string -> unit taskTurn.append_item : item -> unit taskTurn.replace_item : string -> item -> unit taskTurn.delete_item : string -> unit taskTurn.replace_or_append : string option -> item -> unit taskTurn.append_notice : string -> unit taskTurn.halt : string -> unit taskThe older append_message, replace_message, and delete_message names are
accepted as aliases, but the item-oriented names are preferred.
Tool moderation and synchronous/asynchronous host tool execution:
Tool.approve : unit -> unit taskTool.reject : string -> unit taskTool.rewrite_args : json -> unit taskTool.redirect : string -> json -> unit taskTool.call : string -> json -> [ `Ok(json) | `Error(string) ] taskTool.spawn : string -> json -> string taskHost-managed model recipes:
Model.call : string -> json -> [ `Ok(json) | `Refused(string) | `Error(string) ] taskModel.spawn : string -> json -> string taskModel.call_text : string -> string -> string taskModel.call_json : string -> json -> [ `Ok(json) | `Refused(string) | `Error(string) ] taskModel.spawn_text : string -> string -> string taskRecipe names are host-defined. They are not raw provider/model identifiers.
Process
Section titled “Process”Host-managed subprocess execution:
Process.run : string -> string array -> string taskSchedule
Section titled “Schedule”Host-managed delayed reinjection:
Schedule.after_ms : int -> 'e -> string taskSchedule.cancel : string -> unit taskRuntime
Section titled “Runtime”Runtime control and internal-event emission:
Runtime.emit : 'e -> unit taskRuntime.request_compaction : unit -> unit taskRuntime.request_turn : unit -> unit taskRuntime.end_session : string -> unit taskHistory and overlay model
Section titled “History and overlay model”There are three related transcript views:
- canonical history: durable
History_entry.toccurrences stored by the host, each with an application-owned ID; - effective entries: canonical history projected through the durable moderator overlay with canonical/inserted/replacement provenance;
- visible rows: the host/UI presentation derived from effective entries, with stable row IDs and revision numbers.
Turn.* operations do not directly rewrite canonical history. They update a
durable overlay that can:
- prepend synthetic developer items (through the compatibility name
prepend_system), - append synthetic items,
- replace projected items by id,
- delete projected items by id,
- halt the session with a reason.
Replacement preserves the target canonical ID; insertions allocate new host IDs; deletion records a tombstone. Before the next model request, the host computes effective entries and unwraps only their OpenAI payloads at the provider boundary.
Committed overlay batches carry a monotonic revision and immutable operation facts. Interactive hosts may observe those commits immediately, but they reproject visible rows only at foreground-operation safe points.
Safe-point and runtime semantics
Section titled “Safe-point and runtime semantics”The main safe boundaries are:
- session start and resume,
- turn start,
- pre-tool moderation,
- post-tool handling,
- turn end,
- idle internal-event drain,
- host-visible-history refresh.
The turn driver owns request-preparation and tool/turn boundary helpers.
The host owns:
- when idle drains happen,
- when follow-up turns are started,
- how queued async completions wake a session,
- how visible UI state is refreshed,
- and how user input is routed while an operation is already in flight.
Runtime requests
Section titled “Runtime requests”Committed moderator execution may surface:
Request_compactionRequest_turnEnd_session(reason)
Hosts decide how to honor those requests. The shared runtime policy collapses multiple requests rather than treating them as independent side effects.
Host budgets
Section titled “Host budgets”Automatic turns, internal-event drains, and spawned jobs have separate limits and owners. They are not a hard dollar cap. Native/daemon work follows the agent orchestration contract; the detailed budget policy documents the older shared host/controller boundary. Installing a builtin surface does not grant a host capability or override its admission policy.
Internal events
Section titled “Internal events”Runtime.emit(event) buffers a ChatML value transactionally.
On successful commit:
- the buffered event is appended to the session queue;
- later, the host may replay queued events FIFO through the internal-event path.
If the task fails, buffered emitted events are discarded.
Commit, rollback, and suspension
Section titled “Commit, rollback, and suspension”Moderator execution is transactional.
On successful task completion:
- the new state becomes committed,
- transactional local effects become visible in execution order,
- buffered emitted events are enqueued,
- and a pending end-session request halts the session.
On failure:
- the previous committed state stays in place,
- buffered local effects are discarded,
- buffered emitted events are discarded,
- and buffered runtime requests are discarded.
UI-only capabilities
Section titled “UI-only capabilities”The UI surface adds two modules:
module Ui : sig val notify : string -> unit taskend
module Approval : sig val ask_text : string -> string task val ask_choice : string -> string array -> string taskendUi.notify
Section titled “Ui.notify”Ui.notify emits a host-local notice.
It is:
- visible to the current embedding,
- not persisted as canonical history,
- not appended to transcript items automatically,
- and not part of the default non-UI surface.
Approval.ask_text and Approval.ask_choice
Section titled “Approval.ask_text and Approval.ask_choice”Approval requests pause the current live script execution and later resume that same execution with a validated response.
They do not:
- append a fake canonical user item automatically,
- create a second internal-event state machine,
- or alter the durable snapshot format.
The host-visible boundary is:
type pending_ui_request = | Ask_text of { prompt : string } | Ask_choice of { prompt : string; choices : string array }
val pending_ui_request : session -> pending_ui_request optionval resume_ui_request : session -> response:string -> (unit, string) resultAt the moderator wrapper level the same concept is exposed through
Chat_response.Chatml_moderator and Chat_response.Chatml_turn_driver.
Suspension semantics
Section titled “Suspension semantics”While approval is pending:
- ordinary
handle_eventprogression is blocked, - queued internal events may accumulate but are not drained,
- buffered local effects remain uncommitted,
- buffered emitted events remain uncommitted,
current_stateremains the last committed state,- and
resume_ui_requestis the only supported continuation path.
Only one approval prompt may be pending per session at a time.
Validation rules
Section titled “Validation rules”Approval.ask_texttrims surrounding whitespace before returning the value to the script.- An empty trimmed text response does not resume the script.
Approval.ask_choicetrims surrounding whitespace and exact-matches the normalized value against one of the declared choices.Approval.ask_choicewith an emptychoicesarray fails immediately.
Persistence boundary
Section titled “Persistence boundary”Pending approvals are live-session-only.
They are not stored in the moderator snapshot, and restoring a partially suspended approval is unsupported.
chat_tui behavior
Section titled “chat_tui behavior”chat_tui is the reference interactive embedding for the UI surface.
It reuses:
- the ordinary composer/input UI,
- the normal submit path,
- the moderator wakeup and idle-drain controller,
- and visible-history refresh built from effective history.
While approval is pending:
- the prompt is rendered through the existing input UI,
- submit is repurposed to approval submission,
- idle moderator drains for that session do not proceed,
- automatic follow-up turns for that session do not start,
- and no fake canonical user item is appended automatically.
Choosing the right document
Section titled “Choosing the right document”Use this guide for the consolidated runtime picture.
Use these focused documents when you need more detail on one topic:
- ChatML safe-point and effective-history semantics
- ChatML host session-controller contract
- ChatML async completion lifecycle
- ChatML budget policy
- ChatML UI host capabilities
- ChatML language specification
Shell runtime integration
Section titled “Shell runtime integration”Process.run is installed only when ChatMD declares:
<moderator_runtime shell_runtime="moderator-processes"/>The operation creates structured argv and routes through the same immutable
shell registry used by agent tools: resolver/fingerprint, effects,
administrative and manifest policy, capabilities, approval, interceptors,
backend, limits, output finalization, and audit. There is no moderator-only
direct-spawn path. Without a binding, Process.run is unavailable.
Shell-specific script kinds (shell_matcher, shell_reviewer,
shell_before_interceptor, shell_after_interceptor,
shell_effect_analyzer, and shell_audit_filter) use purpose-built surfaces
and do not inherit moderator Process, Model, Tool, filesystem, network,
or UI capabilities. See the shell extension guide.