Plugin tools
Add typed model-callable actions to an Assembly Line agent.
This page documents the typed runtime tool contract used inside an Agent
Plugin. MD-first agents do not discover root tools/ files; select a published
plugin or add a conforming local plugin whose ai.assemblyline extension owns
the executable tool. See MD-First Agents.
For a local plugin implementation, each model-facing tool is exported through
the plugin's statically inspected ai.assemblyline entry. The implementation
may use any internal module layout; a tools/ directory inside the plugin has
no magic discovery semantics. MD-first agent roots never discover it.
The key in the plugin entry is the model-facing tool name. Its definition
points to a contained implementation module. Internal filenames and folders
are ordinary package structure, not another discovery convention.
Minimal Example
// plugins/example/ai.assemblyline/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 };
}
});The plugin entry declares the capability without executing it during compile:
// plugins/example/ai.assemblyline/index.ts
export default {
tools: {
echo: {
definition: "./ai.assemblyline/echo.ts",
description: "Echo a message back to the caller.",
inputSchema: {
type: "object",
properties: { message: { type: "string" } },
required: ["message"]
}
}
}
};Reusable Tool Packs
A published plugin can contribute a reviewed group of tools through its
Agent Plugins v1 package and ai.assemblyline entry. Select the official
structured artifact pack with:
assembly-line add openui agentThe command adds openui to capabilities:, pins it in plugins.lock, and writes
no root wrappers. Runtime packaging ships the exact locked package. Tool packs
do not hold credentials and do not replace connection contracts.
The official pack exposes the complete standard OpenUI component library and accepts declarative brand configuration through the normal plugin config path:
capabilities:
openui:
config:
brand:
name: Example
theme:
background: "#F7F7F5"
textBrand: "#1F2937"
textAccentPrimary: "#2563EB"
footer: Example | ConfidentialBrand configuration supports the name, legal name, OpenUI theme tokens, a data-URI logo, and footer. Component packs and renderers remain reviewed code.
Plugin descriptor
The object under tools.<name> is the compile-time authority contract:
| Field | Accepted value | Default | Effect |
|---|---|---|---|
definition | Existing contained ./ path or package specifier | Required | Identifies the runtime module without executing it during compilation. |
factory | JavaScript export identifier | Default export | Calls the named export as a factory instead of loading a default tool. |
description | Non-empty string | Required | Declares the model-facing purpose. |
inputSchema | Inline JSON Schema object | Required | Declares the accepted arguments. |
outputSchema | Inline JSON Schema object | Omitted | Declares the result contract. |
needsApproval | Boolean or ApprovalPolicy | false | Declares the approval gate. |
capability.visibility | auto, always, deferred, or hidden | always | Controls direct exposure and discovery. auto resolves to always. |
capability.execution | auto, direct, sandbox, or both | direct | Selects the allowed runtime boundary. auto resolves to direct. |
capability.namespace | Non-empty string | Omitted | Groups the tool in discovery surfaces. |
capability.tags, capability.aliases | String arrays | [] | Adds discovery terms. |
timeoutMs | Positive integer | Runtime default | Overrides the tool deadline in milliseconds. |
hasToModelOutput | Boolean | false | Records that the implementation supplies a model-safe projection. |
disabled | Boolean | false | Removes a framework default with the same name or hides this contribution. |
requiredConfig | Uppercase configuration-name array | [] | Adds required non-secret values to the tool's scoped ctx.config view and preflight inventory. |
optionalConfig | Uppercase configuration-name array | [] | Adds optional non-secret values to the same scoped view. A name also listed as required is treated as required. |
config | JSON object | {} | Package-owned factory defaults. The selected plugin's schema-validated config is applied afterward. It has no effect without factory. |
The descriptor is duplicated authority by design: the compiler validates and locks model-visible schemas and policy without importing executable code.
For a reusable published tool plugin, declare its agent-owned options once at the plugin root and consume the merged config in the tool factory:
export default defineAssemblyLinePlugin({
configSchema: {
type: "object",
properties: { index: { type: "string" } },
required: ["index"],
additionalProperties: false
},
tools: {
search: {
definition: "./search.ts",
factory: "createSearchTool",
description: "Search the selected index.",
inputSchema: { type: "object" },
requiredConfig: ["SEARCH_API_URL"],
config: { limit: 20 }
}
}
});capabilities:
private-search:
config:
index: productscreateSearchTool receives { limit: 20, index: "products" }. The runtime
also exposes the declared URL through read-only ctx.config. Authored tools
cannot declare or receive credentials; move authenticated provider operations
into a reviewed connection.
Tool implementation
The module default export, or the value returned by factory, is a
ToolDefinition:
| Field | Accepted value | Effect |
|---|---|---|
description, inputSchema, outputSchema | Same contracts as the descriptor | Supplies runtime metadata. Keep it equal to the locked descriptor. |
execute | (input, ctx) => result | Implements the tool. A model-callable tool must provide it. |
toModelOutput | (output) => projection | Produces the bounded result shown to the model; the full result remains persisted. |
needsApproval | Boolean or ApprovalPolicy | Supplies runtime policy. Plugin or agent exceptions may only narrow or override it through reviewed configuration. |
capability | Visibility, execution, namespace, tags, and aliases | Supplies runtime capability metadata. The descriptor remains the compiled authority. |
sideEffect | none, idempotent, or external | Classifies the operation for approval and audit surfaces. |
timeoutMs | Positive integer | Overrides the runtime tool deadline. |
approvalRequired(reason, sideEffect?) creates an always-approve policy;
sideEffect defaults to external. Return values must be JSON-serializable.
Every durably recordable error thrown
during a model-invoked tool call marks that call failed, emits
tool.execution_failed, returns the error to the model, and leaves the run
active. This applies uniformly to authored tools, built-ins, connection tools,
deferred-tool routing, delegation, and sandbox acquisition. The model may
correct its input, choose another action, or explain the failure to the user.
Only control-plane failures that prevent safe continuation, such as state
persistence failure, cancellation, operation-deadline exhaustion, or an
expired run progress lease, terminate the run.
Failed tool results retain a simple error string and add
failure: { kind, runCanContinue: true }; kind is invalid_input,
configuration_error, or execution_error.
Throw RecoverableToolError when model-supplied input passes the JSON schema
but fails richer tool-specific validation. It classifies the failed result as
invalid_input; it is not required to keep the run alive:
import { RecoverableToolError, defineTool } from "@assemblyline-agents/core";
export default defineTool({
description: "Accept a complete document.",
inputSchema: { type: "object", properties: { document: { type: "string" } }, required: ["document"] },
async execute(input: { document: string }) {
if (!input.document.endsWith("}")) {
throw new RecoverableToolError("Document is incomplete.");
}
return { accepted: true };
}
});Return an error-shaped result instead when rejection is an expected domain outcome that should count as a successfully executed tool call.
Execution Model
Production Node hosts run authored tool modules, execute, and
toModelOutput inside the selected agent sandbox by default. The runtime
brokers scoped ctx APIs back to memory, state, connections, approval, and
delivery services; authored JavaScript is not imported into the host process.
The file-backed bridge publishes live request and response envelopes with
atomic replacement, so a sandbox tool can safely exchange large file contents
through ctx.getSandbox() without observing a partially written JSON frame.
The sandbox receives portable process basics plus declared non-secret
ctx.config. It does not receive host secrets or ctx.channel.env. The
selected sandbox must provide Node.js 22.
Sandboxed authored tools are bundled as self-contained ESM modules. A tool may declare a required neighboring asset with an import; the bundler includes that asset and excludes neighboring files that are not imported:
import prompt from "./prompt.md";
import query from "./query.sql";
import browserSource from "./browser.js" with { type: "text" };
import logoUrl from "./logo.png";
import wasmBytes from "./parser.wasm" with { type: "bytes" };Common text formats (.css, .csv, .graphql, .html, .md, .prompt,
.sql, .txt, .xml, and YAML) become strings. JavaScript or any other file
that must be treated as text uses with { type: "text" }. Images and fonts
become data URLs. .bin and .wasm become Uint8Array values, as does any
file imported with with { type: "bytes" }. JSON keeps its normal parsed
module behavior. Packages that type-check asset imports should provide the
corresponding ambient or adjacent TypeScript declarations.
An embedding host may explicitly set
RuntimeOptions.authoredToolExecution: "direct" only for reviewed code in its
trusted computing base. This policy is host-owned; agent source cannot weaken
it. Even direct tools receive no generic secret map. Framework built-ins,
connection dispatch, and host-provided test stubs remain in the host.
tool.execution_started records the resolved runtimeBoundary ("host" or
"sandbox") for audit and incident review.
ctx.getSandbox() remains useful on direct tools that need only isolated
filesystem or shell work.
Every non-disabled tool contributed by an active plugin starts in the current
surface's capability snapshot unless it declares deferred or hidden visibility. The
always-visible core set is read, write, edit, delete, list, grep,
bash, handoff_artifact, deliver_artifact, load_skill, tool_search, pair, files_search,
and files_mount. pair is
always visible so a pasted binding packet always has a landing spot; see
Connections.
history_search and the workspace tools are deferred. useTool("name")
conditionally promotes a known deferred framework or authored tool into the
initial snapshot.
tool_search finds and activates matching deferred framework, authored, and
connection tools. Their complete schemas appear on the next model call, and
the model invokes them directly. Selection never bypasses a host restriction
or the tool's needsApproval policy.
On engines without native deferred loading, tool_search tokenizes and ranks
descriptive queries across tool names, descriptions, tags, aliases, and
namespaces. Pass query: "" to browse the complete catalog. Results are paged
with a default limit of 8 and a maximum of 20; while hasMore is true, pass
the returned nextOffset as offset in the next call. The response reports
totalMatches, and only tools on the returned page are activated. Engines with
native deferred loading use the engine's own discovery surface instead.
Tools are scoped by plugin activation. A root surface sees framework defaults
and its root plugins; a child sees framework defaults and the plugins inside
its own subagents/<name>/ agent folder. Shared implementations belong in a
published plugin selected by each surface that needs them.
File transfer and user delivery are separate operations:
- A subagent calls
handoff_artifactwith an exact/workspace/...file. The runtime verifies and stores the exact bytes, then exposes an immutable, parent-readable/files/handoffs/<child-run-id>/...path. Later root turns in the same conversation can resolve that exact path from the durable receipt; other conversations and child surfaces cannot. This never attaches the file to the user. - The root calls
deliver_artifactonly for files the user should receive. It accepts an exact/workspace/...file or an exact parent-readable/files/handoffs/...file and selects those bytes for channel delivery.
Both tools enforce the configured byte limit, private content-addressed blob
storage, and internal-path exclusions such as __pycache__, .pyc,
node_modules, and .git. Final delivery reads immutable selection metadata;
it does not rescan /workspace or wait for workspace sync. A path mentioned in
prose never counts as a handoff or attachment.
delegate.deliverables declares required file and hosted-link counts. The
runtime byte-verifies child handoffs and returns safe parent paths. A failed
required handoff is sent back to the same child for one retry and then fails
loudly.
To remove a core default, a reviewed plugin may contribute the same tool name
with disabled: true. Host core-tool policy can also disable or approval-gate
a built-in without changing agent source.
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.principal, ctx.initiator | Canonical current and conversation-initiating actors for authorization checks. |
ctx.approvedToolCall | true when this call has passed the runtime approval gate. |
ctx.askQuestion(question, options?) | Opt-in primitive for products with an input-resume surface. It suspends the run and returns Promise<never>; code after it never runs in this call. Default agents ask clarification in their final response instead. |
ctx.reportProgress(data?) | Persists a run.progress_reported event and renews the active run's no-progress lease. Use it at meaningful milestones inside a single long-running tool operation. |
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.agentState | Reads or atomically updates conversation-scoped hook control state. Writes trigger capability re-evaluation before the next model request. |
ctx.blob, ctx.memory, ctx.resources | Blob, durable memory, and resource APIs. ctx.blob uses the gateway's configured adapter, and writes are private unless the tool explicitly requests public visibility. |
ctx.spawnSubagent({ name, task, expectedOutput?, constraints? }) | Runs a compiled subagent; see Subagents. |
ctx.connections, ctx.channel | Resolved connections and the originating channel view. |
ctx.deliveryManager.schedule(...) | Queues a durable future reply on the current run's immutable outbound route. The caller supplies the response, due instant, and idempotency key, but cannot choose the channel or recipient. |
ctx.selfImprovement, ctx.automationManager, ctx.connectionManager | Skill-writing, dynamic-automation, and dynamic-connection APIs; present only when the matching agent.md mutability ceiling enables them. |
Conventions
- Keep each contribution focused. A cohesive plugin may implement several tools and use any internal module layout.
- Put provider event parsing in the owning channel or connection provider, not tools.
- Put reusable procedures in
skills/, not long tool descriptions. - Gate non-idempotent external writes behind
needsApprovaland use idempotency keys. - Recheck role and tenant authorization inside sensitive tools. Hook-based visibility improves routing but is not an authorization boundary.