Agents and models
Terminal
Chat with an agent in the terminal, or turn it into a one-shot command, with a person confirming actions and answering questions.
mayura/terminal puts an agent in front of a person at a terminal. Use runTerminalChat for an interactive chat
while you develop or for internal tools, and runAgentCommand to ship an agent as a command-line program whose flags
come from its input schema. A person at the terminal can confirm tool calls before they run and answer the agent's
questions.
import { createRuntime, defineAgent } from 'mayura';
import { askPersonTool, confirmBeforeRunning, runTerminalChat } from 'mayura/terminal';
const agent = defineAgent({
id: 'support', version: '1', instructions: 'You help customers with their orders.',
model, input, output,
tools: [lookupOrder, confirmBeforeRunning(refundOrder), askPersonTool],
});
const runtime = createRuntime({
profile: 'ephemeral',
permissions: { allow: [
'model:openai.responses', 'tool:orders.lookup', 'tool:orders.refund', 'effect:write',
'tool:person.ask', 'person:ask',
] },
limits: { maxCostMicros: 100_000 },
});
try {
await runTerminalChat({ agent, runtime, toInput: (message, history) => ({ message, history }) });
} finally {
await runtime.close();
}Chat
Each message runs the agent once. While it works, the chat shows which tool it is using. If the agent streams (see Streaming), the reply appears as it is written; otherwise it is shown when the run completes. Each turn shows what it cost when the cost is above zero.
At the prompt, /help lists commands, /clear forgets the conversation, /cost shows the spend so far, and /exit
(or /quit, or Ctrl+C) leaves. runTerminalChat resolves to { turns, spentMicros }.
| Option | Meaning |
|---|---|
agent, runtime |
Required. The runtime's permissions and limits apply to every turn. |
toInput(message, history) |
Builds the agent's input. The default is chatTranscript (below). |
toText(output) |
Turns the agent's output into what the person reads. The default is outputText. |
title |
Shown at the top. The default is the agent id. |
io |
{ input, output } streams to use instead of the process's terminal. |
The chat remembers the conversation. For an agent whose input is text, the default toInput (chatTranscript,
also exported) sends the first message as it is and, after that, the last 20 turns as a transcript followed by the new
message. For an agent with structured input, write toInput: history holds the earlier turns as
{ role: 'person' | 'agent', text }, as in the example above. /clear starts a new conversation.
outputText(output) returns the output if it is a string, otherwise its first string field among reply, message,
text, answer, content and response, otherwise the output as indented JSON.
A person in the loop
confirmBeforeRunning(tool, options?) returns the same tool, except that before it runs, the person sees its input
and is asked "Allow it?". The default answer is no.
import { confirmBeforeRunning } from 'mayura/terminal';
const refund = confirmBeforeRunning(refundOrder, {
describe: input => `Refund order ${input.orderId} for ${input.amountCents / 100} EUR`,
waitMs: 120_000, // how long to wait for an answer; default 10 minutes
});describe turns the validated input into what the person reads (the default is the input as JSON). The confirmation
runs after the input passed the tool's schema and the runtime's permission checks, and before the tool's own code.
Declining ends the turn. The tool call is refused before any effect, and Mayura ends a run whose tool call fails,
so the turn ends with "The person declined orders.refund." (for a tool with id orders.refund). The chat goes on, and
the person can ask again.
askPersonTool is a tool (id person.ask) the agent calls to ask the person a question and continue with the answer.
Grant tool:person.ask and person:ask. If the person cancels the question, the call is refused and the turn ends.
Both need someone at a terminal. The same agent running anywhere else, such as a server, a scheduled job or a piped
command, refuses them, so a confirmed tool can never run unconfirmed there. The confirmation is built on
withPreflight from mayura, which runs any check you like on a tool's validated input before it executes.
One-shot commands
runAgentCommand runs an agent once from the command line and resolves to an exit code: 0 when the run succeeded,
1 otherwise.
import { runAgentCommand } from 'mayura/terminal';
// The agent's input is z.object({ city: z.string().describe('Where to go.'), days: z.number().int(), budget: z.boolean() }).
process.exitCode = await runAgentCommand({ agent, runtime, name: 'plan-trip' });
await runtime.close();plan-trip --city Paris --days 3
plan-trip --city Rome --no-budget
plan-trip --help
plan-trip --input '{"city":"Paris"}' --json
echo "a weekend in Lisbon" | plan-tripThe input comes from exactly one of these:
--input <json>or--input-file <path>: the whole input as JSON. It cannot be combined with flags or words.- Flags, from the top-level
string,number,integerandbooleanproperties of the agent's input schema (or ofinputJsonSchemawhen you give one), collected into an object. A Zod.describe()text becomes the flag's help. Booleans take no value and also accept--no-<name>. Numbers are checked. - Words on the command line, joined with spaces into one string (only without flags).
- Standard input, when nothing else is given and it is piped (up to 1 MiB of text).
The agent's input schema still validates whatever arrives; the JSON Schema only drives the flags and --help.
| Option | Meaning |
|---|---|
agent, runtime |
Required. |
name |
The command name in --help. The default is the agent id. |
argv |
The arguments. The default is process.argv.slice(2). |
inputJsonSchema |
The input's JSON Schema, for flags and --help. The default is the agent's own input schema. |
toText(output) |
What to print for a successful output. The default is outputText. |
io |
{ stdin, stdout, stderr } streams to use instead of the process's. |
--json prints { status, output, spentMicros } (or { status, error, spentMicros }) instead of text. When standard
output is a terminal and --json is not set, streamed text is printed as it arrives and each tool the agent uses is
listed on standard error. Confirmations and questions are asked only when standard input is a terminal; in a pipe or
a scheduled job they are refused.
parseAgentCommand(argv, schema?, stdin?) and agentCommandHelp(name, schema?) are the parser and help text on
their own, for building a different front end.
Good to know
- Streamed text is a preview: it is the model's raw field. When the final answer differs from it, because a batch guard stopped the stream partway or your output schema rewrote the answer (for example to redact it), the chat and commands print the final answer after it, so the last thing the person reads is the answer that counts.
- Costs shown are from the runtime's budget for each run; see Costs and budgets.
- These helpers are for a person at a terminal. For approvals in a web app or a long-running workflow, see Approvals and human input.
Start from a complete project
The cli-agent starter is a working command-line assistant built from these pieces: a chat and one-shot requests,
file tools confined to one folder, writes confirmed with confirmBeforeRunning, askPersonTool, skills and an
offline stand-in model, with tests for each.
npx mayura init --starter cli-agent --directory my-assistant --applySee mayura init.