Agent tools
Tools shows how to declare and attach a custom tool Artifact.
Command fields description, protocol, command and args are required.
input_schema defaults to {"type":"object","additionalProperties":false}
(empty arguments only). Supplied schemas must have root type: "object" and are
compiled by jsonschema, without its file/HTTP resolution features. Local $ref,
composition and other standard schema features work; there is no custom keyword
subset. Schema defaults never change arguments. Declarations/schemas are capped
at 8 MiB. Calls require a JSON object of at most 64 KiB before any process or output
directory is created. Validation errors show at most five bounded, escaped
instance/schema paths (under 4 KiB), never argument values.
Descriptions must be nonblank, at most 4000 UTF-16 code units, and support only
{artifactName} interpolation (the canonical Artifact ID). Built-in references
take builtin, optional args and optional description. Without args, read,
list, glob, grep and view_image take the model’s input below; with args,
read, list, section and help work on a fixed target, and graph with
args = [] prints the entire current project structure, as described in
Built-in tools. Only explicitly declared tools are listed.
Built-ins execute in-process without output directories (only help starts its
program); view_image applies the same image checks as json image results.
| Built-in | Arguments | Result |
|---|---|---|
graph | Fixed declaration args = []; call {} | Complete name-based Artifact/eval tree, extracted criteria, graph fingerprint and input → consumer relations |
read | {path, offset?, limit?}; offset is a 1-based line (default 1), limit defaults to 80, max 500 | artifactId, resolvedArtifactId, path, lines: [{number, text}], startLine, nullable endLine, lineCount, totalLines only when EOF is known, truncated, nullable nextOffset |
list | {path?, offset?, limit?}; offset is 0-based (default 0), limit defaults to/max 200 | artifactId, path, sorted entries: [{name, path, kind}], totalEntries, truncated, nullable nextOffset |
glob | {pattern, path?}; path must name a directory | Searched path, up to 200 sorted logical files, truncated |
grep | {pattern, path?, glob?, caseInsensitive?, maxResults?}; case-sensitive by default, maxResults defaults to/max 200 | Searched path, matches: [{path, line, text}] (one per matching line, sorted by path then line), truncated |
read.artifactId identifies the Artifact that declared the tool;
resolvedArtifactId identifies the Artifact reached through a child folder.
For example, if app owns the child folder rules declared as spec, its read tool
called with {"path":"rules/requirements.md"} returns ownership metadata
{"artifactId":"app","resolvedArtifactId":"spec","path":"rules/requirements.md"}
alongside the line data. path remains the logical request path, not a physical
machine path. List’s top-level artifactId likewise names the declaring Artifact.
Entry kinds are file, directory, symlink or other. Glob/grep return the
searched logical path, and grep’s matching line numbers start at 1.
Paths are relative logical paths from the tool’s declaring Artifact, including
its children; omitted paths mean its root. Another Artifact’s files are read with
that Artifact’s own tools, which an eval admits when it references the Artifact. They never accept
absolute paths, traversal components or symlinks. Logical paths are at most
4096 UTF-16 code units and reject ASCII controls, backslashes, colons and empty
or ./.. components. The lone . is accepted as
a file Artifact’s virtual root for list/glob/grep; other dot components are
rejected. The registry scope remains the eval’s
admitted Artifacts, not other evals’ references. Paths are opened read-only through
pinned directory descriptors with no-follow component checks; special files cannot
be read. list can report symlink/other entries, but searches skip them.
A file Artifact exposes only its target file, not unrelated siblings in its
working directory. list with no path or path: "." lists that virtual root’s
target file. Read the target by its filename; file paths remain relative to the
containing folder.
Every call validates admitted file targets before invoking a built-in or command tool, even when reuse is disabled. A missing, non-regular or symlink-replaced target returns an error without running the command.
Read returns at most 64 KiB of complete original line bytes, preserving LF,
CRLF and a UTF-8 BOM in each line’s text. Only requested lines are decoded;
invalid UTF-8/NUL is an error, as is an oversized first requested line. An empty
file returns zero lines and totalLines: 0; a page past EOF returns zero lines
with the actual nonzero total. Concatenating returned text values reproduces
the source range without added line-number prefixes.
The glob built-in requires a directory path; it selects matching filenames
without inspecting contents, including binary filenames. grep accepts a file
or directory and skips binary (NUL) or invalid-UTF-8 contents in their entirety.
Its optional glob argument is a filename filter, not a call to the glob tool.
Glob patterns use * within a component and ** across directories, relative to path.
Glob and grep patterns (including grep’s optional glob) must be nonempty and at
most 4096 characters. Globs must be relative, with no leading slash,
backslashes, colons, NUL, or empty/./.. path components. Grep uses Rust regex
syntax, with a 2 MiB compiled-regex size ceiling; an empty, invalid or oversized
regex is an error. Grep’s optional glob filter matches relative file paths
(or the basename when path names a file). Hidden files are included; git ignore
rules do not filter results.
Bounds are 10,000
traversed entries, 8 MiB per grep file, 64 MiB searched,
and 512 KiB per JSON result. Search limits or skipped oversized files set
truncated: true; narrow the path/pattern to continue. Separately, encountering
any one directory with more than 10,000 entries fails list, glob or grep
with an error, not a truncated search result. Listing can page early at the result
byte cap. Built-in arguments use the same bounded JSON Schema admission as commands.
The json protocol receives exactly one request on stdin:
{
"version": 1,
"context": {
"artifactId": "example",
"artifactPath": "/workspace/example",
"outputDir": "/external/private/output",
"tmpDir": "/external/private/tmp",
"scope": {
"example": {"path": "/workspace/example", "kind": "folder", "children": {}}
},
"executionPaths": {"shared/rules.json": "/workspace/shared/rules.json"}
},
"args": {"section": "summary"}
}
Scope entries contain canonical physical paths, kind (file, folder or graph) and
logical child maps. For a file Artifact, context.artifactPath and its
scope entry’s path name the file, not the process cwd; the cwd is its containing
folder. File Artifact references reject /path suffixes.
These protocol keys stay camelCase; declarations use execution_paths and
input_schema. executionPaths is
omitted when empty. Declared execution paths are up to 64 unique workspace-relative
files/directories, resolved without symlinks or copying. This remains
workspace-relative for file Artifacts; it is not relative to their cwd or target.
When an Agent review starts, artifactize pins each execution path of the eval’s
tools: a file to the SHA-256 of its bytes, a directory to the SHA-256 over each
file’s or symlink’s relative path and digest (a symlink inside contributes its target
and is not followed). Directories are only traversed: they do not contribute a
digest or increment the entry counter, so empty directories do not count. The pins
are recorded in the result’s provenance as
executionPaths: {"TOOL": {"PATH": "sha256"}} (by registered tool name, such as
coverage_spec, and declared path), travel with remote records, and stay with the
result when it is reused, so a reused verdict shows which binary produced it. A
path that cannot be pinned (missing, a special file, or more than 1 GiB of file
bytes) fails the review with PREPARATION_FAILED before the Agent is called.
Pins retain at most 10,000 file/symlink entries; after that many have been retained,
encountering any further filesystem entry fails preparation. This is not a total
filesystem traversal bound: directory visits alone, including empty directories,
do not increment the counter. Pins are provenance, not part of the
reuse key: to review again when a binary
changes, cover it in a fingerprint.
Successful stdout is one JSON object with 1–32 content blocks:
{"content":[{"type":"text","text":"..."},{"type":"json","data":{}}]}.
Image blocks are described below.
Text is at most 64 KiB per block; compact JSON data at most 512 KiB per block;
the normalized result at most 8 MiB. Both process streams are bounded at 16 MiB.
The result object accepts only content and optional boolean isError.
Every block is also closed: text permits only type and text; JSON permits
only type and data; image permits only type, mimeType and exactly one of
data or path. Any extra field on a result or block is an invalid-output error,
not metadata passed through to the reviewer. Exit-0 authored errors must have
exactly one nonblank text block, such as
{"isError":true,"content":[{"type":"text","text":"Choose a smaller range."}]}.
These reach the reviewer unchanged. Nonzero exit, crash, malformed/truncated
JSON, and process failures yield generic errors; stderr is never forwarded.
There are no observation receipts.
Image results
A JSON command can return an image block with mimeType and exactly one of
data (standard base64 bytes) or path (an output file):
{"content":[{"type":"image","mimeType":"image/png","data":"<base64-encoded PNG bytes>"}]}
{"content":[{"type":"image","mimeType":"image/png","path":"screenshots/board.png"}]}
Write the second form’s file below the request’s context.outputDir. A relative
path starts there; an absolute path is accepted only when it is still below that
directory. Traversal, symlinks in any component, special files and files outside
that output directory are refused. Artifactize reads and embeds the bytes before
removing the invocation’s output, so the returned result is self-contained base64,
not a dangling local file path.
Images must be nonempty PNG, JPEG or WebP, at most 4 MiB inclusive of
image-file bytes after base64 decoding, not decoded pixels. Detection checks
signatures/container framing rather than full image validity. Those bytes, not
the filename extension, identify the format and must match
image/png, image/jpeg or image/webp respectively. Invalid base64, a MIME
mismatch, unsupported formats such as GIF/BMP, and animated PNG are rejected.
The overall normalized 8 MiB tool-result limit still applies after base64 encoding.
view_image returns the same embedded image form and
uses the same byte/format limits, but reads its file from admitted Artifact scope.
For plain, only top-level declared input_schema.properties can appear as argv
placeholders. Whole-token {query} and embedded --query={query} both work.
Strings substitute literally; other JSON values use compact JSON. Missing values
and NUL bytes fail before spawn. {{ and }} escape literal braces. Substitution
is single-pass: values containing braces, quotes, spaces, $() or semicolons
remain one literal argv element, never shell code. Plain tools receive empty
stdin. Cleaned, lossily decoded stdout becomes one text block, capped at 64 KiB
with an explicit truncation marker; nonzero exit marks that bounded stdout as a
tool error. Stderr is not included. Commands such as rg that exit nonzero for
no matches need an owner wrapper if that should count as successful empty output.
Command executables follow the shared Program lookup
rules, including the file-Artifact exception and rejection of scoped traversal.
JSON-protocol argv may use existing scoped Artifact references, but
cannot add Artifacts outside the eval’s admitted scope. Plain argv uses only its
schema-property placeholders. Commands themselves are never interpolated.
Command calls use the owner’s working directory (the containing folder for a file
Artifact) as cwd, the author’s own environment (the
runtime environment: the real home, no
ARTIFACTIZE_* variables of the caller) with private external TMP/output, a default 120000 ms deadline (1–2147483647), and
process-group cancellation/cleanup. Per-call directories are removed on success,
failure and cancellation, including dropped call futures; caller-owned output
roots remain. Commands are trusted read-only programs, not sandboxed.
Internal callers use tools::Registry::new(&config, "artifact/eval"), list()
and call(name, args, output_root, cancellation).await. Only Agent evals are
accepted. Each Artifact in its admitted scope contributes its Agent declarations
under <name>_<artifactId>; tool-name and session-provenance-name concatenation
collisions are rejected. Human tools
are never listed. Listing creates no directories and runs no owner code. Calls
return normalized ToolResult (the Success or Error variant; read it with content() and
is_error()) for successful, authored and
system-error results. Registry calls do not mutate payloads or declarations. The
Agent loop calls this registry.
Tool sessions
A tool session keeps a harness running across calls during one review. It is not a saved Agent conversation. Declare its start and stop commands in the same Artifact as its bound command tools:
[views.agent_sessions.app]
start = { command = "harness", args = ["start"], execution_paths = ["tools/harness"] }
stop = { command = "harness", args = ["stop"], execution_paths = ["tools/harness"] }
[views.agent_tools.click]
session = "app"
description = "Click an element of the running app."
command = "harness"
args = ["act"]
protocol = "json"
start and stop require command and args; their only optional field is
execution_paths. They use the usual executable lookup, scoped argv and JSON
stdin envelope, with args: {}. They do not take description, protocol,
timeout_ms or concurrency options. A command tool’s session must name a
session in its own Artifact; built-in and Human tools do not accept it.
The harness implements this start contract:
- Read the usual JSON envelope on stdin.
ARTIFACTIZE_SESSION_DIRnames a private directory supplied by artifactize; use it for state shared by bound tools and stop. - When ready, print one newline-terminated line on stdout. Its content is ignored.
- Keep the start process running for the whole session. Its exit ends the session.
Bound tools, whether json or plain, and stop receive the same
ARTIFACTIZE_SESSION_DIR and run in the author’s environment, real home included, like
every command.
Each invocation still has its own private output and temporary directories.
Concurrent reviews have independent session directories and environments, even
on the same Artifact. Choose ports/displays through standard OS mechanisms
(for example port 0 or an automatically selected display); a desktop harness
starts its own headless display. The Agent sees no instance ID and cannot call
start or stop.
Execution-path pinning still happens at review start. A tool session starts only
when the Agent first calls a tool bound to it; later bound calls reuse that session.
An admitted session whose tools are never called starts no process and creates no
session directory. Startup and readiness count toward the review’s timeout.
Spawn failure or exit before readiness is returned to the Agent as that tool call’s
error, not PREPARATION_FAILED. Later calls to the same session return the same
startup error without restarting it. Readiness has no separate fixed bound: start
may prepare while its process lives, until the review times out or is cancelled.
After readiness, stdout and stderr are drained and discarded.
If the start process exits during the review, artifactize immediately kills its
remaining process group/Job and cancels in-flight bound calls. The cleaned group
is released once termination is confirmed, so later review cleanup does not signal
its old numeric process-group ID. A failure to retire its saved identity is reported
without skipping cancellation/join of bound calls or stop. Later bound calls return
The tool session `app` ended.; there is no restart. This is an ordinary tool
error in the saved conversation, not a new event kind.
Every session that started goes through stop and cleanup on every review ending: verdict, error, timeout, cancellation or dropped review future, including a review panic. A start that exits before readiness is cleaned up immediately; otherwise stop is attempted after the Agent loop and before input revalidation, fingerprint recheck and result commit. Cleanup cancels and awaits in-flight bound calls before running stop. Stop uses its own cancellation token and an internal 30-second bound, then artifactize kills and reaps the session group/Job. Its directory and admission records are removed only after all owned-process cleanup is confirmed, even when stop itself fails or hangs. Windows cleanup waits for all Job descendants, with a separate internal 30-second termination bound.
Cleanup does not depend on the program that asked for the review: if that caller shuts down its async runtime, artifactize’s own background executor still runs stop and cleanup. If the artifactize process itself is killed, stop cannot run, so the next tool session cleans up first. Each session directory records the process that owns it and a lease, a marker that its cleanup is still in progress. Before starting, artifactize removes directories whose owner has exited (including one that has exited but was never waited for) or whose lease was released after a failed cleanup, even if the owner is still alive. Directories with an active lease belong to a running review and are left alone.
On Unix, artifactize records each start, stop and bound-call process group before admitting it, and checks the saved process ID and start time before sending a signal, so it never signals a reused process ID, even when the group’s first process has already exited. Windows Jobs end when their owner dies; recovery waits up to 30 seconds for Windows to release open file handles.
A cleanup failure is reported instead of a successful result. When process cleanup is not confirmed, artifactize keeps its journals (the records of which processes it started) so a later start can finish the job; a directory that could not be removed is swept by the next start once its lease is released. If that sweep fails, the journals are kept and the new session is refused rather than admitted. A reused or waited-on result starts nothing.
Start/stop execution-path pins use <session>_<artifact>.start and .stop keys
in the result’s existing executionPaths provenance, including remote records.
tools check statically checks the referenced session’s executables and operands
without running it. tools check --execute on a bound tool and session send
each own fresh lazy sessions: they start on the first bound call and clean up
afterward. A follow-up that calls no bound tools starts nothing.
Internal callers register lazy sessions with
registry.sessions(state, selected_tool_or_none, cancellation),
attach them with registry.with_sessions(&sessions), then release and await
sessions.close().await after their calls. Dropping the guard also starts cleanup.
Session commands remain trusted, not sandboxed: commands can modify the
workspace or escape cleanup by deliberately detaching descendants.
Tool diagnostics
artifactize tools check # every Agent/Human eval's scope
artifactize tools check app/review # same as --eval app/review
artifactize tools check --artifact app --audience agent
artifactize tools check --execute --artifact app --audience agent --tool read --args '{"path":"README.md"}'
artifactize tools check --execute --artifact app --audience human --tool inspect
artifactize tools check --execute --eval app/review --tool coverage_app --args '{}'
tools check discovers and validates declarations, resolves executable availability,
scoped argv operands and declared execution paths, and prints JSON scopes, schemas
and per-tool readiness checks. It is static by default: no owner process, fingerprint
script, database or output directory is created. A positional selector or --eval
selects one Agent/Human eval and cannot be combined with --artifact or
--audience. Explicit execution requires --tool and either an eval selector or
both --artifact and --audience. With --artifact, the short operation name or
its published name is accepted, and the scope is that Artifact and its children.
With an eval, name the tool as that eval publishes it (<tool>_<artifact>); it runs
in the eval’s own scope, so a tool whose operands name another Artifact runs as it
would in that eval’s review.
--args is valid only for Agent execution; Human schemas admit only an empty
object and Human commands take no free arguments. A check never creates a Run,
verdict or cache entry. Agent calls run in the author’s environment with private temporary output (removed after
execution); Human calls use the reviewer’s real environment. Exit codes are
0 for ready/success, 1 for declaration/preflight/tool/cleanup failure, and 2 for
invalid invocation. --repo, --state-dir and --json are accepted; reports are
JSON even without --json.