ToolCall dialect manual¶
ToolCall lets a model make a data-dependent sequence of ordinary mevedel tool calls inside one model turn. The script is interpreted by mevedel; it is never evaluated as Emacs Lisp. Every nested tool call still goes through normal validation, hooks, permissions, snapshots, telemetry, and rendering.
Pass one call or a composed program in ToolCall's expression argument.
Retrieve specialist contracts with ToolSearch. Native core tools can also be
called directly. Use Grep or another purpose-built tool for bulk processing.
Tools marked standalone-only must be the entire expression, without nested calls in their arguments or surrounding forms. This includes Ask, Skill, WaitAgent, UpdateGoal, and wrapped tools not explicitly admitted for composition. Direct-call arguments may be literals, quoted data or direct pure primitive applications. Other argument expressions use the composed path. Read-only Elisp introspection tools support composition.
A direct call returns the underlying tool's result and supported media, and renders as that tool in the view. Composed expressions return the last value and retain their expandable script/child-call display. Hook context and repair feedback are delivered even when an intermediate result is discarded.
For object arguments use keyword plists, arrays use vectors or lists, JSON
false uses :json-false (or nil for a boolean argument), and JSON null uses
:null. Boolean schema arguments also accept :true and :false, normalized
to t and :json-false. Bare true, false and null are variable names,
not constants; an unbound use names the corresponding literal in its error.
Declared object keys are converted to the native adapter's keys.
JSON object syntax is not part of the dialect: its braces read as symbols and
its commas as unquote forms. A value that is not a keyword plist where an
object is expected is refused with the expected form, the value, and a note
when it looks like JSON.
Data no schema describes, such as an element inside a change's after, is
refused when it holds a comma or a bare true, false or null, with the
dialect spelling: [[0 0] [6 29.6]], t, :json-false or nil, :null.
Wrapped tools use the qualified category/name shown by ToolSearch; this
prevents collisions across servers. The installed MCP text adapter exposes
text content only. ToolCall cannot recover media or error metadata discarded
upstream by that adapter.
Values and evaluation¶
The dialect supports numbers, strings, symbols, keywords, lists, vectors, lexical
bindings, and lambdas. nil is false; every other value is true. A script may
contain multiple top-level forms, which are evaluated as an implicit progn.
Vector literals are data: their elements are not evaluated. Nested vectors and
lists still count against the input node and depth limits.
Lambdas are internal callables: neither a tool argument nor the script's final
value may contain one.
The evaluator resolves a call's operator before evaluating its arguments. An unknown function therefore fails without running tool calls hidden in its arguments.
Static operators additionally preflight before execution, including literal indirect references to standalone-only tools. Unknown names in never-taken branches are rejected too. Quoted data, binding names, lambda parameter lists and unexpanded user-macro arguments are skipped. Literal regexps are checked in the same pass. Runtime-generated calls are checked against the same closed roster before dispatch; they cannot always be rejected before earlier tools run. Earlier authorized effects remain recorded and are not rolled back.
let evaluates all initializer expressions in the surrounding environment:
(let ((paths (list "a.el" "b.el"))
(count (length paths))) ; wrong: paths is not bound here
count)
Use let* when a later initializer depends on an earlier binding:
(let* ((paths (list "a.el" "b.el"))
(count (length paths)))
count)
setq changes an existing lexical binding only. It does not create globals.
Syntax¶
Special forms:
quote if progn cond and or let let* setq while
lambda funcall apply mapcar parallel parallel-map
Closed, macro-shaped conveniences:
(when CONDITION BODY...)
(unless CONDITION BODY...)
(push VALUE VARIABLE)
(dolist (VARIABLE LIST) BODY...)
(dotimes (VARIABLE COUNT) BODY...)
push accepts only a plain bound variable as its place. dolist and
dotimes accept only the two-element specification; there is no result form.
The dialect has no macrolet, cl-loop, generalized places, or
guest-visible macro expansion.
Pure data operations¶
The closed primitive table supports these signatures. &optional marks the
remaining optional arguments, and &rest marks repeated arguments. Beyond
these operations, calls resolve only to discovered tools or the program's own
definitions. Path helpers are syntactic and do not access the filesystem.
sort copies its list and uses a fixed ascending comparator.
(car list)
(cdr list)
(caar x)
(cadr x)
(cdar x)
(cddr x)
(cons car cdr)
(list &rest objects)
(append &rest sequences)
(length sequence)
(nth n list)
(nthcdr n list)
(last list &optional n)
(take n list)
(reverse seq)
(sort list)
(member elt list)
(memq elt list)
(assoc key alist &optional testfn)
(assq key alist)
(plist-get plist prop &optional predicate)
(plist-member plist prop &optional predicate)
(concat &rest sequences)
(format string &rest objects)
(substring string &optional from to)
(split-string string &optional separators omit-nulls trim)
(string-join strings &optional separator)
(file-name-nondirectory filename)
(file-name-directory filename)
(file-name-concat directory &rest components)
(file-name-extension filename &optional period)
(file-name-sans-extension filename)
(file-name-base &optional filename)
(string-trim string &optional trim-left trim-right)
(string-prefix-p prefix string &optional ignore-case)
(string-suffix-p suffix string &optional ignore-case)
(string-match-p regexp string &optional start)
(string-search needle haystack &optional start-pos)
(regexp-quote string)
(upcase obj)
(downcase obj)
(capitalize obj)
(number-to-string number)
(string-to-number string &optional base)
(+ &rest numbers-or-markers)
(- &optional number-or-marker &rest more-numbers-or-markers)
(* &rest numbers-or-markers)
(/ number &rest divisors)
(% x y)
(mod x y)
(abs arg)
(max number-or-marker &rest numbers-or-markers)
(min number-or-marker &rest numbers-or-markers)
(float arg)
(truncate arg &optional divisor)
(round arg &optional divisor)
(floor arg &optional divisor)
(ceiling arg &optional divisor)
(= number-or-marker &rest numbers-or-markers)
(/= num1 num2)
(< number-or-marker &rest numbers-or-markers)
(> number-or-marker &rest numbers-or-markers)
(<= number-or-marker &rest numbers-or-markers)
(>= number-or-marker &rest numbers-or-markers)
(eq obj1 obj2)
(eql obj1 obj2)
(equal o1 o2)
(null object)
(not object)
(atom object)
(consp object)
(listp object)
(stringp object)
(numberp object)
(integerp object)
(floatp object)
(symbolp object)
(sequencep object)
(identity argument)
(gensym &optional prefix)
Definitions¶
A script may open with top-level defun and defmacro forms:
(defun read-or-nil (path)
(let ((r (Read :file_path path)))
(if (and (listp r) (plist-get r :error)) nil r)))
(defmacro with-lines (var call &rest body)
(let ((text (gensym)))
`(let* ((,text ,call)
(,var (split-string ,text "\n" t)))
,@body)))
(with-lines lines (Grep :pattern "TODO" :output_mode "content")
(length lines))
Definitions are legal only at the script's top level and are hoisted before the body runs, so they may reference one another regardless of order. A definition name must not collide with a special form, convenience, pure primitive, tool, or earlier definition; nothing is ever shadowed. At least one non-definition body form must remain.
Parameter lists accept plain names, &optional, and one trailing
&rest NAME. Other markers such as &key are rejected, arity is checked on
every call, and duplicate parameters are errors. The same rules apply to
lambda.
Named functions may recurse; a self-call in tail position runs at constant
stack depth, and non-tail recursion is bounded by the stack budget. A
function name is also accepted where a callable is expected, as in
(mapcar 'name list). A macro name is not a value.
One-level backquote builds list templates: `(a ,x ,@items) expands to
quote/list/append calls. Nested backquote, an unquote outside a
backquote, and the dotted (a . ,b) reader convention are rejected.
A macro receives its argument forms unevaluated and runs its body inside the
same closed evaluator, where it may call pure primitives and tools like any
other code. The returned expansion is validated against the reader's contract
and size budgets, then evaluates in the caller's environment. (gensym)
returns a fresh uninterned symbol for hygienic expansions. A macro call
inside a loop re-expands on every iteration; hoist it out of hot loops when
that matters.
Strings and regexps¶
Strings use normal Lisp escapes. In script source:
"\n" ; one newline character
"\\" ; one backslash character
(split-string text "\n" t)
Regexp primitives use Emacs regexp syntax. Parentheses are literal unless
escaped, so this matches a line beginning with (defcustom:
(string-match-p "^(defcustom" line)
Escaped parentheses create a capture group. Because the backslash is inside a Lisp string, it is doubled in script source:
(string-match-p "\\(foo\\)" text)
These rules apply to guest regexp primitives, not nested tool arguments. Grep
uses ripgrep syntax; for example, ^[(]defcustom matches a literal opening
parenthesis without adding another layer of backslash escaping.
Use regexp-quote when matching model- or tool-produced text literally.
The guest regexp subset is deliberately bounded. *, +, ?, and \{n,m\}
are allowed on a single atom — one literal, one escaped character, ., or one
bracket class — so "^[0-9]+$" and "^[ \t]*[0-9]+" work. Rejected before
Emacs's backtracking matcher runs: a quantified group, a quantifier stacked on
another quantifier, more than eight quantified atoms, alternation,
backreferences, and group extensions. Adjacent single-atom quantifiers can
still backtrack polynomially; the quantifier cap and the regexp work budget
bound that residual. Literals, anchors, bracket classes, and ordinary capture
groups remain available. The atomic-work estimate accounts conservatively for
polynomial backtracking by raising input size to the number of quantified
atoms; a pattern that is cheap on short text can therefore be rejected on a
larger input before Emacs's matcher runs. split-string also rejects an empty separator and a
split whose maximum output cannot fit the guest value budget. The same checks
apply to string-trim and split-string trim regexps. An omitted split
separator uses a fixed whitespace regexp; it never reads Emacs configuration.
Tool calls¶
ToolSearch returns current tool contracts and their exact keyword arguments:
(Read :file_path "mevedel.el" :offset 1 :limit 80)
(Grep :pattern "TODO" :path "." :output_mode "files_with_matches")
A successful nested call returns its canonical result, normally a string —
exactly what that tool returns in conversation, including its documented
formatting. Read output carries cat -n style line-number prefixes and may
end with a truncation notice; Grep count mode returns absolute path:count
lines; Glob returns newline-separated absolute paths. Session-level Read
duplicate suppression does not apply to nested calls: a script always receives
file content, and its reads do not poison the conversation's own
duplicate-read state. A failed nested call returns a guest plist:
(:error "message")
Scripts can inspect that value and continue:
(let ((result (Read :file_path path)))
(if (and (listp result) (plist-get result :error))
(list path :unreadable)
(list path (length result))))
Permission denial is different: it aborts the whole ToolCall call. Completed nested calls remain visible in the audit, but the script cannot catch the denial.
Parallel calls¶
Use parallel when a fixed set of calls is independent:
(parallel
(Read :file_path "a.el")
(Read :file_path "b.el"))
Use parallel-map for the same call shape over a list:
(parallel-map
(lambda (path) (Read :file_path path))
paths)
Results stay in source or input order even when calls finish out of order. The
host controls maximum concurrency. Every parallel entry must be one direct
tool call. The parallel-map lambda takes one argument and contains exactly
one direct tool call. Argument expressions may use pure data primitives, but
may not make nested tool calls of their own.
Use sequential forms when a later call depends on an earlier result.
Limits and performance¶
ToolCall bounds script bytes, syntax nodes, nesting depth, evaluation steps, wall time, nested-call count, recursion depth, transformed syntax size, regexp work, numeric size, individual values, and cumulative retained values. Errors name the exceeded budget. Final rendering counts repeated references at their serialized size, so a small shared object cannot expand into an oversized result while printing.
These limits are intentionally generous for orchestration and restrictive for bulk computation. Prefer:
(Grep :pattern "^[(]defcustom" :path "." :output_mode "count")
over reading every file and scanning every line in the interpreter. Narrow wide searches before mapping over their results.
The interpreter yields between short computation slices, so Emacs remains interactive. Scripts in flight are runtime state: if Emacs exits or the session is recovered, the ToolCall call settles as interrupted and does not resume. For root-session scripts, only the envelope call and its bounded ordered child audit are checkpointed, and the checkpoint is written durably twice: once before the first nested call and once at settlement. Both writes publish only the session sidecar; a session that has no sidecar yet is materialized by a full save first. Retained-agent scripts skip this checkpoint because their own interrupted-turn handling settles them. Between the root-session writes, child audit progress is journaled in memory only; an unrelated autosave captures it opportunistically. The performance rationale is in ADR 0111. A crash mid-script therefore recovers the child audit as of the last autosave, not the last child. Recovery turns the surviving checkpoint into an ordinary ToolCall tool row, marks queued or running children interrupted, and consumes the checkpoint with the repaired segment; no lexical environment, stack, timer, or continuation is serialized.
Security boundary¶
Guest identifiers are read into a private symbol table. Calls resolve against closed tables of special forms, audited syntax transformers, pure primitives, the script's own top-level definitions, and the request's ToolCall tool roster. Unknown names fail closed.
The guest cannot evaluate host Lisp or access buffers, processes, files, environment variables, user identity, time, or package state directly. It can reach external state only through nested tools, where normal mevedel authority and audit rules apply.
Host macroexpand-all is never run on guest text. The closed syntax
transformers are dispatched only when their names occur in evaluated operator
position, so quoted data and binding positions remain opaque. Guest defmacro
bodies run inside the same closed evaluator with the same tables and budgets,
so a user macro is a power feature, not a widening of this boundary.