| Title: | AI Agent Runtime |
|---|---|
| Description: | An agent runtime that gives Large Language Models (LLMs) from 'Anthropic' <https://www.anthropic.com/>, 'OpenAI' <https://openai.com/>, 'Moonshot' <https://www.moonshot.ai/>, and 'Ollama' <https://ollama.com/> direct access to a live R session with managed workspace state. Tools execute as R function calls with provenance tracking, and a deterministic retrieval system keeps relevant objects in context across turns. Three entry points: a shell command-line interface (CLI), a console read-eval-print-loop via chat(), and a Model Context Protocol (MCP) server via serve() for external clients. |
| Authors: | Troy Hernandez [aut, cre] (ORCID: <https://orcid.org/0009-0005-4248-604X>), Grant McDermott [ctb] (ORCID: <https://orcid.org/0000-0001-7883-8573>), Jorge Krzyzaniak [ctb], cornball.ai [cph] |
| Maintainer: | Troy Hernandez <[email protected]> |
| License: | Apache License (>= 2) |
| Version: | 0.8.0 |
| Built: | 2026-11-15 08:55:44 UTC |
| Source: | https://github.com/cranhaven/cranhaven.r-universe.dev |
Observers run after every tool call (run, denied, or declined). They
receive a single event list with fields:
add_observer(session, observer)add_observer(session, observer)
session |
A session environment from |
observer |
A function of one argument (the event list). |
call — the call list passed to policy().
decision — the policy decision for the call.
outcome — one of "ran", "deny",
"declined".
result — the string returned to the LLM.
success — logical; TRUE only for "ran" with no
tool error.
elapsed_ms — wall time including policy overhead.
turn_number — the session's tool-call counter.
Errors raised inside an observer are swallowed.
The session, invisibly.
s <- new_session(provider = "anthropic") add_observer(s, function(event) { # An observer is just a function of one argument; record the # outcome for inspection. message(event$outcome) }) length(s$on_tool)s <- new_session(provider = "anthropic") add_observer(s, function(event) { # An observer is just a function of one argument; record the # outcome for inspection. message(event$outcome) }) length(s$on_tool)
Never runs automatically. Only files in the named writer's payload store are eligible; event metadata and other writers' stores are left intact. Expiry decisions append to a separate maintenance stream before deletion. Event retention is a planning value until an explicit event-retention tool is used.
audit_payload_expire(store, days = 30, now = Sys.time(), dry_run = TRUE)audit_payload_expire(store, days = 30, now = Sys.time(), dry_run = TRUE)
store |
A writer's payload store path from a payload reference. |
days |
Retain this many days, default 30. |
now |
Comparison time, default Sys.time(). |
dry_run |
Report eligible hashes without deleting, default TRUE. |
Character vector of eligible (or deleted) hashes.
## Not run: audit_payload_expire(ref$store) ## End(Not run)## Not run: audit_payload_expire(ref$store) ## End(Not run)
Pins keep incident/evaluation evidence through explicit expiry. Pinned bytes still count toward the store quota; capture reports quota omissions when full.
audit_payload_pin(ref, pin = TRUE)audit_payload_pin(ref, pin = TRUE)
ref |
Payload reference from an audit event. |
pin |
TRUE to pin, FALSE to remove the pin. |
The payload reference, invisibly.
## Not run: audit_payload_pin(ref) ## End(Not run)## Not run: audit_payload_pin(ref) ## End(Not run)
Reads only a payload whose bytes still match its recorded SHA-256. Redaction affects the returned view, never the stored record. An omitted, expired, missing or damaged payload is an error, not an empty successful read.
audit_payload_read(ref, redact = TRUE, secrets = character())audit_payload_read(ref, redact = TRUE, secrets = character())
ref |
Payload reference from an audit event or request manifest. |
redact |
Apply display redaction. Defaults to TRUE. |
secrets |
Additional known credential values to hide in the view. |
The decoded JSON object or UTF-8 text, as recorded.
## Not run: audit_payload_read(ref) ## End(Not run)## Not run: audit_payload_read(ref) ## End(Not run)
Uses the auto-log decoder, retaining malformed-line locations and separating a torn final line from interior damage. A missing terminal turn record means unresolved completion. Redaction changes this returned view only; callers can supply additional known credential values. It cannot detect all secrets.
audit_read(path, redact = TRUE, secrets = character())audit_read(path, redact = TRUE, secrets = character())
path |
Path to a writer's JSONL stream. |
redact |
Apply display redaction, default TRUE. |
secrets |
Additional known credential values to hide. |
List with records, auto-run start/end compatibility fields and malformed-record diagnostics. Turn endings remain in records.
audit_read(tempfile())audit_read(tempfile())
Walks the per-room session registry and archives each room's
unarchived Matrix events via the pensar archive ingest. Archived
events are consumed from the session ledger, and their Matrix event
ids are persisted per room under CORTEZA_STATE_DIR so a
restart's backfill is recognized rather than archived again. Silent
no-op when pensar is not installed.
bot_archive_all(sessions, chat = NULL)bot_archive_all(sessions, chat = NULL)
sessions |
A registry environment built by
|
chat |
Optional chat.api client for room-name lookups. When NULL, the room ID is used as the source identifier. |
Integer count of rooms ingested, invisibly.
## Not run: # Requires a running Matrix session registry and the optional # pensar package for the actual archive step. reg <- new.env(parent = emptyenv()) bot_archive_all(reg) ## End(Not run)## Not run: # Requires a running Matrix session registry and the optional # pensar package for the actual archive step. reg <- new.env(parent = emptyenv()) bot_archive_all(reg) ## End(Not run)
Logs in to a Matrix homeserver as the bot account, joins (or records)
the target room, and writes credentials to
tools::R_user_dir("corteza", "config")/matrix.json with file
mode 0600. Call once per host. Model, provider, tools_filter, and
auto_approve_asks are defaults the poll loop uses unless overridden
at call time.
bot_configure( server, user, password, room, model = NULL, provider = "anthropic", tools_filter = NULL, auto_approve_asks = FALSE, bots = NULL, models = NULL, model_badge = c("never", "non_default", "always"), display_name = NULL, fallback = NULL )bot_configure( server, user, password, room, model = NULL, provider = "anthropic", tools_filter = NULL, auto_approve_asks = FALSE, bots = NULL, models = NULL, model_badge = c("never", "non_default", "always"), display_name = NULL, fallback = NULL )
server |
Character. Homeserver base URL. |
user |
Character. Bot localpart or full Matrix ID. |
password |
Character. Bot password. Stored locally so the bot can re-authenticate if its access token is invalidated. |
room |
Character. Room ID or alias the bot should read and post to. If the bot has been invited but not joined, it will be joined. |
model |
Character or NULL. Default model name. |
provider |
Character. LLM provider: "anthropic", "anthropic_claude", "openai", "openai_codex", "moonshot", or "ollama". |
tools_filter |
Character vector or NULL. Passed to
|
auto_approve_asks |
Logical. Who rules on a tool call that
needs approval. Rules in code read every call first, and what they
catch (credentials, writes outside the project, elevated
privileges, other machines, publishing, discarded work) is always
put to a person by reaction in the room. When TRUE, the rest goes
to a monitor: a read-only model in its own process that approves,
refuses, or passes the call on to a person. When FALSE (default) a
person is asked for the rest as well. Nothing is approved unseen.
The monitor's model is set under |
bots |
Character vector or NULL. Full Matrix IDs of other known bot accounts. Their messages only get a reply when they mention this bot, and they are not counted as humans when deciding whether a room gets ungated replies (a room whose only non-bot member is one human is answered without a mention). |
models |
Character vector or NULL. Extra entries for the
|
model_badge |
Character. When to show which model is answering:
|
display_name |
Character or NULL. Base display name the badge rename builds on. Defaults to the localpart of the bot's user id. |
fallback |
Character vector or NULL. What |
Pre-CRAN releases stored the file at ~/.corteza/matrix.json;
that path is still read for backward compatibility, but the next
bot_configure() call writes to the new location.
The saved configuration, invisibly.
## Not run: # Requires a real Matrix server and bot credentials. Configuration # is written under tools::R_user_dir("corteza", "config"). bot_configure( server = "https://matrix.example.org", user = "bot", password = "secret", room = "!roomid:example.org" ) ## End(Not run)## Not run: # Requires a real Matrix server and bot credentials. Configuration # is written under tools::R_user_dir("corteza", "config"). bot_configure( server = "https://matrix.example.org", user = "bot", password = "secret", room = "!roomid:example.org" ) ## End(Not run)
Fetches new messages across all joined rooms and runs turn
against each. Auto-joins any pending invites the bot has received.
Replies are sent back to the originating room. On first run there is
no saved sync token, so this call establishes a baseline and returns
without processing history.
bot_poll( system = NULL, model = NULL, provider = NULL, tools_filter = NULL, timeout = 0L, sessions = NULL, hooks = NULL )bot_poll( system = NULL, model = NULL, provider = NULL, tools_filter = NULL, timeout = 0L, sessions = NULL, hooks = NULL )
system |
Character or NULL. System prompt override. |
model |
Character or NULL. Model override. |
provider |
Character or NULL. Provider override. |
tools_filter |
Character vector or NULL. Tool filter override. |
timeout |
Integer. Long-poll timeout in milliseconds. 0 returns immediately. |
sessions |
Environment from |
hooks |
Named list of functions or NULL, as validated by
|
Pass sessions = NULL (the default) for a stateless one-shot —
each incoming message builds a fresh session. Pass a registry created
by bot_new_session_registry() so a long-running
bot_run keeps a separate history per room (conversations
in different rooms don't cross-contaminate).
An integer count of messages replied to, invisibly.
## Not run: # Single poll cycle against the configured Matrix homeserver. bot_poll(timeout = 5000L) ## End(Not run)## Not run: # Single poll cycle against the configured Matrix homeserver. bot_poll(timeout = 5000L) ## End(Not run)
Drops an archive.signal file in the corteza state directory.
The next iteration of the long-poll loop in bot_run
picks it up, runs bot_archive_all, and removes the
file. Safe to call from any process or scheduler — systemd, Task
Scheduler, launchd, cron, or a separate R session — without needing
to know the bot's PID or share its memory.
bot_request_flush()bot_request_flush()
The signal file path, invisibly.
# Writes a sentinel file under CORTEZA_STATE_DIR (or the package's # R_user_dir data path). Redirect to a tempdir for the example so # we don't touch persistent state. old <- Sys.getenv("CORTEZA_STATE_DIR") Sys.setenv(CORTEZA_STATE_DIR = file.path(tempdir(), "state")) sig <- bot_request_flush() file.exists(sig) unlink(Sys.getenv("CORTEZA_STATE_DIR"), recursive = TRUE) Sys.setenv(CORTEZA_STATE_DIR = old)# Writes a sentinel file under CORTEZA_STATE_DIR (or the package's # R_user_dir data path). Redirect to a tempdir for the example so # we don't touch persistent state. old <- Sys.getenv("CORTEZA_STATE_DIR") Sys.setenv(CORTEZA_STATE_DIR = file.path(tempdir(), "state")) sig <- bot_request_flush() file.exists(sig) unlink(Sys.getenv("CORTEZA_STATE_DIR"), recursive = TRUE) Sys.setenv(CORTEZA_STATE_DIR = old)
Creates one session up front and reuses it across polls so conversation
history accumulates within the process lifetime. Intended as the entry
point for a systemd user unit. A thin wrapper over
bot_run_init plus a bot_run_step
loop; call those two directly when an external scheduler needs to own
the main process.
bot_run( timeout = 30000L, system = NULL, model = NULL, provider = NULL, tools_filter = NULL )bot_run( timeout = 30000L, system = NULL, model = NULL, provider = NULL, tools_filter = NULL )
timeout |
Integer. Long-poll timeout in milliseconds. |
system |
Character or NULL. System prompt override. |
model |
Character or NULL. Model override. |
provider |
Character or NULL. Provider override. |
tools_filter |
Character vector or NULL. Tool filter override. |
Never returns under normal operation. Crashes on fatal error so systemd can restart.
This is a bare bot: it runs with no hooks, so /clear
archives and resets without filing the conversation anywhere, and a
thread reply starts from the thread's own history. A host that wants
segment rooms and rehydration drives the loop itself through
bot_run_init and supplies them.
## Not run: # Run the Matrix bot loop -- typically launched by a systemd unit # rather than from an interactive R session. bot_run() ## End(Not run)## Not run: # Run the Matrix bot loop -- typically launched by a systemd unit # rather than from an interactive R session. bot_run() ## End(Not run)
Performs everything bot_run does before its loop:
builds the per-room session registry, catches up on invites that
predate the saved sync token, and backfills recent room history into
the registry. Returns an opaque state object to drive with
bot_run_step.
bot_run_init( system = NULL, model = NULL, provider = NULL, tools_filter = NULL, hooks = NULL )bot_run_init( system = NULL, model = NULL, provider = NULL, tools_filter = NULL, hooks = NULL )
system |
Character or NULL. System prompt override. |
model |
Character or NULL. Model override. |
provider |
Character or NULL. Provider override. |
tools_filter |
Character vector or NULL. Tool filter override. |
hooks |
Named list of functions or NULL. Policy the loop asks
the host about; see Hooks. NULL, the default, means a bare bot:
|
End-to-end encryption is not set up here. When the config sets
e2ee, the chat.api Matrix adapter owns the Olm account and
Megolm sessions and both directions of encrypted traffic, so there is
no crypto state for this package to build or carry.
Use this with bot_run_step() when an external loop owns the
main process and needs to interleave the Matrix poll with other work
(a scheduler, a multiplexer, an embedding host). For a standalone bot,
call bot_run, which wraps both.
A list holding the session registry, startup session handle,
archive-flush signal path, and the saved poll options. Pass it to
bot_run_step.
corteza runs a conversation. What becomes of one once it ends, and where a thread's earlier context comes from, belong to the host that runs the bot over time (cerebro files segments and folds them into topic rooms). Two slots, both optional:
on_clear(chat, cfg, room_id, title, vault_ref)Called
from the /clear handler once the conversation is archived,
when the user said something in it and the clear was not inside a
thread. chat is the live chat.api client, cfg the
bot config, title the segment title corteza derived from
the transcript's opening line, vault_ref the archive's
vault-relative path (or NULL). Returns list(id, name) for a
room it filed the conversation into, which the acknowledgement
names, or NULL for none. An error is reported and treated as
none: archive-and-reset is the critical path.
thread_context(chat, room_id, thread_root)Called once
per thread session before its first reply. Returns
list(segment, vault) naming the archived conversation the
thread continues, or NULL when it continues nothing; vault
is the transcript's path, vault-relative or absolute. A result
seeds the session with the transcript's tail.
## Not run: # Drive the loop yourself instead of calling bot_run(): state <- bot_run_init() repeat bot_run_step(state, timeout = 30000L) ## End(Not run)## Not run: # Drive the loop yourself instead of calling bot_run(): state <- bot_run_init() repeat bot_run_step(state, timeout = 30000L) ## End(Not run)
Polls /sync once (blocking up to timeout ms, returning
early when a message arrives), runs the agent against any new messages
and posts the replies, then services a pending archive-flush signal.
Mutates the session registry held in state in place, so
successive calls accumulate conversation history.
bot_run_step(state, timeout = 30000L)bot_run_step(state, timeout = 30000L)
state |
A state object from |
timeout |
Integer. Long-poll timeout in milliseconds. |
Invisibly, the integer count of messages replied to this poll.
## Not run: state <- bot_run_init() bot_run_step(state, timeout = 5000L) ## End(Not run)## Not run: state <- bot_run_init() bot_run_step(state, timeout = 5000L) ## End(Not run)
Send a message to a Matrix room
bot_send(text, room_id = NULL, msgtype = "m.text", markdown = FALSE)bot_send(text, room_id = NULL, msgtype = "m.text", markdown = FALSE)
text |
Character. Plain text body. |
room_id |
Character. Matrix room id. Defaults to |
msgtype |
Character. Matrix msgtype, default "m.text". |
markdown |
Logical. If TRUE, also send Matrix custom HTML derived from a conservative markdown subset. |
The event ID of the sent message.
## Not run: # Requires bot_configure() to have run. bot_send("hello from corteza") ## End(Not run)## Not run: # Requires bot_configure() to have run. bot_send("hello from corteza") ## End(Not run)
Run a conversational agent inside your R session. Tools execute as direct function calls, no MCP server needed.
chat( provider = NULL, model = NULL, tools = NULL, session = NULL, max_turns = NULL, web_search = NULL )chat( provider = NULL, model = NULL, tools = NULL, session = NULL, max_turns = NULL, web_search = NULL )
provider |
LLM provider: "anthropic", "openai", "moonshot", or "ollama". Defaults to the saved provider when resuming, otherwise the config value or "anthropic". |
model |
Model name. Defaults to the saved model when resuming with the same provider, otherwise the matching config value or provider default. |
tools |
Character vector of tool names or categories to enable. Categories: file, code, r, data, web, git, chat, memory. Use "core" for file+code+git, "all" for everything (default). |
session |
Session resume control. NULL (default) starts fresh, TRUE resumes the latest session, or a character session key to resume a specific session. |
max_turns |
Integer or NULL. Maximum LLM turns per user prompt
before the loop stops with |
web_search |
Logical or NULL. Enable provider-native (server-side) web search for the conversation. NULL uses the config default (on for supported providers); the model searches inside its own turn with no Tavily key required. |
The session object (invisibly).
if (interactive()) { # Start chatting with defaults from config chat() # Use a specific provider/model chat(provider = "ollama", model = "llama3.2") # Minimal tools for focused work chat(tools = "core") }if (interactive()) { # Start chatting with defaults from config chat() # Use a specific provider/model chat(provider = "ollama", model = "llama3.2") # Minimal tools for focused work chat(tools = "core") }
Walks getOption("corteza.local_models") (default
c("gpt-oss:120b", "gpt-oss:20b")) and returns the first one that
is currently installed in the local Ollama server. Returns NULL if
Ollama is unreachable or none of the candidates are installed.
Cached per R process.
default_local_model()default_local_model()
Character scalar model name, or NULL.
# NULL when Ollama isn't running locally; a model name otherwise. model <- default_local_model() is.null(model) || is.character(model)# NULL when Ollama isn't running locally; a model name otherwise. model <- default_local_model() is.null(model) || is.character(model)
Install the corteza command-line tool to a directory in your
PATH. On Unix (Linux, macOS) installs the Rscript shebang binary.
On Windows installs a .cmd wrapper alongside the script so
corteza works from cmd.exe / PowerShell.
install_cli(path = NULL, force = FALSE)install_cli(path = NULL, force = FALSE)
path |
Directory to install to. Default is |
force |
Overwrite existing installation. |
Requires:
r (littler) for fast R script execution (Unix only —
Windows uses Rscript).
The llm.api package for LLM connectivity
The corteza package itself
After installation, run corteza from any terminal (you may
need to add the install directory to PATH; the function prints the
PATH hint if it isn't already there).
The installed script path, invisibly.
## Not run: install_cli() install_cli("/usr/local/bin") ## End(Not run)## Not run: install_cli() install_cli("/usr/local/bin") ## End(Not run)
Deprecated: use bot_archive_all()
matrix_archive_all(sessions, mx_sess = NULL, chat = NULL)matrix_archive_all(sessions, mx_sess = NULL, chat = NULL)
sessions |
A registry environment built by
|
mx_sess |
Ignored. Kept so calls written for corteza 0.7.1 still
run; pass |
chat |
Optional chat.api client for room-name lookups. When NULL, the room ID is used as the source identifier. |
See bot_archive_all.
## Not run: matrix_archive_all(sessions) # use bot_archive_all() ## End(Not run)## Not run: matrix_archive_all(sessions) # use bot_archive_all() ## End(Not run)
Deprecated: use bot_configure()
matrix_configure( server, user, password, room, model = NULL, provider = "anthropic", tools_filter = NULL, auto_approve_asks = FALSE, bots = NULL, models = NULL, model_badge = c("never", "non_default", "always"), display_name = NULL, fallback = NULL )matrix_configure( server, user, password, room, model = NULL, provider = "anthropic", tools_filter = NULL, auto_approve_asks = FALSE, bots = NULL, models = NULL, model_badge = c("never", "non_default", "always"), display_name = NULL, fallback = NULL )
server |
Character. Homeserver base URL. |
user |
Character. Bot localpart or full Matrix ID. |
password |
Character. Bot password. Stored locally so the bot can re-authenticate if its access token is invalidated. |
room |
Character. Room ID or alias the bot should read and post to. If the bot has been invited but not joined, it will be joined. |
model |
Character or NULL. Default model name. |
provider |
Character. LLM provider: "anthropic", "anthropic_claude", "openai", "openai_codex", "moonshot", or "ollama". |
tools_filter |
Character vector or NULL. Passed to
|
auto_approve_asks |
Logical. Who rules on a tool call that
needs approval. Rules in code read every call first, and what they
catch (credentials, writes outside the project, elevated
privileges, other machines, publishing, discarded work) is always
put to a person by reaction in the room. When TRUE, the rest goes
to a monitor: a read-only model in its own process that approves,
refuses, or passes the call on to a person. When FALSE (default) a
person is asked for the rest as well. Nothing is approved unseen.
The monitor's model is set under |
bots |
Character vector or NULL. Full Matrix IDs of other known bot accounts. Their messages only get a reply when they mention this bot, and they are not counted as humans when deciding whether a room gets ungated replies (a room whose only non-bot member is one human is answered without a mention). |
models |
Character vector or NULL. Extra entries for the
|
model_badge |
Character. When to show which model is answering:
|
display_name |
Character or NULL. Base display name the badge rename builds on. Defaults to the localpart of the bot's user id. |
fallback |
Character vector or NULL. What |
See bot_configure.
## Not run: matrix_configure(server, user, password, room) # use bot_configure() ## End(Not run)## Not run: matrix_configure(server, user, password, room) # use bot_configure() ## End(Not run)
Deprecated: use bot_poll()
matrix_poll( system = NULL, model = NULL, provider = NULL, tools_filter = NULL, timeout = 0L, sessions = NULL, crypto = NULL, hooks = NULL )matrix_poll( system = NULL, model = NULL, provider = NULL, tools_filter = NULL, timeout = 0L, sessions = NULL, crypto = NULL, hooks = NULL )
system |
Character or NULL. System prompt override. |
model |
Character or NULL. Model override. |
provider |
Character or NULL. Provider override. |
tools_filter |
Character vector or NULL. Tool filter override. |
timeout |
Integer. Long-poll timeout in milliseconds. 0 returns immediately. |
sessions |
Environment from |
crypto |
Ignored. Kept so calls written for corteza 0.7.1 still run; the chat transport handles encryption now. |
hooks |
Named list of functions or NULL, as validated by
|
See bot_poll.
## Not run: matrix_poll() # use bot_poll() ## End(Not run)## Not run: matrix_poll() # use bot_poll() ## End(Not run)
Deprecated: use bot_request_flush()
matrix_request_flush()matrix_request_flush()
See bot_request_flush.
## Not run: matrix_request_flush() # use bot_request_flush() ## End(Not run)## Not run: matrix_request_flush() # use bot_request_flush() ## End(Not run)
Deprecated: use bot_run()
matrix_run( timeout = 30000L, system = NULL, model = NULL, provider = NULL, tools_filter = NULL )matrix_run( timeout = 30000L, system = NULL, model = NULL, provider = NULL, tools_filter = NULL )
timeout |
Integer. Long-poll timeout in milliseconds. |
system |
Character or NULL. System prompt override. |
model |
Character or NULL. Model override. |
provider |
Character or NULL. Provider override. |
tools_filter |
Character vector or NULL. Tool filter override. |
See bot_run.
## Not run: matrix_run() # use bot_run() ## End(Not run)## Not run: matrix_run() # use bot_run() ## End(Not run)
Deprecated: use bot_run_init()
matrix_run_init( system = NULL, model = NULL, provider = NULL, tools_filter = NULL, hooks = NULL )matrix_run_init( system = NULL, model = NULL, provider = NULL, tools_filter = NULL, hooks = NULL )
system |
Character or NULL. System prompt override. |
model |
Character or NULL. Model override. |
provider |
Character or NULL. Provider override. |
tools_filter |
Character vector or NULL. Tool filter override. |
hooks |
Named list of functions or NULL. Policy the loop asks
the host about; see Hooks. NULL, the default, means a bare bot:
|
See bot_run_init.
## Not run: matrix_run_init() # use bot_run_init() ## End(Not run)## Not run: matrix_run_init() # use bot_run_init() ## End(Not run)
Deprecated: use bot_run_step()
matrix_run_step(state, timeout = 30000L)matrix_run_step(state, timeout = 30000L)
state |
A state object from |
timeout |
Integer. Long-poll timeout in milliseconds. |
See bot_run_step.
## Not run: matrix_run_step(state) # use bot_run_step() ## End(Not run)## Not run: matrix_run_step(state) # use bot_run_step() ## End(Not run)
Deprecated: use bot_send()
matrix_send(text, room_id = NULL, msgtype = "m.text", markdown = FALSE)matrix_send(text, room_id = NULL, msgtype = "m.text", markdown = FALSE)
text |
Character. Plain text body. |
room_id |
Character. Matrix room id. Defaults to |
msgtype |
Character. Matrix msgtype, default "m.text". |
markdown |
Logical. If TRUE, also send Matrix custom HTML derived from a conservative markdown subset. |
See bot_send.
## Not run: matrix_send("hello") # use bot_send() ## End(Not run)## Not run: matrix_send("hello") # use bot_send() ## End(Not run)
Returns a closure suitable for the tool_executor argument of
turn. Each tool call is forwarded to the connected MCP
server via llm.api::mcp_call.
mcp_tool_executor(conn)mcp_tool_executor(conn)
conn |
An open MCP connection (from |
A function with signature function(name, args) that
returns an MCP-format result list.
## Not run: # Needs an open MCP connection to a running corteza::serve(). conn <- llm.api::mcp_connect("tcp://localhost:7850") executor <- mcp_tool_executor(conn) s <- new_session(provider = "anthropic") turn("Hello", s, tool_executor = executor) ## End(Not run)## Not run: # Needs an open MCP connection to a running corteza::serve(). conn <- llm.api::mcp_connect("tcp://localhost:7850") executor <- mcp_tool_executor(conn) s <- new_session(provider = "anthropic") turn("Hello", s, tool_executor = executor) ## End(Not run)
Returns an environment with sensible defaults. Adapters set channel-
specific fields (e.g. approval_cb, tools_filter) before
calling turn.
new_session( channel = c("cli", "console", "matrix"), history = NULL, model_map = NULL, provider = "anthropic", tools_filter = NULL, system = NULL, approval_cb = NULL, max_turns = 10L, verbose = FALSE, plan_mode = FALSE, web_search = NULL, base_url = NULL, max_tokens = NULL, reasoning_effort = NULL, thinking_budget_tokens = NULL, cache = NULL, compaction_prompt = NULL, context_edit = NULL )new_session( channel = c("cli", "console", "matrix"), history = NULL, model_map = NULL, provider = "anthropic", tools_filter = NULL, system = NULL, approval_cb = NULL, max_turns = 10L, verbose = FALSE, plan_mode = FALSE, web_search = NULL, base_url = NULL, max_tokens = NULL, reasoning_effort = NULL, thinking_budget_tokens = NULL, cache = NULL, compaction_prompt = NULL, context_edit = NULL )
channel |
Character, one of "cli", "console", "matrix". |
history |
List of prior messages, or NULL. |
model_map |
Named list with |
provider |
LLM provider passed to |
tools_filter |
Character vector passed to |
system |
System prompt override (NULL for built-in default). |
approval_cb |
Function called when policy returns |
max_turns |
Maximum LLM turns per call. |
verbose |
Print tool call progress. |
plan_mode |
Logical. When TRUE, the session is in plan mode:
the LLM is told to research and propose without acting, the policy
engine denies write/exec tool calls (except |
web_search |
Logical or NULL. When TRUE and the provider
supports it, |
base_url |
Character or NULL. Endpoint for the
|
max_tokens |
Integer or NULL. Per-response output-token budget,
forwarded to |
reasoning_effort |
Character or NULL. How hard a reasoning
model should think before answering, e.g. |
thinking_budget_tokens |
Integer or NULL. Extended-thinking
budget for Anthropic models, forwarded to |
cache |
Character or NULL. Anthropic prompt-cache TTL:
|
compaction_prompt |
Character or NULL. Optional domain-specific instructions for summarizing older history during context compaction. NULL uses corteza's general coding-agent summary brief. The prompt affects only the summarizer; the main agent system prompt is unchanged. |
context_edit |
NULL to inherit configuration, a logical enable switch, or a list with 'enabled' and 'cadence' ('"pressure"' or '"always"'). Context editing requires the session journal and a supported history shape. |
An environment holding the session state.
# Build a stateless session for the CLI channel without making any # network calls. The returned environment carries history, the # active provider/model, and the approval callback. s <- new_session(channel = "cli", provider = "anthropic") is.environment(s) identical(s$provider, "anthropic")# Build a stateless session for the CLI channel without making any # network calls. The returned environment carries history, the # active provider/model, and the approval callback. s <- new_session(channel = "cli", provider = "anthropic") is.environment(s) identical(s$provider, "anthropic")
Prints one line per tool call suitable for an interactive REPL:
" [tool] hint (N lines)\n". The hint is a short summary of
the call (file path, code snippet, search pattern) computed by
tool_hint().
observer_progress()observer_progress()
A function to pass to add_observer.
obs <- observer_progress() s <- new_session(provider = "anthropic") add_observer(s, obs)obs <- observer_progress() s <- new_session(provider = "anthropic") add_observer(s, obs)
Returns a decision list(model, approval, reason). model is
"cloud" or "local"; approval is "allow",
"ask", or "deny".
policy(call, config = NULL)policy(call, config = NULL)
call |
A list describing the tool call. See the file header in
|
config |
Optional config list from |
When config is supplied, the project's
approval_mode / dangerous_tools / per-tool
permissions are overlaid on top of the default tensor: a
tool the user has configured as "ask" or "deny" will
have its decision promoted accordingly. Safety verdicts (credential
paths, plan mode) still win because those represent invariants the
user can't waive from config.
Both corteza::chat() and the CLI tool dispatch loop pass
their session's config through here so the /permissions
contract advertised by both surfaces is enforced consistently.
A decision list with fields model, approval,
reason.
# A bare-environment read_file call resolves under the default # built-in policy without needing any session config. decision <- policy(list(name = "read_file", arguments = list(path = "DESCRIPTION"))) decision$approval# A bare-environment read_file call resolves under the default # built-in policy without needing any session config. decision <- policy(list(name = "read_file", arguments = list(path = "DESCRIPTION"))) decision$approval
Start the corteza MCP server. This exposes R tools to MCP clients like Claude Desktop, VS Code, or the corteza CLI.
serve(port = NULL, cwd = NULL, tools = NULL, expose_subagents = NULL)serve(port = NULL, cwd = NULL, tools = NULL, expose_subagents = NULL)
port |
Port number for socket transport. If NULL, uses stdio transport. |
cwd |
Working directory for the server. Defaults to current directory. |
tools |
Character vector of tools or categories to enable. Categories: file, code, r, data, web, git, chat. Use "core" for file+code+git, "all" for everything (default). |
expose_subagents |
Whether MCP clients may call the subagent tools ('spawn_subagent', 'query_subagent', 'collect_subagent', 'list_subagents', 'kill_subagent'). 'NULL' (default) defers to the 'subagents$expose_over_mcp' config flag (itself FALSE by default); 'TRUE'/'FALSE' overrides it. Off by default because a spawned subagent runs its own agent loop and spends autonomously on the host's LLM credentials – an unattended MCP client could otherwise trigger unbounded cost the client never sees. When on, cumulative subagent spend is capped by 'subagents$mcp_spend_cap_usd' (default $5.00). |
The server supports two transport modes:
- **stdio** (default): For Claude Desktop and other MCP clients. Communication happens via stdin/stdout.
- **socket**: For the corteza CLI and R clients. Listens on a TCP port.
## Tools Provided
- 'read_file', 'write_file', 'replace_in_file', 'list_files', 'grep_files' - File operations - 'run_r' - Execute R code in the server session - 'bash' - Run shell commands - 'r_help' - Query package docs via saber (exports, function help) - 'installed_packages' - List installed packages - 'web_search' - Search the web via Tavily (requires TAVILY_API_KEY) - 'fetch_url' - Fetch web content - 'git_status', 'git_diff', 'git_log' - Git operations - 'chat', 'chat_models' - LLM chat (requires llm.api)
NULL (runs until interrupted or client disconnects)
## Not run: # For Claude Desktop (stdio) serve() # For corteza CLI (socket) with all tools serve(port = 7850) # Minimal tools for small context models serve(port = 7850, tools = "core") # Specific categories serve(port = 7850, tools = c("file", "git")) ## End(Not run)## Not run: # For Claude Desktop (stdio) serve() # For corteza CLI (socket) with all tools serve(port = 7850) # Minimal tools for small context models serve(port = 7850, tools = "core") # Specific categories serve(port = 7850, tools = c("file", "git")) ## End(Not run)
Performs pre-turn setup common to all channels:
session_setup( channel = c("cli", "console", "matrix"), cwd = getwd(), provider = NULL, model = NULL, tools = NULL, system = NULL, approval_cb = NULL, history = NULL, load_project_context = TRUE, validate_api_key = TRUE, verbose = FALSE, max_turns = 50L, web_search = NULL )session_setup( channel = c("cli", "console", "matrix"), cwd = getwd(), provider = NULL, model = NULL, tools = NULL, system = NULL, approval_cb = NULL, history = NULL, load_project_context = TRUE, validate_api_key = TRUE, verbose = FALSE, max_turns = 50L, web_search = NULL )
channel |
Character, one of |
cwd |
Working directory. Defaults to the current directory. |
provider |
Character or NULL. LLM provider override. NULL falls
back to |
model |
Character or NULL. Model override. NULL falls back to
|
tools |
Character vector, NULL, or the string |
system |
Character or NULL. System prompt. NULL auto-builds via the
saber-backed context manifest when |
approval_cb |
Function or NULL. Approval callback for
|
history |
List or NULL. Prior conversation messages to seed
the session with (each entry a list with |
load_project_context |
Logical. When TRUE, auto-call the saber manifest and rendered system prompt. Channels that supply their own system prompt pass FALSE. |
validate_api_key |
Logical. When TRUE, error if the provider's API key env var is unset or empty. |
verbose |
Logical. Passed through to |
max_turns |
Integer. Passed through to |
web_search |
Logical or NULL. Provider-native web search toggle
passed through to |
Loads project + global corteza config from cwd.
Resolves provider, model, and verifies the required API environment variable is set.
Registers built-in skills and loads user/project skills and
skill docs from tools::R_user_dir("corteza", "data")/skills
and <cwd>/.corteza/skills.
Loads skill packages declared in the config.
Optionally builds the system prompt and source manifest via
load_context_bundle(cwd).
Returns a new_session() built from the above.
A session environment from new_session, with an
extra cwd field and, for an auto-built system prompt, a
context_manifest field.
## Not run: # Requires the relevant provider API key in the environment. s <- session_setup("cli", provider = "anthropic", load_project_context = FALSE) s$model ## End(Not run)## Not run: # Requires the relevant provider API key in the environment. s <- session_setup("cli", provider = "anthropic", load_project_context = FALSE) s$model ## End(Not run)
Install a skill from a path or URL
skill_install(source, target_dir = NULL, force = FALSE)skill_install(source, target_dir = NULL, force = FALSE)
source |
Path to skill directory or URL |
target_dir |
Installation directory. Default is
|
force |
Overwrite if exists |
Installed skill name
# Install into a throwaway directory rather than the user's # R_user_dir, so this example doesn't mutate state. src <- file.path(tempdir(), "demo_skill") dir.create(src, showWarnings = FALSE) writeLines(c( "---", "name: demo", "description: A demo skill", "---", "Demo body." ), file.path(src, "SKILL.md")) dest <- file.path(tempdir(), "skills_lib") skill_install(src, target_dir = dest) unlink(src, recursive = TRUE) unlink(dest, recursive = TRUE)# Install into a throwaway directory rather than the user's # R_user_dir, so this example doesn't mutate state. src <- file.path(tempdir(), "demo_skill") dir.create(src, showWarnings = FALSE) writeLines(c( "---", "name: demo", "description: A demo skill", "---", "Demo body." ), file.path(src, "SKILL.md")) dest <- file.path(tempdir(), "skills_lib") skill_install(src, target_dir = dest) unlink(src, recursive = TRUE) unlink(dest, recursive = TRUE)
List installed skills
skill_list_installed(skill_dir = NULL)skill_list_installed(skill_dir = NULL)
skill_dir |
Skills directory |
Data frame with skill info
# List skills from an empty tempdir; returns a zero-row data frame # with the documented columns. empty <- file.path(tempdir(), "empty_skills") dir.create(empty, showWarnings = FALSE) skill_list_installed(skill_dir = empty) unlink(empty, recursive = TRUE)# List skills from an empty tempdir; returns a zero-row data frame # with the documented columns. empty <- file.path(tempdir(), "empty_skills") dir.create(empty, showWarnings = FALSE) skill_list_installed(skill_dir = empty) unlink(empty, recursive = TRUE)
Remove an installed skill
skill_remove(name, skill_dir = NULL)skill_remove(name, skill_dir = NULL)
name |
Skill name |
skill_dir |
Skills directory |
Invisible TRUE on success
# Install a demo skill into a tempdir, then remove it. The # installed name is the source directory's basename. src <- file.path(tempdir(), "demo_skill") dir.create(src, showWarnings = FALSE) writeLines(c("---", "name: demo_skill", "description: A demo skill", "---"), file.path(src, "SKILL.md")) dest <- file.path(tempdir(), "skills_lib") name <- skill_install(src, target_dir = dest) skill_remove(name, skill_dir = dest) unlink(src, recursive = TRUE) unlink(dest, recursive = TRUE)# Install a demo skill into a tempdir, then remove it. The # installed name is the source directory's basename. src <- file.path(tempdir(), "demo_skill") dir.create(src, showWarnings = FALSE) writeLines(c("---", "name: demo_skill", "description: A demo skill", "---"), file.path(src, "SKILL.md")) dest <- file.path(tempdir(), "skills_lib") name <- skill_install(src, target_dir = dest) skill_remove(name, skill_dir = dest) unlink(src, recursive = TRUE) unlink(dest, recursive = TRUE)
Executes test_*.R files in a skill directory.
skill_test(path, verbose = TRUE)skill_test(path, verbose = TRUE)
path |
Path to skill directory |
verbose |
Print test output |
List with passed, failed, errors
# A skill directory with no test_*.R files returns a zero-result # summary rather than erroring. p <- file.path(tempdir(), "skill_no_tests") dir.create(p, showWarnings = FALSE) skill_test(p, verbose = FALSE) unlink(p, recursive = TRUE)# A skill directory with no test_*.R files returns a zero-result # summary rather than erroring. p <- file.path(tempdir(), "skill_no_tests") dir.create(p, showWarnings = FALSE) skill_test(p, verbose = FALSE) unlink(p, recursive = TRUE)
Pairs with 'subagent_query(..., wait = FALSE)'. Returns the reply text once the child finishes its turn, or NULL while the query is still running. Result is read exactly once: after a successful collect the pending slot is cleared, so the next async query can fire.
subagent_collect(id, wait = TRUE, timeout = 60L)subagent_collect(id, wait = TRUE, timeout = 60L)
id |
Subagent identifier (UUID, prefix, or sequence number). |
wait |
If TRUE (default), block up to 'timeout' seconds waiting for the child to finish. If FALSE, poll once and return immediately. |
timeout |
Maximum seconds to block when 'wait = TRUE'; 'Inf' waits until the child replies. On timeout the child is left running; caller may collect again later or kill explicitly. |
Reply text (character) when ready; NULL when still running.
## Not run: id <- subagent_spawn("background research") subagent_query(id, "what's in DESCRIPTION?", wait = FALSE) # ... do other work ... subagent_collect(id, wait = TRUE, timeout = 30) subagent_kill(id) ## End(Not run)## Not run: id <- subagent_spawn("background research") subagent_query(id, "what's in DESCRIPTION?", wait = FALSE) # ... do other work ... subagent_collect(id, wait = TRUE, timeout = 30) subagent_kill(id) ## End(Not run)
Kill a subagent.
subagent_kill(id)subagent_kill(id)
id |
Subagent identifier (UUID, prefix, or sequence number). |
Invisible TRUE if killed, FALSE if not found.
# Unknown id is a silent no-op (returns FALSE), so this is safe to # run during R CMD check without a live subagent. subagent_kill("no-such-id")# Unknown id is a silent no-op (returns FALSE), so this is safe to # run during R CMD check without a live subagent. subagent_kill("no-such-id")
Returns a list of info objects per agent: id/seq/task/started_at/ time_remaining/pending plus model/age/cumulative usage and a best-effort live token count for idle agents ('NA' for busy).
subagent_list()subagent_list()
List of subagent info objects.
# Empty when no subagent has been spawned yet -- safe to call any time. subagent_list()# Empty when no subagent has been spawned yet -- safe to call any time. subagent_list()
Sends a prompt to a running subagent. Inside the child it runs through [turn()] with the child's persistent turn session: the LLM replies, any tool calls it makes resolve against the child's in-process skill registry, and history accumulates across queries.
subagent_query( id, prompt, wait = TRUE, timeout = 60L, return_name = NULL, report = FALSE )subagent_query( id, prompt, wait = TRUE, timeout = 60L, return_name = NULL, report = FALSE )
id |
Subagent identifier. Accepts the canonical UUID, a unique UUID prefix, or the per-session sequence number printed by 'subagent_list()' / '/agents'. |
prompt |
Prompt to send. |
wait |
If TRUE (default), block up to 'timeout' seconds for the child's reply and return the reply text. If FALSE, fire the prompt and return the canonical id invisibly; caller must collect via [subagent_collect()]. |
timeout |
Maximum seconds to block when 'wait = TRUE'; 'Inf' waits until the child replies. On timeout the query stays pending: collect it later with [subagent_collect()] or end it with [subagent_kill()]. |
return_name |
Optional single name or '.h_NNN' handle for a value the child should hand back. When set, the child must have left the result bound under that name (e.g. via 'run_r'); the resolved value is stashed in the parent handle store and the reply gains a '[stored as .h_NNN]' block referencing it. Requires a subagent with 'run_r' (the 'work' preset). For 'wait = FALSE' the name is captured now and applied when collected. |
report |
Logical. When TRUE, require a structured handoff with status, notes, findings, concerns, deviations, open questions, and feedback. The ordinary character reply remains the return value; the report is rendered into it for model callers and retained in its 'corteza_report' attribute for R callers. |
Both paths fire the prompt the same way, as a one-shot call on the child's 'callr::r_session'. With 'wait = FALSE' the call returns immediately and the parent collects the reply later with [subagent_collect()]. With 'wait = TRUE' the call collects on the parent's behalf, but only up to 'timeout' seconds: a child that has not replied by then is left running with the query pending, and the call returns NULL, so the wait is bounded by default ('Inf' opts out). A subagent can only carry one in-flight query at a time: firing a second one while the first is pending raises an error.
Reply text (character) when 'wait = TRUE' and the child replied within 'timeout', with a handle block appended when 'return_name' resolved; NULL (invisibly) when the timeout elapsed and the query is still pending. Canonical id (character, invisibly) when 'wait = FALSE'.
## Not run: # Requires LLM credentials in the child's environment. id <- subagent_spawn("read R/skill.R and summarize", preset = "minimal") subagent_query(id, "what does this file do?", wait = TRUE) subagent_kill(id) ## End(Not run)## Not run: # Requires LLM credentials in the child's environment. id <- subagent_spawn("read R/skill.R and summarize", preset = "minimal") subagent_query(id, "what does this file do?", wait = TRUE) subagent_kill(id) ## End(Not run)
Starts a fresh 'callr::r_session' with corteza loaded and its tool registry set up. Stores the handle in the package-level registry keyed by subagent id.
subagent_spawn( task, model = NULL, tools = NULL, preset = NULL, parent_session = NULL, config = NULL )subagent_spawn( task, model = NULL, tools = NULL, preset = NULL, parent_session = NULL, config = NULL )
task |
Task description (stored for bookkeeping; not yet fed into an agent loop). |
model |
Optional model override (reserved for later use). |
tools |
Optional explicit tool filter (character vector). Overrides 'preset' when provided. Fixed for the lifetime of the child – cannot be expanded after spawn. |
preset |
Preset name (fixed for the lifetime of the child). '"investigate"' (default): 'read_file', 'grep_files', 'r_help', 'web_search', 'fetch_url'. '"work"': investigate + 'bash', 'write_file', 'replace_in_file', 'list_files', 'git_status', 'git_diff', 'git_log', 'run_r'. '"minimal"': 'read_file', 'grep_files'. '"monitor"': the read-only hall monitor used to supervise unattended auto runs – 'read_file', 'grep_files', 'list_files', 'git_status', 'git_diff', 'git_log', and provider-native web search explicitly off. |
parent_session |
Parent session object; read for nested-spawning control and session-key derivation. |
config |
Config list. |
Permissions: subagents have no interactive approval channel back to the parent or user. The child's 'approval_cb' denies by default and there is no mid-run escalation path. Whatever capability the child needs must be granted at spawn time through 'preset' or 'tools'. If a task may need shell, write, or network capability, pick a preset that includes it (or pass an explicit 'tools' list); otherwise the child should report that it is blocked rather than retry.
Subagent ID (character).
if (interactive()) { # Spawns a callr::r_session child loaded with corteza; the # registry is in-memory and dies with the parent R session, so # we wrap in interactive() to keep R CMD check from leaving # children behind. id <- subagent_spawn("look up the package version", preset = "minimal") subagent_kill(id) }if (interactive()) { # Spawns a callr::r_session child loaded with corteza; the # registry is in-memory and dies with the parent R session, so # we wrap in interactive() to keep R CMD check from leaving # children behind. id <- subagent_spawn("look up the package version", preset = "minimal") subagent_kill(id) }
Sends prompt to the configured LLM with tool use enabled. Every
tool call the LLM makes is routed through policy before
being dispatched.
turn(prompt, session, tool_executor = NULL, tools = NULL)turn(prompt, session, tool_executor = NULL, tools = NULL)
prompt |
Character. User prompt. |
session |
A session environment created by |
tool_executor |
Function or NULL. Dispatcher with signature
|
tools |
List or NULL. Tool schemas to pass the LLM. NULL uses
the in-process skill registry (filtered by |
Tool dispatch is pluggable via tool_executor, but the CLI and
chat() both leave it NULL: tools run in-process through the
default call_skill dispatcher against the local skill registry.
serve() is a separate MCP server for external clients only; it
is not part of the CLI's tool path. Pass an explicit
function(name, args) -> list executor only when dispatching
tools somewhere other than the in-process registry.
When the provider refuses the request with a limit error (HTTP 429,
503, or 529, or a body naming a rate, usage, or quota limit) and the
session has a fallback chain, the turn is retried on the next
"model provider" entry in that chain. The chain comes from
session$fallback (the Matrix config's fallback key, see
bot_configure) or the cwd config's fallback
key. A provider that hit a limit is skipped process-wide for
fallback_cooldown_minutes (default 30); a configured
fallback_primary_retry_at can hold the primary until a weekly
reset boundary. Provider-native histories are bridged when the wire
changes, and a partially completed tool run resumes from its captured
results so no completed tool call runs twice. Fallback replies identify
their actual model/provider; API-key fallbacks carry a prominent billable
usage warning. Errors that are not limits are rethrown untouched.
A list with reply (character), session (the
updated session environment; also mutated in place), route
(the actual model/provider and fallback credential mode), and
termination (status done, incomplete, or
cancelled, with a machine-readable reason), and audit
(the stream path and canonical turn ID, or NULL when recording is disabled).
## Not run: # Requires ANTHROPIC_API_KEY (or the configured provider's key) and # a network connection to the LLM. s <- new_session(provider = "anthropic") out <- turn("Say hello", s) out$reply ## End(Not run)## Not run: # Requires ANTHROPIC_API_KEY (or the configured provider's key) and # a network connection to the LLM. s <- new_session(provider = "anthropic") out <- turn("Say hello", s) out$reply ## End(Not run)
Remove the corteza command-line tool.
uninstall_cli(path = NULL)uninstall_cli(path = NULL)
path |
Directory where corteza is installed. Default matches
|
TRUE if removed, FALSE if not found, invisibly.
## Not run: uninstall_cli() ## End(Not run)## Not run: uninstall_cli() ## End(Not run)
Runs the gRPC service a fluffychat live-voice client talks to
(AllocateVoice, Converse, ReportTurn). Blocks
until interrupted. Run it as its own R process, next to – not inside
– the room poll loop.
voice_serve( config = NULL, address = NULL, hooks = list(), poll_ms = 200L, max_events = Inf )voice_serve( config = NULL, address = NULL, hooks = list(), poll_ms = 200L, max_events = Inf )
config |
Config list as from |
address |
|
hooks |
Named list overriding side effects (tests/harnesses
only): |
poll_ms |
Milliseconds each poll waits for events. |
max_events |
Stop after handling this many events (tests only; the default never stops). |
Requires the rgrpc and RProtoBuf packages (Suggests),
and a media allocator named in the config: voice.allocator
(the gpu.ctl base URL) plus voice.allocator_token (the
service credential its front requires on every mint). Without
either, AllocateVoice refuses and names the missing key.
Voice sessions generate in spoken register: their system prompt
carries an instruction to write plain speakable prose (no markdown,
numbers said aloud), because the reply is synthesized verbatim.
voice.speech_style in the config replaces that instruction's
text; typed room chat is unaffected either way.
Invisibly, the number of events handled.