tools/
Each file in tools/ becomes one model-facing tool. Add a tool when the agent
needs to do work through reviewed app-runtime code.
The filename stem is the tool name: tools/record_note.ts compiles to the
record_note tool. Use snake_case filenames so tool names match what the
model sees.
Minimal Example
// tools/echo.ts
import { defineTool } from "@assemblyline-agents/core";
export default defineTool({
description: "Echo a message back to the caller.",
inputSchema: {
type: "object",
properties: {
message: { type: "string" }
},
required: ["message"]
},
async execute(input: { message: string }) {
return { message: input.message };
}
});
Full Options
| Field | Type / values | Default | Effect |
|---|---|---|---|
description | string (required) | — | Model-facing purpose statement; drives tool selection. |
inputSchema | JSON Schema (required) | — | Argument contract enforced before execute. |
outputSchema | JSON Schema | — | Declared result contract. |
execute | (input, ctx) => result | — | Tool body; runs in the trusted app runtime. |
toModelOutput | (output) => projection | — | Bounded projection shown to the model; full result is persisted. |
needsApproval | boolean | ApprovalPolicy | false | Approval gate. approvalRequired(reason, sideEffect?) builds an always-approve policy; sideEffect defaults to "external". |
defaultEnabled | boolean | inferred | Forces the tool visible up front instead of behind deferred discovery. |
sideEffect | "none" | "idempotent" | "external" | — | Side-effect class recorded for approval and audit surfaces. |
capability | { visibility?, execution?, namespace?, tags?, aliases? } | — | Discovery metadata. visibility: auto | always | deferred | skill | hidden. execution: auto | direct | sandbox | both. |
Return values must be JSON-serializable. A thrown error marks the tool call
failed, surfaces { error } to the model, and fails the run — an authored
tool throwing means the agent's own code broke. Return an error-shaped result
instead of throwing when the condition is something the model should recover
from.
Execution Model
Tool code runs in the trusted app runtime by default. Use ctx.getSandbox()
only when the tool needs isolated filesystem or shell work.
Assembly Line starts model runs with core harness tools and keeps authored tools
behind deferred discovery unless you list them in agent.ts defaultTools or
declare capability metadata.
Approval Gates
Use approval gates for durable side effects or sensitive operations:
import { approvalRequired, defineTool } from "@assemblyline-agents/core";
export default defineTool({
description: "Record a durable note.",
inputSchema: {
type: "object",
properties: { note: { type: "string" } },
required: ["note"]
},
needsApproval: approvalRequired("Recording a note is a durable side effect.", "idempotent"),
async execute(input: { note: string }, ctx) {
await ctx.emit("note.recorded", {
note: input.note,
idempotencyKey: ctx.idempotencyKey("record-note")
});
return { recorded: true };
}
});
Authored tools that perform non-idempotent external writes should use
ctx.idempotencyKey(...) or a destination-level dedupe key derived from the
tool-call id.
Durable Steps
Use ctx.step(...) to cache completed substeps inside the current run. If the
same run is retried or resumed and the same step key is reached again, Assembly Line
returns the persisted JSON result and records durable_step.replayed instead of
running the body again.
import { defineTool } from "@assemblyline-agents/core";
export default defineTool({
description: "Hydrate a customer profile once per run.",
inputSchema: {
type: "object",
properties: { customerId: { type: "string" } },
required: ["customerId"]
},
async execute(input: { customerId: string }, ctx) {
const profile = await ctx.step(
`hydrate-customer:${input.customerId}`,
async () => {
const response = await fetch(`https://api.example.com/customers/${input.customerId}`, {
headers: { "Idempotency-Key": ctx.idempotencyKey(`customer:${input.customerId}`) }
});
return response.json();
},
{ metadata: { customerId: input.customerId } }
);
return { profile };
}
});
Step keys are scoped to the current run, and step results must be
JSON-serializable. A crash inside the step body can still run the body again, so
external writes should still use ctx.idempotencyKey(...) or a destination
dedupe key. ctx.step(...) is completed-step replay, not universal deterministic
workflow replay.
Safe Model Output
Use toModelOutput when the runtime should persist rich results but show the
model only a bounded projection:
export default defineTool({
description: "Look up a record and return a safe summary.",
inputSchema: {
type: "object",
properties: { lookup: { type: "string" } },
required: ["lookup"]
},
async execute(input: { lookup: string }) {
return {
summary: `Found ${input.lookup}`,
internalScore: 0.98,
internalTrace: ["vector", "rerank", "policy"]
};
},
toModelOutput(output: { summary: string }) {
return { summary: output.summary };
}
});
Execution Context
execute(input, ctx) receives a ToolExecutionContext:
| Member | Effect |
|---|---|
ctx.runId, ctx.agentRevision | Identity of the current run and compiled revision. |
ctx.approvedToolCall | true when this call has passed the runtime approval gate. |
ctx.askQuestion(question, options?) | Suspends the run to ask the user; returns Promise<never> — code after it never runs in this call. |
ctx.emit(eventType, data?) | Records a durable run event. |
ctx.idempotencyKey(scope) | Stable per-run dedupe key for external writes. |
ctx.step(key, fn, options?) | Completed-step replay cache (see Durable Steps). |
ctx.getSandbox() | Acquires the run sandbox for isolated file/shell work. |
ctx.blob, ctx.memory, ctx.resources | Blob, durable memory, and resource APIs when available. |
ctx.spawnSubagent({ name, task, expectedOutput?, constraints? }) | Runs a compiled subagent; see Subagents. |
ctx.connections, ctx.channel | Resolved connections and the originating channel view. |
ctx.selfImprovement, ctx.automationManager, ctx.connectionManager | Skill-writing, dynamic-automation, and dynamic-connection APIs; present only when the matching agent.ts policy enables them. |
Conventions
- One tool per file; keep each tool focused on one capability.
- Put provider event parsing in
channels/, not tools. - Put reusable procedures in
skills/, not long tool descriptions. - Gate non-idempotent external writes behind
needsApprovaland use idempotency keys.