Tools

A3S Code keeps a registry of available tools. toolNames() returns the current session tool surface. That surface is assembled from the workspace capability set plus session-level integrations, so a non-local workspace can intentionally hide tools it cannot service.

Tool activity reaches clients through the session event stream, so a UI can render starts, output, errors, and completion without parsing terminal text.

Tool Surface

LayerToolsRegistration rule
Workspace-gated builtinsread, write, edit, patch, grep, glob, ls, bash, gitRegistered only when WorkspaceServices advertises the required capability. For example, a browser or S3 workspace can expose file tools while hiding bash or local git.
Runtime builtinsweb_fetch, web_search, batch, programRegistered by the core tool executor. batch and program receive the current scoped invoker, so their usable inner tools depend on the session surface and keep the caller's governance scope.
Session bootstrap toolstask, parallel_task, generate_object, search_skills, SkillAdded while building an AgentSession. Delegation tools disappear when manual delegation is disabled; generate_object requires the configured LLM client; skill tools use the effective skill registry.
TUI workflow toolsdynamic_workflow and, after /login, optional host runtime toolsRegistered by the a3s code host. dynamic_workflow powers ultracode and DeepResearch with A3S Flow replay; host runtime tools are login-gated integrations.
Dynamic integrationsmcp__<server>__<tool> and host-registered toolsAdded from MCP managers or host code after discovery.

Use toolNames() / toolDefinitions() in tests or application diagnostics when a workflow depends on a specific tool being visible. Tool visibility is not a security grant. Model-selected tool calls inside send, run, and stream pass through active-skill restrictions, permission policy, confirmation, hooks, budget, queue/timeouts, cancellation, recursive-call protection, output sanitization, artifact limits, and workspace path checks. Direct SDK calls such as session.tool(...) are host control-plane calls with a different explicit policy, described below. Use activeTools() for a different question: which tool calls are currently running in an active operation.

One Invocation Kernel

The runtime attaches an origin to every invocation:

OriginExamplesPermission / confirmation policy
AgentA model emits a tool call during send or streamFull model-facing policy and HITL apply.
Governed nestedInner calls from a model-owned batch, program, workflow, or public custom InvocationRuntimeRe-enters the model-facing policy and inherits the invocation stack, cancellation, budget, hooks, and sandbox. Ambient host-direct context cannot change this origin.
Host directsession.tool(...), typed read/write/git helpers, session.program(...), and direct task helpersTrusted control plane: model-facing permission/HITL are skipped because the host selected the call.
Trusted host-direct nestedA built-in batch, program, or dynamic workflow that the host called directly invokes a host-selected childRetains trusted control-plane authority for that built-in nested operation. Third-party tools cannot construct this origin through the public API.

Model sub-runs created by a host-direct Skill, Task, or custom tool start again with the Agent origin. A public custom tool can make nested calls only through InvocationRuntime, which always creates the governed nested origin. This prevents one direct call to arbitrary extension code from becoming a reusable authority token.

All origins use the same invocation kernel. Pre-hooks can block them; budget checks run before side effects; queue/timeout, cancellation, recursive invocation protection, post-hooks, and security-provider output sanitization remain active. A nested call cannot fall back to a raw registry while a scoped invoker is installed.

The host-direct policy is not an end-user authorization system. The embedding application must authenticate and authorize a user before translating their request into a direct SDK helper. Closing the session cancels an in-flight host-direct tool through the session cancellation scope.

Bounded Tool Contracts

Governed agent, nested, and session calls validate arguments against each tool's cached JSON Schema before confirmation or side effects. Tools also publish per-invocation scheduling capabilities: read-only, idempotent, resumable, cancellation-safe, pagination support, maximum parallelism, and output kind.

read, ls, glob, Git log/list/diff, and web_fetch return explicit continuation cursors or offsets. Shell capture is byte-bounded across both streams, retains the beginning and end with exact accounting, and still exposes stdout and stderr separately to a sandbox host. A command deadline includes both output draining and child.wait(), so closing both pipes cannot evade it. Timeout and cancellation terminate the complete Unix process group. Large file changes replace full before/after event payloads with bounded previews and unified diff text plus hashes, sizes, and artifact references.

Repository-context modes

Use read.files when the relevant paths are already known and one bounded response is cheaper than several tool turns:

JSON
{
"files": [
{ "path": "src/lib.rs" },
{ "path": "src/config.rs", "offset": 40, "limit": 80 }
],
"max_output_bytes": 65536
}

The shared byte budget includes headers and the continuation text. Results stay in request order, and one unreadable member does not discard successful members. When metadata.batch.truncated is true, copy metadata.batch.continuation into the next call's files value; its offsets and remaining limits resume without repeating completed lines.

grep.output_mode selects the smallest useful result shape:

ModeResult
contentMatching lines and optional context (default)
files_with_matchesLexically cursor-paginated matching paths
countLexically cursor-paginated matching-line counts per file
summaryFull-scan line and file totals without rendered match content

Built-in workspace backends perform non-content scans without constructing discarded match text. S3 results set metadata.search.truncated and warn when the backend's object scan limit makes totals or paths incomplete.

glob keeps backend relevance or recency order by default. Set sort: "path" before cursor pagination when stable lexical pages are required.

For exact-string changes, call edit with dry_run: true to return the same before/after diff metadata without writing. Then apply the edit with expected_replacements set to the previewed count, and optionally add max_replacements as an independent upper bound. Dry runs advertise read-only capability and are safe for batch parallelization.

batch accepts at most 32 calls and applies at most 16-way concurrency. It fans out only when every child declares safe read-only, idempotent behavior; mutating and unknown tools are serialized. A partial batch identifies failed indices while treating the orchestration as completed, so callers retry only failed items. parallel_task has the same 32-task bound and settles cancelled children before publishing terminal state.

web_search reports complete, partial, or failed in metadata. Empty results with engine errors are failures rather than successful empty searches. Timeout, cancellation, invalid-argument, partial-failure, and rate-limit errors carry structured error kinds.

Structured Output with generate_object

The generate_object tool asks the configured LLM for a JSON object, validates the response against a JSON Schema, and returns the validated object only on a zero-exit result. It works in two ways:

  1. Agent-driven: The LLM sees generate_object in its tool list and calls it autonomously when structured output is needed.
  2. Direct call: Your application calls session.tool('generate_object', ...) to bypass model-driven tool selection. The tool itself still calls the configured LLM.
TypeScript
const result = await session.tool('generate_object', {
schema: {
type: 'object',
required: ['sentiment', 'confidence'],
properties: {
sentiment: { type: 'string', enum: ['positive', 'negative', 'neutral'] },
confidence: { type: 'number', minimum: 0, maximum: 1 },
},
},
prompt: 'Classify: "This product is amazing!"',
schema_name: 'sentiment',
mode: 'tool',
max_repair_attempts: 2, // retries if schema validation fails
});
if (result.exitCode !== 0) {
throw new Error(result.output);
}
const { object } = JSON.parse(result.output);
// object = { sentiment: "positive", confidence: 0.95 }

Parameters

ParameterTypeRequiredDescription
schemaobjectyesJSON Schema used to validate the output
promptstringyesWhat to generate or extract
schema_namestringnoShort name (default: "result")
schema_descriptionstringnoDescription for the synthetic tool
systemstringnoOptional system prompt
modestringno"auto" / "strict" / "json" / "tool" / "prompt" (default: auto)
max_repair_attemptsintegerno0–5 (default: 2)
timeout_msintegernoIndependent generation deadline, 1,000–600,000 ms (default: 120,000)

Modes

  • tool: Injects a synthetic tool whose parameters are the schema. This is the current cross-provider default.
  • prompt: Appends schema instructions to the prompt. It is useful as a prompt-only fallback, but depends more on model compliance.
  • auto: Currently resolves to tool.
  • strict / json: Accepted by the API, but currently resolve to tool because provider-level response_format wiring is not enabled in this runtime path.

Streaming

When called through session.stream(), partial objects are emitted as tool_output_delta events. Snapshots are rate-limited to one event per 100 ms; objects over the event budget emit byte accounting instead of duplicating the full value in the stream:

TypeScript
const stream = await session.stream('Extract all invoices...');
while (true) {
const { value: ev, done } = await stream.next();
if (done) break;
if (!ev) continue;
if (ev.type === 'tool_output_delta' && ev.toolName === 'generate_object') {
const { object_partial } = JSON.parse(ev.text);
renderProgress(object_partial);
}
}

Repair Loop

If the LLM output fails schema validation, the tool automatically retries by feeding the validation errors back to the model. This handles edge cases like missing required fields or wrong enum values without application-level retry logic.

Direct Tool Calls

SDK callers can call deterministic tools directly:

TypeScript
const files = await session.glob('src/**/*.rs');
const hits = await session.grep('PermissionPolicy');
const status = await session.git('status');
const output = await session.bash('cargo test -p a3s-code-core');
const raw = await session.tool('read', { file_path: 'README.md' });
const schemas = session.toolDefinitions();
const hitLines = hits.split('\n').filter(Boolean).length;
console.log(files.length, hitLines, output.length, schemas.length);
console.log(status.output);
console.log(raw.output);

Direct calls execute inside the session workspace and should be treated as privileged host operations. They do not claim the session's single-flight conversation lease because they do not update transcript history.

For delegated child work, use SDK helpers over the same core tools:

TypeScript
await session.task({
agent: 'explore',
description: 'Find auth files',
prompt: 'Inspect auth-related files and return a compact evidence list.',
});
await session.tasks([
{ agent: 'explore', description: 'Find tests', prompt: 'Locate auth tests.' },
{
agent: 'verification',
description: 'Check risk',
prompt: 'Review auth edge cases.',
},
]);

Automatic subagent delegation also uses these core tools. autoParallel: false disables only automatic parallel fan-out; it does not remove parallel_task or session.tasks(...).

Programmatic Tool Calling

PTC is the next step beyond a single direct call. The program tool runs a sandboxed JavaScript script in an embedded QuickJS VM. The script defines async function run(ctx, inputs) and replaces repeated model-tool turns with one bounded program.

Instead of spending LLM turns on:

Text
grep -> read -> grep -> read -> summarize

the model can ask program to run a script:

JavaScript
// search-auth.js
export default async function run(ctx, inputs) {
const hits = await ctx.grep(inputs.query, { glob: '*.rs' });
const files = await ctx.glob('crates/**/*.rs');
const snippets = [];
for (const file of files.slice(0, 20)) {
const content = await ctx.readFile(file);
if (content.includes(inputs.query)) {
snippets.push({ file, preview: content.slice(0, 1200) });
}
}
return {
summary: `Found ${snippets.length} candidate files for ${inputs.query}`,
evidence: snippets,
rawSearch: hits,
};
}

Run it through the SDK helper with either inline source or a workspace-relative .js or .mjs file path:

JavaScript
await session.program({
path: 'scripts/ptc/search-auth.js',
inputs: { query: 'PermissionPolicy' },
allowedTools: ['grep', 'glob', 'read'],
limits: {
timeoutMs: 30000,
maxToolCalls: 30,
maxOutputBytes: 65536,
},
});

session.program(...) is equivalent to session.tool('program', { type: 'script', language: 'javascript', ... }) but uses SDK-native naming. When allowedTools / allowed_tools is omitted, the script can call every registered tool except program. Provide an allow-list when a workflow should run with a smaller capability surface.

The QuickJS VM receives no filesystem, network, subprocess, or environment permissions. The only useful capabilities are the ctx methods wired back to A3S Code tools. PTC returns a readable ToolResult.output and structured data in ToolResult.metadataJson. Keep large raw output out of the prompt; summarize findings, evidence refs, risks, and suggested next actions.

In a3s code, PTC is also used by DynamicWorkflowRuntime. Recursive program, dynamic_workflow, and parallel_task calls are kept out of the default TUI PTC allow-list. When a dynamic workflow needs local parallel subagents, it schedules a Flow step named parallel_task; the TUI host runs the native parallel_task implementation outside QuickJS. After /login, dynamic workflow PTC steps may call ctx.tool("runtime", ...) when a host runtime tool is registered in the session.

Tool Results

ToolResult includes name, output, exitCode, and optional metadataJson. Direct session.tool(...), session.program(...), session.git(...), session.writeFile(...), session.ls(...), session.editFile(...), and session.patchFile(...) calls return ToolResult. The typed read/search/shell helpers return simpler values: readFile, grep, and bash return strings, while glob returns a string array. Long outputs should be summarized before they are fed back to the model.

Verification

Use verification commands to turn "done" into evidence:

TypeScript
const report = await session.verifyCommands('release readiness', [
{
id: 'unit',
kind: 'test',
description: 'Run core tests',
command: 'cargo test -p a3s-code-core',
required: true,
timeoutMs: 120000,
},
]);